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