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!

There are four commonly used XML libraries in C: TinyXML-2, PugiXML, Xerces-C, and RapidXML. 1.TinyXML-2 is suitable for environments with limited resources, lightweight but limited functions. 2. PugiXML is fast and supports XPath query, suitable for complex XML structures. 3.Xerces-C is powerful, supports DOM and SAX resolution, and is suitable for complex processing. 4. RapidXML focuses on performance and parses extremely fast, but does not support XPath queries.

C interacts with XML through third-party libraries (such as TinyXML, Pugixml, Xerces-C). 1) Use the library to parse XML files and convert them into C-processable data structures. 2) When generating XML, convert the C data structure to XML format. 3) In practical applications, XML is often used for configuration files and data exchange to improve development efficiency.

The main differences between C# and C are syntax, performance and application scenarios. 1) The C# syntax is more concise, supports garbage collection, and is suitable for .NET framework development. 2) C has higher performance and requires manual memory management, which is often used in system programming and game development.

The history and evolution of C# and C are unique, and the future prospects are also different. 1.C was invented by BjarneStroustrup in 1983 to introduce object-oriented programming into the C language. Its evolution process includes multiple standardizations, such as C 11 introducing auto keywords and lambda expressions, C 20 introducing concepts and coroutines, and will focus on performance and system-level programming in the future. 2.C# was released by Microsoft in 2000. Combining the advantages of C and Java, its evolution focuses on simplicity and productivity. For example, C#2.0 introduced generics and C#5.0 introduced asynchronous programming, which will focus on developers' productivity and cloud computing in the future.

There are significant differences in the learning curves of C# and C and developer experience. 1) The learning curve of C# is relatively flat and is suitable for rapid development and enterprise-level applications. 2) The learning curve of C is steep and is suitable for high-performance and low-level control scenarios.

There are significant differences in how C# and C implement and features in object-oriented programming (OOP). 1) The class definition and syntax of C# are more concise and support advanced features such as LINQ. 2) C provides finer granular control, suitable for system programming and high performance needs. Both have their own advantages, and the choice should be based on the specific application scenario.

Converting from XML to C and performing data operations can be achieved through the following steps: 1) parsing XML files using tinyxml2 library, 2) mapping data into C's data structure, 3) using C standard library such as std::vector for data operations. Through these steps, data converted from XML can be processed and manipulated efficiently.

C# uses automatic garbage collection mechanism, while C uses manual memory management. 1. C#'s garbage collector automatically manages memory to reduce the risk of memory leakage, but may lead to performance degradation. 2.C provides flexible memory control, suitable for applications that require fine management, but should be handled with caution to avoid memory leakage.


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

Atom editor mac version download
The most popular open source editor

SublimeText3 Linux new version
SublimeText3 Linux latest version

mPDF
mPDF is a PHP library that can generate PDF files from UTF-8 encoded HTML. The original author, Ian Back, wrote mPDF to output PDF files "on the fly" from his website and handle different languages. It is slower than original scripts like HTML2FPDF and produces larger files when using Unicode fonts, but supports CSS styles etc. and has a lot of enhancements. Supports almost all languages, including RTL (Arabic and Hebrew) and CJK (Chinese, Japanese and Korean). Supports nested block-level elements (such as P, DIV),

Zend Studio 13.0.1
Powerful PHP integrated development environment

SecLists
SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.