Written by 08:07 Database development, Testing • 2 Comments

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.

//A module for sending messages in C#. Version 1.
public class MessagingService
{
    public void SendMessage(Guid messageAuthorId, Guid messageRecieverId, string message)
    {
        //A repository object stores a message in a database
        new MessagesRepository().SaveMessage(messageAuthorId, messageRecieverId, message);
        //check if the user is online  
        if (UsersService.IsUserOnline(messageRecieverId))
        {
            //send a toast notification calling the method of a static object  
            NotificationsService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
        }
    }
}

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.

//A module for sending messages in C#. Version  2.
public class MessagingService: IMessagingService
{
    private readonly IUserService _userService;
    private readonly INotificationService _notificationService;
    private readonly IMessagesRepository _messagesRepository;

    public MessagingService(IUserService userService, INotificationService notificationService, IMessagesRepository messagesRepository)
    {
        _userService = userService;
        _notificationService = notificationService;
        _messagesRepository = messagesRepository;
    }

    public void AddMessage(Guid messageAuthorId, Guid messageRecieverId, string message)
    {
        //A repository object stores a message in a database.  
        _messagesRepository.SaveMessage(messageAuthorId, messageRecieverId, message);
        //check if the user is online  
        if (_userService.IsUserOnline(messageRecieverId))
        {
            //send a toast message
            _notificationService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
        }
    }
}

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

[TestMethod]
public void AddMessage_MessageAdded_SavedOnce()
{
    //Arrange
    //sender
    Guid messageAuthorId = Guid.NewGuid();
    //receiver who is online
    Guid recieverId = Guid.NewGuid();
    //a message sent from a sender to a receiver
    string msg = "message";
    // stub for the IsUserOnline interface of the IUserService method
    Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
    userServiceStub.Setup(x => x.IsUserOnline(It.IsAny<Guid>())).Returns(true);
    //mocks for INotificationService and IMessagesRepository
    Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
    Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
    //create a module for messages passing mocks and stubs as dependencies 
    var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object,
                                                repositoryMoq.Object);

    //Act
    messagingService.AddMessage(messageAuthorId, recieverId, msg);

    //Assert
    repositoryMoq.Verify(x => x.SaveMessage(messageAuthorId, recieverId, msg), Times.Once);
   
}

[TestMethod]
public void AddMessage_MessageSendedToOffnlineUser_NotificationDoesntRecieved()
{
    //Arrange
    //sender
    Guid messageAuthorId = Guid.NewGuid();
    //receiver who is offline
    Guid offlineReciever = Guid.NewGuid();
    //message sent from a sender to a receiver
    string msg = "message";
    // stub for the IsUserOnline interface of the IUserService method
    Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
    userServiceStub.Setup(x => x.IsUserOnline(offlineReciever)).Returns(false);
    //mocks for INotificationService and IMessagesRepository
    Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
    Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
    // create a module for messages passing mocks and stubs as dependencies
    var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object,
                                                repositoryMoq.Object);
    //Act
    messagingService.AddMessage(messageAuthorId, offlineReciever, msg);

    //Assert
    notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, offlineReciever, msg),
                                    Times.Never);
}

[TestMethod]
public void AddMessage_MessageSendedToOnlineUser_NotificationRecieved()
{
    //Arrange
    //sender
    Guid messageAuthorId = Guid.NewGuid();
    //receiver who is online
    Guid onlineRecieverId = Guid.NewGuid();
    //message sent from a sender to a receiver 
    string msg = "message";
    // stub for the IsUserOnline interface of the IUserService method
    Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
    userServiceStub.Setup(x => x.IsUserOnline(onlineRecieverId)).Returns(true);
    //mocks for INotificationService and IMessagesRepository
    Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
    Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
    //create a module for messages passing mocks and stubs as dependencies
    var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object,
                                                repositoryMoq.Object);

    //Act
    messagingService.AddMessage(messageAuthorId, onlineRecieverId, msg);

    //Assert
    notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, onlineRecieverId, msg),
                                    Times.Once);
}

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.

Tags: , Last modified: September 23, 2021
Close