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

от автора

На понимание факта, что юнит тесты это не только инструмент борьбы с регрессией в коде, но также и отличная инвестиция в качественную архитектуру меня натолкнул топик, посвященный модульному тестированию в одном англоязычном .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); } 

ссылка на оригинал статьи http://habrahabr.ru/post/210518/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *