The Art of net unit testing
Opening : Previous we learned basic unit Testing Basics and introductory examples. But how do we write tests if the method we're testing depends on an external resource, such as a file system, a database, a Web service, or something that's hard to control? To solve these problems, we need to create test stubs , pseudo-objects , and mock objects . In this article we will begin to touch on these core technologies, using stubs to break dependencies, use mock objects for interactive testing, and use the isolation framework to support future and usability features.
Series Catalog:
1. Getting Started
2. Core Technologies
3. Test code
4. Design and process
First, to break the dependency-stub 1.1 Why use stubs?
When the object that we are testing depends on another object that you cannot control (or is not yet implemented), the object may be a Web service, system time, thread dispatch, or many other things.
So the important question is: Your test code does not control what value the dependent object returns to your code, nor does it control its behavior (for example, you want to touch an exception).
Therefore, you can use stubs in this case.
1.2 Stub introduction
(1) External dependencies
An external dependency is an object in the system that the tested code interacts with, but you cannot control the object. (Common external dependencies include: File system, thread, memory, time, etc.)
(2) Stub
A stub isa controllable alternative to a dependency (or collaborator) that exists in the system. By using stubs, you do not have to deal with this dependency directly while you are testing your code.
1.3 External dependencies in the Discovery project
Continuing with the Logan case in the previous article, let's say that our Isvalidlogfilename method reads the configuration file first, and returns True if the configuration file says it supports this extension:
public bool Isvalidlogfilename (string fileName) {// reads the configuration file //Returns True if the configuration file says that the extension is supported )
So here's the problem: once the test is dependent on the filesystem, we're doing integration testing that will bring all the problems associated with integration testing-running slowly, needing to be configured, testing multiple content at a time, and so on.
In other words, although the logic of the code itself is perfectly correct, this dependency can cause the test to fail.
1.4 Avoiding direct dependencies in the project
To get rid of direct dependencies, you can refer to the following two steps:
(1) Find the external interface or API used by the object being tested;
(2) Replace the underlying implementation of this interface with something you can control;
For our Logan project, we want to make the replacement instance inaccessible to the file system, thus breaking the file system dependency. Therefore, we can introduce an indirect layer to avoid direct dependency on the file system. The code that accesses the file system is quarantined in a Fileextensionmanager class, which is then replaced by a stub class, as shown in:
In, we introduced the stub extensionmanagerstub to break the dependency, and now we need the code not to know or care about the internal implementation of the extension manager it uses.
1.5 refactoring code to improve testability
There are two types of refactoring methods that break dependencies, which are interdependent , and they are referred to as type A and type B refactoring.
(1)a type abstract the concrete class into an interface or a delegate;
Below we practice the Decimation interface to make the underlying implementation replaceable, continuing with the Isvalidlogfilename method described above.
Step1. We separate the code that deals with the file system into a separate class so that we can replace the call to this class in the code in the future.
① using the extracted classes
View Code
② Defining the extracted classes
View Code
Step2. Then we extract an interface Iextensionmanager from a known class Fileextensionmanager.
View Code
Step3. Create a simple stub code that implements the Iextensionmanager interface as a replaceable underlying implementation.
View Code
Thus, the Isvalidlogfilename method can be reconstructed:
View Code
However, the test method here is to make a direct call to the specific class, we must find a way to let the test method invoke pseudo-object instead of the original implementation of Iextensionmanager, so we think of DI(Dependency injection), then we need B-type refactoring.
(2)B-type refactoring code, so that it can inject the pseudo-implementation of such a delegate and interface.
We have just thought of dependency injection, the main manifestation of dependency injection is constructor injection and attribute injection, so here we mainly look at how the hierarchy of constructors and the hierarchy of attributes inject a pseudo-object.
① injecting pseudo-objects through constructors
Based on the process shown, we can refactor the Loganalyzer code:
View Code
Next, add the new test code:
View Code
Note: here the pseudo-stub class and the test code are placed in a file, because this pseudo-object is used only within this test class. It is much easier to locate, read, and maintain code than by manually implementing pseudo-objects and test code in different files and placing them in a file.
② injecting pseudo-objects through property settings
Constructor injection is just one of the methods, and properties are often used to implement dependency injection.
Depending on the process shown, we can refactor the Loganalyzer class:
View Code
Next, add a test method to the attribute injection mode:
View Code
Note: You can use attribute injection if you want to indicate that a dependency of the class being tested is optional, or that the test can be trusted to use this dependency instance created by default.
1.6 Extraction and rewriting
Extraction and rewriting is a powerful technology that can replace dependencies directly and be fast and clean, allowing us to write fewer interfaces and more virtual functions.
To continue with the example above, first transform the tested class (located in Manulife.logan), add a virtual factory method that returns a real instance, and use the factory method in code normally:
View Code
Secondly In the Retrofit test project (located in Manulife.LogAn.UnitTests), create a new class that declares that the new class inherits from the class being tested, creating a common field for the type of interface (Iextensionmanager) we want to replace (no Property Get and set methods are required):
View Code
Finally, to retrofit the test code, here we create a new derived class instead of an instance of the class being tested, configure the public field for this new instance, and set it to the stub instance Fakeextensionmanager we created in the test:
View Code
Second, interactive testing-simulated objects
The unit of work may have three final results, so far we have written only the first two types of tests: return values and change system state. Now, let's learn how to test the third end result-invoking a third-party object.
2.1 The difference between a mock object and a stub
The difference between a mock object and a stub is small, but the difference between the two is subtle, but important. The fundamental difference between the two is:
Stubs do not cause tests to fail, while mock objects can.
Shows the difference between a stub and a mock object, and you can see that the test uses the mock object to verify that the test failed.
2.2 First Manual Mock object
The method of creating and using a mock object is similar to using a stub, except that the mock object does one more thing than the stub: it holds the history of the communication, which is then used for expected (expection) validation.
Assuming that our tested project Loganalyzer need to interact with an external Web service, the Web service receives an error message each time Loganalyzer encounters a file name that is too short. Unfortunately, the Web service you are testing is not fully implemented yet. Even if this is done, using this Web service can lead to too much testing time.
Therefore, we need to refactor the design, create a new interface, and then use this interface to create a mock object. This interface only includes the Web service methods that we need to invoke.
Step1. The interface is extracted and the code being tested can use this interface instead of invoking the Web Service directly. It then creates a mock object that implements the interface, which looks much like a stub, but it also stores some state information and then tests the information to assert that the mock object is called correctly.
View Code
Step2. Consuming web Service using Dependency injection (here is constructor injection) in the class being tested:
View Code
Step3. To test Loganalyzer with mock objects:
View Code
As you can see, in the test code here we assert the mock object, not the Loganalyzer class, because we are testing the interaction between the Loganalyzer and the Web service .
2.3 Using both mock objects and stubs
Suppose we have to loganalyzer not only to invoke the Web service, but also if the Web service throws an error, Loganalyzer needs to record the error in another external dependency, which is to send the error e-mail to the web Service administrator, as shown in the following code:
View Code
As you can see, here Loganalyzer has two external dependencies: Web service and e-mail service. We see that this code contains only the logic to invoke the external object, there is no return value, and there is no change in the state of the system, so how do we test that Loganalyzer correctly invokes the e-mail service when the Web service throws an exception?
We can use the stub to replace the Web service in the test code to simulate the exception, and then impersonate the mail service to check the call. The content of the test is Loganalyzer interaction with other objects.
Step1. Extract email interface, package email class
View Code
Step2. Encapsulating the EmailInfo class, overriding the Equals method
View Code
Step3. Create Fakeemailservice mock objects and transform fakewebservice into stubs
View Code
Step4. Retrofit Loganalyzer class to fit two service
View Code
Step5. Write the test code, create the expected object, and assert all the properties using the expected object
View Code
Summary: Each test should only test one thing, and the test should also have a maximum of a mock object . A test can specify only one of the three final results of a work cell, otherwise chaos.
Iii. Isolation (analog) Framework 3.1 Why use the isolation framework
For complex interaction scenarios, it may be inconvenient to manually write mock objects and stubs, so we can use the isolation framework to help us automatically generate stubs and mock objects at run time.
An isolation framework is a set of programmable APIsthat use this set of APIs to create pseudo-objects much easier, much faster, and much more concise than manual writing.
The main function of the isolation framework is to help us generate a dynamic pseudo -object, which is any stub or mock object created at runtime, and its creation does not require manual code (hard Coding).
3.2 About the Nsubstitute isolation framework
Nsubstitute is an open source framework, the source code is implemented in C #. You can get its source code here: Https://github.com/nsubstitute/NSubstitute
Nsubstitute A more focused alternative (substitute) concept. Its design goal is to provide an excellent test alternative. NET Simulation framework. It is a simulation testing framework, with the simplest syntax, which allows us to focus more on testing work, reduce our testing configuration work to meet our testing needs, and help complete the testing effort. It provides the most frequently used test features and is easy to use, with statements that are more natural-language and more readable. For beginners of unit testing or for developers who focus solely on testing, it has a simple, friendly syntax and uses fewer lambda expressions to write the perfect test program.
Nsubstitute is using the Arrange-act-assert test mode, you just have to tell it how it should work, and then assert the request you expect to receive, and you're done. Because you have more important code to write, not to consider whether you need a mock or a stub.
In. NET projects, we can still install Nsubsititute through NuGet:
3.3 Simulating objects with Nsubstitute
Nsub is a restricted framework that is best suited for creating pseudo-objects for interfaces. Let's go on to the previous example to see the following code, which is a handwritten pseudo-object, Fakelogger, which checks whether the log call is executed correctly. We do not use the isolation framework here.
View Code
Now let's see how to forge an object using Nsub, in other words, the fakelogger that we wrote manually before will not have to be manually written here:
View Code
It is important to note that:
(1) ILogger interface itself does not have this received method;
(2) The Nsub namespace provides an extension method received, which can assert a method that invokes a pseudo-object in the test;
(3) by calling received () before Logerror (), it is nsub whether this method of asking a pseudo-object is called.
3.4 Using Nsubstitute analog values
If the method returned by the interface is not empty, how do you return a value from the dynamic pseudo-object that implements the interface? We can return a value using the Nsub coercion method:
View Code
If we do not want to care about the parameters of the method, that is, regardless of the parameters, the method should always return a value, so that the test is easier to maintain, so we can use the Nsub parameter matching device:
View Code
Arg.any<type>, known as parametric matching, is widely used in the isolation framework to control parameter handling.
If we need to simulate an exception, we can also use nsub to solve:
View Code
Here, a assert.throws validation is used to verify that the method that was tested did throw an exception. When and do two methods as the name implies when what happened, what happens after the incident to trigger something else. It is important to note that the When method must use a lambda expression.
3.5 using both mock objects and stubs
Here we combine two types of pseudo-objects in one scenario: one as a stub and the other as a mock object.
To continue the previous example, Loganalyzer to use a MailServer class and a WebService class, this time the requirements change: If the Log object throws an exception, Loganalyzer needs to notify the Web service, as shown in:
We need to make sure that if the log object throws an exception, Loganalyzer will notify WebService of the problem. The following is the code for the class being tested:
View Code
Now we are testing with Nsubstitute:
View Code
Here we don't need to implement artifacts by hand, but the readability of the code is poor because there is a bunch of lambda expressions, but it also helps us avoid using the method name strings in our tests.
Iv. Summary
In this article we have learned the core techniques of unit testing: stubs, mock objects, and isolation frameworks. Using stubs can help us break the dependency, and the difference between a mock object and a stub is primarily that the stub does not cause the test to fail, while the mock object can. The simplest way to tell if you are using a stub is that the stub never causes the test to fail, and the test always asserts the class being tested. With the isolation framework, the test code is easier to read and easier to maintain, with the emphasis on helping us save a lot of time writing mock objects and stubs.
Resources
(1) Roy osherove, Jin Yu, "The Art of Unit Testing (2nd edition)"
(2) Ten years of ingenuity, "Nsubsititue Complete Handbook"
(3) Zhang Shanyu, "Unit Test simulation framework: Nsubstitute"
Zhou Xurong
Source: http://edisonchou.cnblogs.com
The Art of net unit testing