Что такое unit test
Юнит тесты. Первый шаг к качеству
Однажды меня попросили рассказать о юнит тестировании в javascript, но прежде чем рассказывать о тестировании в мире front-end, надо было сделать небольшой обзор юнит тестирования как такового. В результате чего на свет и появилась эта статья, в которой я попытался рассказать о самых важных моментах в юнит тестировании.
Несмотря на различные трактовки юнит тестирования, есть несколько вещей которые объединяют этот термин.
Но есть моменты, в определении юнит тестирования, которые до сих являются спорными. В частности, что рассматривается под юнитом (единицей тестирования)? Подход ООП рассматривает класс как юнит, процедурный (или функциональный) подход, рассматривает одну функцию как юнит. Некоторые разработчики берут несколько классов и считают это юнитом, или берут набор методов в качестве юнита. Но на самом деле это ситуационная вещь, команда сама решает, что должно быть единицей тестирования в их системе.
Преимущества юнит-тестирования очевидны:
Важное различия в юнит тестировании, это какой тип тестирования вы выберите: Solitary (одинокий) и Sociable (общительный) тест. Термины впервые ввел Jay Fields.
Sociable (общительный) тест — это тест который использует реальные методы (или классы), которые входят в тестируемую единицу. Например, вы тестируете метод «цена» из класса заказов. Методу «цена» необходимо вызвать методы из класса клиент и продукт. В данном виде тестов будут вызваны именно эти методы, и ошибка в этих методах приведет к ошибке теста. Методы из классов клиент и продукт называется партнеры (collaborators).
Solitary (одинокий) тест — это тест, который в качестве партнеров использует дубли (TestDouble). Тест-дубли — это общий термин для любого случая, в котором вы заменяете реальный объект, исключительно для целей тестирования.
Хорошую классификация дублей сделал Жерар Мезарос (Gerard Meszaros), более подробно об этом можно почитать здесь
Каждый из этих методов тестирования имеет свои достоинства и недостатки, и между сторонниками этих двух методов ведутся горячие споры. Сторонников Solitary (одинокий) тестов также условно называют Mock-исты (Mock — подделка), а сторонников Sociable (общительный) тестов условно называют Classicists (не смог найти аналогов в русском языке). Хочется отметить, что сторонники Sociable (общительного) тестирования, также используют дубль-тесты для доступа к внешним ресурсам, например, к БД. Отчасти, это делается по причине скорости доступа. Но использовать дубли для доступа к внешним ресурсам это не абсолютное правило, если доступ к ним стабилен и достаточно быстр, то можно обойтись и без дублей. В любом случае разработчик сам решает, когда ему лучше применить дубли.
Одно из достоинств техники тестирования Solitary (одинокий) в том, что разработчики фокусируются на поведении приложения, а не на состоянии. Недостаток в том, что подделки могут замаскировать ошибку, которая присутствует в методе-партнере. Поэтому, при использовании тестов-дублей необходимо выполнять интеграционное тестирование. Достоинство тестирования Sociable (общительный) в том, что это уже по сути начальное интеграционное тестирование, но недостаток в том, что если упадет один метод это приведет к падению всех тестов, связанных с этим методом, что затрудняет отладку.
Я не буду подробно останавливаться на достоинствах и недостатках того или иного подхода в тестировании, об этом можно почитать у Фаулера в статье Mocks Aren’t Stubs
Основные свойства unit-тестирования – это небольшой объем, сделанный самим программистом, и скорость – что означает, что они могут выполняется часто во время программирования.
Разработчики могут выполнять их после любого изменения в коде. Но не обязательно запускать всегда все тесты, достаточно выполнить только те тесты, которые взаимодействуют с кодом, над котором вы трудитесь в текущий момент.
В конце 1990-х годов Кент Бек разработал технику «разработка через тестирование» (Test-Driven Development, TDD), как часть экстремального программирования. Эта техника для построения ПО, которая управляет процессом разработки через написание тестов. В сущности, повторяет три простых правила:
Написание теста первым дает два преимущества:
1. Это способ получить само-тестируемый код
2. Думая сначала о тесте вы заставляете себя думать об интерфейсе самого кода. Эта фокусировка на интерфейсе и на том как вы используете класс помогает вам разделить интерфейс от реализации.
Самая большая ошибка при использовании данной методологии — это пренебрежение третьим шагом, рефакторинг. Это приводит к тому, что код будет “грязным” (но по крайней мере, будут тесты).
BDD (Behaviour Driven Development) или разработка на основе поведения, появилось в процессе эволюции unit-тестирования и разработана Дэном Нортом (Dan North) в 2006г. Как утверждает сам автор, методология должна помочь людям изучить TDD. Она появилось из agile практик и предназначена сделать их более доступными и эффективными для команд-новичков в Agile.
Со временем, BDD стало охватывать более широкую картину agile-анализа и автоматическое приемочное тестирование.
Это привело к тому, что сами тесты стали переименовывать в поведение (спецификации), что позволило сфокусироваться на том, что объекту нужно сделать. Таким образом, разработчики стали создавать для себя документацию и записывать названия тестов в виде предложений. Они обнаружили, что созданная документация, стала доступна бизнесу, разработчикам и тестерам.
Считается, что разработка на основе поведения одно из ответвлений Mock-стилей (или Solitary-тест), т.е. тесты преимущественно строятся с использованием дублей.
Позднее, появился стиль написания тестов Given-When-Then, или, как его стали называть, спецификация поведения системы. Стиль был разработан Дэном Нортом (Dan North), совместно с Крисом Маттисом (Chris Matts). Идея заключается в том, чтобы разбить написание тестового сценария на три раздела:
Описание: Пользователь продает акции.
Сценарий: Пользователь запрашивает продажу до закрытия торгов
Дано (Given): У меня есть 100 акций MSFT и 150 акций APPL и время до закрытия торгов.
Когда (When): Я прошу продать 20 акций MSFT
Тогда (Then): У меня должно остаться 80 акций MSFT и 150 акций APPL и заявка на продажу 20 акций должна быть выполнена.
Не взирая на то, что с момента появлений методологий TDD и BDD прошло довольно много времени, многие разработчики до сих пор спорят друг с другом о целесообразности их применения. Кто-то утверждает, что нет необходимости писать тесты перед кодом, другие заявляют, что написание тестов после кода бессмысленно. Но и та и другая стороны согласны в одном, что тесты нужно писать! Методология BDD с точки зрения программистов, как утверждает сам ее автор (BDD IS LIKE TDD IF…), не отличается от TDD. Там используются все те же правила, что и в TDD: тест, код, рефакторинг. Отличие заключается в том, что BDD охватывает более широкую публику. Спецификации становятся доступными не только программистам, но и людям, не разбирающимся в коде, но имеющим отношение к разработке ПО. Таким образом, в процесс создания тестов подключается вся команда: аналитики, тестеры, менеджеры.
Одно очевидное преимущество юнит-тестов в том, что они могут радикально уменьшить число ошибок, которые попадают в продукт. В основе этого лежит культура, в результате которой разработчики думают о написании кода и тестов вместе.
Но самое большое преимущество не в том, чтобы просто избегать ошибок в продукте, а в уверенности в том, что вы можете вносить изменения в систему. Старый код часто является ужасной картиной, где разработчики боятся его менять. Даже исправление одной ошибки может быть опасно, т.к. вы можете создать больше ошибок, чем исправите. В таких случаях, добавление новых возможностей происходит очень медленно, вы также боитесь сделать рефакторинг системы, увеличивая тем самым технический долг (TechnicalDebt) и попадаете в плохую спираль, где каждое изменение заставляет людей боятся еще большего изменения.
С тестами другая картина. Здесь люди уверены, что фиксация ошибок, может быть сделана безопасно, потому что, если вы допустили оплошность, то детектор ошибок сработает, и вы можете быстро восстановить и продолжить. С помощью этой системы безопасности, вы можете всегда поддерживать код в хорошей форме и уже не окажетесь в плохой спирали.
В качестве детектора ошибок (само-тестируемая система) выступает процесс выполнения серии автоматических тестов (не только юнит), и вы уверены, что тесты пройдут и ваш код не содержит существенных дефектов. Если кто-то в команде случайно сделает ошибку, сработает детектор. Выполняя тесты часто, несколько раз в день, вы можете обнаружить ошибки сразу после их появления, поэтому вы можете просто посмотреть последние изменения, что значительно облегчает поиск ошибок. Никакой программный эпизод не завершен без рабочего кода, и тестов, поддерживающих его работу.
Само тестируемая система – это часть Continuous Integration (непрерывная интеграция) и Continuous Delivery (непрерывная доставка), но это тема уже выходит за рамки данной статьи.
Одним из важных действий команды, которая практикует различные тесты – это реакция на ошибку в продукте. Обычная реакция команды, это сначала написать тест, чтобы обнажить ошибку, и только потом попытаться исправить ее. Часто написанием этого теста будет серия тестов, которая постепенно сужает область действия до тех пор, пока вы не добьетесь юнит-теста, который эмулирует ошибку. Эта техника гарантирует, что после исправления ошибки, она останется фиксированной. Позиция должна быть в том, что любая ошибка, это не просто провал в коде, это также провал в защите тестирования.
В качестве детектора ошибок или автоматических тестов, выступают не только unit-тесты, но также интеграционные тесты, и другие автоматические тесты. Но unit-тесты здесь играют основу, т.к. написать их просто и выполняются они очень быстро.
Высокоуровневые тесты — это вторая линия обороны. Если вы получили ошибку в высокоуровневом тестировании, то это не просто ошибка в коде, это отсутствующий или некорректный юнит тест!
Unit-тестирование от начинающего начинающим
На написание статьи меня сподвигнул этот пост. В нём приведено описание инструментов и некоторая теоретическая информация.
Сам я только начинаю разбираться в unit-тестировании и тестировании вообще, поэтому решил поделиться некоторой информацией касательно этого дела. А также систематизировать свои знания и навыки. Далее постараюсь объяснить процесс тестирования по шагам простым обывательским языком, так как нигде в интернете не нашёл разжёванного описания, по шагам так сказать. Кому интересно и кто хочет попробовать всё-таки разобраться, добро пожаловать.
Что такое автоматизированное тестирование и unit-тестирование я писать не буду, для этого есть википедия.
Для наших тестов будем использовать, наверное самый популярный фрэймворк – PHPUnit. Для начала нам необходимо его утановить. Делать это проще всего через PEAR. Как это сделать, написано в документации. Используется две команды(из документации):
Естественно, путь к PEAR должен быть прописан в PATH. Когда загрузятся необходимые файлы, наш PHPUnit будет полностью готов к тестированию нашего кода.
Let’s Rock
Итак, начнём. Пусть у нас будет какая-то модель данных. В ней два атрибута: строка и число. Есть метод-сеттер и методы для сохранения и загрузки значений (в файл).
Мы определили базовые методы и атрибуты классов. Так как у нас пока ничего не читается и не пишется, по условию возвращаем false.
Теперь отложим на время нашу модель и займёмся тестом. Тест представляет собой обычный класс, унаследованный от базового класса (в нашем случае PHPUnit_Framework_TestCase). Методы этого класса, и есть тесты. Создадим папку unit для нашего теста.
TestModelTest — наш тест-класс для класса TestModel.
testTrue() — непосредственно тест. В нём мы определяем сценарии для конкретных случаев. В данном тесте мы просто проверим, что true является true 🙂 Это делается при помощи метода assertTrue (assert-англ-утверждать). Т.е. мы утверждаем, что true является истинной.
Запустим наш тест. PHPUnit достаточно указать папку, в которой лежат все наши тесты.
Ура, наш тест работает! Идём далее.
TDD – Test Driven Development – подход, при котором, грубо говоря, сначала пишутся тесты, а потом постепенно, исходя из них, пишется основной класс. Подробнее в википедии. Пойдём этим путём. Каркас модуля у нас уже есть. Требования тоже. Теперь напишем тестовые случаи, исходя из наших требований.
Мы описали все три случая в трёх методах. Для каждого свой. Теперь запустим тесты:
Damn! Ну ничего, так и должно быть 🙂 Теперь добавим немного кода в нашу модель.
Думаю, в коде ничего не должно вызывать затруднений.
Уже лучше. Уже проходит в два раза больше проверок. Идём по порядку:
1. testStringCannotBeEmpty. Строка не может быть пустой. Добавляем проверку:
2. testIntMustBeGreaterThanTenAdnSmallerThanTwenty. Условие 10 setAttributes(20,’test3′); Мы не рассмотрели крайний случай! Исправляем:
Запускаем наши тесты:
Ура, все три теста прошли. Наша модель удовлетворяет поставленным требованиям. Что и требовалось 🙂
Что такое юнит-тесты и почему они так важны
Бывает, кодишь 10 минут, а дебажишь 2 часа. Чтобы такого не случилось, пилите юнит-тесты. Михаил Фесенко рассказал, как их правильно готовить.
Oli Scarff / Staff / GettyImages
Фесенко Михаил, можно просто Фес. Разработчик, раньше работал системным администратором, пишет на чём скажут, но пока писал на PHP, Go, Python, Bash. Сейчас работает в «Яндекс.Облаке», до этого работал во «ВКонтакте». Любит жену, кино и снимать видео =)
Юнит-тест (unit test), или модульный тест, — это программа, которая проверяет работу небольшой части кода. Разработчики регулярно обновляют сайты и приложения, добавляют фичи, рефакторят код и вносят правки, а затем проверяют, как всё работает.
Тестировать систему целиком после каждого обновления — довольно муторно и неэффективно. Поэтому обновлённые или исправленные части кода прогоняют через юнит-тесты.
Особенности юнит-тестов
На практике используют разные тесты — их разделяют по уровню абстракции с помощью пирамиды Майка Кона :
Чем выше тест в пирамиде, тем больше частей программы он затрагивает. Высокоуровневые тесты «ближе к бизнесу»: они проверяют бизнес-логику и пользовательские процессы. А те, что внизу пирамиды, помогают найти проблемы в отдельных частях кода. Например, какую-нибудь функцию, которая генерирует имя файла.
В отличие от них, юнит-тесты нужны в следующих случаях:
Некоторые программисты пишут только юнит-тесты, а на интеграционные или E2E-тесты жалеют времени. На самом деле нужно покрывать систему всеми видами тестов, чтобы знать, как взаимодействуют друг с другом разные части программы, какие промежуточные результаты они выдают. Но в то же время, если юнит-тесты показывают ошибку, её покажет и интеграционный, и E2E-тест.
Процесс юнит-тестирования
Для юнит-тестирования подключают тестовые фреймворки — они позволяют «мокать», то есть имитировать функции. В коде больших проектов много зависимостей: одна функция вызывает другую и влияет на разные части программы. Но, как правило, достаточно проверить функции «в вакууме», отдельно от остального кода. Для этого и нужен тестовый фреймворк — он моделирует условия, в которых функция А вызывает функцию Б изолированно от других функций.
Простой пример: у нас есть функция на Go, которая получает id бэкапа и возвращает имя бэкап-файла:
Протестируем её с помощью набора входных и выходных данных. Они должны учитывать все ситуации, поэтому не забываем про негативные кейсы — когда программа возвращает ошибку. Вот набор тестовых данных:
В первую очередь я прописал запрещённые данные (-1 и 0) и слишком большое значение (10200300). Когда пользователь их вводит, функция не должна возвращать результат. Вместо этого мы ждём сообщения об ошибке: BAD_ID или BACKUP_ID_TOO_BIG. Когда же функция получает валидный id, она выводит отформатированное имя файла, например Backup#000010.
А вот и код самого теста:
Порой код для тестирования даже больше основного — и это норма. Но иногда всё-таки стоит задуматься, на самом ли деле тест должен быть таким объёмным. Я бы посоветовал покрывать тестами только те фрагменты кода, которые вы планируете менять. Или сложные части, которые, скорее всего, придётся чинить или поддерживать.
Некоторые разработчики мокают всё подряд. Из-за этого тесты становятся хрупкими, а код — сложным и непонятным. На самом деле для юнит-тестирования достаточно лишь немного переписать код, а огромные функции лучше разбить на более мелкие.
В старой хорошей книге «Экстремальное программирование» есть классная мысль: сначала пишите тест, а только потом программу. Это клёвый подход, но не все могут так делать (а кто-то просто не хочет тратить время).
Как покрыть код юнит-тестами
Есть разработчики, которые не проводят модульное тестирование: «Ой, у нас большой проект, и переписать 1000 строк под тесты или замокать их — слишком запарно». На самом деле покрыть код тестами несложно. Вот несколько советов.
Написали код — напишите тест. Я видел много проектов, в которых юнит-тесты писали по принципу «новый код — новый тест». Думаю, это правильный подход, ведь, когда добавляешь в программу что-то новое, она часто ломается. К тому же, если писать тесты сразу, не придётся переворачивать весь код, когда он разрастётся.
Есть более жёсткий принцип: новый код без тестов на ревью не принимается. Конечно, он работает, если сроки не горят, — иначе программист рефакторит или покрывает его тестами позже.
Используйте тестовый фреймворк. В тестировании не нужно изобретать велосипед. Для популярных языков уже есть готовые решения, поэтому достаточно вбить в поиске test frameworks, и вы получите целый список. Вот, например, результат для Python:
Пишите простые тесты. Надо понимать, что происходит с входными данными и какой результат должна вернуть функция. Если непонятно — меняем нейминг и разбиваем функции на более мелкие, избавляемся от зависимостей. Пусть одна функция принимает результат, а другая возвращает. Так проще тестировать.
Допустим, у нас есть такая функция:
Её не нужно прогонять через юнит-тест, потому что тогда придётся мокать process_a, process_b и prepare_output. Тут нужен интеграционный тест, который проверит, как эти компоненты взаимодействуют между собой. Вообще, если код сложно покрывать юнит-тестами, используйте интеграционные — они проверяют общую работу системы, модуля или библиотеки.
Не забывайте про негативные тесты. Это the best practice. Что произойдёт, если передать в программу неправильные данные? Какую ошибку она выведет и выведет ли?
Покрывайте тестами все циклы и if-else. Этот совет касается кода, который нужно поддерживать. Если ему не следовать, на одной из итераций правок вы или ваш коллега просто всё сломаете.
Проверяйте качество тестов. Сделать это поможет мутационное тестирование. Мутационный фреймворк случайно меняет константы и значения в условных операторах и циклах, создаёт копию кода, в которой поочерёдно меняет условия. Например, было >= или было COUNT=3, а стало COUNT=10. Каждая замена тестируется: если код поменялся, а тесты не упали, значит, код не покрыт тестами.
На мутационное тестирование уходит много времени. Можно подключить плагин, который считает code coverage по тесту и выдаёт отчёт. Например, у нас покрыто тестами 43 тысячи строк кода, а 10 тысяч — нет. Значит, code coverage 81%. Но тут важен не только сам процент, но и качество — какие именно фрагменты кода и какими именно тестами покрыты. Например, не всё может быть под юнит-тестами — часть может перекрываться интеграционными.
Обеспечьте достаточный процент покрытия кода. Года три-четыре назад я был фанатиком стопроцентного покрытия. Конечно, безумно круто, когда ты всегда знаешь, что именно сломалось. Но в продакшне этого добиться сложно — да и не нужно. Исключение — маленькие проекты или «жёсткие» команды, для которых полное покрытие в приоритете.
На самом деле, code coverage в 70–90% — уже крутой показатель, но и меньше 70% — тоже плохо. И ещё важный момент: новый код не должен понижать уровень code coverage.
Проверить code coverage можно с помощью coveralls.io:
Coveralls принимает результаты тестов и выдаёт отчёт: показывает процент покрытия и как он изменился с последнего теста.
Не делайте хрупкие тесты. Если тест нестабильный и регулярно падает, его называют хрупким. Его результат может зависеть от дня недели, времени суток, чётности или нечётности запуска. Бывает, две функции работают параллельно и на итоговый результат влияет то, какая из них закончит выполняться первой. Такие функции лучше разбивать на несколько простых и тестировать по отдельности. Мокайте всё что нужно, чтобы сделать тест управляемым, но не переборщите — иначе код будет сложно поддерживать.
Допустим, мы написали юнит-тесты для двух функций. Но не учли, что первая функция сохраняет данные в глобалке, а вторая из-за этого меняет своё поведение. В результате первый тест проходит нормально, а второй падает или ведёт себя странно. А всё потому, что мы не сбросили состояние глобальной переменной.
Следите за скоростью тестов. Тесты должны работать быстро. Если они проверяют кусок кода 10–15 минут — разработчики устанут ждать и отключат их нафиг. Поэтому регулярно проверяйте скорость, ищите узкие места и оптимизируйте тесты. Если есть проблемы, подключитесь через дебаггер — возможно, основной код плохо оптимизирован и искать проблему нужно в продакшне.
Преимущества юнит-тестов
Если у вас ещё остались сомнения, писать юнит-тесты или нет, вот несколько аргументов за. Итак, чем полезны юнит-тесты.
Упрощают работу — находят ошибки, которые вы можете не заметить (меня это много раз спасало). Например, меняешь одну строчку, чтобы поправить логи, а ломается весь код. Благодаря тестам я узнавал об этом ещё до продакшна.
Понятно документируют код. Если вам неочевидно, как работает та или иная функция, можно пройти дальше по коду или открыть юнит-тест. По нему сразу видно, какие параметры принимает функция и что отдаёт после выполнения. Это упрощает жизнь тем, кто работает с чужим кодом.
Помогают ничего не сломать при рефакторинге. Бывает, что код написан непонятно и ты не можешь его отрефакторить, потому что наверняка что-то сломаешь в продакшне. А с тестами код можно смело рефакторить.
Упрощают разработку. Кажется, что юнит-тесты всё усложняют, ведь нужно написать в два раз больше кода — не только функцию, но и тест к ней. Но я много раз убеждался: когда пишешь код без тестов, потом тратишь гораздо больше времени на поиск и исправление ошибок.
Бывает, бац-бац — и в продакшн, а потом понеслось: исправляешь код первый, второй, третий раз. И постоянно вспоминаешь, как тестировать его вручную. У меня даже были файлики с входными данными для таких проверок. Тогда я тестировал программы вручную, по бумажке, и тратил на это уйму времени. А если бы написал юнит-тест, нашёл бы эти баги сразу и не переписывал код по несколько раз.
В коммерческой разработке без юнит-тестов никуда
Сейчас в коммерческой разработке без тестов почти не работают — а в большинстве компаний от разработчиков даже требуют покрывать код юнит-тестами. Везде, где я работал в последние несколько лет, тоже было такое правило. Ведь если в команде кто-то факапит, то может развалиться вся работа — а тестирование как раз защищает от краха.
Современные компании подписывают SLA — гарантируют работоспособность сервиса. Если продукт упадёт, бизнесу придётся заплатить деньги. Поэтому лучше подождать тестов и не катить код, который положит весь продакшн. Даже если сайт или приложение пролежат всего две минуты, это ударит по репутации и дорого обойдётся компании.
Чтобы лучше понять юнит-тесты, изучите тестовые фреймворки вашего языка. А потом найдите крупные open-source-проекты, которые их используют, и посмотрите, как они работают. Можно даже скачать проект и поиграть с тестами, чтобы глубже погрузиться в тему.
Чтобы познать тонкости разработки и тестирования приложений, лучше сразу учиться у практикующих профессионалов. Приходите в университет Skillbox, выбирайте курс и осваивайте программирование под присмотром экспертов.