Правильный баланс закрытости и тестируемости
Прочитав многие статьи, об архитектуре и юнит тестах, я начал разрываться и путаться, как выстраивать архитектуру и взаимодействие классов в приложениях, для соблюдения инкапсуляции и достаточного покрытия тестами. Почти везде рекомендуется использовать слабую связанность для тестируемости, а также помечать публичными только то, что должны знать внешние модули приложения. Но использовав такой подход возникают проблемы с созданием тестов для закрытых классов и методов и зачастую вижу советы не тестировать такие элементы. Но часто бывает, что эти классы осуществляют важную работу и не тестировать их модульно неудобно. Прошу подсказать куда копать, чтобы выбраться из этого адового цикла.
P.s. В основном использую платформу.net core 3.1 (C#)
Upd: Насущная проблема в тестировании нескольких реализаций одного интерфейса. Каждая реализация и имеет условный метод StartWorking и несколько полей. О конкретных реализациях другим модулям (сервисам) нет необходимости, т.к. фабрика выбирает реализацию на основе входных данных. Поэтому классы этих реализаций закрыты.
Upd2:
Конкретный пример сейчас привести не могу, но модель взаимодействия такая:
IExportWorker<T> {
public Stream TemplateStream { get; set; }
public Stream OutPutStream { get; set; }
public IEnumerable<T> InputModels { get; set; }
public void Export();
}
Конкретные реализации Этого интерфейса при вызове метода Export записывают в выходной поток необходимое количество копий частей данных из потока TemplateStream с вставкой данных из InputModels.
Экземпляры реализаций создаются фабриками которые имеют общий интерфейс.
Пару реализаций работает с пакетом DocumentFormat.OpenXML и в выходном потоке дают docx и xlsx файлы. Другая выдает pdf файл, еще одна bmp рисунок. Каждая из них использует свои механизмы преобразования данных и задействует дополнительные классы. Эти классы не обязаны быть известны остальному модулю, а тем более другим сборкам.
И все эти классы сложно тестировать из-за закрытости.
Ответы (1 шт):
Добавлю еще один источник:
Достаточно хорошо и подробно тема тестирования раскрыта в книге Роя Ошероува "Искусство автономного тестирования"
https://www.labirint.ru/books/427977/point/gm/?point=gg37&utm_source=g_ads&utm_medium=cpc&utm_campaign=smart_shoping_hudogka&product_partition_id=377094676304&product_id=427977&gclid=CjwKCAiA24SPBhB0EiwAjBgkhmo2KhjC0YE2nvxMbNuCvPp92aZZRbmtnit9PjnAth20b5detm32JRoCvB8QAvD_BwE
Если писать кратко по организации тестов:
- Попробуйте провести анализ, ответить на вопросы: есть ли необходимость в тестировании закрытых классов, методов, не возвращающих значений, или можно проверить публичный метод, а результат работы класса проверить по изменению его зависимостей? Будет ли полезен данный тест-кейс? Каким образом можно протестировать поведение объекта без вмешательства в код на данном этапе?
- Необходимо разобрать тест-кейс, какую работу мы проверяем и какие зависимости нам необходимы для теста.
- Необходимо понимать концепцию работы со ссылочными типами, об этом чуть ниже в примере(см. комментарии в тесте)
- Необходимо понимать принцип IoC, с его помощью мы можем писать хорошо тестируемые сущности, которые довольно удобно использовать одновременно в основном коде и тестах, заменяя лишь неважные в данном тесте зависимости.
Пример: когда тестируем взаимодействие службы с брокером сообщений, нам не важно, что за объект представляет базу данных, мы можем сделать фейковый объект класса работы с БД.
В некоторых ситуациях мы не можем проверить результат работы Метода в Классе, т.к. метод может не возвращать значения (void), либо Метод может быть закрытым, в таком случае мы можем выделить некое ожидаемое поведение от нашей Службы, либо Мок-Объекта, то есть судить о корректной работе проверяемого класса, по изменениям свойств, либо вызову определенных методов в других классах.
Представим, что наш класс-служба уже написана по принципу IoC, в конструкторе класса мы отключили зависимости от реальных классов: В примере ниже Служба будет получать сообщение из Брокера Сообщений, реализация которого заранее неизвестна.
На первый взгляд тестирование метода StartService() кажется затруднительным, т.к. он не возвращает значений, обычный Assert нам не поможет. Но мы можем проверить работу Службы по вызову метода Брокера Сообщений, значит к нему обратилась наша Служба, и бизнес-логика класса работает корректно, а также тест упадет, если в методе StartService() будет изменена логика, либо удалена строка кода.
public interface IMessageBroker
{
void DequeueMessage();
}
public class MessengerService
{
private readonly IMessageBroker _messageBroker;
public MessengerService(IMessageBroker messageBroker)
{
_messageBroker = messageBroker;
}
public void StartService()
{
_messageBroker.DequeueMessage();
}
}
[TestFixture]
public class MessengerServiceTests
{
[Test]
public void MessageShouldBeDequeuedFromMessageBrokerWhenServiceStart()
{
// Arrange
var messageBrokerMock = new Mock<IMessageBroker>();
// Настраиваем Мок-Объект Брокера Сообщений таким образом, что для успешного прохождения теста на нем должен вызваться метод DequeueMessage()
messageBrokerMock.Setup(q => q.DequeueMessage());
// Применяем принцип внедрения заранее настроенной тестовой зависимости в конструктор
var serviceUnderTests = new MessengerService(messageBrokerMock.Object);
// Act
// Вызываем проверяемый метод на тестируемом классе
serviceUnderTests.StartService();
// Assert
// За счет ссылочного типа у Мок-Объекта, мы можем отслеживать его состояние из теста
// и проверить вызов метода на Мок-Объекте
messageBrokerMock.VerifyAll();
}
}