Дайте-ка угадаю: вы согласны с тем, что писать тесты — это хорошо. Это повышает надежность системы, ускоряет разработку, проект с хорошим тестовым покрытием поддерживать легко и приятно, а TDD — это вообще почти идеал процесса разработки. Но не у вас в проекте. То есть, оно клёво, но, к сожалению, сейчас столько работы — просто завал. Куча задач, одних только критических багов — два десятка, плюс надо срочно дописать этот модуль и еще написать письмо заказчику… Так что тесты, наверное, будем прикручивать уже в конце, если время останется. Или в следующем проекте. Нет, ну там точно полегче будет. Скорее всего.
Как, узнали ситуацию?
Так вот — чушь всё это. Сфера ИТ — бесконечна, как вселенная, куча работы будет всегда. Можно или начать писать тесты прямо сейчас, или не сделать этого никогда. Я тут набросал короткий план, как начать это делать за 10 шагов, по шагу в день, по 10 минут на шаг. И когда я говорю «10 минут» я имею в виду не «3 с половиной часа» и не «ну сколько-то времени, лучше побольше», а именно 600 секунд. Если у вас нету в день 600 секунд свободного времени — срочно меняйте проект, работу, профессию, страну проживания (нужное подчеркнуть), потому что это не жизнь, а каторга какая-то. Поехали.
Не вздумайте начинать писать собственный фреймворк с нуля — оно вам надо? Тратить неделю на выбор оптимального фреймворка (да, я видел такую оценку времени на это в планах) — тоже глупо. Вот вам рецепт: набирайте в Гугле best test framework for %language% site:stackoverflow.com. Открываете первые 5 ссылок. Закрываете те из них, где рейтинг вопроса или первого ответа около нуля. Из оставшихся вкладок можно смело брать любой рекомендованный фреймворк из первой тройки с максимальным рейтингом. С вероятностью в 99.5% он вам подойдет. Поскольку на данный шаг вы пока потратили минуты 3, то оставшиеся 7 можно потратить на то, чтобы перейти на сайт фреймворка и посмотреть примеры его использования. Скорее всего, там всё будет просто и понятно (иначе он не был бы в топе рекомендаций). Но если вдруг нет — выберите другой по тому же алгоритму.
Написать Hello, world! нам раз плюнуть. Вот, например, на С++.
А теперь сделаем две вещи.
Во-первых, вынесем генерацию выводимого текста в отдельные функции. Да, в две. Это для того, чтобы потом их можно было тестировать.
Во-вторых, вынесем написанные функции куда-нибудь из данного файла. В зависимости от подхода и применяемого языка это могут быть просто отдельные файлы кода или библиотека. Это нужно для того, чтобы потом эти функции вызывать из тестов.
У нас будет так:
О подключении фреймворка к проекту наверняка очень хорошо написано на сайте фреймворка. Или на stackoverflow. Или на Хабре. Вот я, к примеру, когда-то описывал подключение Google Test. Обычно всё сводится к созданию нового проекта консольного исполняемого приложения (в скриптовых языках — отдельного скрипта), подключению к нему фрейворка парой include (import\using), подключению к проекту тестируемого кода (включением самих файлов с кодом или подключением библиотеки) — ну и всё. Если вы не верите, что этот шаг можно сделать за 10 минут — откройте Youtube, напишите в поиск название своего фреймворка и пронаблюдайте 20 видеороликов примерно одинакового содержимого, которые это доказывают.
Для начала нам нужно выяснить:
На эти вопросы обычно отвечают все те же самые статьи, о которых я говорил выше. Не лезьте сразу в дебри фреймворка. Начните с простых тестов, которые проверяют простые вещи. Тут ведь как в спорте — не надо сразу рвать большой вес, нужно сначала поставить правильную технику.
Вот, к примеру, пару тестов для нашего Hello world! на упомянутом выше Google Test:
Мы уже умеем подключать фреймворк к проекту. Помните, делали на шаге №3? Всё получилось. Теперь давайте сделаем это для боевого проекта. Положите все необходимые файлы фреймворка себе под SVN\Git\TFS\чего-у-вас-там. Сделайте тестовый проект. Подключите к нему фреймворк. Включите сборку тестового проект в процесс сборки вашего продукта. Проверьте сборку в дебаг и релиз-конфигурациях. Комитните тестовый проект, запустите сборку на билд-сервере. Всё должно быть ок. Не нагружайте пока ваших коллег появлением тестового проекта — во-первых, вы ничего не сломали, во-вторых, хвастаться вам тоже пока нечем.
Вы помните, каким образом мы выше вынесли из Hello world! часть функционала во внешний код? Обратите внимание, какими получились эти функции: они не зависят ни от глобальных переменных, ни от состояния каких-то объектов, ни от внешних данных из файлов или баз данных. Резальтат зависит только от переданных аргументов. Найдите в своём проекте что-то аналогичное. Наверняка ведь у вас есть какие-нибудь функции конвертации чего-то куда-то, сериализации\десериализации, упаковки\распаковки, шифрования\дешифрования и т.д. Не думайте пока о том, насколько нужный и полезный функционал вы тестируете. Ваша задача — написать простой тест, но для боевого проекта. Запустить, увидеть «1 тест успешно пройден».
Кстати, именно на этом этапе очень часто к скептикам приходит озарения. Вдруг оказывается, что самый простой тест, на самую элементарную функциональность — вдруг провалился. Лезем в код — и вдруг находим что-то типа
И оказывается, что основной код просто пока вызывал эту функцию с такими аргументами, что всё было ок, но в любой момент это могло измениться.
Вы уже умеете тестировать простые вещи. Теперь разберитесь как тестировать что-то, имеющее внешние зависимости. Посмотрите, как ваш фреймворк предлагает делать подготовку к запуску теста и очистку после него. Разберитесь, что такое моки и стабы. Подумайте как протестировать какой-нибудь ваш код, читающий данные из файла или из базы. Легко ли подменить источник входных данных? Может быть стоит слегка изменить код, чтобы это стало легче? Сделайте это, если нужно. Напишите для этого кода тест.
Как обычно выглядит ваша работа над багом? Вы берёте его из багтрекера, пробуете воспроизвести, если не получается — возвращаете тестеру, если получается, занимаетесь отладкой для понимания его местоположения, находите кусок кода с ошибкой, исправляете, тестируете, отдаёте тестеру. Отлично. А теперь попробуйте при работе над следующим багом между шагами «находите ошибку» и «исправляете» добавить ещё один шаг — написать тест на эту ошибку. Такой, чтобы он падал для текущего кода. Это огромное кайф, исправить код — и не лезть тестировать его руками, а запустить падавший пару минут назад тест и увидеть «успешно» на его выходе. Кроме этого эстетического удовольствия, этот тест можно отдать тестеру и использовать в дальнейшем для регрессионного тестирования (а ещё — для тестирования побочных веток продукта, проекта «в поле», и т.д.). Конечно, не всё и не всегда можно так протестировать, бывает тяжело с UI, с кроссбраузерностью, с многопоточностью. Не заморачивайтесь в случае, если написание теста займёт у вас много-много часов. В конце-концов, эта технология ведь призвана облегчить вашу жизнь, а не заставить плясать под свою дудку.
Как обычно выглядит ваша работа при разработке нового функционала? Наверное, вы сначала думаете. Потом проектируете то, что будете делать — набрасываете названия интерфейсов, классов, потом названия методов, наполняете их реализацией, запускаете, отлаживаете. Отлично, менять почти ничего не надо. Просто в тот момент, когда у вас уже есть интерфейсы, классы и названия методов, но еще нет их реализации — напишите для них тесты. Простенькие — вызвали метод — проверили результат. Обратите внимание, как уже на этом этапе вы заметите нелогичность некоторых имён, недостаток или излишество аргументов в методах, ненужные или отсутствующие зависимости и т.д.. При этом сейчас пока что это исправить — почти ничего не стоит (ведь реализация ещё не написана). Подправили архитектуру, дописали тесты, запустили — увидели кучу проваленных тестов. Отлично, так и должно быть. Написали реализацию, запустили тесты — увидели большинство из них пройденными, исправили ошибки, добились успешного прохождения всех тестов — отлично, дело сделано. Вы чувствуете, как хорошо стало, какое моральное удовлетворение вы получили? Оно слегка напоминает удовольствие от получения какой-то ачивки в игре. А почему? А потому, что его можно измерить! «Код проходит 18 тестов при тестовом покрытии в 90%» — это звучит круче, намного круче чем «ну, фича вроде бы реализована, я так потыкал немножко, кажется, не падает». Это даёт право гордится. Идешь домой — и чётко понимаешь, что-то за день сделал полезное, это «что-то» измеримо, ощутимо, реально.
В тестах мало смысла, если их не запускать. Запускать их вручную — долго и бессмысленно. Наверняка у вас есть билд-сервер с каким-нибудь TeamCity или CruiseControl, где собирается ваш продукт. Так вот, большинство хороших билд-серверов сразу, из коробки, поддерживают запуск тестов и даже парсят их логи и рисуют красивые отчёты. Соответствие тут, конечно, не «все совместимы со всеми», но если вы взяли тестовый фреймворк по совету в начале статьи — шансы на то, что всё заработает очень высоки. К примеру, упомянутые мною TeamCity и Google Test прекрасно дружат между собой.
Вот и решайте, стоило оно того или нет.
Где-то пункта после 8-го — хорошее время чтобы представить тестовый проект вашей команде. Объясните в 2-3 абзаца что и как, покажите простенький пример теста, заметьте, что, мол, «feel free to add your own tests», но особо не напирайте пока. Если у вас писать тесты было не принято, скорее всего первым впечатлением будет осторожный скепсис и непонимание. Это быстро лечится после второго-третьего упоминания на совещании о том, что, мол «а этот баг мы нашли благодаря тесту» или «а вот тут написан тест и мы сразу узнаем, если оно сломается снова». Программисты — народ рациональный, они поймут и подтянутся.
Как, узнали ситуацию?
![](https://habrastorage.org/storage2/d11/bd8/820/d11bd88203f9641e5547150c5633db6b.jpg)
1. Выбираем фреймворк для тестов
Не вздумайте начинать писать собственный фреймворк с нуля — оно вам надо? Тратить неделю на выбор оптимального фреймворка (да, я видел такую оценку времени на это в планах) — тоже глупо. Вот вам рецепт: набирайте в Гугле best test framework for %language% site:stackoverflow.com. Открываете первые 5 ссылок. Закрываете те из них, где рейтинг вопроса или первого ответа около нуля. Из оставшихся вкладок можно смело брать любой рекомендованный фреймворк из первой тройки с максимальным рейтингом. С вероятностью в 99.5% он вам подойдет. Поскольку на данный шаг вы пока потратили минуты 3, то оставшиеся 7 можно потратить на то, чтобы перейти на сайт фреймворка и посмотреть примеры его использования. Скорее всего, там всё будет просто и понятно (иначе он не был бы в топе рекомендаций). Но если вдруг нет — выберите другой по тому же алгоритму.
2. Пишем Hello world!
Написать Hello, world! нам раз плюнуть. Вот, например, на С++.
Hello world!
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!" << endl;
return 0;
}
А теперь сделаем две вещи.
Во-первых, вынесем генерацию выводимого текста в отдельные функции. Да, в две. Это для того, чтобы потом их можно было тестировать.
Hello world! после рефакторинга
#include <iostream>
#include <string>
using namespace std;
string GetHello()
{
return "Hello";
}
string GetAdressat(string adressat)
{
return adressat;
}
int main()
{
cout << GetHello() + " " + GetAdressat("world") + "!" << endl;
return 0;
}
Во-вторых, вынесем написанные функции куда-нибудь из данного файла. В зависимости от подхода и применяемого языка это могут быть просто отдельные файлы кода или библиотека. Это нужно для того, чтобы потом эти функции вызывать из тестов.
У нас будет так:
HelloFunctions.h
#include <string>
using namespace std;
string GetHello();
string GetAdressat(string adressat);
HelloFunctions.cpp
#include "HelloFunctions.h"
string GetHello()
{
return "Hello";
}
string GetAdressat(string adressat)
{
return adressat;
}
HelloWorld.cpp
#include <iostream>
#include "HelloFunctions.h"
using namespace std;
int main()
{
cout << GetHello() + " " + GetAdressat("world") + "!" << endl;
return 0;
}
3. Подключаем фреймворк к Hello world!
О подключении фреймворка к проекту наверняка очень хорошо написано на сайте фреймворка. Или на stackoverflow. Или на Хабре. Вот я, к примеру, когда-то описывал подключение Google Test. Обычно всё сводится к созданию нового проекта консольного исполняемого приложения (в скриптовых языках — отдельного скрипта), подключению к нему фрейворка парой include (import\using), подключению к проекту тестируемого кода (включением самих файлов с кодом или подключением библиотеки) — ну и всё. Если вы не верите, что этот шаг можно сделать за 10 минут — откройте Youtube, напишите в поиск название своего фреймворка и пронаблюдайте 20 видеороликов примерно одинакового содержимого, которые это доказывают.
4. Разбираемся с возможностями фреймворка
Для начала нам нужно выяснить:
- Как написать один юнит-тест
- Как запустить юнит-тесты
На эти вопросы обычно отвечают все те же самые статьи, о которых я говорил выше. Не лезьте сразу в дебри фреймворка. Начните с простых тестов, которые проверяют простые вещи. Тут ведь как в спорте — не надо сразу рвать большой вес, нужно сначала поставить правильную технику.
Вот, к примеру, пару тестов для нашего Hello world! на упомянутом выше Google Test:
#include "HelloFunctions.h"
#include "gtest/gtest.h"
class CHelloTest : public ::testing::Test {
};
TEST_F(CHelloTest, CheckGetHello)
{
ASSERT_TRUE(GetHello() == "Hello");
}
TEST_F(CHelloTest, GetAdressat)
{
ASSERT_TRUE(GetAdressat("world") == "world");
ASSERT_FALSE(GetAdressat("not world") == "world");
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
5. Подключаем фреймворк к настоящему проекту
Мы уже умеем подключать фреймворк к проекту. Помните, делали на шаге №3? Всё получилось. Теперь давайте сделаем это для боевого проекта. Положите все необходимые файлы фреймворка себе под SVN\Git\TFS\чего-у-вас-там. Сделайте тестовый проект. Подключите к нему фреймворк. Включите сборку тестового проект в процесс сборки вашего продукта. Проверьте сборку в дебаг и релиз-конфигурациях. Комитните тестовый проект, запустите сборку на билд-сервере. Всё должно быть ок. Не нагружайте пока ваших коллег появлением тестового проекта — во-первых, вы ничего не сломали, во-вторых, хвастаться вам тоже пока нечем.
6. Тестируем что-нибудь простое
Вы помните, каким образом мы выше вынесли из Hello world! часть функционала во внешний код? Обратите внимание, какими получились эти функции: они не зависят ни от глобальных переменных, ни от состояния каких-то объектов, ни от внешних данных из файлов или баз данных. Резальтат зависит только от переданных аргументов. Найдите в своём проекте что-то аналогичное. Наверняка ведь у вас есть какие-нибудь функции конвертации чего-то куда-то, сериализации\десериализации, упаковки\распаковки, шифрования\дешифрования и т.д. Не думайте пока о том, насколько нужный и полезный функционал вы тестируете. Ваша задача — написать простой тест, но для боевого проекта. Запустить, увидеть «1 тест успешно пройден».
Кстати, именно на этом этапе очень часто к скептикам приходит озарения. Вдруг оказывается, что самый простой тест, на самую элементарную функциональность — вдруг провалился. Лезем в код — и вдруг находим что-то типа
return 12; // TODO: implement later
И оказывается, что основной код просто пока вызывал эту функцию с такими аргументами, что всё было ок, но в любой момент это могло измениться.
7. Тестируем что-нибудь посложнее
Вы уже умеете тестировать простые вещи. Теперь разберитесь как тестировать что-то, имеющее внешние зависимости. Посмотрите, как ваш фреймворк предлагает делать подготовку к запуску теста и очистку после него. Разберитесь, что такое моки и стабы. Подумайте как протестировать какой-нибудь ваш код, читающий данные из файла или из базы. Легко ли подменить источник входных данных? Может быть стоит слегка изменить код, чтобы это стало легче? Сделайте это, если нужно. Напишите для этого кода тест.
8. Пишем тест на баг
Как обычно выглядит ваша работа над багом? Вы берёте его из багтрекера, пробуете воспроизвести, если не получается — возвращаете тестеру, если получается, занимаетесь отладкой для понимания его местоположения, находите кусок кода с ошибкой, исправляете, тестируете, отдаёте тестеру. Отлично. А теперь попробуйте при работе над следующим багом между шагами «находите ошибку» и «исправляете» добавить ещё один шаг — написать тест на эту ошибку. Такой, чтобы он падал для текущего кода. Это огромное кайф, исправить код — и не лезть тестировать его руками, а запустить падавший пару минут назад тест и увидеть «успешно» на его выходе. Кроме этого эстетического удовольствия, этот тест можно отдать тестеру и использовать в дальнейшем для регрессионного тестирования (а ещё — для тестирования побочных веток продукта, проекта «в поле», и т.д.). Конечно, не всё и не всегда можно так протестировать, бывает тяжело с UI, с кроссбраузерностью, с многопоточностью. Не заморачивайтесь в случае, если написание теста займёт у вас много-много часов. В конце-концов, эта технология ведь призвана облегчить вашу жизнь, а не заставить плясать под свою дудку.
9. Первый раз TDD
Как обычно выглядит ваша работа при разработке нового функционала? Наверное, вы сначала думаете. Потом проектируете то, что будете делать — набрасываете названия интерфейсов, классов, потом названия методов, наполняете их реализацией, запускаете, отлаживаете. Отлично, менять почти ничего не надо. Просто в тот момент, когда у вас уже есть интерфейсы, классы и названия методов, но еще нет их реализации — напишите для них тесты. Простенькие — вызвали метод — проверили результат. Обратите внимание, как уже на этом этапе вы заметите нелогичность некоторых имён, недостаток или излишество аргументов в методах, ненужные или отсутствующие зависимости и т.д.. При этом сейчас пока что это исправить — почти ничего не стоит (ведь реализация ещё не написана). Подправили архитектуру, дописали тесты, запустили — увидели кучу проваленных тестов. Отлично, так и должно быть. Написали реализацию, запустили тесты — увидели большинство из них пройденными, исправили ошибки, добились успешного прохождения всех тестов — отлично, дело сделано. Вы чувствуете, как хорошо стало, какое моральное удовлетворение вы получили? Оно слегка напоминает удовольствие от получения какой-то ачивки в игре. А почему? А потому, что его можно измерить! «Код проходит 18 тестов при тестовом покрытии в 90%» — это звучит круче, намного круче чем «ну, фича вроде бы реализована, я так потыкал немножко, кажется, не падает». Это даёт право гордится. Идешь домой — и чётко понимаешь, что-то за день сделал полезное, это «что-то» измеримо, ощутимо, реально.
10. Прикручиваем запуск тестов к CI-серверу
В тестах мало смысла, если их не запускать. Запускать их вручную — долго и бессмысленно. Наверняка у вас есть билд-сервер с каким-нибудь TeamCity или CruiseControl, где собирается ваш продукт. Так вот, большинство хороших билд-серверов сразу, из коробки, поддерживают запуск тестов и даже парсят их логи и рисуют красивые отчёты. Соответствие тут, конечно, не «все совместимы со всеми», но если вы взяли тестовый фреймворк по совету в начале статьи — шансы на то, что всё заработает очень высоки. К примеру, упомянутые мною TeamCity и Google Test прекрасно дружат между собой.
Послесловие
Дотошный читатель может заметить, что пункты начиная где-то с седьмого-восьмого скорее всего не впишутся в заявленные в заголовке «10 минут на шаг». Что тут можно сказать? Считайте, что я, нехороший человек, вас слегка наколол. Однако, если вы на практике с праведным негодованием прошли эти пункты, то:- У вас уже есть проект, к которому прикручены тесты. Они запускаются, работают, их больше нуля и они уже приносят вам пользу.
- Вы получили опыт во всём этом деле.
- Во второй раз у вас получится серьёзно быстрее.
Вот и решайте, стоило оно того или нет.
Где-то пункта после 8-го — хорошее время чтобы представить тестовый проект вашей команде. Объясните в 2-3 абзаца что и как, покажите простенький пример теста, заметьте, что, мол, «feel free to add your own tests», но особо не напирайте пока. Если у вас писать тесты было не принято, скорее всего первым впечатлением будет осторожный скепсис и непонимание. Это быстро лечится после второго-третьего упоминания на совещании о том, что, мол «а этот баг мы нашли благодаря тесту» или «а вот тут написан тест и мы сразу узнаем, если оно сломается снова». Программисты — народ рациональный, они поймут и подтянутся.