Эта статья — адаптация главы книги «Руководство по карьере полного программного обеспечения». Её автор, Джон Сонмез (John Sonmez) пишет её и выкладывает некоторые главы на свой сайт.
Что такое TDD и модульное тестирование [перевод] - 1

Краткий глоссарий для новичков

Модульное тестирование или юнит-тестирование (unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы. Идея состоит в том, чтобы писать тесты для каждой нетривиальной функции или метода. Регрессоинное тестирование (regression testing) — обобщённое название для всех видов тестирования программного обеспечения, направленных на обнаружение ошибок в уже протестированных участках исходного кода. Такие ошибки — когда после внесения изменений в программу перестаёт работать то, что должно было продолжать работать, — называют регрессионными ошибками. Красный результат, fail — провал теста. Отличие ожидаемого результата от фактического. Зелёный результат, pass — положительный результат теста. Фактический результат не отличается от полученного. ***
Что такое TDD и модульное тестирование [перевод] - 2
С подходом «разработка через тестирование» (Test Driven Development, TDD) и модульным тестированием у меня сложились весьма неоднозначные отношения, плавно переходящие от любви к ненависти и обратно. Я был страстным фанатом и одновременно подозрительным скептиком относительно использования этой, да и других, «лучших практик». Причина моего отношения обоснована тем, что в процессах разработки ПО наметилась серьезная проблема: разработчики, а порой и менеджеры, применяют некий инструмент и методологии лишь потому, что те относятся к «лучшим практикам». Реальная же причина их применения остается непонятой. Однажды я приступил к работе над неким проектом, и в процессе мне сообщили, что мы будем модифицировать код, покрытый огромным количеством модульных тестов. Шутка ли, их было около 3000. Обычно это хороший знак, сигнал о том, что разработчики применяют передовые методологии. Код при таком подходе чаще всего структурирован, а в его основе лежит продуманная архитектура. Словом, наличие тестов меня обрадовало, уже потому, что это означало облегчение моей работы наставника программистов. Поскольку модульные тесты у нас уже были, мне оставалось лишь подключить команду разработчиков к их поддержке и начинать писать собственный код. Я открыл IDE (интегрированную среду разработки) и загрузил проект.
Что такое TDD и модульное тестирование [перевод] - 3
Это был большой проект! Я нашёл папку с надписью «unit tests». «Отлично, — подумал я. — Запустим и посмотрим, что произойдет. Это заняло всего несколько минут, и, к моему удивлению, все тесты прошли, всё было зеленым («зеленый» — положительный результат работы теста. Сигнализирует о том, что код работает, как предполагается. Красным цветом отмечается «провал» или fail, то есть тот случай, когда код работает неправильно — прим. переводчика). Они все прошли проверку. В этот момент во мне проснулся скептик. Как так, три тысячи модульных тестов, и все сразу взяли — и дали положительный результат? За свою долгую практику я не мог вспомнить случая начала работы с проектом, чтобы в коде не было ни одного негативного модульного теста. Что же делать? Проверять вручную! ЧЯ выбрал один случайный тест, не самый показательный, зато было сразу понятно, что он проверяет. Но, разбираясь с ним, я заметил кое-какую несуразицу: в тесте не было сравнений с ожидаемым результатом (asserts)! То есть в действительности вообще ничего не проверялось! В тесте были некие шаги, они выполнялись, но в конце теста, где он должен сверять фактический и ожидаемый результат, проверки не было. «Тест» ничего не тестировал. Я открыл еще один тест. Еще лучше: оператор сравнения с результатом, был закомментирован. Блестяще! Это отличный способ сделать пробный проход, просто закомментируйте код, который заставляет его терпеть неудачу. Я проверил ещё один тест, затем ещё один… Ни один из них ничего не проверял. Три тысячи тестов, и все — полностью бесполезны. Существует огромная разница между написанием модульных тестов и пониманием модульного тестирования и разработки, основанной на тестах (TDD).

Что такое модульное тестирование?

Что такое TDD и модульное тестирование [перевод] - 4
Основная идея модульного тестирования заключается в том, чтобы написать тесты, в которых проверена наименьшая «единица» кода. Модульные тесты обычно написаны на том же языке программирования, что и исходный код приложения. Они создаются непосредственно для проверки этого кода. То есть модульные тесты — это код, который проверяет корректность другого кода. Слово «тест» в контексте я использую достаточно либерально, потому что модульные тесты в каком-то смысле тестами не являются. Они ничего не испытывают. Я имею в виду, что при запуске модульного теста вы обычно не обнаруживаете, что какой-то код не работает. Вы это обнаруживаете во время написания теста, поскольку вы будете менять код до тех пор, пока тест не станет зелёным. Да, код может измениться позже, и тогда ваш тест может потерпеть неудачу. Так что в этом смысле модульный тест является регрессионным тестом. Модульный тест не похож на обычный тест, где у вас есть несколько шагов, которые вы собираетесь выполнить, и вы видите, работает ли программное обеспечение правильно или нет. В процессе написания модульного теста вы обнаруживаете, делает ли код то, что он должен или нет, и будете менять код до тех пор, пока тест не будет пройден.
Что такое TDD и модульное тестирование [перевод] - 5
Почему бы не написать модульный тест, и не проверить, проходит ли он? Если рассуждать так, то модульные тесты превращаются в некие абсолютные требования к определенным модулям кода на очень низком уровне. Вы можете считать модульный тест абсолютной спецификацией. Модульный тест определяет, что в этих условиях, с этим конкретным набором входных данных, есть результат, который вы должны получить от этого модуля кода. Истинное модульное тестирование позволяет определить наименьшую связную единицу кода, которая в большинстве языков программирования - по крайней мере, объектно-ориентированных - является классом.

Что иногда называют модульным тестированием?

Что такое TDD и модульное тестирование [перевод] - 6
Часто модульное тестирование путают с интеграционным тестированием. Некоторые «модульные тесты» проверяют более одного класса или тестируют большие единицы кода. Множество разработчиков утверждают, что они пишут модульные тесты, хотя на деле пишут whitebox-тесты на низком уровне. Не стоит спорить с этими ребятами. Просто знайте, что на самом деле они пишут интеграционные тесты, а настоящие модульные тесты изолированно от других частей проверяют наименьшую единицу кода. Еще одна вещь, которую часто называют модульным тестированием — модульные тесты без сверки с ожидаемым значением. Другими словами, модульные тесты, которые на самом деле ничего не тестируют. Любой тест, модульный он или нет, должен включать в себя некую проверку — мы называем её сверкой фактического результата с ожидаемым. Именно эта сверка и определяет, проходит тест или терпит неудачу. Тест, который всегда проходит, бесполезен. Тест, который всегда терпит неудачу, бесполезен.

Ценность модульного тестирования

Почему я — страстный приверженец модульного тестирования? Почему вредно называть «модульным тестированием» обобщенное тестирование, которое включает в себя проверку не наименьшего блока, изолированного от другого кода, а большего куска кода? В чём беда, если часть моих тестов не сверяют полученный и ожидаемый результаты? Они, по крайней мере, выполняют код. Попытаюсь объяснить.
Что такое TDD и модульное тестирование [перевод] - 7
Существует две основных причины для проведения модульного тестирования. Первая — улучшить дизайн кода. Помните, как я сказал, что модульное тестирование — это не вполне тестирование? Когда вы пишете правильные модульные тесты, вы вынуждаете себя изолировать наименьшую единицу кода. Эти попытки приведут к тому, что вы можете обнаружить проблемы в структуре самого кода. Вам может быть очень сложно изолировать проверочный класс и не включать его зависимости, и это может заставить вас понять, что ваш код слишком тесно связан. Вы можете обнаружить, что базовая функциональность, которую вы пытаетесь протестировать, распространяется на несколько модулей, что приведёт к мысли о недостаточной когерентности кода. Садясь за написание модульного теста, вы внезапно можете обнаружить (и поверьте, так бывает!), что вы понятия не имеете, что должен делать код. Соответственно, вы никак не сможете написать для него модульный тест. И, конечно, вы можете найти реальную ошибку в реализации кода, поскольку модульный тест заставляет вас думать о нестандартных вариантах и проверять разные наборы входных данных, которые вы, возможно, не учли.
Что такое TDD и модульное тестирование [перевод] - 8
Если при создании модульных тестов вы строго придерживаетесь правила «тестируем самую маленькую единицу кода изолировано от других», вы непременно обнаружите всевозможные проблемы с этим кодом и дизайном этих модулей. В жизненном цикле разработки программного обеспечения модульное тестирование является скорее оценочной деятельностью, чем тестирующей. Вторая основная цель модульного тестирования — создать автоматизированный набор регрессионных тестов, который может работать как спецификация поведения программного обеспечения на низком уровне. Что это значит? Когда вы месите тесто, вы его не ломаете. С этой точки зрения, модульные тесты — это тесты, конкретнее — регрессионные тесты. Однако цель модульного тестирования состоит не в том, чтобы просто строить регрессионные тесты. На практике модульные тесты крайне редко отлавливают регрессии, так как изменение единицы кода, который вы тестируете, почти всегда содержит изменения самого модульного теста. Регрессионное тестирование намного эффективнее на более высоком уровне, когда тестируется код, как «чёрный ящик», потому что на этом уровне внутренняя структура кода может быть изменена, в то время как внешнее поведение, как ожидается, останется прежним. Модульные тесты в свою очередь проверяют внутреннюю структуру, поэтому, когда эта структура изменяется, модульные тесты не терпят неудачу. Они становятся неприменимыми, и теперь их нужно изменить, выбросить или переписать. Теперь вы знаете больше об истинной цели модульного тестирования, чем очень многие ветеранов разработки программного обеспечения.

Что такое разработка через тестирование (TDD)?

Что такое TDD и модульное тестирование [перевод] - 9
В процессе разработки ПО хорошая спецификация — на вес золота. Подход TDD заключается в том, что прежде, чем написать какой-то код, вы сначала пишете тест, который будет служить спецификацией, то есть определять, что должен делать этот код. Это чрезвычайно мощная концепция разработки программного обеспечения, но зачастую её неправильно используют. Обычно применение концепции «разработка через тестирование» означает использование модульных тестов для управления созданием кода приложения. Но на самом деле этот подход можно применять на любом уровне. Однако в этой статье мы будем считать, что применяем модульное тестирование для нашего приложения. Подход TDD переворачивает всё с ног на голову, и вместо того чтобы сначала писать код, а затем писать модульные тесты для проверки этого кода, вы сначала напишите модульный тест, а затем напишите код, чтобы этот тест стал зелёным. Таким образом, модульное тестирование «управляет» разработкой кода. Этот процесс повторяется снова и снова. Вы пишете еще один тест, который определяет больше функциональности того, что должен делать код. Затем вы пишете и модифицируете код, добиваясь успешного завершения теста. После того, как вы получили «зелёный» результат, вы приступаете к рефакторингу кода, то есть реорганизуете или очищаете его, чтобы сделать более кратким. Часто эту цепочку процессов называют «Красный-Зелёный-Рефакторинг» потому что сначала модульный тест не проходит (красный), затем пишется код, подстраиваясь под тест, добиваясь, чтобы он успешно завершился (зелёный), и, наконец, код оптимизируется (рефакторинг).

Что является целью TDD?

Что такое TDD и модульное тестирование [перевод] - 10
Подход «разработка через тестирование» (TDD), как и модульное тестирование, может быть использовано неправильно. Очень легко назвать то, что вы делаете «TDD», и даже следовать практике, при этом не понимая, почему вы поступаете именно так. Самая большая ценность TDD заключается в том, что тесты проводят для получения качественных спецификаций. TDD — это, по сути, практика написания точных спецификаций, которые могут быть автоматически проверены до написания кода. Тесты — это лучшие спецификации, потому что они не лгут. Они не скажут вам после двух недель мучения с кодом «я имел в виду совершенно не это». Тесты, если они правильно написаны, либо успешно выполняются, либо терпят неудачу. Тесты недвусмысленно указывают, что именно должно происходить при определенных обстоятельствах. Таким образом, цель TDD — дать нам полное понимание того, что нам нужно реализовать до того момента, как мы начали реализовывать. Если вы начинаете разработку с TDD, и не можете понять, что именно тест должен проверить, значит, вам нужно задать больше вопросов. Другая важная роль TDD заключается в сохранении и оптимизации кода. Поддержка кода — дорогое удовольствие. Я часто шучу, что лучший программист — тот, кто напишет самый краткий код, который решит какую-то задачу. Или даже тот, кто докажет, что эту задачу решать не нужно, и тем самым полностью удалит код, поскольку именно этот программист нашел верный способ уменьшить количество ошибок и снизить стоимость обслуживания приложения. Используя TDD, вы можете быть абсолютно уверены, что не пишете никакого ненужного кода, поскольку вы будете писать код только для прохождения тестов. Существует принцип разработки программного обеспечения под названием YAGNI (you ain’t going to need it), или «вам это не понадобится». TDD предотвращает YAGNI.

Типичный рабочий процесс разработки через тестирование (TDD)

Что такое TDD и модульное тестирование [перевод] - 11
Понять смысл TDD с чисто академической точки зрения сложно. Поэтому давайте рассмотрим пример TDD-сессии. Представьте себе, что вы садитесь за стол и быстренько делаете набросок того, что, по вашему мнению, будет высокоуровневым дизайном функции, позволяющей пользователю входить в приложение и изменять свой пароль, если он его забудет. Вы решаете, что начнете с первой реализации функции входа в систему, создав класс, который будет обрабатывать всю логику для процесса входа в систему. Вы открываете свой любимый редактор и создаете модульный тест, который называется «Пустой логин не позволяет пользователю войти в систему». Вы пишете код модульного теста, который сначала создает экземпляр класса Login (который вы еще не создали). Затем вы пишете код для вызова метода в классе Login, который передает пустое имя пользователя и пароль. Наконец, вы пишете сверку с ожидаемым результатом, проверку, что пользователь с пустым логином действительно не вошел в систему. Вы пытаетесь запустить тест, но он даже не компилируется, потому что у вас нет класса Login. Вы исправляете эту ситуацию, и создаете класс Login вместе с методом в этом классе для входа в систему, а другой — для проверки состояния пользователя, чтобы узнать, вошли ли они в систему. Пока что вы не реализовали функциональность этого класса и нужный нам метод. Вы запускаете тест на этом этапе. Теперь он компилируется, но сразу же выдает fail.
Что такое TDD и модульное тестирование [перевод] - 12
Теперь вы возвращаетесь к коду, и реализуете функциональность, чтобы пройти тест. В нашем случае, это означает, что мы должны получить результат: «пользователь не вошёл в систему». Вы снова запускаете тест, и теперь он проходит. Переходим к следующему тесту. Теперь представим, что вам нужно написать тест под названием «Пользователь вошел в систему, если он ввёл действительное имя пользователя и пароль». Вы пишете модульный тест, который создает экземпляр класса Login и пытается войти в систему с именем пользователя и паролем. В модульном тесте вы пишете утверждение, что класс Login должен дать утвердительный ответ на вопрос, вошёл ли пользователь в систему. Вы запускаете этот новый тест, и, конечно же, он терпит неудачу, потому что ваш класс Login всегда возвращает, что пользователь не вошел в систему. Вы возвращаетесь в свой класс Login и реализуете некоторый код для проверки входа пользователя в систему. В этом случае вам придется выяснить, как изолировать этот модуль. На данный момент самый простой способ проделать это — жестко указать имя пользователя и пароль, которые вы использовали в своем тесте, и если они соответствуют, то выдать результат «пользователь вошёл в систему». Вы вносите это изменение, выполняете оба теста, и они оба проходят. Приступаем к последнему шагу: вы смотрите на созданный код, и ищете способ его реорганизации и упрощения. Таким образом, алгоритм TDD:
  1. Создали тест.
  2. Написали код под этот тест.
  3. Зарефакторили код.

Выводы

Что такое TDD и модульное тестирование [перевод] - 13
Это всё, что я хотел рассказать о модульном тестировании и TDD на этом этапе. На самом деле есть много сложностей, связанных с попытками изолировать модули кода, поскольку код бывает очень сложный и путанный. Очень мало классов существует в полной изоляции. Вместо этого у них есть зависимости, и эти зависимости имеют зависимости и так далее. Чтобы справиться с такими ситуациями, ветеран TDD использует макеты-пустышки (Mock), которые помогают изолировать отдельные классы, подменяя объекты в зависимых модулях. Эта статья — лишь обзорное и несколько упрощённое введение в модульное тестирование и TDD, мы не будем вдаваться в подробности о модулях-пустышках и других методах TDD. Идея состоит в том, чтобы дать вам основные концепции и принципы TDD и модульного тестирования, которые, надеюсь, у вас теперь есть. Оригинал — https://simpleprogrammer.com/2017/01/30/tdd-unit-testing/