Типология Test Doubles

от автора

Тестовые дублёры (англ. Test Doubles) — это объекты или модули, используемые в автоматизированных тестах в качестве замены некоторых частей тестируемой системы (англ. SUT, System Under Test).

  • Многие программисты называют все тестовые дублёры одним словом: Mock.

  • Другие выделяют ровно два типа: Stub и Mock — вероятно, они вдохновляются статьёй Mocks Aren’t Stubs Мартина Фаулера

  • Третьи различают целых пять типов тестовых дублёров — именно столько описано в книге XUnit Test Patterns.

В этой статье мы обсудим пять типов тестовых дублёров: Dummy, Stub, Spy, Mock, Fake.

Программисты по-разному видят типологию тестовых дублёров.

Программисты по-разному видят типологию тестовых дублёров.

Как появилась статья

Меня зовут Сергей Шамбир, и я backend-разработчик в TravelLine (разрабатываю на C# / .NET).

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

В этой области есть две характерные черты:

  1. Много бизнес-логики для автоматизации внутренних бизнес-процессов

  2. Много интеграций с другими внутренними и внешними системами — причём смежные системы часто построены на разных технологических платформах, что создаёт зоопарк протоколов взаимодействия

Статья Мартина Фаулера на тему интеграций: You Can’t Buy Integration.

Эти особенности влияют на разные уровни тестов:

  1. Вследствие бо́льшей значимости интеграций с различными API в моих интеграционных тестах много тестовых дублёров, заменяющих неуправляемые внепроцессные зависимости

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

По интеграционным тестам расскажу подробнее: я фокусируюсь на интеграционных приёмочных тестах. Этот тип тестов можно кратко описать так:

  1. Они написаны языком, понятным экспертам предметной области, и используют язык Gherkin (с помощью библиотеки Reqnroll); при этом тесты документируют требования к сервису на языке, понятном не только разработчику

    • Следовательно, это приёмочные тесты (англ. Acceptance Tests, или Customer Tests в терминах Extreme Programming)

  2. Они тестируют систему в основном как чёрный ящик (Black Box), то есть разработчик стремится проверять наблюдаемое поведение

    • Следовательно, это функциональные тесты (англ. Functional Tests)

  3. Они используют реальную СУБД в docker-контейнере, но заменяют тестовыми дублёрами все неуправляемые внешние зависимости (включая API сервисов)

    • Следовательно, это интеграционные тесты по определениям книги «Принципы Unit-тестирования» Владимира Хорикова

    • По модели Test Sizes от Google, они называются тестами среднего размера (англ. Medium Size)

Модульные тесты и статический анализ служат дополнительным инструментом, однако основной вклад в уверенность в коде и в улучшение процесса разработки вносят именно интеграционные приёмочные тесты.

Как читать статью

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

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

Что такое тестовый дублёр

Термин «тестовый дублёр» (англ. Test Double) происходит от кинематографа:

  • в английском языке дублёров реальных актёров называют double

  • каскадёров, в частности, называют stunt double

Пять типов кратко

Для объектно-ориентированных программ определения типов тестовых дублёров можно сформулировать так:

  1. Dummy — простейший дублёр, который реализует интерфейс заменяемого объекта, но ничего не делает и не возвращает осмысленных данных

  2. Stub — дублёр, обеспечивающий контроль над опосредованным вводом (indirect input) тестируемой системы

  3. Spy — дублёр, записывающий весь опосредованный вывод (indirect output) тестируемой системы

  4. Mock — дублёр, сравнивающий опосредованный вывод с заранее настроенными ожиданиями

  5. Fake — полноценная и самостоятельная, но фальшивая реализация интерфейса заменяемого объекта

Термины «опосредованный ввод» и «опосредованный вывод» описаны подробно далее.

Примеры дублёров

Тип дублёра

Пример класса

Описание примера

Dummy

DummyLogger

Реализует ILogger, но ничего не делает.

Stub

StubCurrencyDataSource

Источник данных о валютах, возвращающий статические данные.

Spy

SpyMailSender

Реализует IMailSender. Ничего не отправляет, но запоминает переданные письма и позволяет тесту проверить их.

Mock

MockMailSender

Реализует IMailSender. Настраивается тестом на шаге Arrange (Given), а на шаге Act (When) проверяет ожидания по вызываемым методам и их параметрам.

Fake

FakeUserRepository

Реализует IUserRepository. С точки зрения тестируемой системы, неотличим от настоящего репозитория. Однако это фальшивка: он хранит данные не в БД, а в оперативной памяти.

Различные шаги тестового сценария — Arrange, Act, Assert (они же Given, When, Then в терминах BDD) описаны подробно далее.

Абстрактные модели, связанные с тестовыми дублёрами

1. Модель опосредованного ввода/вывода

Эта модель помогает выбрать тип дублёра, подходящий к определённой ситуации.

Опосредованный ввод / вывод (англ. indirect input / output) — это та часть входных и выходных данных, которыми тестируемая система (SUT) обменивается с другими объектами (Collaborators):

  • Опосредованный ввод — данные, которые тестируемая система читает из других объектов / систем

  • Опосредованный вывод — данные, которые тестируемая система пишет в другие объекты / системы

Очевидно, что взаимодействие с другим объектом / системой бывает трёх типов:

  1. Только чтение данных из другого объекта / системы — одностороннее взаимодействие, т.е. только опосредованный ввод

  2. Только запись данных данных в другой объекта / систему — одностороннее взаимодействие, т.е. только опосредованный вывод

  3. Чтение и запись данных — двустороннее взаимодействие

Конкретные примеры

Раскидаем примеры выше по типу взаимодействия тестируемой системы с дублёром:

Пример класса

Способ взаимодействия с дублёром

Что реально делает дублёр

DummyLogger

Запись данных

Никуда ничего не пишет

StubCurrencyDataSource

Чтение данных

Возвращает статические данные

SpyMailSender

Запись данных

Запоминает данные, чтобы тест прочитал их

MockMailSender

Запись данных

Сравнивает вызовы методов и параметры с ожиданиями

FakeUserRepository

Двустороннее (чтение/запись)

Хранит данные в памяти (например, в словаре)

Общие правила

Можем обобщить, переходя от конкретных примеров к общим правилам:

Тип дублёра

Способ взаимодействия с дублёром

Dummy

любой, обычно запись данных

Stub

чтение данных

Spy

запись данных либо двустороннее (Spy+Stub)

Mock

запись данных либо двустороннее (Mock+Stub)

Fake

любой, обычно двустороннее

Spy+Stub и Mock+Stub

Дублёры типов Spy и Mock могут одновременно принадлежать типу Stub, то есть:

  • Если дублёр сочетает в себе Spy и Stub, то он записывает весь вывод и возвращает статически определённый ввод

  • Если дублёр сочетает в себе Mock и Stub, то он сравнивает вызываемые методы и параметры с ожиданиями и возвращает в ответ данные, заданные при настройке Mock

В этом случае имена классов дублёров обычно содержат слово Spy или Mock, но не содержат слова Stub.

2. Модель Arrange-Act-Assert

Разные типы дублёров могут использоваться на разных шагах теста.

Модульные и интеграционные тесты удобно пишутся по модели AAA: Arrange, Act, Assert

  1. На шаге Arrange настраивается состояние до вызова тестируемых действий (методов)

  2. На шаге Act происходит вызов тестируемого действия (метода)

  3. На шаге Assert происходит проверка результатов

Некоторые авторы выделяют четвёртый шаг — Cleanup или Teardown — выполняющий освобождение ресурсов и откат изменений, способных повлиять на другие тесты.

Минутка BDD

Существует подход Behavior Driven Development, в рамках которого в том числе происходит смена названий: вместо Arrange / Act / Assert используются глаголы Given / When / Then.

Когда-то в прошлом я думал, что смена глаголов — это и есть суть BDD (ха-ха!). Как следствие, я считал BDD не очень полезной методикой.

Суть BDD конечно не в глаголах, а в описании приёмочных тестов (функциональных тестов) на языке, понятном всем членам продуктовой команды — product owner, системным аналитикам, тестировщикам, разработчикам

  • Если BDD сочетается с разработкой через User Story (со списком Acceptance Criteria для каждой Истории), то возникает огромный позитивный эффект для коммуникаций и процессов как внутри команды, между смежными командами и между командой и представителями заказчика

  • Если BDD сочетается с языком Gherkin, то тесты будут лаконичными и устойчивыми к рефакторингу

О тестах на Gherkin вы можете прочитать в статье Интеграционные тесты для ASP.NET Core, а ниже показан простой пример такого теста:

Сценарий: Можем создать несколько продуктов и обновить один     Пусть добавили продукты:       | Код    | Описание                   | Цена  | Количество |       | A12345 | Что-то из льняного волокна | 49,90 | 300        |       | B99999 | Женский ободок для волос   | 99,00 | 12         |      Когда обновляем продукты:       | Код    | Описание                    | Цена  | Количество |       | A12345 | Фуфайка из льняного волокна | 49,90 | 400        |      Тогда получим список продуктов:       | Код    | Описание                    | Цена  | Количество |       | A12345 | Фуфайка из льняного волокна | 49,90 | 400        |       | B99999 | Женский ободок для волос    | 99,00 | 12         |

Типы дублёров и шаги AAA

Вернёмся к нашим дублёрам и рассмотрим их взаимосвязь с шагами AAA.

Все типы дублёров так или иначе внедряются как зависимости — через DI-контейнер, через конструктор либо через параметры.

Однако есть различия во взаимодействии дублёра с разными шагами теста.

Тип дублёра

Взаимодействующие шаги AAA

Dummy

Stub

На шаге Arrange в Stub могут быть записаны данные

Spy

На шаге Assert из Spy могут быть прочитаны данные

Mock

Mock полностью настраивается на шаге Arrange (либо в методе Setup), и срабатывает автоматически на шаге Act.

Иногда проверки откладываются до шага Assert (если вызываются явно).

Fake

Fake должен работать и без настройки в тесте.

В некоторых случаях тест взаимодействует с Fake, изменяя его начальное состояние на шаге Arrange либо проверяя финальное состояние на шаге Assert.

Примеры реализации дублёров на C#

Общие принципы реализации всех типов тестовых дублёров:

  • Все типы дублёров не используются в нормальном режиме работы тестируемой системы

  • В тестируемый код все типы дублёров так или иначе внедряются как зависимости — через DI-контейнер, через конструктор либо через параметры вызываемых методов

  • Следует исключить использование дублёров в production — для этого их можно поместить в пакет, подключаемый только в тестах, или иным образом сделать недоступными для тестируемой системе при сборке/запуске в production

Пример Dummy

Dummy-объекты реализуют шаблон проектирования Null Object и отличаются тем, что предназначены только для тестов.

Класс DummyLogger
/// <summary> /// Реализует ILogger, но ничего не делает. /// </summary> public class DummyLogger : ILogger {     public void Log(Level level, string message, params object[] arguments)     {         // Улыбаемся и машем.     } }

Пример Stub

Stub-объекты содержат в себе статические данные либо загружают их с диска.

Кроме того, Stub-объект может иметь методы для записи данных на шаге Arrange (Given) теста.

Класс StubCurrencyDataSource
/// <summary> /// Источник данных о валютах, возвращающий статические данные. /// </summary> public class StubCurrencyDataSource: ICurrencyDataSource {     private readonly List<CurrencyData> _currencies = [         new CurrencyData("Российский рубль", "RUB"),         new CurrencyData("Китайский юань", "CNY"),         new CurrencyData("Бразильский реал", "BRL"),         new CurrencyData("Индийская рупия", "INR"),         new CurrencyData("Южноафриканский рэнд", "ZAR"),     ];      public IEnumerable<string> GetSupportedCurrencyCodes()     {         return _currencies.Select(data => data.Code());     }      public string GetNameByCode(string code)     {         return _currencies.First(data => data.Code == code).Name;     }      private record CurrencyData(         string Name,         string Code     ); }

Mock-объекты

Mock обычно настраивается специальными библиотеками с помощью DSL (Domain Specific Language), при этом синтаксис такого DSL может быть абсолютно разным для разных языков программирования или библиотек. Поэтому примера Mock не будет, вместо этого см. страницу Quickstart в wiki пакета Moq.

Примеры Spy

Spy-объекты схожи на Mock по своему назначению, но отличаются в реализации:

  • Mock строится на библиотечном коде, а тест лишь настраивает его на шаге Arrange (Given) на определённые ожидания о том, какие методы и с какими параметрами будут вызваны, после чего Mock-объект сверяет ожидания и фактические вызовы/параметры либо сразу на шаге Act (When), либо позже на шаге Assert (Then)

  • Spy пишется вручную и просто сохраняет параметры вызовов на шаге Act (When), позволяя тесту прочитать их на шаге Assert (Then)

Реализация Spy может делегировать работу оригинальному объекту либо ничего не делать, но запоминание полученных данных происходит в любом случае.

Класс SpyMailSender
/// <summary> /// Ничего не отправляет, но запоминает переданные письма ///  и позволяет тесту проверить их. /// </summary> public class SpyMailSender : IMailSender {     private readonly List<MimeMessage> _sentMails = [];      public Task SendAsync(MimeMessage mail)     {         _sentMails.Add(x);         return Task.CompletedTask; // Усё готово, шеф!     }      public MimeMessage FindMailByToName(string toName)     {         return _sentMails.Single(             message => message.To.Any(x => x.Name == toName)         );     } }

Пример Fake

Fake-объекты не требуют предварительной настройки (но могут позволять тесту записать дополнительные данные).

Ключевой критерий удачного Fake: для тестируемой системы он неотличим от подменяемого объекта, но упускает ключевой аспект поведения — в нём нет взаимодействия с внепроцессными зависимостями.

Класс FakeUserRepository

Fake-репозитории обычно строятся на двух инструментах:

  • Словари (ассоциативные массивы) вместо реляционных таблиц, при этом ключ словаря соответствует первичному ключу реляционной таблицы, а поиск по другим ключам выполняется линейно (потому что в тесте мало данных)

  • Простая генерация ID — например, путём инкремента целочисленной переменной или генерации UUID библиотечными средствами

/// <summary> /// Фальшивка: не обеспечивает персистентности. /// Хранит данные в оперативной памяти вместо реальной БД. /// </summary> public class FakeUserRepository : IUserRepository {     private Dictionary<string, User> _users = new();      public User? Find(string id)     {         return _users.GetOrDefault(id);     }      public List<User> FindAllByJobTitle(string jobTitle)     {         return _users.Values             .Where(u => u.JobTitle == jobTitle)             .ToList();     }      public void Add(User user)     {         _users[user.Id] = user;     }      public void Update(User user)     {         _users[user.Id] = user;     }      public void Delete(User user)     {         _users.Remove(user.Id);     } }

Доводы в пользу Fake-объектов

По моему личному опыту, Fake часто оказывался выгоднее, чем комбинация Mock и Stub или Spy и Stub:

  1. Не требующий настройки Fake-объект делает тесты устойчивее к изменениям структуры программы, не меняющим её наблюдаемое поведение (будь то рефакторинг или оптимизация)

  2. Процесс написания Fake-объекта даёт программисту намного лучшее понимание подменяемого объекта или внешней системы

  3. Готовый Fake понижает сложность написания дополнительных тест-кейсов, что мотивирует коллег писать достаточно много тестов и полноценно сопровождать ранее написанные тесты

  4. С хорошо написанным Fake-объектом тест имеет больше шансов найти потенциальные ошибки, поскольку точнее имитирует поведение подменяемой системы

Поэтому Fake — мой любимый тип тестового дублёра.

Этимология слов

Чтобы лучше запомнить разницу между типами тестовых дублёров, полезно разобраться с нюансами соответствующих английских слов:

Слово

Значение слова

Dummy

1) Детская пустышка;

2) Грубо сделанный макет предмета, например, учебная мишень из дерева

3) Манекен, муляж

Stub

1) Остаток (стены);

2) Корешок (в отрывной чековой книжке)

Spy

Тайный агент, собирающий сведения

Mock

Имитация, мимикрирующая под оригинал

Fake

Фальшивая копия — подделка под оригинал, которая выглядит настоящей

Для понимания разницы между Mock и Fake сравним два выражения:

  • Mock exam — репетиция экзамена, приближённая к настоящему экзамену

  • Fake exam — поддельный экзамен, разновидность мошенничества

Подытожим

Мы разобрали пять типов тестовых дублёров:

  1. Dummy — простейший дублёр, который реализует интерфейс заменяемого объекта, но ничего не делает и не возвращает осмысленных данных

  2. Stub — дублёр, обеспечивающий контроль над опосредованным вводом (indirect input) тестируемой системы

  3. Spy — дублёр, записывающий весь опосредованный вывод (indirect output) тестируемой системы

  4. Mock — дублёр, сравнивающий опосредованный вывод с заранее настроенными ожиданиями

  5. Fake — полноценная и самостоятельная, но фальшивая реализация интерфейса заменяемого объекта

Есть вопросы, которые не затронуты в этой статье:

  1. Как выбирать тестовые дублёры для модульных тестов?

  2. Как выбирать тестовые дублёры для интеграционных тестов?

  3. Когда следует отказаться от дублёров и использовать оригинальные объекты?

Вопросы №1 и №3 хорошо раскрыты в книге «Принципы Unit-тестирования» Владимира Хорикова.

Вопрос №2 я планирую раскрыть детальнее в своих следующих статьях.

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

Какие типы Test Doubles вы успешно применяли в реальных проектах?

50% Dummy4
50% Stub4
12.5% Spy1
62.5% Mock5
25% Fake2
25% Я не пишу тесты2
25% В моих тестах всё натуральное2

Проголосовали 8 пользователей. Воздержался 1 пользователь.

ссылка на оригинал статьи https://habr.com/ru/articles/871916/


Комментарии

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

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