A simple case of layer-3 architecture decoupling and unit test business domains --- constructor assignment traditional nunit test examples bad taste? --- Refactoring proposal mspec introduction --- AAA syntax rhino mock --- I play my automock --- the highest level of laziness
Handy weapon Library:
Nunit
Mspec
Rhino mock
Automocking
For the tools involved in this article, see the previous article: My. Net weapons library ------ list of required tools for the new. Net Architecture
Layer-3 architecture decoupling and unit testing
Dependency injection di greatly helps test modularization. The dependency between layers is almost truth.
For example, after the dependency on Data Reading and Writing is replaced by irepository, all the classes that use irepository, such as the examservice at the Serivce layer, only need to pass in a mock irepository class during testing, you do not need to use a real database to test it.
Another layer of controller also uses the service layer. I also propose an iexamservice interface for the implementation of the service layer, and pass in the mock class of iexamservice in the Controller constructor. Therefore, it is easy to focus the test on the behavior and functions of the controller. We can even test and implement the Controller class before implementing the examservice class. This is the advantage of dependency injection.
This complete set of layers, decoupling and testing we have achieved, and formed a standardized process and forming a framework. Now it's easy to implement step by step, and you can even automatically generate this part of code later. However, this part is not the focus of this article.
Simple case of Business Domain --- constructor assignment
When our attention is shifted to the business domain, the situation changes quietly. In the business domain, there are more and more complex dependencies between classes. In contrast, the three layers are simple.
Here, I will give a brief background introduction to the exam (exam) Class I am taking. We should be familiar with the tests. Let's take a good look at what we are familiar. In addition, I take the social examination for analysis purposes.
An exam has three important elements:Exam code(Exam definition );Test Area(Beijing and Hunan );Exam date. These three elements uniquely identify an exam. That is to say, in the same exam area, the same exam is defined on the same date and I think it is the same exam. Simple logic. To reflect this logic, I put these three elements in the constructor of the examination class. Why? The absence of any element does not mean the existence of the examination object. Therefore, it must be passed in at the beginning of construction. From another perspective, the examination area + examination definition + date is the examination Business ID, which is a unique identifier and must be consistent throughout the business object.
Check the Code:
public class Exam { public Exam(District district, ExamDef exam_def, Date date) { District = district; ExamDef = exam_def; Date = date; } }
The constructor transmits three objects from the outside and assigns them to the three non-attributes of the test. These three attributes are read-only, private is used for Nhibernate and constructors. Why? As mentioned above, they are business-driven and there is no meaning to modify them after they are created.
Check the Code:
public class Exam { public Exam(District district, ExamDef exam_def, Date date) { District = district; ExamDef = exam_def; Date = date; } public virtual ExamDef ExamDef { get; private set; } public virtual District District { get; private set; } public virtual Date Date { get;private set; } }
Traditional nunit test example
Now, the background is sufficient. Let's test this function. Hey, wait, we ...... Is there a function now? Yes! The test description is,
When constructing an exam class from the constructor chain, the corresponding values should be assigned to the three attributes.
Yes, it is simple enough to make us clear at a glance and complex enough. We need to test it to ensure its functionality. 1. Make sure that it is run-overwrite test; 2. Make sure that it is based on my design-behavior test.
Check the Code:
[TestFixture] public class when_create_an_exam { [Test] public void it_should_assign_parameters_to_properties() { //Arrange var stub_exam_def = new ExamDef("98"); var stub_district = new District("01"); var stub_date = new Date(2011, 1, 1); //Action var subject = new Exam(stub_district, stub_exam_def, stub_date); //Assert Assert.AreEqual(stub_district,subject.District); Assert.AreEqual(stub_exam_def,subject.ExamDef); Assert.AreEqual(stub_date,subject.Date); } }
I am not here to introduce three intermediate variables and the definition of the other three classes. My naming method was also ill-founded, and I am not here to justify it. Let's just look at the actual content: create three types of instances for testing. As for the specific content of these three classes, I don't really care about them. So stub is used to indicate that I don't care. One of the core concepts of DDD is its name. Finally, my assertion only checks whether the attribute value matches the input value of the constructor. OK, complete!
Bad taste? --- Proposal of Reconstruction
For a period of time. Let's look back at this test, which may be slightly uncomfortable. In particular, we also have more classes with similar constructor assignment functions, and more complex functions waiting for us to test. We are doing commercial software, aren't we? The more similar tests, the more. These small inconveniences increase.
What are the problems with this test?
1. The test has three parts: establish the test environment, call the tested function (the tested ontology), and assert. The above code, I have even deliberately separated the three parts with annotations, but it is still not a syntax-level separation.
2. the dependency on third-party classes is serious. This is the focus of this article-unit test modularization. For the exam class, examdef and district are all third parties.
3. There are too many test codes. There are actually only three lines to be tested. Although this is not a matter of principle, it is worth solving in the spirit of better, faster, and stronger.
Well, you have raised too many questions and I cannot solve them all at once. More than three? Yes, our slogan is "as long as it is good ".
Introduction of mspec --- AAA syntax
Let us solve these problems one by one based on the principles of election and reconstruction. Yes, the test also needs to be restructured. Is there a bug in the test code? It's not surprising. You never met? Oh, because you don't write test code at all.
With regard to the three-stage test, I have seen that some people have indeed reconstructed the nunit framework step by step to form a good testing framework. I don't have to worry about it here. go directly to the mspec tool! There is a three-stage test, called the AAA syntax, Which is arrange, action, and assert. 3A-level syntax, cool!
Mspec uses its own terms, namely establish, because, and it. Let's take a look at the modified test code to see what it means.
Check the Code:
public class When_create_an_exam_by { private Establish context = () => { stub_exam_def = new ExamDef("98"); stub_district = new District("01"); stub_date = new Date(2011, 1, 1); }; private Because of = () => subject = new Exam(stub_district, stub_exam_def, stub_date); private It should_assign_to_properties = () => { subject.District.ShouldEqual(stub_district); subject.ExamDef.ShouldEqual(stub_exam_def); subject.Date.ShouldEqual(stub_date); }; private static ExamDef stub_exam_def; private static District stub_district; private static Date stub_date; private static Exam subject; }
Let's take a look at the test results to understand the meaning of the code, that is, the document.
See:
Upgrading from nunit to mspec gives you a fresh feeling. You may not get used to it at first. However, once you get used to it, you don't want to go back.
Rhino mock --- show me
Okay. Let's take a look at the second question. At the beginning, we almost thought this was a big problem. Isn't it just to create a dependency beauty directly? The creation is complete, just a line of code. Still, please note that we are doing commercial software. Once expanded, a class cannot be only one or two classes, especially indirectly associated, and more will be pulled out of the radish and brought out the mud. Take this examination class as an example. In our actual project, it also has the attributes of the examination subject list, and has indirect contact with the examinee through the examination class. The application type is related to the order type and transaction type. Considering all these cascading relationships, do I create all the classes to test the constructor assignment function?
Further thinking, we will provide a natural solution to abstract the test area class and test definition class into two interfaces. The constructor will input the interface definition instead of the class itself. This is an imitation of dependency injection between layers. But believe me, this is another nightmare. Business domains and layers are completely different environments. I don't want to discuss it too deeply. I may not be able to write an independent article.
Fortunately, we have another tool, rhino mock, to help us solve class simulation problems. The modified test code is as follows. The only impact is that you need to add a non-parameter constructor that is at least protected to the simulated class. This is not a big problem. If you use nhib.pdf in the project at the same time, there will be similar requirements.
Check the Code:
Public class when_create_an_exam {private establish context = () =>{ stub_exam_def = mockrepository. generatemock <examdef> (); stub_district = mockrepository. generatemock <district> (); stub_date = mockrepository. generatemock <date> ();};//... unmodified Code omitted here}
As you can see, this refactoring has omitted the exam code, exam area code, and other information that you don't care about at all.
Automocking --- the highest level of laziness
This is not enough. The last problem is to fill our stomachs with a pancake.
We will introduce automocking and automatic simulation. When your test class inherits from the automock specification class, it automatically creates a subject for you to be tested, and automatically creates a simulated object based on the parameter definitions of the tested object builder. The method of referencing these simulated objects,
Very simple dependency <examdef> is the dependency of dependency injection. There is no need for too many explanations-such as the actual name.
Look at the code again:
public class When_create_an_exam:Specification<Exam> { private It should_assign_to_properties = () => { subject.District.ShouldEqual(DependencyOf<District>()); subject.ExamDef.ShouldEqual(DependencyOf<ExamDef>()); subject.Date.ShouldEqual(DependencyOf<Date>()); }; }
Three lines of implementation code correspond to three lines of test code. Concise cannot be more concise.