The 1th of the series: How to create "stitches". Seam (seam) is a concept that needs to be known.
This article is the 2nd article, which is about how to avoid writing hard-to-test code when building objects. Most of the conceptual content of this article comes from this blog post of Misko hevery.
Build
Or use the car example above.
Usually, we are going to build the car first, assemble the car, and then we'll drive it.
Software development is similar, we should construct the object after the completion of the use of it. But sometimes, developers add some program logic during the construction process. This is the equivalent of the car is not finished, we drove it to go for a ride. It is not very good to do so.
A constructor is a method that the class uses to create its instance object, where the code is used to prepare the object. But sometimes developers do some other work in the constructor, such as building dependencies, executing initialization logic, and so on.
In constructors (or, more so, in the process of building), doing this extra work makes testing extremely difficult. This is because, like initializing dependencies, invoking services, setting state logic, and so on, these work will throw the "stitches" used for testing. Causes the mock to be unavailable.
In short, doing too much work in the construction process can hinder testing .
Danger Signal
- The new keyword appears in the constructor/field declaration
- If you need to create a dependency in the constructor, this creates a tight coupling between the class and the dependency. This has been mentioned before, so need to inject dependency. But simple value types, such as strings, lists, dictionary, and so on, are still possible.
- Calling a static method in a constructor/field declaration
- Static methods cannot be mocks, nor can they be injected.
- The constructor appears in the process Control logic code
- This makes it difficult to test the logic directly. We can only construct the object in different ways, testing and confirming the state of the object. This state is usually hidden from direct testing. In fact, if it were not for the assignment code, it might be the problem code.
- Non-assignment code appears in constructor
- There is another initialization function (i.e. the constructor is gone, but the object is not fully initialized)
How to solve the problem?
- do not create dependencies in constructors, they should be injected . They are then assigned to the private variables of the class in the constructor.
- When you need to build an object graph (a set of objects that have referential relationships), and also include objects that require some building parameters, you should use factory, builder mode, or the dependency injection of the IOC container to separate the construction of these objects.
- Avoid writing logic code in constructors , such as conditions, loops, calculations, and so on. You can't put the logic code in another way, and then call the method ...
It's all about avoiding the mix of object building and object behavior, because they're hard to test together.
Finally, the first thing you need to know, according to Angular founder Misko Hevery, is that:
The structure of the object is divided into two types, one of which can be injected, and the other is new.
An injected object can be made up of a bunch of other, injected objects. They can work for objects that can be new. An injected object is usually a service that implements an interface, like what Iunitofwork, IRepository, Ixxxservice, and so on.
The object that can be new is the end point in the object graph, such as the entity or value objects (value object).
For ease of testing, for these two types of constructs, there are the following rules:
An injected object can be injected in a constructor request (inject) Other can inject an object, but cannot request a new object in the constructor.
Conversely, an object that can be new can request additional new objects in the constructor, but cannot request an injected object in the constructor function .
Example of the first example
This is not right, and the new words in the build process will result in tight coupling and cannot be used to replace them with Test double. Some services can be expensive if they are not replaced in the test.
The correct notation is to use dependency injection:
A second example
In this example, Usercontroller only requires two dependencies for UserService and Loggingservice. But UserService also relies on userrepository.
But it is wrong to write this, which causes tight coupling between Usercontroller and userrepository, and the configuration UserService is not usercontroller responsibility.
The correct wording is:
And UserService is also best to inject dependency.
And if UserService is not injected userrepository in the constructor function:
So that's what the controller should write:
However, it is best to use the constructor injection notation.
A third example
To be careful, this example has more than one error.
First it has the conditional Judgment logic code; It also uses the static variable of applicationstate.isrunning (which is the global state); And in the constructor also does the UserService configuration work, this is not the usercontroller responsibility.
Try to avoid global variables, which cannot be isolated, and testing can be cumbersome, such as when one of the tests changes the value of a static variable in a parallel test that could cause another test to fail.
But roughly speaking, this example can be said to be a mistake, how to configure UserService is not the responsibility of usercontroller, so the correct way is to remove the UserService configuration related code, let it manage it yourself:
A fourth example
In this example, the Loggingservice log method requires an area type object, which is a value object.
So the mistake is that you should not inject new objects into the objects that can be injected. If you do this, the tests will not be isolated.
The correct approach should be to pass in as a parameter to the method:
A fifth example
If a class similar to initalize () or similar means is present, it is likely that the object has too much responsibility.
It is easy to modify it so that the respective classes are responsible for their own content. Remove the Initialize () method.
For example, this is not exhaustive, please see the angular author's blog post for details.
How to create an object at Test/runtime
The Usercontroller in the example above is the object we need to use, and at runtime, the code might be:
It is still a bit cumbersome to build this object, and its class diagram is as follows:
So the test setup process can be cumbersome:
Of course, you can use a mock instead of a direct new. It was a lot of trouble anyway.
Using the Factory
So we can use factory and other modes to put the work of building Usercontroller into the factory:
This can be called:
Using the IOC container
If the project uses an IOC container, you can also use the following usage:
First introduced here.
. NET Core TDD prequel: Writing code that is easy to test--building objects