Why Using Unit Tests is a Great Investment into High-Quality Architecture

I have decided to write this article in order to show that unit tests are not only a tool to grapple with regression in the code but is also a great investment into a high-quality architecture. In addition, a topic in the English .NET community motivated me to do this. The author of the article was Johnnie. He described his first and last day in the company involved in the software development for business in the financial sector. Johnnie was applying for the position – of a developer of unit tests. He was upset with the poor code quality, which he had to test. He compared the code with a junkyard stuffed with objects that clone each other in any unsuitable places. In addition, he could not find abstract data types in a repository: the code contained only binding of implementations that cross request each other.

Johnnie realizing all the uselessness of module testing in this company outlined this situation to the manager, refused from further cooperation, and gave a valuable advice. He recommended that a development team go on courses to learn instantiating objects and using abstract data types. I do not know if the manager followed his advice (I think he did not). However, if you are interested what Johnnie meant and how using module testing can influence the quality of your architecture, you are welcome to read this article.

Dependency Isolation is a base of module testing

Module or unit test is a test that verifies the module functionality isolated from its dependencies. Dependency isolation is a substitution of real-world objects, with which the module being tested interacts, with stubs that simulate the correct behavior of their prototypes. This substitution allows focusing on testing a particular module, ignoring a possible incorrect behavior of its environment. A necessity to replace dependencies in the test causes an interesting property. A developer who realizes that their code will be used in module tests has to develop using abstractions and perform refactoring at the first signs of high connectivity.

I am going to consider it on the particular example.

Let’s try to imagine what a personal message module might look like on a system developed by the company from which Johnnie escaped. And how the same module would look like if developers were to apply unit testing.

The module should be able to store the message in the database and if the person to whom the message was addressed is in the system — display the message on the screen with a toast notification.

Let’s check what dependencies our module has.

The SendMessage function invokes static methods of the Notificationsservice and Usersservice objects and creates the Messagesrepository object that is responsible for working with the database.

There is no problem with the fact that the module interacts with other objects. The problem is how this interaction is built, and it’s not built successfully. Direct access to third-party methods has made our module tightly linked to specific implementations.

This interaction has a lot of downsides, but the important thing is that the Messagingservice module has lost the ability to be tested in isolation from the implementations of the Notificationsservice, Usersservice and Messagesrepository. Actually, we cannot replace these objects with stubs.

Now let’s look at how the same module would look like if a developer were to take care of it.

As you can see, this version is much better. The interaction between objects is now built not directly but through interfaces.

We do no need to access static classes and instantiate objects in methods with business logic anymore. The main point is that we can replace all dependencies by passing stubs for testing into a constructor. Thus, while enhancing code testability, we could also improve both a testability of our code and architecture of our application. We refused from direct using implementations and passed instantiation to the layer above. This is exactly what Johnnie wanted.

Next, create a test for the module of sending messages.

Specification on tests

Define what our test should check:

  • A single call of the SaveMessage method
  • A single call of the SendNotificationToUser() method if the IsUserOnline() method stub over the IUsersService object returns true
  • There is no SendNotificationToUser() method if the IsUserOnline() method stub over the IUsersService object returns false

Following these conditions can guarantee that the implementation of the SendMessage message is correct and does not contain any errors.

Tests

The test is implemented using the isolated Moq framework

To sum it up, looking for an ideal architecture is a useless task.

Unit tests are great to use when you need to check the architecture on lose coupling between modules. Still, keep in mind that designing complex engineering systems is always a compromise. There is no ideal architecture and it is not possible to take into account all the scenarios of the application development beforehand. The architecture quality depends on multiple parameters, often mutually exclusive. You may solve any design problem by adding an additional level of abstraction. However, it does not refer to the problem of a huge amount of abstraction levels. I do not recommend thinking that interaction between objects is based only on abstractions. The point is that you use the code that allows interaction between implementations and is less flexible, which means that it does not have a possibility to be tested by unit tests.

Rustem Musairov

Rustem Musairov

Rustem Musairov has been designing and developing software for over fifteen years, ten of which he has been developing on .Net. Now, he is practicing the theory of building high-load services on this platform.
Rustem Musairov

Rustem Musairov

Rustem Musairov has been designing and developing software for over fifteen years, ten of which he has been developing on .Net. Now, he is practicing the theory of building high-load services on this platform.

  • Arni Leibovits

    Great article! We use very similar testing techniques. Though I would recommend using Mock.Of for shorter setups and using “var” for more readable code.