Модульное тестирование класса, который использует DI без тестирования внутренних

12

У меня есть класс, который рефакторинг в 1 основной класс и 2 меньших класса. Основные классы используют базу данных (как это делают многие мои классы) и отправляют электронные письма. Таким образом, у основного класса есть IPersonRepositoryи IEmailRepositoryинъекция, которая, в свою очередь, отправляет 2 меньшим классам.

Теперь я хочу провести юнит-тестирование основного класса и научился не тестировать юниты внутренней работы класса, потому что мы должны иметь возможность изменять внутреннюю работу, не прерывая юнит-тесты.

Но , как класс использует IPersonRepositoryи IEmailRepositoryя ИМЕЮ указать (макет / манекен) результаты для некоторых методов для IPersonRepository. Основной класс вычисляет некоторые данные на основе существующих данных и возвращает их. Если я хочу проверить это, я не вижу, как я могу написать тест, не указав, что IPersonRepository.GetSavingsByCustomerIdвозвращает x. Но тогда мой модульный тест «знает» о внутренней работе, потому что он «знает», какие методы издеваться, а какие нет.

Как я могу протестировать класс с внедренными зависимостями, не зная теста о внутренностях?

фон:

По моему опыту, многие подобные тесты создают mock для репозиториев, а затем либо предоставляют правильные данные для mocks, либо проверяют, был ли вызван определенный метод во время выполнения. В любом случае, тест знает о внутренностях.

Теперь я видел презентацию о теории (которую я слышал ранее), что тест не должен знать о реализации. Во-первых, потому что вы не тестируете, как это работает, но и потому, что когда вы сейчас меняете реализацию, все модульные тесты не проходят, потому что они «знают» о реализации. Хотя мне нравится, что тесты не знают о реализации, я не знаю, как это сделать.

Мишель
источник
1
Как только ваш основной класс ожидает IPersonRepositoryобъект, этот интерфейс и все методы, которые он описывает, больше не являются «внутренними», так что это на самом деле не является проблемой теста. Ваш реальный вопрос должен заключаться в том, «как я могу реорганизовать классы в более мелкие единицы, не выставляя слишком много публике». Ответ таков: «Держите эти интерфейсы в тонусе» (например, придерживаясь принципа сегрегации интерфейса). Это ИМХО пункт 2 в ответе @ DavidArno (думаю, мне не нужно повторять это в другом ответе).
Док Браун

Ответы:

7

Основной класс вычисляет некоторые данные на основе существующих данных и возвращает их. Если я хочу проверить это, я не вижу, как я могу написать тест, не указав, что IPersonRepository.GetSavingsByCustomerIdвозвращает x. Но тогда мой модульный тест «знает» о внутренней работе, потому что он «знает», какие методы издеваться, а какие нет.

Вы правы в том, что это нарушение принципа «не проверяйте внутренности» и является обычным явлением, которое люди игнорируют.

Как я могу протестировать класс с внедренными зависимостями, не зная теста о внутренностях?

Есть два решения, которые вы можете принять, чтобы обойти это нарушение:

1) Обеспечить полный макет IPersonRepository. Ваш описанный в настоящее время подход состоит в том, чтобы связать макет с внутренней работой тестируемого метода, только насмехаясь над методами, которые он вызовет. Если вы поставляете макеты для всех методов IPersonRepository, то вы удаляете эту связь. Внутренняя работа может меняться, не влияя на макет, что делает тест менее хрупким.

Преимущество этого подхода заключается в простоте механизма DI, но он может создать большую работу, если ваш интерфейс определяет много методов.

2) Не вводите IPersonRepository, ни вводите GetSavingsByCustomerIdметод, ни вводите сберегательную стоимость. Проблема с внедрением целых реализаций интерфейса состоит в том, что вы затем вводите («говорите, не спрашивайте») систему «спрашивайте, не говорите», смешивая два подхода. Если вы используете подход «чистого DI», метод должен быть обеспечен (указан) точным методом для вызова, если ему требуется экономия, вместо того, чтобы получить объект (который он затем должен эффективно запросить для вызова метода).

Преимущество этого подхода состоит в том, что он устраняет необходимость в имитациях (помимо методов тестирования, которые вы вводите в тестируемый метод). Недостаток заключается в том, что это может привести к изменению сигнатур методов в соответствии с требованиями изменения метода.

Оба подхода имеют свои плюсы и минусы, поэтому выберите тот, который лучше всего соответствует вашим потребностям.

Дэвид Арно
источник
1
Мне очень нравится идея № 2. Я не знаю, почему я не думал об этом раньше. Я уже несколько лет создаю зависимости от IRepositories, не задаваясь вопросом, почему я создал зависимость от целого IRepository (который во многих случаях будет содержать довольно много сигнатур методов), в то время как он имеет зависимость только от одного или 2 метода. Поэтому вместо внедрения IRepository я мог бы лучше внедрить или передать необходимую (только) сигнатуру метода.
Мишель
1

Мой подход заключается в создании «фиктивных» версий репозиториев, которые читают из простых файлов, которые содержат необходимые данные.

Это означает, что отдельный тест не «знает» о настройке макета, хотя, очевидно, весь тестовый проект будет ссылаться на макет и иметь установочные файлы и т. Д.

Это позволяет избежать сложной настройки фиктивных объектов, требуемой для макетных сред, и позволяет вам использовать «фиктивные» объекты в реальных экземплярах вашего приложения для тестирования пользовательского интерфейса и тому подобного.

Поскольку «макет» полностью реализован, а не специально настроен для тестового сценария, изменение реализации, например, позволяет предположить, что теперь GetSavingsForCustomer также должен удалить клиента. Не нарушайте тесты (если, конечно, они действительно не нарушают тесты), вам нужно всего лишь обновить вашу одиночную импровизированную реализацию, и все ваши тесты будут работать с ней без изменения их настройки

Ewan
источник
2
Это все еще приводит к тому, что я создаю макет с данными именно для методов, с которыми работает тестируемый класс. Поэтому у меня все еще есть проблема, что, когда реализация изменится, тест будет прерван.
Мишель
1
отредактированный, чтобы объяснить, почему мой подход лучше, хотя все еще не идеален для сценария, я думаю, что вы имеете в виду
Ewan
1

Модульные тесты - это обычно whitebox-тесты (у вас есть доступ к реальному коду). Поэтому в некоторой степени нормально знать внутреннее устройство, однако для новичков это проще не делать, поскольку не следует проверять внутреннее поведение (например, «сначала вызывается метод a, затем b, а затем a снова»).

Впрыскивать макет, который предоставляет данные, можно, так как ваш класс (единица) зависит от этих внешних данных (или поставщика данных). Тем не менее, вы должны проверить результаты (не способ, чтобы добраться до них)! например, вы предоставляете экземпляр Person и проверяете, что электронное письмо было отправлено на правильный адрес электронной почты, например, путем предоставления имитацией для электронной почты, которая не делает ничего, кроме сохранения адреса электронной почты получателя для последующего доступа вашим тестом -код. (Я думаю, что Мартин Фаулер называет их «заглушки», а не «издевается»)

Энди
источник