Как научиться писать юнит-тесты, в которых будет смысл и не ошалеть от скуки?
Хочу признаться: за всю жизнь я написал меньше 10 юнит-тестов. Это при том, что люблю и часто пишу библиотечные проекты. Каждый раз перед началом проекта я говорю себе: в этот раз я буду писать юнит-тесты. Естественно, нарушаю обещание.
В оправдание могу сказать, что просто не понимаю, как подступиться к этой задаче. Допустим, я написал некий контейнер на Яве. Но чтобы хотя бы наполовину покрыть качественно различные состояния контейнера тестами, надо написать в несколько раз больше кода, чем занимает сам контейнер! А если он содержит слабые ссылки? А если он потокобезопасный? Сами тесты в этих случаях становится непросто написать. Особенно так, чтобы они были корректными.
Я вижу, что чем сложнее интерфейс, тем многократно сложнее его протестировать.
Вопрос: есть ли что-то, что существенно упрощает описанные выше проблемы. Ну, например, шаблоны тестов для тех же контейнеров. Или системы, упрощающие тестирование потокобезопасного кода. Или можно перестать беспокоиться и писать только высокоуровневые тесты? Или я что-то глобально не так понимаю?
Знакомая проблема. Хорошо писать тесты для простой функциональности. Но что делать, когда нужно тестировать не просто отдельные функции, а ещё и их комбинации? Что делать, когда количество комбинаций входных данных — огромно, или вообще бесконечно, и каждая комбинация может привести к ошибке?
Для себя нашёл частичный выход: пишу высокоуровневые тесты, а низкоуровневые заменяю множеством assert-ов. Assert проще написать, поскольку не надо воспроизводить контекст ни программно, ни мысленно — assert всегда выполняется в нужном контексте. И при запуске высокоуровнего теста фактически выполняется гораздо больше проверок, чем написано в самом тесте. Assert-ты также очень помогают в отладке — с их помощью быстрее локализуются ошибки.
Кстати, для библиотечных проектов совершенно необходимо писать и проекты, эти библиотеки использующие. Иначе — библиотека получается гарантированно ненужной. Так вот, само такое приложение и тесты к нему являются одновременно и тестами библиотеки.
Кстати, для библиотечных проектов совершенно необходимо писать и проекты, эти библиотеки использующие.
Я знаю только 2 мотивации писать библиотеку: 1) есть проект, в котором она нужна; 2) библиотека — это курсач или диплом. Во втором случае, к сожалению, ваше правило не соблюдается.
Ну во первых написание в несколько раз большего кода не является проблемой. Ибо есть правило: «написание кода не является узким местом разработки». Узкое место это например постоянный реинжениринг дизайна. Тестов и должно быть больше, чем оснвоного кода, но код тестов должен быть простым, и легко пишушимся. Что-то подали на вход, что-то получили на выходе.
Что касается сложных тестов, то проблема в дизайне кода. Что бы правильно задизайнить нужны не малые скилы. Я тоже постоянно натыкаюсь на такие проблемы. Но со временем понимаю, что многие вещи, которые раньше казались нетестируемыми, теперь уже ясно как переписать, что бы легко протестировать. Главное мотивация. Кто хочет — ищет возможности. Кто не хочет — ищет причины.
И последнее, всегда есть вещи которые практически нельзя протестировать, например многопоточность. Это нормально. Просто такой код должен быть локализован.
Не вижу, как дизайн кода решает проблему. Я специально сделал акцент на количестве состояний системы. Даже если у того же контейнера всего 2 метода: put и get, но внутри очень сложная логика, например значения влияют друг на друга или еще что-то, то написать один тест с put, за которым следует get — очевидно недостаточно. Так как я говорю о юнит-тестах, то подразумевается что разбить систему на части без потери смысла уже нельзя, то есть тестируем уже минимальные блоки, для которых можно определить согласованное видимое извне состояние.
Если у вас очень сложная логика, то у вас будет много тестов — я не понимаю что в этом плохого. Плохо то, что у вас сложная логика. Есть там принцип персональной ответственности, декомпозиция. Сложность написания тестов — всего лишь индикатор переусложненной лигики.
Можно разбить сложный метод на несколько используя средства для автоматизированного рефакторинга и после этого тестировать их отдельно.
Можно упростить архитектуру приложения, чтобы как в вашем случае значения не влияли друг на друга и т.д.
Главное изолировать то, что вы тестируете от всего остального. И речь не только о сигнатуре метода, который вы тестируете, но и о всех остальных внешних зависимостях в коде. Их нужно мокать.
Вообще разработка с использованием модульных тестов требует архитектурной готовности системы. И большое количество состояний тестов — это именно проблема дизайна системы. Это значит что модули ваши слишком связаны друг с другом и нужно их разрабатывать так, чтобы они были менее связаны.
Вы рассуждаете о разработке приложения. Библиотеку можно упростить, не считая того, что ее суть — быть сложной, чтобы решать задачу, которую не решают существующие библиотеки. Все сравнительное простое на популярных языках уже написано.
Зависимость значений я взял из головы. Тут нет речи о какой-то связности, кроме единственного контейнера я ничего и не упоминал.
leventov вы возможно объединяете понятие сложный и огромный. ООП позволяет разбивать огромные системы на простые малозависимые черные ящики. TDD позволяет почувствовать в каком классе\методе у вас слишком много связанного и сложного, нащупать нарушения принципов SOLID.
Писать тесты не скучно, кому-то возможно скучно менять антипаттерны (переиспользование синглтонов, god object, etc)
Так как вопрос в общем, то в общем уменьшение использования состояний уменьшает сложность тестирования. Например, при использовании функцонального стиля.
Попробуй почитать об TDD - Test-driven development. Это техника разработки через тестирование, которая позволить сделать процесс написания юнит-тестов частью написания кода, точнее ты будешь писать код который бы удовлетворял написанному тесту. Такая техника сделает процесс написания юнит-тестов намного интересней и эффективней, а время затрачиваемое на кодирование в целом уменьшиться, потому что уменьшиться время обычно затрачиваемое на отладку и поиск ошибок.