Concepts of maintainable unit tests
The following article will explain some concepts about unit tests and some good practices about write maintainable unit tests. Although, the example are written in C#, the article concepts and topic are language independent.
I have previously write about unit testing in following articles:
Unit testing ASP.NET Core Identity
Inversion of control and Dependency injection in Typescript
General concepts
Let’s take the following custom method:
Let’s suppose we want test the SumArray
method, we can define a set of test cases:
We can test some cases, for example: in case of single value, zero value and multiple value. Are the green tests proof the code is correct? Tests are not proving the correctness of code. Tests are demonstrating that code runs as expected for a certain input.
A.A.A pattern
The A.A.A pattern stands for : Assume, Act Assert pattern. Fox example, let’s take the following unit test:
We can identify three different parts:
- Arrange everything we need to run the experiment. In this case, we simply instantiate a CustomArray object;
- Act: we invoke the add method and capture the result. The “act” represents the star of the unit testing show. All of the arranging leads up to it, and everything afterward amounts to retrospection;
- Assert: the invocation of the Assert class probably gave that one away. But the assert concept in the unit test represents a general category of action that you cannot omit and have a unit test. It asserts the hypothesis itself. Asserting something represents the essence of testing;
Tests naming convention
Another important aspect of testing is the naming convention of tests. First of all, tests are usually contained inside another project with specular structure similar to the main project/folder. Secondly, test files and classes have the same name of the subject under test (sut).
Name of the test method will appear during test execution, and then we wish to know what exact testing scenario has passed or failed by only reading the test method name, and possibly the name of its enclosing class. The name of the method and the test will come first. Then, we want to know the conditions under which the test is conducted. This part of the name will closely relate to the arrange part of the unit test. Finally, the third part will indicate the desired behavior. This is fine when testing behavior. We could also use this third segment to indicate expected state or return value.
Let’s look an example:
The firm of the test method is composed by:
- The name of the tested method:
SumArray
, which we can call subject ; - The condition of the subject;
- The expected assertion;
This is NOT the only(or the best) naming convention. Keep in mind, the important thing is that the firm of the test method identifies correctly the intention of the test.
Choose what to test
Tests are liability. They are limiting our freedom to change production code later and some code changes will cause refactoring on our test suite. So it is very important choose what and how to test our code. There are two way to verify our code and prove the correct behavior: state testing and interaction testing.
State test
State testing checks the state of the subject class. It usually follows this workflow:
- Perform the operation;
- Read the state after the operation;
- Compare to the expected value;
In order to perform the operation and then check values of interest, the class has to lend access to its state. What about encapsulation principle? Encapsulation does not mean the state is inaccessible. It prevents indiscriminate exposure of state.
Interaction test
Interaction testing means that we are observing calls that are placed on certain objects. When we were implementing state tests, we had to make sure that state is measurable. The same will have to be with interactions. First of all, how do we measure interactions?
The classes that are covered by integration tests usually implement interfaces. There are a lot of Mocking framework for every language and platform that allows you to count how many times a certain method is called.
Secondly, interaction tests should be used to test interactions only, not implementation. That means that certain interaction itself was the requirement, and then interaction tests help prove the requirement was satisfied. If you use interaction tests to prove that one class has used its dependency in a certain way, then be warned that some implementation changes will cause the test to fail, even though the new implementation is entirely correct.
Modelling dependencies
It is important how we design dependencies communication between our classes, in order to write stable an maintainable unit tests. We can group dependencies into two types:
- “Classic” dependency: it is a simple container of value, we might share an state a receive some value back;
- Service: it performs an operation and gives feedback about the performed operation;
In case of unit testing, we can replace our class dependencies with a map, which serves canned answers to the class on the test, this is called Stub. Another way to replace our class dependencies is by using a Mock, which simulates the real behavior of our dependency.
Stub and mocking
First of all, it is useful to use mocks to measure interaction, and stub to substitute a dependency. Stubs have much less responsibility on behalf of the unit test and they make tests easy to maintain. Mocks make tests more rigid and with a lot of responsibilities.
There’s a lot of confusion about what each term means, and many people seem to use them interchangeably. The basic difference is that stubs can’t fail tests, and mocks can. The following diagram describes a test using stub. When using a stub, the assert is performed on the class under test. The stub aids in making sure the test runs smoothly:
On the other hand, the test will use a mock object to verify whether the test failed or not. The following diagram shows the interaction between a test and a mock object. Notice that the assert is performed on the mock:
The class under test communicates with the mock object, and all communication is recorded in the mock. The test uses the mock object to verify that the test passes.
In general, state test usually rely on stubs either interaction test usually rely on mocks.
Refactoring our application to make testable
In order to break the dependency between our code under test we can use common design patterns, refactoring, and techniques, and introduce one or more seams into the code. We just need to make sure that the resulting code does exactly the same thing. Here are some techniques for breaking dependencies.
Extract an interface to allow replacing underlying implementation
In general, every class which perform an external job or has an external dependency needs to be covered by an interface. The interface describe the methods of the external component, the class provides an implementation of that component.
Inject stub implementation into a class under test
There are several proven ways to create interface-based seams in our code—places where we can inject an implementation of an interface into a class to be used in its methods. Here are some of the most nota- ble ways:ù
- Receive an interface at the constructor level;
- Receive an interface as a property get or set;
In the first case, we add a new constructor (or a new parameter to an existing constructor) that will accept an object of the interface type we extracted earlier. The constructor then sets a local field of the interface type in the class for later use by our method or any other.
The second one, we add a property get and set for each dependency we’d like to inject. We then use this dependency when we need it in our code under test.
In this scenario, we go back to the basics, where a class initializes the manager in its constructor, but it gets the instance from a factory class. The Factory pattern is a design that allows another class to be responsible for creating objects.
Others unit testing best practices
Finally, other important points about unit testing are: one assertion per test method, avoid test interdependance, integrate tests with build automation;
One assertion per test method
Common wisdom says that one unit test should have one assertion. Let’s take an sequence of assertion:
The line 14
assert that the result should not be null. The line 15
asserts that the the first cell of the array should be equal to 3. What if the first assertion fails? The second assertion might pass or fail as well, consequently it results inconclusive.
In conclusion, Avoid multiple assertion if they are testing unrelated claims.
Avoid test interdependance
Avoid tests interdependence at all costs. The test runner will execute your stuff in whatever order it pleases and, depending on the specific runner you use (advanced topic), it might even execute them in parallel.
Integrate tests with build automation
The power of the automated build cannot and should not be ignored. Running tests lets you know whether you have broken any existing or new functionality. Integrating your code with the other projects will indicate whether or not you broke the compilation of the code or things that are logically dependent on your code.
Cheers:)
Sources:
Writing Highly Maintainable Unit Tests – Zoran Horvat
The Art of Unit Testing: with examples in C# – Roy Osherove
Cover credits: http://deweysaunders.com/