Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Отчего применение юнит тестов это хорошая инвестиция в добротную архитектуру

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

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

Модульным либо юнит тестом именуется тест, проверяющий функционал модуля в изоляции от его зависимостей. Под изоляцией зависимостей воспринимается подмена реальных объектов, с которыми взаимодействует тестируемый модуль, на заглушки, имитирующие правильное поведение своих прототипов. Такая подмена разрешает сосредоточиться на тестировании определенного модуля, игнорируя вероятность некорректного поведения его окружения. Из необходимости в рамках теста подменять зависимости вытекает увлекательное качество. Разработчик, понимающий, что его код будет применяться в том числе и в модульных тестах, вынужден разрабатывать, пользуясь всеми превосходствами абстракций, и рефакторить при первых знаках возникновения высокой связанности. В его коде начинают возникать фабрики и 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

Выполнение этих 3 условий гарантирует нам, что реализация способа 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);
}
Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB