Почему использование юнит тестов это отличная инвестиция в качественную архитектуру

в 21:12, , рубрики: moq, tdd, unit test, архитектура, Программирование, тестирование, метки: , , ,

На понимание факта, что юнит тесты это не только инструмент борьбы с регрессией в коде, но также и отличная инвестиция в качественную архитектуру меня натолкнул топик, посвященный модульному тестированию в одном англоязычном .net сообществе. Автора топика звали Джонни и он описывал свой первый (и последний) день в компании, занимавшейся разработкой программного обеспечения для предприятий финансового сектора. Джонни претендовал на вакансию разработчика модульных тестов и был расстроен низким качеством кода, который ему вменялось тестировать. Он сравнил увиденный им код со свалкой, набитой объектами, бесконтрольно создающими друг друга в любых непригодных для этого местах. Также он писал, что ему так и не удалось найти в репозитории абстрактные типы данных, код состоял исключительно из туго переплетенных в один клубок реализаций, перекрестно вызывающих друг друга. Джонни, понимая всю бесполезность применения практики модульного тестирования в этой компании, обрисовал ситуацию нанявшему его менеджеру и, отказавшись от дальнейшего сотрудничества, дал напоследок ценный, с его точки зрения, совет. Он посоветовал отправить команду разработчиков на курсы, где бы их смогли научить правильно инстанцировать объекты и пользоваться преимуществами абстрактных типов данных. Я не знаю, последовал ли менеджер совету (думаю, что нет), но если вам интересно, что имел в виду Джонни и как использование практик модульного тестирования может повлиять на качество вашей архитектуры, добро пожаловать под кат, будем разбираться вместе.

Изоляция зависимостей — основа модульного тестирования

Модульным или юнит тестом называется тест, проверяющий функционал модуля в изоляции от его зависимостей. Под изоляцией зависимостей понимается подмена реальных объектов, с которыми взаимодействует тестируемый модуль, на заглушки, имитирующие корректное поведение своих прототипов. Такая подмена позволяет сосредоточиться на тестировании конкретного модуля, игнорируя возможность некорректного поведения его окружения. Из необходимости в рамках теста подменять зависимости вытекает интересное свойство. Разработчик, понимающий, что его код будет использоваться в том числе и в модульных тестах, вынужден разрабатывать, пользуясь всеми преимуществами абстракций, и рефакторить при первых признаках появления высокой связанности. В его коде начинают появляться фабрики и IoC контейнер, а на столе книга gof про паттерны.

Пример для наглядности

Давайте попытаемся представить, как мог бы выглядеть модуль отправки личных сообщения в системе, разработанной компанией, из которой сбежал Джонни. И как бы выглядел этот же модуль, если бы разработчики применяли модульное тестирование. Наш модуль должен уметь cохранять сообщение в базе данных и, если пользователь, которому было адресовано сообщение, находится в системе — отображать сообщение на его экране всплывающим уведомлением.

//Модуль отправки сообщений на языке C#. Версия 1. 
public class MessagingService
{
    public void SendMessage(Guid messageAuthorId, Guid messageRecieverId, string message)
    {
        //объект репозиторий сохраняет текст сообщения в базе данных  
        new MessagesRepository().SaveMessage(messageAuthorId, messageRecieverId, message);
        //проверяем, находится ли пользователь онлайн
        if (UsersService.IsUserOnline(messageRecieverId))
        {
            //отправляем всплывающее уведомление, вызвав метод статического объекта
            NotificationsService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
        }
    }
}

Давайте посмотрим — какие зависимости есть у нашего модуля. В функции SendMessage вызываются статические методы объектов NotificationsService, UsersService и создается объект MessagesRepository, ответственный за работу с базой данных. В том, что наш модуль взаимодействует с другими объектами проблемы нет. Проблема в том, как построено это взаимодействие, а построено оно не удачно. Прямое обращение к методам сторонних объектов сделало наш модуль крепко связанным с конкретными реализациями. У такого взаимодействия есть много минусов, но для нас главное то, что модуль MessagingService потерял возможность быть протестированным в отрыве от реализаций объектов NotificationsService, UsersService и MessagesRepository. Мы действительно не можем в рамках модульного теста, подменить эти объекты на заглушки.
Теперь давайте посмотрим, как выглядел бы этот же модуль, если бы разработчик позаботился о его тестируемости.

//Модуль отправки сообщений на языке C#. Версия 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)
    {
        //объект репозиторий сохраняет текст сообщения в базе данных  
        _messagesRepository.SaveMessage(messageAuthorId, messageRecieverId, message);
        //проверяем, находится ли пользователь онлайн
        if (_userService.IsUserOnline(messageRecieverId))
        {
            //отправляем всплывающее уведомление
            _notificationService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
        }
    }
}

Эта версия уже намного лучше. Теперь взаимодействие между объектами строится не напрямую, а через интерфейсы. Мы больше не обращаемся к статическим классам и не инстанцируем объекты в методах с бизнес логикой. И, самое главное, все зависимости мы теперь можем подменить, передав в конструктор заглушки для теста. Таким образом, добиваясь тестируемости нашего кода, мы смогли параллельно улучшить архитектуру нашего приложения. Нам пришлось отказаться от прямого использования реализаций в пользу интерфейсов и мы перенесли инстанцирование на слой находящийся уровнем выше. А это именно то, чего хотел Джонни.

Пишем тест к модулю отправки сообщений

Спецификация на тест

Определим, что именно должен проверять наш тест.

  • факт однократного вызова метода IMessageRepository.SaveMessage
  • факт однократного вызова метода INotificationsService.SendNotificationToUser(), в случае если метод IsUserOnline() стаба над объектом IUsersService вернул true
  • отсутствие вызова метода INotificationsService.SendNotificationToUser(), в случае если метод IsUserOnline() стаба над объектом IUsersService вернул false

Выполнение этих трех условий гарантирует нам, что реализация метода SendMessage корректна и не содержит ошибок.

Сам тест

Тест реализован с помощью изоляционного фреймворка Moq

[TestMethod]
public void SendMessageFullTest()
{
    //Arrange
    //отправитель
    Guid messageAuthorId = Guid.NewGuid();
    //получатель, находящийся онлайн
    Guid onlineRecieverId = Guid.NewGuid();
    //получатель находящийся оффлайн
    Guid offlineReciever = Guid.NewGuid();
    //сообщение, посылаемое от отправителя получателю
    string msg = "message";
    // стаб для метода IsUserOnline интерфейса IUserService
    Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
    userServiceStub.Setup(x => x.IsUserOnline(onlineRecieverId)).Returns(true);
    userServiceStub.Setup(x => x.IsUserOnline(offlineReciever)).Returns(false);
    //моки для INotificationService и IMessagesRepository
    Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
    Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
    //создаем модуль сообщений, передавая в качестве его зависимостей моки и стабы
    var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object,
                                                repositoryMoq.Object);

    //Act. Отправка сообщения пользователю находящемуся онлайн
    messagingService.AddMessage(messageAuthorId, onlineRecieverId, msg);

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

    //Сбрасываем счетчики вызовов
    repositoryMoq.ResetCalls();
    notificationsServiceMoq.ResetCalls();

    //Act. Отправка сообщения пользователю находящемуся оффлайн
    new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object, repositoryMoq.Object)
        .AddMessage(messageAuthorId, offlineReciever, msg);

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

Автор: steamru

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js