Table of Contents
Introduction
How to perform unit testing
Solution
in conclusion
Home Backend Development C++ MockManager in Unit Tests - Builder Mode for Mocking

MockManager in Unit Tests - Builder Mode for Mocking

Apr 04, 2025 am 08:06 AM
typescript

MockManager in Unit Tests - Builder Mode for Mocking

I wrote about this a few years ago, but not in much detail. This is a more refined version of the same idea.

Introduction

Unit testing is both a blessing and a curse for developers. They allow quick testing of features, readable usage examples, scenarios for components involved in rapid experimentation. But they can also become messy, requiring maintenance and updates every time the code changes, and if done lazily, you can't hide the error instead of revealing it.

I think the reason unit testing is so difficult is that it is related to testing, not code writing, and that unit testing is written in the opposite way to most other code we write.

In this post, I will provide you with a simple pattern for writing unit tests that will enhance all the benefits while eliminating most cognitive dissonance with normal code. Unit testing will remain readable and flexible while reducing duplicate code and no additional dependencies are added.

How to perform unit testing

But first, let's define a good unit test suite.

To test a class correctly, it must be written somehow. In this post, we will introduce classes that use constructor injection for dependencies, which is my recommended way to do dependency injection.

Then, in order to test it, we need:

  • Covering positive scenarios - Use various combinations of settings and input parameters to cover the entire function when a class performs what it should do
  • Covering negative scenarios - Class fails in the correct way when setting or input parameters are wrong
  • Simulate all external dependencies
  • Keep all test settings, operations, and assertions in the same test (commonly called the arrange-act-assert structure)

But this is easier said than done, because it also means:

  • Set the same dependencies for each test, copy and paste a lot of code
  • Set up very similar scenarios, make changes only once between tests, repeating a lot of code again
  • Nothing is generalized and encapsulated, this is what developers usually do in all their code
  • Writing a lot of negative examples for very few positive examples feels like more test code than functional code
  • All of these tests must be updated for each change to the test class

Who likes this?

Solution

The solution is to use the builder software pattern to create smooth, flexible and readable tests in the array-act-assert structure while encapsulating the setup code in a class to complement the unit test suite of specific services. I call it mockmanager mode.

Let's start with a simple example:

 // the tested class
Public class calculater
{
    private readonly itkenparser tokenparser;
    private readonly imathopementfactory operationfactory;
    private readonly icache cache;
    private readonly ilogger logger;

    public calculate(
        itkenparser tokenparser,
        imathopperationfactory operationfactory,
        icache cache,
        ilogger logger)
    {
        this.tokenparser = tokenparser;
        this.operationfactory = operationfactory;
        this.cache = cache;
        this.logger = logger;
    }

    public int calculate(string input)
    {
        var result = cache.get(input);
        if (result.hasvalue)
        {
            logger.loginformation("from cache");
            return result.value;
        }
        var tokens = tokenparser.parse(input);
        ioperation operation = null;
        foreach(var token in tokens)
        {
            if (operation is null)
            {
                operation = operationfactory.getoperation(token.operationtype);
                continue;
            }
            if (result is null)
            {
                result = token.value;
                continue;
            }
            else
            {
                if (result is null)
                {
                    throw new invalidoperationexception(" could not calculate result");
                }
                result = operation.execute(result.value, token.value);
                operation = null;
            }
        }
        cache.set(input, result.value);
        logger.loginformation("from operation");
        return result.value;
    }
}
Copy after login

This is a calculator, as tradition. It receives a string and returns an integer value. It also caches the results of specific inputs and records some content. The actual operation is abstracted by imathopperationfactory, and the input string is converted to a token by itkenparser. Don't worry, this is not a real course, just an example. Let's look at a "traditional" test:

 [testmethod]
Public void calculate_additionworks()
{
    // arrange
    var tokenparsermock = new mock<itkenparser>();
    tokenparsermock
        .setup(m => m.parse(it.isany<string>()))
        .returns(
            new list<calculatortoken> {
                Calculatortoken.addition, Calculatortoken.from(1), Calculatortoken.from(1)
            }
        );

    var mathoperationfactorymock = new mock<imathopetionfactory>();

    var operationmock = new mock<ioperation>();
    operationmock
        .setup(m => m.execute(1, 1))
        .returns(2);

    mathoperationfactorymock
        .setup(m => m.getoperation(operationtype.add))
        .returns(operationmock.object);

    var cachemock = new mock<icache>();
    var loggermock = new mock<ilogger>();

    var service = new calculate(
        tokenparsermock.object,
        mathoperationfactorymock.object,
        cachemock.object,
        loggermock.object);

    // act
    service.calculate("");

    //assert
    mathoperationfactorymock
        .verify(m => m.getoperation(operationtype.add), times.once);
    operationmock
        .verify(m => m.execute(1, 1), times.once);
}
</ilogger></icache></ioperation></imathopetionfactory></calculatortoken></string></itkenparser>
Copy after login

Let's open it a little bit. For example, even if we don't actually care about the logger or cache, we have to declare a mock for each constructor dependency. In the case of operating the factory, we must also set a simulation method that returns another simulation.

In this particular test, we mainly wrote settings, one line of act and two line of assert. Also, if we want to test how cache works in a class, we have to copy and paste the entire content and then change how we set up the cache mock.

There are also some negative tests to consider. I've seen many negative tests do something similar: "Setting up what should fail. Testing it fails", which introduces a lot of problems, mainly because it can fail for a completely different reason, and most of the time these tests follow the internal implementation of the class rather than its requirements. A correct negative test is actually a completely positive test with only one wrong condition. For simplicity, this is not the case here.

So, getting back to the point, here is the same test, but using mockmanager:

 [testmethod]
public void calculate_additionworks_mockmanager()
{
    // arrange
    var mockmanager = new calculatemockmanager()
        .withparsedtokens(new list<calculatortoken> {
            Calculatortoken.addition, Calculatortoken.from(1), Calculatortoken.from(1)
        })
        .withoperation(operationtype.add, 1, 1, 2);

    var service = mockmanager.getservice();

    // act
    service.calculate("");

    //assert
    mockmanager
        .verifyoperationexecute(operationtype.add, 1, 1, times.once);
}

</calculatortoken>
Copy after login

Unpacking, no mention of cache or loggers, as we don't need to do any setup there. Everything is packaged and readable. Copy and paste this and change some parameters or some lines is no longer ugly. There are three methods executed in arrange, one in act and one in assert. Only the substantial simulation details are abstracted: no mention of the moq framework here. In fact, this test looks the same regardless of which simulation framework you decide to use.

Let's take a look at the mockmanager class. Now this will seem complicated, but remember we only write it once and use it a lot. The overall complexity of this class is to make unit tests easy to read by humans, easy to understand, update, and maintain.

 public class CalculatorMockManager
{
    private readonly Dictionary<operationtype>> operationMocks = new();

    public Mock<itokenparser> TokenParserMock { get; } = new();
    public Mock<imathoperationfactory> MathOperationFactoryMock { get; } = new();
    public Mock<icache> CacheMock { get; } = new();
    public Mock<ilogger> LoggerMock { get; } = new();

    public CalculatorMockManager WithParsedTokens(List<calculatortoken> tokens)
    {
        TokenParserMock
            .Setup(m => m.Parse(It.IsAny<string>()))
            .Returns(
                new List<calculatortoken> {
                    CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1)
                }
            );
        return this;
    }

    public CalculatorMockManager WithOperation(OperationType operationType, int v1, int v2, int result)
    {
        var operationMock = new Mock<ioperation>();
        operationMock
            .Setup(m => m.Execute(v1, v2))
            .Returns(result);

        MathOperationFactoryMock
            .Setup(m => m.GetOperation(operationType))
            .Returns(operationMock.Object);

        operationMocks[operationType] = operationMock;

        return this;
    }

    public Calculator GetService()
    {
        return new Calculator(
                TokenParserMock.Object,
                MathOperationFactoryMock.Object,
                CacheMock.Object,
                LoggerMock.Object
            );
    }

    public CalculatorMockManager VerifyOperationExecute(OperationType operationType, int v1, int v2, Func<times> times)
    {
        MathOperationFactoryMock
            .Verify(m => m.GetOperation(operationType), Times.AtLeastOnce);
        var operationMock = operationMocks[operationType];
        operationMock
            .Verify(m => m.Execute(v1, v2), times);
        return this;
    }
}
</times></ioperation></calculatortoken></string></calculatortoken></ilogger></icache></imathoperationfactory></itokenparser></operationtype>
Copy after login

All mocks required by the test class are declared as public properties, allowing any customization of unit tests. There is a getservice method that will always return an instance of the class being tested and all dependencies are fully mocked. Then there is the with* method, which automatically sets up various scenarios and always returns to the simulation manager so that they can be linked. You can also use specific assertion methods, although in most cases you will compare some output to expected values, so these are just to abstract the verification method of the moq framework.

in conclusion

This pattern now aligns test writing with code writing:

  • Abstract things you don't care about in any context
  • Write once, use multiple times
  • Human-readable self-recording code
  • Small method of low circle complexity
  • Intuitive code writing

Now writing unit tests is both simple and consistent:

  1. Instantiate the mock manager of the class you want to test (or write one according to the above steps)
  2. Write specific scenarios for tests (automatically complete existing covered scenario steps)
  3. Use the test parameters to execute the method you want to test
  4. Check that everything is in line with expectations

Abstraction does not stop at simulation frameworks. The same pattern can be applied to each programming language! The mock manager construct would be very different for typescript or javascript or something, but the unit tests would look almost the same.

Hope this helps!

The above is the detailed content of MockManager in Unit Tests - Builder Mode for Mocking. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

Java Tutorial
1655
14
PHP Tutorial
1252
29
C# Tutorial
1226
24
5 common JavaScript memory errors 5 common JavaScript memory errors Aug 25, 2022 am 10:27 AM

JavaScript does not provide any memory management operations. Instead, memory is managed by the JavaScript VM through a memory reclamation process called garbage collection.

How does Vue3+TypeScript+Vite use require to dynamically introduce static resources such as images? How does Vue3+TypeScript+Vite use require to dynamically introduce static resources such as images? May 16, 2023 pm 08:40 PM

Question: How to use require to dynamically introduce static resources such as images in a Vue3+TypeScript+Vite project! Description: When developing a project today (the project framework is Vue3+TypeScript+Vite), it is necessary to dynamically introduce static resources, that is, the src attribute value of the img tag is dynamically obtained. According to the past practice, it can be directly introduced by require. The following code: Write After uploading the code, a wavy line error is reported, and the error message is: the name "require" cannot be found. Need to install type definitions for node? Try npmi --save-dev@types/node. ts(2580) after running npmi--save-d

How to implement data type conversion function in TypeScript using MySQL How to implement data type conversion function in TypeScript using MySQL Jul 29, 2023 pm 02:17 PM

How to implement data type conversion function in TypeScript using MySQL Introduction: Data type conversion is a very common requirement when developing web applications. When processing data stored in a database, especially when using MySQL as the back-end database, we often need to convert the data in the query results to the type we require. This article will introduce how to use MySQL to implement data type conversion in TypeScript and provide code examples. 1. Preparation: Starting

How to develop high-performance computing functions using Redis and TypeScript How to develop high-performance computing functions using Redis and TypeScript Sep 20, 2023 am 11:21 AM

Overview of how to use Redis and TypeScript to develop high-performance computing functions: Redis is an open source in-memory data structure storage system with high performance and scalability. TypeScript is a superset of JavaScript that provides a type system and better development tool support. Combining Redis and TypeScript, we can develop efficient computing functions to process large data sets and make full use of Redis's memory storage and computing capabilities. This article will show you how to

How to use TypeScript in Vue3 How to use TypeScript in Vue3 May 13, 2023 pm 11:46 PM

How to declare a type with field name enum? By design, the type field should be an enumeration value and should not be set arbitrarily by the caller. The following is the enumeration declaration of Type, with a total of 6 fields. enumType{primary="primary",success="success",warning="warning",warn="warn",//warningaliasdanger="danger",info="info",}TypeSc

Changes in Vue3 compared to Vue2: Better TypeScript type inference Changes in Vue3 compared to Vue2: Better TypeScript type inference Jul 07, 2023 pm 01:05 PM

Changes in Vue3 compared to Vue2: Better TypeScript type inference Vue is a popular JavaScript framework for building user interfaces. Vue3 is the latest version of the Vue framework, with a lot of improvements and optimizations based on Vue2. One of them is improvements in TypeScript type inference. This article will introduce the improvements in type inference in Vue3 and illustrate them through code examples. In Vue2, we need to manually configure the Vue component

Develop scalable front-end applications using Redis and TypeScript Develop scalable front-end applications using Redis and TypeScript Aug 01, 2023 pm 09:21 PM

Title: Developing Scalable Front-End Applications Using Redis and TypeScript Introduction: In today’s Internet age, scalability is one of the key elements of any application. Front-end applications are no exception. In order to meet the growing needs of users, we need to use efficient and reliable technology to build scalable front-end applications. In this article, we will introduce how to use Redis and TypeScript to develop scalable front-end applications and demonstrate its application through code examples. Introduction to Redis

Write better code with TypeScript in PHP Write better code with TypeScript in PHP Jun 19, 2023 pm 06:31 PM

With the continuous development of JavaScript, front-end engineers have gradually become aware of some problems in JavaScript itself, such as the lack of type checking and modularity, which often cause confusion and errors in large projects. In order to solve these problems, TypeScript came into being and became an increasingly popular language in front-end development. In the field of back-end development, PHP has always been an extremely popular scripting language. Therefore, combine TypeScript to develop PHP applications

See all articles