Coding — Unit-тестирование
Модульное тестирование или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.
Модульное тестирование служит мне, как разработчику в достижении двух целей:
1. Определить интерфейс разрабатываемого класса еще до его реализации
2. Проверить, не привело ли добававление нового функционала к ошибкам в уже существующем коде
Отладчик больше не нужен?
В комментариях к одной из статей на welinux я упоминал, что достаточно давно не пользуюсь отладчиками, благодаря модульному тестированию. Давайте посмотрим, как тесты помогают избежать запуска отладчика. Пример, конечно надуманый, но он таков для простоты изложения.
Предположим у нас есть класс Product, который умеет считать цену.
1 2 3 4 5 6 7 8 9 10 11 |
|
Код демонстрирует обычную магазинную накрутку в 40%. Предположим, что бизнес ставит задачу выводить на страницах сайта цены со скидкой в 5%.
1 2 3 4 5 6 7 8 9 10 11 |
|
Теперь цены на всем сайте выводятся якобы правильно... ОДНАКО!!!! Через пару дней операторы начинают замечать, что в админском интерфейсе все цены опустились, и Вы получаете багрепорт приблизительно следующего содержания:
Все цены в админке выводятся на 10-20 рублей ниже, чем во внутренней базе!
Вы загружены по самый нехочу и уже забыли о той пятипроцентной скидке, которую ввели позавчера. Тут в дело вступает отладчик...
СТОП!!!!
Давайте посмотрим как будут развиваться события при использовании модульного тестирования. До недавнего времени я пользовался SimpleTest для PHP и считаю, что PHPUnit стоит использовать только тогда, когда возможностей SimpleTest явно не хватает, потому примеры тестов будут для SimpleTest.
Тот код, который показан выше должен быть покрыт тестом приблизительно следующего содержания:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Попробуем разобраться. Функция setUp выполняется перед каждым тестовым методом, потому в ней мы выполняем инициализацию БД, для того, чтобы в каждом тестовом методе иметь уверенность в новом чистом окружении. Однако стоит очищать окружение с умом: в некоторых тестах нужно чистое окружение, в некоторых специально подготовленное, в некоторых окружение вообще не важно. Вам решать - в каком окружении отрабатывать тесты, но чуть ниже я помогу с этим разобраться. Метод testProductGetPrice тестирует правильность отдаваемого классом значения - ключевыми здесь являются последние две строки. Если вы передаете в метод assertEqual два одинаковых значения - тест считается пройденым, но если значения не совпадают - тест провален и на экране красуется сообщение о несоответствии значений. assertEqual - не единственная проверка. Cуществует еще множество удобных проверок: assertTrue, assertFalse, assertNull, assertNotNull, assertPattern и т.д.
Перед началом изменения кода ( это я про скидку в 5% ) запускаем тесты и видим, что все в порядке. Теперь пробуем изменить код и видим, что наши изменения привели к ошибкам.
Мы конечно можем переписать тест, а далее все пойдет по тому же сценарию что и раньше, но я говорил, что пример надуман ради простоты. В более сложной ситуации вы не будете слепо менять метод и тест, понимая, что где-то getPrice может быть использован для получения РЕАЛЬНОЙ цены.
Лучше мы напишем еще один тест. Да да! Сначала тест, а потом код...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Мы решили ввести параметр в метод получения прайса. Теперь запустим тест и получим Fatal error, т.к. метод объявлен без параметров.
Изменим код:
1 2 3 4 5 6 7 8 9 10 11 |
|
Таким образом мы избавились от фатальной ошибки и получили ошибку в модульных тестах. Тесты запускаются, но последний сообщает нам о том, что код работает не так, как ожидал от него тест.
Продолжим работу над кодом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Запускаем тесты и видим, что сообщений об ошибках нет. Теперь остается найти в коде все те места, где нужно выводить цену со скидкой, и добавляем в вызов метода getPrice скидку в процентах.
Казалось бы на выполнение задачи мы потратили больше времени, чем обычно, но поверьте - это иллюзия. В более сложной ситуации на поиск допущенной подобным образом ошибки может уйти пара часов, а то и дней. Модульные тесты сразу покажут, что последние изменения привели к ошибкам, и покажут к каким.
Вот еще пример теста, который провалится при подходе при прямом изменении метода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
При упомянутых выше изменениях ( это снова про 5% ) эти два теста провалятся, НО testGetBasketTotalSum нужно будет переписать, а вот testGetAverageProductPrice должен остаться неизменным, но при этом должен нормально проходить.
Внутри метода Basket::getTotalSum() будет вызываться метод $product->getPrice(5) , потому нам нужно изменить тест:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Теперь несколько истин:
Тесты нужно запускать часто.
Тесты нужно запускать очень часто.
Тесты нужно запускать перед каждым изменением кода
Тесты нужно запускать после каждого изменения кода
Тесты должны выполняться быстро, чтобы не лень было их запускать так часто.
Не тестируйте очевидные вещи - это пустая трата времени.
Если вы собираетесь изменить поведение какого-либо модуля системы - сначала покройте его тестами и убедитесь, что он работает правильно.
Вот таким образом модульные тесты помогают избежать запуска отладки.
Проектирование интерфейсов
Теперь поговорим о другом аспекте использования модульных тестов - проектирование интерфейсов.
Когда я добавляю в систему какой-либо абсолютно новый функционал я не всегда сразу готов ответить как бы я хотел использовать этот функционал в клиентском коде. Именно в такие моменты я начинаю писать тесты, перед тем, как писать код.
1 2 3 4 5 6 7 8 9 10 |
|
Сначала бывает что-то неуклюжее вроде приведенного выше кода. Вся фишка в том, что я уже пытаюсь использовать тот функционал, которого нету, и еще ДО реализации функционала вижу, что он неудобен! Далее делаю его удобней.
1 2 3 4 5 6 7 8 9 10 |
|
Документация к коду
Модульные тесты являются хорошими и простыми примерами использования вашего кода, потому они могут служить неплохой документацией.
Подводя итог
Существует мнение, что написание автоматических модульных тестов - есть пустая трата времени. Это не так! Документация, правильное проектирование интерфейсов, отсутствие необходимости отлаживать код - вот те моменты в которых модульное тестирование может значительно сократить время разработки. К тому же, для того, чтобы писать быстрые и понятные тесты разработчикам приходится делать части системы менее зависимыми друг от друга, а это сильно повышает шансы на повторное использование кода как в этой же системе, так и в других системах. Представьте себе что две трети оттестированного правильно работающего кода можно будет взять из старого проекта и не разрабатывать этот функционал для новой системы. Сколько времени вы сэкономите? На сколько новая система будет дешевле?