This post is part of Roadmap for unit tests, if you haven’t seen it yet, please read it to have clear picture of unit tests topics that I find useful.
We continue unit tests fundamental topics, this time with practical aspects such as frameworks, naming conventions, and unit test elements that are good to know to write a valuable unit test.
Unit test fundamental topics part 2
Frameworks are the basic blocks from which we build unit tests, we find in them syntax for writing unit tests, it’s life cycle and assertions. Currently, the most popular framework is xUnit, due to the low amount of additional code and ease of learning the basics, alternatives are nUnit and MSTest from Microsoft, which can be found mainly in older projects.
Patterns are proven ways to write unit tests that eliminate the most common errors and make them easier to understand. Patterns are grouped into naming conventions and structures that helps to navigate through unit tests.
Structures are ways to prepare the contents of a unit test by naming or grouping it properly in such a way that the test is legible and easy to maintain. I have defined the following structures which I consider to be valuable:
- Sut/TestCandidate is a pattern that recommends naming the unit that is being tested as Sut (abbreviation from software under tests) or TestCandidate. In this approach, we can quickly find out what is the subject of the test after the first look at the unit test, and also find situations in which we test something completely different than it was assumed.
- Arrange/Act/Assert is an approach in which we divide the unit test into 3 phases. The first phase is Arrange, in which we prepare data for the test, e.g. we create required objects that will be passed to the action that we will test. In the Act phase, we perform the actions that we want to test, preferably one or several simple methods. In the last phase called Assert, we check whether the result of the action is what we expect, in this phase we can also release resources if there is such a need.
- Test harness is used to gather those parts of the unit test that are duplicated in many tests, e.g. preparing objects and storing methods that allow to increase the readability and shorten the code of the unit test itself, so that its readability and maintainability is at a high level.
Naming conventions define the way of naming the test so that you can quickly see what the unit test is doing just by reading the name of the test. The name patterns that I encountered most often are:
Test doubles allow you to mimic dependencies to limit the components involved into test and to detach dependencies that would require additional resources such as database. Depending on their behavior and advancement, we distinguish the following types:
- Dummies are the simplest objects, they serve to fill the interface so that the system under tests can be created, they often don’t participate in the test itself,
- Fakes are implementations that is supposed to imitate dependency behavior, but are significantly simplified compared to the production version. An example is a database in memory,
- Stubs have hardcoded responses for method calls, they allows to limit the implementation of dependencies to only the actions that are used in unit test,
- Spies allows not only to imitating dependency actions, but also to count calls and verify that the action was performed,
- Mocks are pre-programmed test doubles with specific results, allows to create specific connections between actions, can raise exceptions and are the most advanced objects.
Test doubles frameworks help you create test doubles more easily and reduce the time needed to prepare a test. You can use the Moq or NSubtitute libraries to create a test double, while AutoFixture library can be used to generate test data and create simple objects.
Test double has several specific dependencies that may require additional efforts to imitate them. These are http, database, file and process. In each of them you can try to provide an interface that could be implemented as a double test, but sometimes we want to have an implementation closer to production code. To achieve this we can use one of the following approaches
- Http communication can be performed with HttpClient, we can mock it using RichardSzalay.MockHttp and set the expected responses to avoid creating an interface for HttpClient in simple solutions where it is not necessary,
- Database can be mocked via Microsoft.EntityFrameworkCore.InMemory to run directly in the process memory. Thanks to this, it will be possible to verify that implementation using the Entity Framework can be tested,
- File are difficult dependency to mock. They can be mocked in two ways, either by creating the appropriate files as test dependencies that will be used only for tests or by custom implementation of the IFileSystem interface from System.IO.Abstractions,
- Processes have to be analyzed before deciding which approach to mocking them will be appropriate. It’s possible to prepare a process that will imitate the production process or wrap the process class into the interface and mock the interface.
Assertions are operations in which we verify the obtained result and compare it with the expected one to confirm that the code is working as expected or there is an error in it. There are following types of assertions
- Values is the simplest and most beneficial assertion type. We compare the given value with the expected result, e.g. whether the method returned true as a result of the user update,
- Collections is a more extensive type of assertions. In this case, we compare the entire collections of objects. We can additionally take into account their order, count or whether the collection meets the specified requirements,
- Exceptions is the type of assertion that verify whether the system under test returns a specific exception during the exceptional situation, in addition, it’s possible to verify message or internal exceptions,
- Executions is the least advantageous type of assertion due to the tight binding of the implementation to the expected behavior. This assertion type is useful when we need to verify behavior that returns no result, such as sending email via SMTP.
Similar to test doubles, assertions have some useful libraries that simplify writing tests. First of all, we can use the assertions provided by the unit test frameworks, but recently it’s becoming more and more popular to write fluent-style assertions. For this we can use Fluent assertions or Shouldly libraries.
Tests have their own life cycle that we can divide into the lifetime for single test and the lifetime for test suite. Usually, we are interested in the moment before test started, called startup, and the moment after its execution called cleanup. We can use test life cycle to prepare data, dependencies or dispose them. Depending on which unit test framework we choose, hooking up to the test life cycle may be slightly different.
These are basic information about the practical aspects of unit tests. In the last post of this series, I will explain advanced topics from the roadmap.
You can find other posts from this series here
What do you think about practical topics of unit tests that I find useful? Would you add any points? If you consider it valuable, please leave a comment below and share it with others! It helps me verify whether such knowledge is needed by others.