In the previousArticleUnit test training series: (1) unit test concepts and necessity as mentioned in the last section, unit test is fully prepared for testing first and test-driven, I also briefly explained the implementation process. Many of my friends are very interested in this and hope to learn more about how to implement it.
IsolationIs the most important concept in unit testing. A unit test method must be isolated from all dependencies. Dependencies include environment dependencies (I/O, network, database, system time, etc.) and external class and method dependencies. Therefore, isolation ensures that unit testing is a test with the minimum granularity.
However, isolation also causes Unit TestingLimitations, Mainly in the following two aspects:
1.The impact of modifying a method on the system cannot be detected through unit testing.
Unit testing isolates dependencies on other methods. Therefore, when a method is changed due to refactoring or bug modification, running an existing unit test can only detect whether the modified method is still in line with the previous expectations. However, modification of this method has any impact on the entire system, it is completely impossible to know by running the unit test !!
Many of my friends have always mistakenly confused integration testing and unit testing, and thought that unit testing can detect the impact of a method change on the entire system. This idea is obviously wrong: because each unit test method isolates the tested method from other methods and the external environment, each tested method does not depend on the specific implementation of other methods. Therefore, even if the implementation of other classes or methods changes, as long as the interface remains unchanged, it will not affect the current unit test !!
2.Unit Tests have little effect on demand changes.
The first thing that needs to be changed is the unit test! The unit test focuses on whether the inbound and outbound items (input and output values) of each method meet the expected values. When a demand change occurs, it means that the expected value of the relevant method has changed. Previous unit tests are no longer valuable and need to be rewritten.
For the above two reasonsCodeThe value of additional unit tests has also become very weak. After an existing system append a unit test, the unit test only plays a role in refactoring the internal implementation of a method (such as modifying bugs andAlgorithmOptimization without modifying the current call relationship and related interfaces)
Use unit test based on test first
Having said so many limitations, it is estimated that it will hit everyone's enthusiasm. Is unit testing so useless? This is not the case, but the unit test scenario is not correct.
Both test-first and test-driven are specific practices of the goal-oriented methodology. The purpose is to determine the purpose of writing the code and the verification method before writing the code. Let's look at unit testing. unit testing only focuses on the functions of a single method and whether the inbound and outbound items of this method meet the expectations. This is basically consistent with the goal of testing first. It can be said that unit testing is the specific implementation method of testing first.
With this tone, let's take a look at how to implement the unit test according to the test-first guidance, because the focus of this article is still the unit test, I will not discuss how to implement test first or test drive.
We start by getting a specific business function module.
I. Design a brief class diagram and the relationship between classes
First, we will briefly design the relationship between the basic class diagrams and classes for this module (although some agile methods do not require testing first, they only do test cases, then write the test code and implementation code, but here I personally follow the class structure method of the first function design)
At this stage, you only need to define several classes, and there is no need to design all the members in the class at the beginning. (We recommend that you use the project option view class digoal in Visual Studio to synchronize code and class design)
Let's take a look at the following example: In an Order System, the products in each order in the order details are divided into clothing and digital products, while the clothing price source is VANCL, and the digital product price source is newegg. first, this order system requires an order pricing function.
As shown in, the main classes and their relationships are defined. In addition to some data attributes, there are no methods for defining these classes.
The productorder class, that is, the Order class, must implement the method count, which contains an attribute orderdetails of the ilist <baseorderdetail> type.
Baseorderdetail class, abstract class, including productid and amount attributes and an ipriceprovide type attribute priceprovider, and a count method, that is, each detail totals its total price.
Clothingorderdetail class, a subclass of baseorderdetail, that is, order details of the clothing class. The priceprovider attribute of this class should be an instance of the vanclprovider class.
Digialorderdetail class, a subclass of baseorderdetail, that is, order details of the digital class. The priceprovider attribute of this class should be an instance of the neweggprovider class.
Ipriceprovide interface, which defines the querypricebyproductid method for obtaining product prices from a third party.
The vanclprovider class implements the ipriceprovide interface and provides the product price query of VANCL.
The neweggprovider class implements the ipriceprovide interface and provides the newegg product price query.
2. Write unit tests for a public method of a class
After the relationships between classes and classes are designed, we begin to design the class members and methods based on the needs of business functions to implement the functions of the class. When designing a class method, it is designed according to the requirements of the business. Therefore, when writing a class's public method, what effect should be achieved for this class, it should be very clear. After the goal is defined, you can write unit tests, even though the implementation code does not exist. Based on the example above, we first compile the unit test of the count method of the productorder class. At this time, the count method is not actually implemented, even the count method does not exist (the nonexistent count method is called using the dynamitic keyword in the unit test code ).
The count method of the productorder class is used to calculate the prices of all goods contained in all documents. Therefore, we can analyze and find that the count method depends on the count method of the baseorderdetail class, the orderdetails attribute contains multiple baseorderdetail classes, which means that when writing unit tests, we need to replace these baseorderdetail classes with mock objects.
Unit Test for count /// <Summary> /// A test for the method count /// </Summary> [Testmethod ()]Public Void Testcount (){ // Arrange Dynamic Target = New Productorder (); Decimal Expected = 8748.55 m; // Mock a jacket order detail and set return value of the method count (), No matter real price and amount. Mock <baseorderdetail> mockjacketorderdetail = New Mock <baseorderdetail> (); mockjacketorderdetail. Setup (E => E. Count (). Returns (350.55 m );// Mock A iPad order detail and set return value of the method count (), No matter real price and amount. Mock <baseorderdetail> mockipadorderdetail = New Mock <baseorderdetail> (); mockipadorderdetail. Setup (E => E. Count (). Returns (3499.00 m ); // Mock A iPad order detail and set return value of the method count (), No matter real price and amount. Mock <baseorderdetail> mocknotebookdetail = New Mock <baseorderdetail> (); mocknotebookdetail. Setup (E => E. Count (). Returns (4899.00 m); target. orderdetails = New List <baseorderdetail> (); target. orderdetails. Add (mockjacketorderdetail. Object); target. orderdetails. Add (mockipadorderdetail. Object); target. orderdetails. Add (mocknotebookdetail. Object ); // Acction Decimal Actual = target. Count (); // Assert Assert. areequal (actual, expected );}
In this unit test code, it is estimated that the implementation of the count method depends on the orderdetails attribute, and the total price of all the baseorderdetail objects in it, while the baseorderdetail price is through baseorderdetail. obtained by the count () method. We only test the count () method of productorder, so we do not need to think deeply about how baseorderdetail. Count is implemented. We only need to mock the baseorderdetail. Count () method.
Therefore, in this Code, three mock baseorderdetail objects are added, their count () method return values are set, and all of them are added to the ordedetails attribute of productorder. At this point, this unit test is finished, but our count method has not been implemented yet. Therefore, an exception is thrown when you run the unit test.
Ii. True implementation logic of writing methods
Then, let's implement the actual logic of this method.
Unit Test for count/// <Summary>/// Count the total of the all orders./// </Summary>/// <Returns> </returns>Public DecimalCount (){DecimalResult; Result =This. Orderdetails. sum (E => E. Count ());ReturnResult ;}
Now, run the previous unit test and you will find that the unit test has passed successfully. Do you understand: For a unit test, you only need to know the expected values of the method to be tested and the items that the method may depend on, whether or not these methods have been actually implemented. Even the dependencies can be written at the beginning, but you can modify the unit test code to isolate them when you find that you need to call other methods during code implementation.
Here we will write another example to help you better understand it. This time, write a unit test for the count method on baseorderdetail.
unit test for count // // a test for count // [testmethod ()] Public void testcount () { // arrange baseorderdetail target = New clothingorderdetail (); decimal expected = 1000.00 m; // action decimal actual = target. count (); // assert assert. areequal (expected, actual) ;}
This code is the simplest unit test framework. If executed, an exception is thrown because the count method is not implemented yet. Next we will compile the implementation of the count method,
Unit Test for count /// <Summary> /// A abstract class of baseorderdetail. /// </Summary> Public Abstract Class Baseorderdetail { /// <Summary> /// The identity of the current product. /// </Summary> Public Guid productid {Get ; Set ;} /// <Summary> /// The amount of the current detail. /// </Summary> Public Int Amount { Get ; Set ;} /// <Summary> /// A third-part price provider. /// </Summary> Public Ipriceprovide priceprovider {Get ; Set ;} /// <Summary> /// Count the totoal of the current oder detail /// </Summary> /// <Returns> </returns> Public Virtual Decimal Count (){ Decimal Result; Decimal Price = This . Priceprovider. querypricebyproductid (This . Productid); Result = price * This . Amount; Return Result ;}
We can see that the count method in baseorderdetail depends on its own property priceprovider, and this property should be based on the order type to specify the corresponding vanclprovder class instance. If we run the unit test again, it will inevitably cause an empty reference exception because the priceprovider attribute is not assigned a value at this time. (You can use IOC containers such as autofac or unity to implement initialization in actual running code)
Therefore, we need to add a mock object to the unit test to isolate the dependency on the external object. The modified unit test code is as follows:
Unit Test for count /// <Summary> /// A test for count /// </Summary> [Testmethod ()]Public Void Testcount (){ // Arrange Baseorderdetail target = New Clothingorderdetail (); Decimal Expected = 5555 m; target. Amount = 100; mock <ipriceprovide> mockpriceprovider = New Mock <ipriceprovide> (); mockpriceprovider. setup (E => E. querypricebyproductid (it. isany <guid> ())). returns (55.55 m); target. priceprovider = mockpriceprovider. object; // Action Decimal Actual = target. Count ();// Assert Assert. areequal (expected, actual );}
So far, both unit tests have successfully passed the verification.
3. modify unit test and implementation code when requirements change
We assume that our development has entered a certain stage (two classes and methods have been implemented and are stable). At this time, the demand has changed, let's take a look at how to perform this operation.
In the first case, the requirement is changed to: if the order details are of the clothingorderdetail type, the count method needs to add an additional 10 yuan postage after multiplying the price by the quantity. In this case, you only need to overload the count method of baseorderdetail in the clothingorderdetail class. Therefore, write a new unit test and method implementation.
From this point, we can note the characteristics of unit test: Although we have modified the requirements and implementation of baseorderdetail, the unit test of the count method in productorder cannot detect this change, even after the baseorderdetail count method is modified, an exception is thrown, and the exception caused by the count method in baseorderdetail cannot be learned through the unit test. Only the count method in productorder will also have an exception. This is a limitation of unit testing.
In the second case, the demand is changed to that if the total order quota exceeds 5000, you can enjoy a 100 discount. At this point, you can first modify the expected value of the productorder. Count Method in the unit test to 8648.55, and then modify the actual code implementation as follows:
unit test for count // // count the total of the all orders. // //
Public decimal count () { decimal result; Result = This . orderdetails. sum (E => E. count (); // rreat offer If (result> = 5000) {result = Result-100;} return result ;}
We can see that this is actually adding a conditional judgment branch to the implementation code. We may need to write a unit test (Omitted) separately for the condition branch ).
The third case is that the demand changes significantly, causing serious changes to the code structure. In this case, all previous unit tests and implementation code have been voided, and even compilation fails.
4. Optimize the implementation in a method
In fact, unit testing can play a role after development. It is only used in this scenario to optimize the implementation of a method. For example, the querypricebyproductid method of the vanclprovider class was previously queried from the database, instead, it reads data from the local cache. If the cache does not exist, it reads data from the database. Because the expected value is changed, it only modifies the implementation method of this method, therefore, after modification, it is still possible to pass the unit test to check whether the modification is correct, that is, whether the new modification still meets the early requirements (If the expected value is not changed, the unit test will not change.).
Misunderstanding:
Many of my friends have been mixing unit test and integration test, so there is always a misunderstanding:
I think that as long as unit tests are written for all methods, after the code changes in the future, you can run the unit test to quickly obtain all the use cases affected by current code changes and their impact scope through computer verification. In fact, if you see it carefully, you should already know that this is actually impossible.
Isolation of unit tests results in code impact not being passed: implementation of a method causes exceptions, it does not affect the successful passing of other methods that have called it in the original unit test (the mock method replaces the actual method execution ).
Summary:
Through the above examples, we describe how to combine the idea of testing first to perform unit testing. The core point is:Goal firstBefore implementing a method, consider what the goal is to be achieved by this method, write the detection means (unit test) for this goal, and finally implement this method, and use the previously conceived and compiled testing methodsVerify that this implementation has achieved the expected goal.
On the other hand, this process also strongly reflects that unit testing, as a means of testing, actuallyAfter code implementation, maintenance can play a very small role.:
When the demand changes, the expected value of the demand changes, meaning that the unit test needs to be modified or rewritten. At this time, the original unit test has no effect on subsequent checks.
Only when the requirement is not changed and the existing code is optimized without modifying the structure, can the modified Code still meet the previous requirements. However, in practical applications, there are few such scenarios.