ТРИ СТОЛПА ЮНИТ-ТЕСТИРОВАНИЯ
Ну ладно, теперь мы знаем что такое TDD и для чего оно. Хотя, нет... На самом
деле мы знаем, как работает TDD, но вообщего-то TDD это про
тестирование. Если более точно, про юнит-тестирование. На что должны быть
похожи тесты? Сейчас разберем.
ЧТО ТАКОЕ ЮНИТ?
Почему подобные тесты называютя юнит-тестами? Ответ прост: потому, что
тестируется одна единца (юнит) кода. Очень хороший ответ, но что такое "единица" кода?
Метод, класс, модуль? Юнит - это просто наименьшая часть системы, которую вы
можете протестировать. Если планируете протестировать систему, то попробуйте
потестировать ее часть. А можете разделить часть на составляющие, которые тоже
можно потестировать? Если да, то значит часть еще не юнит, а что-то
покрупнее.В общем должно быть ясно... а если нет, то почитайте про рекурсию.
ЧТО ТАКОЕ ЮНИТ-ТЕСТ?
Хорошо, теперь у нас есть наименьшая часть, которую можно потестировать. Эта
часть может быть методом, классом, не важно. Во-первых: подумайте о простом, но
показательном сценарии использования этой части. Давайте представим сервис
авторизации. Простейшим сценарием будет: ввести правильное имя пользователя и
неправильный пароль (я не говорю, что это должно быть первым сценарием, только
одним из многих). Во-вторых, подумаем над ожидаемым поведением. Что должно
произойти при выполнении указанного сценария? Давайте предположим, что система
должна показать сообщение "Неправильный пароль". И это будет нашим тестом. Ах
да! А как мы получим существующего в базе данных пользователя? Нам нужно
подключиться к базе, файлу, ну, или где там данные хранятся... И теперь очень
важный момент: разделение ответственности, изоляция. Мы должны тестировать нашу
систему изолированной от "внешнего мира". Мы не хотим тестировать нашу базу
данных, нам нужно протестировать сервис авторизации. Так давайте используем
какие-нибудь фиктивные данные (про фэйки, стабы, моки и изоляцию я расскажу
как-нибудь в другой раз) для тестирования только нашего сервиса.
СТОЛП ПЕРВЫЙ: НАДЕЖНОСТЬ
Юнит-тесты должны быть нашим парашютом, страховочным канатом. А значит мы
должны доверять им, иначе они бесполезны. Если твои тесты не до конца надежны,
значит они тебе вообще не нужны. Это когда у тебя вечно зеленая полоса (индикатор
прохождения тестов в xUnit фрэймворках) или тесты не тестируют то, для чего предназначены.
Эпогей проблемы - после изменения в коде ты прогоняешь тесты, они
проходят, но ты не уверен, все ли хорошо, или что-то в системе все-таки
сломалось. Но если ты следуешь принципу "написал тест - тест упал - код
написал - тест прошел - выполнил рефакторинг", то ты можешь быть уверен, все с
тестами в порядке. Во-первых, ты видел свои тесты красными (каждый в свое
время). А во-вторых, ты знаешь, что они прошли, т.к. ты написал
соответствующий код. Теперь ты знаешь, что тесты все зеленые, т.к. твой модуль
в порядке. Не нужно его еще как-то проверять.
Совет: Пусть вам нужно изменять чей-то код (предположим, что код имеет
юнит-тесты :)). Вначале попробуйте удалить какую-нибудь строку кода или
изменить "если-для-пока" ветку и прогоните тесты. Если тесты прошли успешно -
дело фигово. Дырявый парашют вам попался, лучше с ним не прыгать. Или прыгать,
но не удивляться тому, как быстро земля приближается.
СТОЛП ВТОРОЙ: ПОДДЕРЖИВАЕМОСТЬ
Очевидно, что код нуждается в поддержке так же, как и код продукта. Если
меняется код продукта (из-за рефакторинга, исправления ошибки или добавления
новой функциональности) что-то в тестах скорее всего сломается (они даже могут не
скомпилироваться). Исправление тестов - важная задача! Чем больше тестов, тем
больше внимания они требуют. Мы можем уменьшить затрачиваемое на исправление
тестов время тем, что будем писать тесты только на публичные методы. Если вы
чувствуете, что нужно протестировать внутренний метод, то имеет место признак
плохого дизайна ("а код-то вонючий!"). Возможно функционально этот
внутренний метод связан еще с каким-то куском кода. Еще одна вещь, которую
что можно сделать с тестами - удалить дублирование кода; убрать изменяющийся код
(конструкторы, инициализации) в одно место. Мы можете использовать паттерн "Фабричный метод"
или обычный "set up" для ваших тестов. Если какие-то из тестов требуют
особенной инициализации переменных, разделите их на два тестовых класса (да,
два тестовых класса могут тестировать метод, почему нет?). Тесты должны
позволять выполнять рефакторинг также часто как и код.
СТОЛП ТРЕТИЙ: ЧИТАЕМОСТЬ
Последнее по порядку, но не по значимости - читаемость тестов. Юнит-тесты -
это выполняемая спецификация. И эта спецификация должна быть доступна для
понимания при прочтении. Наверное, самое главное - хорошие названия.
Я не могу сказать, что соглашение о названиях, которое использую я
самое лучше, но оно работает для меня. То же самое советует Roy Osherove. Вот
шаблон:
ЧтоТестируем_ПриКакихУсловиях_ЧтоОжидаемПолучить
Т.е. на примере сервиса авторизации:
Аутентификация_ВведенНекорректныйКод_ДолженВернутьСообщениеОНеправильномПароле.
Во-первых, почему имя тестового метода так важно? Потому что в каждом,
насколько я знаю, запускателе тестов это первая и наиболее заметная нформация об
упавшем тесте. Если вы последуете моему совету, то вам больше ничего не
понадобится. Вам будет понятно, в каком именно месте возникла проблема. Ни
дополнительные сообщения утверждений, ни комментарии больше не нужны.
Имя метода - вот что нужно. Не переживайте о возможно длинном названии тестового метода.
Следующим важным аспектом является использование одной проверки в одном тесте. Это очень сильно
связано с именем тестового метода. Если у вас несколько проверок в тесте, то
как его назвать? Если возникают трудности с названием метода, то это явный
намек на то, что тест корявый. А если в тесте используется единственное
утверждение, то зачем тогда нужно дополнительное сообщение в утверждении?
Смысл утверждения ясен из тестового метода, в котором использовано
утверждение.
Еще очень рекомендуется использовать паттерн "ArangeActAssert" (подготовил,
выполнил, проверил) для организации структуры тестов. Он определяет построение теста из 3
частей. Первая - "подготовил". В этой части выполняются все приготовления для
следующего выполнения: инициализации, подготовка заглушек и прочее. Вторая
часть - "выполнил". Здесь вызывается тестируемые метод. И третья часть -
"проверил". Это место для утверждения. Все три части можно выделять
комментариями или областями, но в принципе в этом не дожно быть необходимости.
Я разделяю эти части пустыми строками. И при отсутствии пустых строк в самих
частях теста этого для меня достаточно.
Если вас заинтересовала тема, рекомендую посмотреть видео Roy Osherove с
конференции NDC2008
(http://osherove.com/videos/2009/8/25/unit-testing-best-practices.html).