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; } }
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>
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>
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>
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:
- Instantiate the mock manager of the class you want to test (or write one according to the above steps)
- Write specific scenarios for tests (automatically complete existing covered scenario steps)
- Use the test parameters to execute the method you want to test
- 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!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

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

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics











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

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 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

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 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 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

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

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
