Многие разработчики очень любят пренебрегать тестированием собственных задач. Признайтесь, отдавали ли вы задачи в тест, при этом практически не проверяя их? А потом доделывали и опять отдавали, доделывали и отдавали.. А ведь лучше вас unit тестирование никто не проведет, ведь лучше всего знаете и понимаете код — именно вы. Если программист захочет, то с легкостью придумает и воспроизведет граничные ситуации и сразу же сможет их проверить и пофиксить. Роль тестирования сложно недооценить, в наше время умненькие дяденьки даже методологии разработки придумали, главным смыслом которых является написание тестов, а потом уже и самого кода. Рассмотрим на примере, что значит TDD и BDD, а также как выглядят код и тесты, полученные в результате разработки с помощью этих методологий.
Начнем с небезызвестной методологии под названием TDD (Test-driven development). В соответствии с ее принципами, сначала разработчик пишет тест, затем заставляет тест работать, далее рефакторит получившийся код и так по кругу, пока не будут реализованы все необходимые функции. Посмотрим, как выглядит разработка через тестирование на примере разработки калькулятора. «О, нет, опять калькулятор! Когда же в статьях будут реалистичные примеры?» - воскликните вы. Реалистичных примеров не обещаю в рамках этой статьи, а вот калькулятор будет работать не просто с числами, а с калориями. Калькуляторы калорий для человеческих особей существуют, а вот для бегемотиков нет. Исправим это недоразумение. Для написания unit тестов воспользуемся фреймовроком PHPUnit, следовательно, тестируемый код будем писать на php.
Итак, есть тривиальный пример - бегемотик желает знать, сколько грамм он наберет, если съест 14 килограмм молодого австралийского крокодила. Опытные британские диетологи утверждают, что после такого обеда бегемотик наберет 252 грамма жира. Проверим!
class Hippopotamus_Fitness_CalculatorTest extends PHPUnit_Framework_TestCase
{
private $_calculator;
public function setUp()
{
$this->_calculator = $calculator = new Hippopotamus_Fitness_Calculator();
}
public function testGetWeightGainAfterDinnerCrocodile()
{
$weightGain = $this->_calculator->getWeightGain('crocodile', 14);
$this->assertEquals(252, $weightGain);
}
}
Для того, чтобы похудеть, бегемотикам необходимо заниматься спортом. Африканские гроссмейстеры утверждают, что 60-минутная игра в шахматы помогает сжечь 174 грамма жира. Коду мы не верим, код мы проверим!
public function testGetWeightReductionAfterPlayingChess()
{
$weightReduction = $this->_calculator->getWeightReduction('chess', 60);
$this->assertEquals(174, $weightReduction);
}
Для полноты картины, попробуем посчитать, сколько грамм сбросит бегемотик, если часок драгой поиграет в доту2.
public function testGetWeightReductionAfterUndefinedGame()
{
$weightReduction = $this->_calculatorgetWeightReduction('dota2', 60);
$this->assertEquals(null, $weightReduction);
}
Эх, нисколько! Потому что бегемотики в доту не играют.
Запускаем тесты и видим, что все тесты проваливаются. Пока все идет по плану!
Ну а теперь пришло время заставить тесты работать и написать сам фитнес калькулятор для бегемотиков.
Давайте для начала, для простоты изложения, выделим все необходимые данные в отдельный класс и будем использовать его при разработке сценариев. Это не самое лучшее архитектурное решение, но остановимся на этом варианте.
class Hippopotamus_Fitness_Calories
{
/**
* Количество калорий, сжигаемых за 1 минуту игры
*
* @var array
*/
CONST COUNT_IN_GAMES = [
'hide_and_seek' => 132,
'сatch-up' => 432,
'chess' => 87
];
/**
* Количество калорий в 1 кг продукта
*
* @var array
*/
CONST COUNT_IN_FOODS = [
'grass' => 21,
'water_plant' => 32,
'crocodile' => 540
];
/**
* Количество калорий, необходимых для сжигания 1 грамма жира
*
* @var int
*/
CONST СOUNT_FOR_BURN_FAT = 30;
}
А теперь начнем писать код, и будем писать до тех пор, пока все тесты не будут зелеными. Вот что у меня получилось.
class Hippopotamus_Fitness_Calculator
{
public function getWeightGain($food, $weight)
{
if (!isset(Hippopotamus_Fitness_Calories::COUNT_IN_FOODS[$food])) {
return null;
}
return (Hippopotamus_Fitness_Calories::COUNT_IN_FOODS[$food] * $weight) /
Hippopotamus_Fitness_Calories::СOUNT_FOR_BURN_FAT;
}
public function getWeightReduction($game, $time)
{
if (!isset(Hippopotamus_Fitness_Calories::COUNT_IN_GAMES[$game])) {
return null;
}
return (Hippopotamus_Fitness_Calories::COUNT_IN_GAMES[$game] * $time) /
Hippopotamus_Fitness_Calories::СOUNT_FOR_BURN_FAT;
}
}
Запускаем тесты и видим:
Time: 183 ms, Memory: 14.25Mb
OK (3 tests, 3 assertions)
Все тесты зеленые! Теперь, по всем канонам, код необходимо порефакторить и сделать читаемым, но дабы статья не разрасталась до немыслимых размеров, на этом мы остановимся. Давайте лучше сформулируем основные преимущества и недостатки этой методологии.
Итак, основные преимущества TDD: + программист продумывает детали и интерфейс еще до реализации, что помогает ему абстрагироваться и посмотреть на функциональность со стороны пользователя. + если есть тесты, программист может менять легаси-код, не боясь что-то отвалить (если и отвалит, тесты скажут ему об этом). + уменьшаются ошибки в коде, дефекты, время отладки и т.д. и т.п.
Без дегтя нет и меда, поэтому пара слов о недостатках: + в некоторых случаях увеличивается время разработки + unit тесты обычно пишутся тем же человеком, который пишет и тестируемый код. Если разработчик не разобрался в требованиях, то ошибки будут как в тестах, так и в коде. + не всегда руководство соглашается понимать, что tdd — это круто и стоит увеличивать расходы на разработку на этапах внедрения этой практики.
Теперь рассмотрим более модный молодой подход BDD - Behaviour Driven Development. Он тоже предполагает написание сначала тестов, а потом кода, но с некоторыми отличием в подходе написания тестов. Ой, не тестов! А сценариев. В этом подходе не существует слова «тест». Все, что мы привыкли считать тестом, здесь считается сценарием или поведением. Поэтому исключаем из названий функций слово «test» и пишем названия понятным даже для пм-ов языком. Название шагов сценария должны показывать, что должен делать этот сценарий, какое у него поведение.
Итак, поведенческие сценарии будем писать с использованием Behat. Сначала опишем тестируемую функцию:
Feature: getWeightReduction
In order to calculate weight loss after games
As a hippopotamus
I need to be able to know how many grams will help to lose a different kind of sport activity
Теперь опишем шаги сценария:
Scenario: Calculation of the loss of kilograms during a game of chess within an hour
Given calculator
And I have a game named "chess"
And I have a time - 60 minutes
When I call getWeightReduction
Then I should get: 174 grams
Запустим наш сценарий:
Даже на этом этапе мы уже можем запускать тесты, которые проваливаются. Заметьте, что умненький behat выдает шаблон шагов, которые необходимо реализовать. Реализуем описанный сценарий с помощью behat:
class FeatureContext extends BehatContext
{
private $_calculator;
private $_output;
/**
* @Given /^calculator$/
*/
public function iCreateCalculator()
{
$this->_calculator = new Hippopotamus_Fitness_Calculator();
}
/**
* @Given /^I have a game named "([^"]*)"$/
*/
public function iHaveAGame($arg1)
{
$this->_calculator->setGame($arg1);
}
/**
* @Given /^I have a time - (\d+) minutes$/
*/
public function iHaveATime($arg1)
{
$this->_calculator->setTime($arg1);
}
/**
* @When /^I call getWeightReduction$/
*/
public function iCallGetWeightreduction()
{
$this->_output = $this->_calculator->getWeightReduction();
}
/**
* @Then /^I should get: (\d+) grams$/
*/
public function iShouldGetGrams($arg1)
{
if ((int)$arg1 !== $this->_output) {
throw new Exception("Actual grams is:\n" . $this->_output);
}
}
}
Запускаем наш сценарий, который также «красный». Заставим его работать. Будем использовать все тот же класс с константами, что и в первом случае.
class Hippopotamus_Fitness_Calculator
{
private $game;
private $time;
public function setGame($game)
{
$this->game = $game;
}
public function setTime($time)
{
$this->time = $time;
}
public function getWeightReduction()
{
if (!isset(Hippopotamus_Fitness_Calories::COUNT_IN_GAMES[$this->game])) {
return null;
}
return (Hippopotamus_Fitness_Calories::COUNT_IN_GAMES[$this->game] * $this->time) /
Hippopotamus_Fitness_Calories::СOUNT_FOR_BURN_FAT;
}
}
Снова запускаем сценарий и видим следующий результат.
Таким образом, любой человек может описать множество сценариев:
Scenario: Calculation of the loss of kilograms during a game of chess within an hour
Given calculator
And I have a game named "chess"
And I have a time - 60 minutes
When I call getWeightReduction
Then I should get: 174 grams
Scenario: Calculation of the loss of kilograms during a game of chess within an hour
Given calculator
And I have a game named "hide_and_seek"
And I have a time - 30 minutes
When I call getWeightReduction
Then I should get: 132 grams
Scenario: Calculation of the loss of kilograms during a game of chess within an hour
Given calculator
And I have a game named "catch-up"
And I have a time - 15 minutes
When I call getWeightReduction
Then I should get: 216 grams
Лично мне больше нравится результат после TDD разработки, но как говорится, для определенных целей определенные инструменты.
Преимущества BDD: * сценарии понятны не только вам, но и менеджерам и другим далеким от разработки людям * сценарии описывают, как конечные пользователи будут использовать системы * на выходе получается почти готовая отличная документация
Недостатки BDD: * все те же, что и у TDD ;)
Свое изложение мне пора заканчивать, но нами остались нетронуты еще ADDT, DDT, KDT и ODT .. о которых обязательно будет отдельная статья. Всем добра!