Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
50. Фреймворки для создания тестовых заглушек
«Расскажите, как использовать тестовые заглушки для модульного тестирования? Приведите примеры создания заглушек для зависимостей и проверки того, что код ведёт себя ожидаемым образом».
Хороший ответ
Использование фреймворков, вроде Moq или NSubstitute в .NET-приложениях для модульного тестирования предполагает создание заглушек для зависимостей, которые могут быть у класса. Это позволяет всесторонне протестировать класс в изоляции.
Тестовые заглушки – общий термин, описывающий все разновидности фиктивных зависимостей в тестах. Среди них можно выделить два основных:
- Моки (mock) – позволяют эмулировать и проверять выходные взаимодействия, т.е. вызовы, совершаемые тестируемой системой к её зависимостям для изменения их состояния. Например, моком можно заменить почтовый сервер, на который отправляется письмо.
- Стабы (stub) – помогают эмулировать входные взаимодействия, т.е. вызовы к зависимостям для получения входных данных. Например, стабом можно заменить базу, из которой запрашиваются данные. Стабы настраиваются для возвращения разных значений для разных сценариев.
Вот пример использования фреймворка NSubstitute:
using NSubstitute;
using Xunit;
public class ServiceTests
{
private IService _sut;
private ICalculator _calc;
public ServiceTests()
{
// Создаём заглушку для зависимости ICalculator
_calc = Substitute.For<ICalculator>();
_sut = new Service(_calc);
}
[Fact]
public void AddNumbers_ShouldReturnCorrectSum()
{
// Задаём поведение метода Add
_calc.Add(5, 10).Returns(15);
var result = _sut.AddNumbers(5, 10);
Assert.Equal(15, result);
// Проверяем, что Add был вызван
_calc.Received(1).Add(5, 10);
}
}
NSubstitute позволяет проверять, что методы мок-объекта были вызваны с ожидаемыми аргументами. Это важно для того, чтобы убедиться, что тестируемый класс правильно взаимодействует со своими зависимостями. Для этого в NSubstitute используется метод
Received(), как показано в примере выше.Рекомендации
1. Проверка поведения: важно всегда проверять поведение, а не детали реализации. Т.е. фокусироваться на том, что возвращает мок или его влияние на результат, а не на том, как именно он это делает.
Замечание: не стоит проверять взаимодействие со стабами. В примере выше ICalculator по сути является стабом. Он предоставляет входные значения для сервиса. Проверка
Received() показана для примера, но она излишняя и делает тест хрупким. Тест не должен интересоваться, как тестируемая система получает результат, если результат верный.При этом заглушка может быть моком и стабом одновременно, когда в тесте задаётся поведение одного метода зависимости, а проверяется взаимодействие с другим методом. Например, при тестировании сервиса бронирования такой заглушкой может быть репозиторий, где задаётся поведение метода проверки наличия свободных мест, а проверяется, что сервис бронирования вызывает метод резервации места в репозитории.
2. Один мок на тест: в идеале для каждого теста следует создавать отдельный мок-объект. Это помогает избежать взаимного влияния тестов друг на друга и делает их более понятными.
3. Чёткая настройка взаимодействий: важно явно задавать взаимодействия с моками и проверять их — это обеспечит ясность тестов и упростит их дальнейшее сопровождение.
Распространённые ошибки
- Чрезмерное использование заглушек: делает тесты хрупкими и излишне сложными. Заменять стоит только те зависимости, которые являются внешними для тестируемого класса. Использование заглушек для приватных зависимостей, простых структур данных или сущностей обычно приводит к тому, что тесты ломаются при малейшем изменении структуры данных.
И даже внешние зависимости не всегда стоит заменять заглушками. Например, база данных, которая используется только вашим приложением, и к которой не имеют доступа внешние системы, полностью скрыта от клиентов вашей системы. Её даже можно заменить другой базой, и никто не заметит. Здесь вместо замены базы заглушкой в тестах можно рассмотреть тестирование с использованием БД в памяти.
- Игнорирование реальных реализаций: заменяя слишком много объектов — особенно те, что содержат бизнес-логику, которую тоже следовало бы протестировать, — вы рискуете пропустить серьёзные проблемы интеграции или логические ошибки в реальном коде.
- Мокирование конкретных классов: Moq позволят задать поведение отдельных методов зависимости, при этом вызывая методы реального объекта, поведение которых не было задано. Например:
var moq = new Mock<Calculator> {CallBase = true };
// метод Add должен быть виртуальным
moq.Setup(x => x.Add(5, 10)).Returns(15);Настройка
CallBase = true позволяет моку сохранить реальное поведение базового класса, если оно не было переопределено. Таким образом вызов moq.Add(5, 10) в тесте будет использовать заданное поведение, а вызов, например, moq.Multiply(5, 10) будет использовать метод реального объекта Calculator. Так можно, к примеру, задать заглушки только для методов класса, меняющих состояние системы (пишущих в базу), при этом вызывая реальные методы, не меняющие состояния (читающие из базы).Это один из антипаттернов тестирования. Если вам приходится так делать, это часто означает, что класс зависимости объединяет две ответственности, и стоит его разделить.
Источники:
- https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
- «Принципы юнит-тестирования» (Владимир Хориков — СПб.: Питер, 2022).