Распространенные ошибки при написании юнит-тестов, Катерина Павленко
HTML-код
- Опубликовано: 10 фев 2025
- Все знают, что писать юнит-тесты нужно. Но иногда слишком сложно: код пишешь десять минут, а тесты все два часа. А когда дело доходит до поддержки и расширения старых тестов, иногда проще удалить их и написать всё заново. Рассмотрим частые ошибки, приводящие к этому, их причины и способы избежать.
Катерина Павленко на конференции Web Standards Days 18 августа 2018 в Санкт-Петербурге - wsd.events/201...
Слайды: wsd.events/201...
Очень понятно, спасибо
1ый пример с кофе. допустим мы методы покупки кофе и его готовки вынесли в отдельные зависимости (сервисы). Вроде код от этого не поменялся, это просто рефакторинг. Однако этот класс уже нельзя протестировать вместе с этими зависимостями (или можно?), т.к. это уже другой unit. соответственно придется делать на них mock-и. Вопрос - а как теперь тестировать юнит тестом такой класс, нужно ли его тестировать вообще и не вылезет там всех тех же проблем которые были при тестировании приватных методов по отдельности .ведь наши тест по сути останется такими же как в ситуации с тестированием приватных методов = основной тест будет тестировать то, что вызвались эти самые mock-и, а отдельный тесты на созданные сервисы-зависимости будут тестировать их публичных методы на покупку кофе и на варку конфе. В итоге ситуация и эффект от нее ровно тот же что от тестирования приватных методов, хотя мы и сделали все по красоте, разве нет?
или вот еще пример. Скажем у меня есть сервис и есть репозиторий. есть метод service.GetAccountById(id) и внутри в реализации вызывается 1 строчка с аналогичным вызовом в репозитории. Например я сделал на репозиторий(этот метод) какие-то тесты. если следовать правилу "тестируем контракты а не реализацию" я должен на этот сервисный метод с 1 строчкой кода сделать тест (какой??? и надо ли вообще делать тесты для такого рода простой реализации? если мы тестируем контракты а не реализацию то нам поидее надо на любой, даже такой контракт делать тест). Окей, допустим я что-то там протестировал. А теперь финт ушами и я решил бизнес логику которая была внутри репозитория вытащить на уровень сервисов, т.к. ей место там. Теперь мне надо менять 2 теста - тот что относится к репозиторию и тот что к сервисам (там ведь теперь куча бизнес логики вместо 1 вызова).
Вот вопрос - если мы "тестируем контракты" - почему в этой ситуации столько гимора случилось, мы же контракт сервиса не меняли.
Пока что этот что прошлый пример мне подсказывает что юниты которые мы тестируем это не отдельный класс нифига, а некая цепочка связанных классов. Но как определить границы этих юнитов в этом случае. И сюда же лезет то что несколько классов это типа уже интеграционные тесты, а не юнит.
И получается по этим 2м примерам что рефакторинг ломает тесты, а такого же быть не должно, т.к. мы же не реализацию тестируем.
Эх на бумажке все просто, а на деле, сами понимаете....
Чем больше изучаю юнит тесты тем они мне меньше нравятся.
Вот никогда не писал тесты, сейчас "тимлид заставляет".
Как ни посмотришь у всех всегда (в ютубах и возможно во всяких книжках про
тесты) все четко выходит и юнит тесты это прям одна польза, однако на
практике вообще все не так получается и от них проблем больше чем
пользы.
Пока я вижу смысл только покрывать тестами какие-то сложные вычисления с
минимумом (а лучше с 0лем) зависимостей, то есть только tricky места в
которых легко допустить ошибку, но блин во всех местах делать такого
рода юнит тесты, мне пока кажется это больше проблем чем толку
(переубедите меня)
Может мне какие-то книжки умные нужно почитать
За целый год такой длинный вопрос никто не удостоил даже парой строк ответа. Надо исправлять :)
"Ведь наши тест по сути останется такими же как в ситуации с тестированием приватных методов = основной тест будет тестировать то, что вызвались эти самые mock-и, а отдельный тесты на созданные сервисы-зависимости будут тестировать их публичных методы на покупку кофе и на варку кофе. В итоге ситуация и эффект от нее ровно тот же что от тестирования приватных методов, хотя мы и сделали все по красоте, разве нет?"
Это хорошо замечено. Да, если выносить приватные методы в отдельные классы, тестировать их и потом как моки проверять в изначальном классе, то это проблема. Тесты должны быть устойчивыми ко внутренней имплементации (рефакторингу), но быть чувствительными к изменению поведения. Когда мы выносим приватные методы, то это никак не меняет поведения в системе, требования к начальному классу по готовке кофе остаются точно такими же - мы имеем дело с рефакторингом, а значит тесты не должны меняться. Мы можем поменять приватные методы, разбить их еще на большее количество методов, можем вынести их в отдельный класс - как угодно - тесты для начального класса от этого не меняются, они остаются на месте, их не надо переносить в новые классы. При этом новые классы - это деталь имплементации, поэтому их лучше делать package private/private/internal и т.п, чтоб они не были доступны публично снаружи. Ведь мы и не собирались всем предоставлять раздельно доступ к покупке кофе и отдельно к варке. Только "все в одном" мы и хотели, а как оно будет устроено внутри уже не важно для тех, кто пользует такой модуль. И для тестов тоже, ведь тесты по-сути и есть пользователи модуля.
На второй вопрос про репозиторий примерно тот же ответ. Если тестировать можно легко через более высокий уровень абстракции, который уже ближе к тому, что модуль и должен предоставлять наружу - так и надо делать. Ведь через вышестоящие методы все внутренние зависимости так или иначе тоже вызываются и тестируются.
"Пока что этот что прошлый пример мне подсказывает что юниты которые мы тестируем это не отдельный класс нифига, а некая цепочка связанных классов. Но как определить границы этих юнитов в этом случае. И сюда же лезет то что несколько классов это типа уже интеграционные тесты, а не юнит."
Модуль для тестирования - это не всегда класс или метод. Это какой-то набор требований к подсистеме. И сложность тестирования как раз и заключается в правильном определении насколько высокий этот уровень, какой же есть публичный интерфейс (api) этого модуля, что должно быть доступно из-вне, а что лишь приватная деталь. Но иногда приходится уровень тестируемой абстракции понижать если это упрощает тестирование. Юнит может означать много классов в цепочке, верно. Типичное разбиение на "юнит тесты" и "интеграционные" обычно только запутывает. Тесты должны быть всегда с интеграцией в некой степени. Возьмем, например, тривиальный пример "найти максимальное число из 3-ех". И если тестировать только эту функцию в отдельности, то все скажут, что это не интеграционный тест. Но внутри можно использовать не простой if-else, а вызов библиотечной функции max(a,b). Получается, что это уже интеграционный тест на библиотечную функцию. Мало смысла в этой классификации. Тут более важно насколько тест остается быстрым, надежным, стабильным к рефакторингу и чувствительным к изменению поведения (при изменении которого уже надо менять тест). И это всегда компромисс - большая надежность и приближенность к реальности будет часто медленней.
"Может мне какие-то книжки умные нужно почитать".
- Классика Kent Beck "Test Driven Development. By Example", которую мало кто читает, но уже тогда изначально были ответы на подобные вопросы.
- Очень важные есть пару видео от Ian Cooper про тесты и TDD.
- Ищи все по этим темам от Robert Martin (Uncle Bob) - есть видео лекции, есть прямо уроки про тесты/TDD на практике, есть у него еще блог clean coder, на котором можно найти десяток-два статей достаточно важных для понимания.
- Кое-что можно найти от Martin Fowler - видео и блог.
- Достойная книга, с которой по большей части можно соглашаться, это "Unit testing. Principles, Practices, and Patterns" от Vladimir Khorikov.
Супер 🔥