Тавтологическими иногда называют тесты, которые слишком тесно связаны с нижележащей имплементацией, буквально повторяя ее шаг-в-шаг. Их регулярно ругают, и в целом, кажется, девелоперы умеют их избегать и распознавать. При давлении менеджмента иногда такие тесты возникают задним числом в попытке наверстать покрытие.
Однако бывают и сложные случаи.
Тут важно понимать, что наш тест в данном случае — это спецификация правила, поведения. То есть он предписывает, в каком порядке компоненты дергать, не потому что следует за ними, а потому что следует впереди них. В этом смысле его логично написать более абстрактно: например, если нужно вызвать несколько сервисов — все равно, параллельно или последовательно, то тест должен описывать именно это. Если тесты перебрасываются объектами, то ему, возможно, не следует совать нос в поля этого объекта. Поэтому наш тест может выглядеть почти как имплементация, или как самая простая и наивная имплементация какого-то поведения, но при этом знать меру в деталях и не являться тавтологией.
Иногда эти кейсы, правда, перетекают друг в друга. Т.е. у нас может быть несколько вызовов легаси-библиотек, которые ничего толком не возвращают, или обмениваются непонятно чем, в порядке, неизвестно кем заведенным, и при этом не очень понятно, кто кому «предписывает» поведение. Тестировать это — все равно что объяснять маршрут водителю автобуса, поэтому лучше всего действовать по первому сценарию.
Однако бывают и сложные случаи.
- Первый случай — это тесты, воспроизводящие поведение библиотек. Типичный случай — http-client из какой-нибудь библиотеки, где нужно создать клиента, запихнуть ему куки и заголовки и послать запрос. Смысла тестировать каждый шаг нет, там еще могут попасться статические методы, new и прочие тестабилити-киллеры, поэтому проще всего завернуть его в тонкий класс-обертку. Юнит-тесты такому классу не нужны. Тестировать этот класс можно в контексте интеграционных тестов зависимостей, то есть мы вызываем его против реального сервиса и убеждаемся, что библиотека работает как ожидается. Важно, чтобы класс-обертка не содержал дополнительной логики, которая может быть протестирована в рамках юнит-теста.
- Второй случай — это тесты, проверяющие что-то, что не возвращает результата (или возвращает невнятное), соответственно, единственный способ проверить, что что-то было вызвано — это Spy со счетчиком. Это валидный кейс, хотя и ограниченный. Сюда могут относиться вызванные хранимые процедуры, отсылки писем.
- Третий случай — это тесты стратегий, политик и прочих behavioral patterns. Например, мы хотим, чтобы при создании заказа у нас попала запись в базу, потом он встал в очередь заказов, а затем что-то записалось в логи и емейлы.
Тут важно понимать, что наш тест в данном случае — это спецификация правила, поведения. То есть он предписывает, в каком порядке компоненты дергать, не потому что следует за ними, а потому что следует впереди них. В этом смысле его логично написать более абстрактно: например, если нужно вызвать несколько сервисов — все равно, параллельно или последовательно, то тест должен описывать именно это. Если тесты перебрасываются объектами, то ему, возможно, не следует совать нос в поля этого объекта. Поэтому наш тест может выглядеть почти как имплементация, или как самая простая и наивная имплементация какого-то поведения, но при этом знать меру в деталях и не являться тавтологией.
Иногда эти кейсы, правда, перетекают друг в друга. Т.е. у нас может быть несколько вызовов легаси-библиотек, которые ничего толком не возвращают, или обмениваются непонятно чем, в порядке, неизвестно кем заведенным, и при этом не очень понятно, кто кому «предписывает» поведение. Тестировать это — все равно что объяснять маршрут водителю автобуса, поэтому лучше всего действовать по первому сценарию.