Статьи

Рефакторинг устаревшего кода: часть 4 — наши первые юнит-тесты

Старый код Гадкий код. Сложный код. Код спагетти. Глупый бред. В двух словах, Кодекс Наследия . Это серия, которая поможет вам работать и справляться с этим.

Один из ключевых моментов рефакторинга полностью унаследованного кода — это когда мы начинаем извлекать из него маленькие кусочки и начинаем писать целевые модульные тесты для этих маленьких кусочков. Но это может быть довольно сложно, особенно если у вас есть код, который написан так, что будет трудно скомпилировать или запустить его, если его части не хватает. Мы не можем безопасно выполнять большие операции с кодом, который до сих пор едва понимаем, и только золотой мастер-тест позволяет нам полностью его сломать. К счастью, есть некоторые методы, которые могут помочь нам.

За последние двадцать лет истории автоматизированного тестирования термин « модульное тестирование» определялся многими способами. Первоначально речь шла о объеме кода, используемого в тесте. Модульный тест был тестом, который проверял наименьшую возможную единицу определенного языка программирования.

В этом смысле для нашего PHP-кода модульный тест — это тест, выполняющий одну функцию или метод. Когда мы программируем в объектно-ориентированном стиле, наши функции организованы в классы. Все тесты, связанные с одним классом, обычно называются Test Case .

Существует около 25 других определений термина модульного тестирования , поэтому мы не будем вдаваться в каждое из них. Хотя эти определения довольно разные, у всех них есть две общие черты. Это приводит нас к, вероятно, наиболее принятому определению.

Модульный тест — это тест, который выполняется за миллисекунды и тестирует фрагмент кода изолированно.

Мы должны отметить два ключевых слова в определении: миллисекунды — наши тесты должны выполняться быстро, очень быстро; и изоляция — мы должны тестировать наш код настолько изолированно, насколько это возможно. Эти два ключевых слова идут рука об руку, потому что для ускорения испытаний мы должны уменьшить их объем. Базы данных, сетевые коммуникации, пользовательские интерфейсы, они слишком медленные, чтобы тестироваться таким образом. Нам нужно найти и изолировать достаточно маленький кусок кода, чтобы мы могли скомпилировать (при необходимости) и запустить этот код в порядке миллисекунд, то есть менее чем за десять миллисекунд, потому что это будет сантисекунда. Наш тестовый фреймворк добавит небольшие накладные расходы по сравнению с чистым временем выполнения кода, но это незначительно.

Если структура кода позволяет, рекомендуется начинать с написания тестов для любого кода, который мы действительно можем протестировать. Это поможет нам начать создавать покрытие, а также заставит нас сконцентрироваться и понять небольшие кусочки кода. Помните, мы занимаемся рефакторингом, мы не хотим менять поведение. На самом деле, на этом начальном этапе мы не хотим вообще менять производственный код, если это возможно.

Нам нужно проанализировать наши три файла, чтобы увидеть, что мы можем проверить, а что нет.

GameRunner.php основном не имеет логики. Мы создали его как делегацию. Можем ли мы проверить это? Конечно, мы могли. Должны ли мы проверить это? Нет, мы не должны Несмотря на то, что некоторые методы в техническом смысле могут быть протестированы, если в них нет логики, мы, вероятно, не хотим их тестировать.

RunnerFunctions.php — это RunnerFunctions.php история. Там есть две функции. run() — большая функция, выполняющая весь цикл работы системы. Это не то, что мы можем легко проверить. И он также не имеет возвращаемого значения, он просто выводит на экран, поэтому нам нужно было бы захватить вывод и сравнить строки. Это не очень типично для модульного тестирования. С другой стороны, isCurrentAnswerCorrect() возвращает простое значение true или false в зависимости от некоторых условий. Можем ли мы проверить это?

1
2
3
4
5
6
function isCurrentAnswerCorrect() {
    $minAnswerId = 0;
    $maxAnswerId = 9;
    $wrongAnswerId = 7;
    return rand($minAnswerId, $maxAnswerId) != $wrongAnswerId;
}

Мы уже понимаем, что этот код генерирует случайное число и сравнивает его с идентификатором неправильного числа.

Шаг 1 — перейдите на GoldenMasterTest.php и отметьте все тесты как пропущенные. Мы пока не хотим их запускать. Когда мы начнем создавать юнит-тесты, мы будем запускать нашего золотого мастера реже. Поскольку мы пишем новые тесты и не модифицируем производственный код, более важна быстрая обратная связь.

Шаг 2 — создайте новый тест RunnerFunctionsTest.php в нашем каталоге Test рядом с GoldenMasterTest.php . Теперь подумайте о простейшем тестовом коде, который вы можете написать. Какой минимум необходим для его запуска? Ну, это как-то так:

1
2
3
4
5
6
7
8
9
require_once __DIR__ .
 
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase {
 
    function testItCanFindCorrectAnswer() {
 
    }
 
}

Нам нужен файл RunnerFunctions.php , поэтому мы проверяем, что он может быть включен и не RunnerFunctions.php ошибку. Остальная часть кода — это чистый шаблон, просто скелетный класс и пустая тестовая функция. Но что теперь? Что мы делаем дальше? Вы знаете, как мы можем обмануть rand() чтобы вернуть то, что мы хотим? Я еще не знаю. Итак, давайте рассмотрим, как это работает прямо сейчас.

Мы знаем, как заполнить генератор случайных чисел, и что, если мы попытаемся заполнить его некоторыми числами, это сработает? Мы можем написать код в нашем тесте, чтобы выяснить, как что-то работает.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function testItCanFindCorrectAnswer() {
 
    srand(0);
    var_dump(rand(0,9));
    srand(1);
    var_dump(rand(0,9));
    srand(2);
    var_dump(rand(0,9));
    srand(3);
    var_dump(rand(0,9));
    srand(4);
    var_dump(rand(0,9));
 
}

Мы также знаем, что идентификаторы наших вопросов находятся в диапазоне от нуля до девяти. Это производит вывод ниже.

1
2
3
4
5
int(8)
int(8)
int(7)
int(5)
int(9)

Ну, это не выглядит очень очевидным. На самом деле я не вижу логики в том, как мы можем определить значения, которые выдаст функция rand() . Нам нужно будет изменить наш производственный код, чтобы мы могли вводить нужные нам значения.

Когда большинство людей говорят о «зависимости», они думают о связях между классами. Это наиболее распространенный случай, особенно в объектно-ориентированном программировании. Но что, если мы немного обобщим термин? Забудьте о классах, забудьте об объектах, сконцентрируйтесь только на значении «зависимости». От чего зависит наш метод rand(min,max) ? Это зависит от двух значений. Минимум и максимум.

Можем ли мы контролировать rand() по этим двум параметрам? Разве rand() предсказуемо не возвращает одно и то же число, если min и max совпадают? Посмотрим.

1
2
3
4
5
6
7
8
9
function testItCanFindCorrectAnswer() {
 
    var_dump(rand(0,0));
    var_dump(rand(1,1));
    var_dump(rand(2,2));
    var_dump(rand(3,3));
    var_dump(rand(4,4));
 
}

Если мы правы, каждая строка должна выводить число от нуля до четырех предсказуемым образом.

1
2
3
4
5
int(0)
int(1)
int(2)
int(3)
int(4)

Это выглядит довольно предсказуемо для меня. Отправляя одно и то же число для min и max в rand() мы можем быть уверены, что сгенерируем ожидаемое число. Теперь, как мы делаем это для нашей функции? У него нет параметров!

Вероятно, наиболее распространенным способом внедрения зависимостей в метод является использование параметров со значениями по умолчанию. Это сохранит текущую функциональность функции, но позволит нам контролировать ее поток при тестировании.

1
2
3
4
function isCurrentAnswerCorrect($minAnswerId = 0, $maxAnswerId = 9) {
    $wrongAnswerId = 7;
    return rand($minAnswerId, $maxAnswerId) != $wrongAnswerId;
}

Модификация isCurrentAnswerCorrect() таким образом сохранит его текущее поведение и позволит нам одновременно протестировать его. Вы можете снова включить своего золотого мастера и запустить его сейчас. Производственный код был изменен, мы должны быть уверены, что не сломали его.

Как isCurrentAnswerCorrect() выглядит наш isCurrentAnswerCorrect() , тестирование — это просто отправка десяти значений для каждого возможного числа, возвращаемого rand() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function testItCanFindCorrectAnswer() {
 
    $this->assertTrue(isCurrentAnswerCorrect(0,0));
    $this->assertTrue(isCurrentAnswerCorrect(1,1));
    $this->assertTrue(isCurrentAnswerCorrect(2,2));
    $this->assertTrue(isCurrentAnswerCorrect(3,3));
    $this->assertTrue(isCurrentAnswerCorrect(4,4));
    $this->assertTrue(isCurrentAnswerCorrect(5,5));
    $this->assertTrue(isCurrentAnswerCorrect(6,6));
    $this->assertFalse(isCurrentAnswerCorrect(7,7));
    $this->assertTrue(isCurrentAnswerCorrect(8,8));
    $this->assertTrue(isCurrentAnswerCorrect(9,9));
 
}

Эта тестовая функция была построена путем запуска наших тестов после каждой строки. Теперь, когда наши тесты очень быстрые, мы можем выполнять их практически непрерывно. На самом деле есть инструменты для запуска тестов, как только файл меняется, и я слышал о программистах, которые постоянно проводят свои тесты, и они просто смотрят на строку состояния теста в конце каждой команды. Когда вы программируете, вы знаете, чего ожидать, если тест не стал зеленым, когда вы думали, что должен, вы сделали что-то не так. Их обратная связь выглядит настолько узкой, что почти наверняка что-то пошло не так в последней строке или команде, которую они написали.

Хотя это может показаться экстремальной разработкой, основанной на тестировании, я думаю, что это полезно, особенно когда вы разрабатываете алгоритмы. Я лично предпочитаю запускать свои тесты нажатием комбинации клавиш, одной клавиши. И поскольку тесты помогают мне разрабатывать мои программы, мой ярлык для запуска тестов — F1 .

Давайте вернемся к нашему бизнесу. Этот тест с десятью утверждениями выполняется за 66 мс, около 6,6 мс на утверждение. Каждое утверждение вызывает и выполняет часть нашего кода. Это похоже на то, как мы определили модульные тесты в начале этого урока.

Вы assertFalse() для числа семь? Бьюсь об заклад, половина из вас пропустил это. Он похоронен глубоко внутри множества других утверждений. Трудно определить. Я думаю, что это заслуживает своего собственного теста, поэтому мы приводим однозначный случай неправильного ответа.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
function testItCanFindCorrectAnswer() {
    $this->assertTrue(isCurrentAnswerCorrect(0, 0));
    $this->assertTrue(isCurrentAnswerCorrect(1, 1));
    $this->assertTrue(isCurrentAnswerCorrect(2, 2));
    $this->assertTrue(isCurrentAnswerCorrect(3, 3));
    $this->assertTrue(isCurrentAnswerCorrect(4, 4));
    $this->assertTrue(isCurrentAnswerCorrect(5, 5));
    $this->assertTrue(isCurrentAnswerCorrect(6, 6));
    $this->assertTrue(isCurrentAnswerCorrect(8, 8));
    $this->assertTrue(isCurrentAnswerCorrect(9, 9));
}
 
function testItCanFindWrongAnser() {
    $this->assertFalse(isCurrentAnswerCorrect(7, 7));
}

Поскольку мы находимся в поиске рефакторинга, чтобы сделать код лучше и легче для понимания, мы не должны забывать о наших тестах. Они так же важны, как и наш производственный код. Мы должны держать наши тесты в чистоте и легко понять. Мы должны реорганизовать наши тесты, и мы должны сделать это, как только мы увидим, что с ними что-то не так, и только когда они проходят. Таким образом, производственный код может проверить наши тесты. Если у нас есть зеленый тест, мы проводим его рефакторинг, и он становится красным, мы прервали тест. Мы можем просто отменить несколько шагов и попробовать еще раз.

Мы могли бы извлечь правильные номера ответов в массив и использовать их для генерации правильных ответов.

1
2
3
4
5
6
function testItCanFindCorrectAnswer() {
    $correctAnserIDs = [0, 1, 2, 3, 4, 5, 6, 8, 9];
    foreach ($correctAnserIDs as $id) {
        $this->assertTrue(isCurrentAnswerCorrect($id, $id));
    }
}

Это проходит, но также вводит некоторую логику. Может быть, мы могли бы извлечь это в пользовательском утверждении. Это может быть немного экстремально для такого простого теста, но это хорошая возможность понять концепцию.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function testItCanFindCorrectAnswer() {
    $correctAnserIDs = [0, 1, 2, 3, 4, 5, 6, 8, 9];
    $this->assertAnswersAreCorrectFor($correctAnserIDs);
}
 
function testItCanFindWrongAnser() {
    $this->assertFalse(isCurrentAnswerCorrect(7, 7));
}
 
private function assertAnswersAreCorrectFor($correctAnserIDs) {
    foreach ($correctAnserIDs as $id) {
        $this->assertTrue(isCurrentAnswerCorrect($id, $id));
    }
}

Теперь это помогло нам двумя способами. Сначала мы переместили логику прохождения каждого элемента массива и проверки его в закрытом методе. Поскольку мы обычно держим наши частные методы в конце класса, вне поля зрения логики более высокого уровня в открытых методах, нам удалось поднять абстракцию нашего теста. В методе теста нам не важно, как ответы проверяются на правильность. Мы заботимся о идентификаторах, которые должны представлять правильные ответы. Вторым преимуществом является отрыв реализации от подготовки. Сохранение правильных идентификаторов ответов в тесте помогло нам отделить детали реализации от предпосылки, которую мы должны протестировать.

Одна из самых распространенных ошибок, которую любой из нас допускает при написании теста, — повторять то, что есть в рабочем коде. Это как дублирование кода, так и скрытая зависимость, как правило, от некоторых значений или констант. В нашем случае зависимость от идентификатора ответа, который представляет неправильный ответ.

Но как доказать эту зависимость? На первый взгляд это кажется простым дублированием одного значения. Чтобы ответить на вашу дилемму, задайте себе вопрос: «Если мои тесты не пройдут, если я решу изменить неправильный идентификатор ответа?» , Конечно, ответ — нет. Изменение простой константы в рабочем коде не повлияет на поведение или логику. Таким образом, тесты не должны провалиться.

Это звучит здорово! Но как это сделать? Ну, самый простой способ — это просто представить нужную переменную как переменную открытого класса, предпочтительно статическую или постоянную. В нашем случае, поскольку у нас нет класса, мы можем просто сделать его глобальной переменной или константой.

01
02
03
04
05
06
07
08
09
10
11
include_once __DIR__ .
 
const WRONG_ANSWER_ID = 7;
 
function isCurrentAnswerCorrect($minAnswerId = 0, $maxAnswerId = 9) {
    return rand($minAnswerId, $maxAnswerId) != WRONG_ANSWER_ID;
}
 
function run() {
    // … //
}

Сначала измените файл RunnerFunctions.php чтобы isCurrentAnswerCorrect() использовал локальную переменную вместо константы. Затем запустите свои юнит-тесты. Это гарантирует нам, что изменения, которые мы внесли в производственный код, ничего не сломали. Теперь пришло время для теста.

1
2
3
function testItCanFindWrongAnswer() {
       $this->assertFalse(isCurrentAnswerCorrect(WRONG_ANSWER_ID, WRONG_ANSWER_ID));
   }

Измените testItCanFindWrongAnswer() чтобы использовать ту же самую константу. Поскольку файл RunnerFunctions.php включен в начало тестового файла, объявленная константа будет доступна для теста.

Теперь, когда мы полагаемся на WRONG_ANSWER_ID для нашего testItCanFindWrongAnswer() , не должны ли мы testItCanFindCorrectAnswer() рефакторинг нашего теста, чтобы testItCanFindCorrectAnswer() также testItCanFindCorrectAnswer() на ту же самую константу? Ну, мы должны. Это не только облегчит понимание нашего теста, но и сделает его более надежным. Да, потому что если бы мы выбрали неверный идентификатор ответа, который уже есть в списке правильных ответов, определенных в тесте, этот конкретный случай не прошел бы тест, даже если производственный код все равно был бы правильным.

01
02
03
04
05
06
07
08
09
10
11
12
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase {
 
    function testItCanFindCorrectAnswer() {
        $correctAnserIDs = $this->getGoodAnswerIDs();
        $this->assertAnswersAreCorrectFor($correctAnserIDs);
    }
 
    private function getGoodAnswerIDs() {
        return [0, 1, 2, 3, 4, 5, 6, 8, 9];
    }
 
}

В то время как наличие чисел для правильных ответов в самой функции теста было хорошей идеей в какой-то момент, так как мы изменяем наш тест, чтобы все больше полагаться на значения, предоставляемые рабочим кодом, мы также хотим скрыть детали о числах , Первым шагом является применение рефакторинга Extract Method и получение его собственным методом.

01
02
03
04
05
06
07
08
09
10
11
12
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase {
 
    function testItCanFindCorrectAnswer() {
        $correctAnserIDs = $this->getGoodAnswerIDs();
        $this->assertAnswersAreCorrectFor($correctAnserIDs);
    }
 
    private function getGoodAnswerIDs() {
        return array_diff(range(0,9), [WRONG_ANSWER_ID]);
    }
 
}

Мы значительно изменили getGoodAnswerIDs() . Прежде всего, мы генерируем список с помощью range() вместо того, чтобы вводить все возможные идентификаторы вручную. Затем мы вычитаем из массива элемент, содержащий WRONG_ANSWER_ID . Теперь список правильных идентификаторов ответов также не зависит от значения, установленного в неправильном идентификаторе ответа. Но достаточно ли этого? Как насчет минимального и максимального идентификаторов? Разве мы не можем извлечь их так же? Ну что ж, посмотрим.

01
02
03
04
05
06
07
08
09
10
11
12
13
include_once __DIR__ .
 
const WRONG_ANSWER_ID = 7;
const MIN_ANSWER_ID = 0;
const MAX_ANSWER_ID = 9;
 
function isCurrentAnswerCorrect($minAnswerId = MIN_ANSWER_ID, $maxAnswerId = MAX_ANSWER_ID) {
    return rand($minAnswerId, $maxAnswerId) != WRONG_ANSWER_ID;
}
 
function run() {
    // … //
}

Это выглядит довольно мило. Константы использовались только как значения по умолчанию для параметров функции isCurrentAnswerCorrect() . Это по-прежнему позволяет нам вводить требуемые значения при тестировании, а также совершенно ясно, что означают эти параметры. В качестве приятного побочного эффекта небольшой блок констант в верхней части файла начал подсвечивать значения RunnerFunctions.php использует наш файл RunnerFunctions.php . Ницца!

Только не забудьте повторно включить из золотого мастер-теста testOutputMatchesGoldenMaster() теста testOutputMatchesGoldenMaster() . Введенные нами константы используются только в тесте «Золотой мастер». Наши модульные тесты всегда сокращают эти значения.

Теперь нам нужно обновить наш модульный тест, чтобы использовать константы.

01
02
03
04
05
06
07
08
09
10
11
12
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase {
 
    function testItCanFindCorrectAnswer() {
        $correctAnserIDs = $this->getGoodAnswerIDs();
        $this->assertAnswersAreCorrectFor($correctAnserIDs);
    }
 
    private function getGoodAnswerIDs() {
        return array_diff(range(MIN_ANSWER_ID,MAX_ANSWER_ID), [WRONG_ANSWER_ID]);
    }
 
}

Это было просто и легко. Нам просто нужно было изменить параметры метода range() .

Последний шаг, который мы можем сделать с нашим тестом, это очистить беспорядок, который мы оставили в нашем testItCanFindCorrectAnswer() .

1
2
3
4
function testItCanFindCorrectAnswer() {
    $correctAnserIDs = $this->getGoodAnswerIDs();
    $this->assertAnswersAreCorrectFor($correctAnserIDs);
}

Мы можем наблюдать две основные проблемы с этим кодом. Сначала несоответствие в именовании. Однажды мы назвали ответы правильными, а затем назвали их хорошими . Мы должны принять решение по одному из двух. Правильный кажется грамматически более подходящим. Правильное — это противоположность неправильному, а хорошее — противоположность плохому.

01
02
03
04
05
06
07
08
09
10
11
12
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase {
 
    function testItCanFindCorrectAnswer() {
        $correctAnserIDs = $this->getCorrectAnswerIDs();
        $this->assertAnswersAreCorrectFor($correctAnserIDs);
    }
 
    private function getCorrectAnswerIDs() {
        return array_diff(range(MIN_ANSWER_ID,MAX_ANSWER_ID), [WRONG_ANSWER_ID]);
    }
 
}

Мы переименовали наш частный метод согласно рассуждениям выше. Но этого недостаточно. Нам нужно решить еще одну проблему. Мы присваиваем значение, возвращаемое приватным методом, переменной, чтобы использовать эту же переменную в следующей строке. И это единственный вариант использования переменной. В нашем случае переменная была там, потому что она дала дополнительное разъяснение о том, что означает массив чисел. У него было свое применение и область применения. Но теперь, когда у нас есть метод с почти одинаковым именем, выражающий ту же концепцию, переменная изжила себя. Это ненужное задание.

Мы можем использовать встроенную переменную рефакторинга, чтобы удалить переменную и вызвать метод напрямую, вместо того, чтобы использовать переменную в следующей строке.

01
02
03
04
05
06
07
08
09
10
11
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase {
 
    function testItCanFindCorrectAnswer() {
        $this->assertAnswersAreCorrectFor($this->getCorrectAnswerIDs());
    }
 
    private function getCorrectAnswerIDs() {
        return array_diff(range(MIN_ANSWER_ID,MAX_ANSWER_ID), [WRONG_ANSWER_ID]);
    }
 
}

Теперь, что действительно круто, так это то, что мы начали с двух строк кода, которые были не очень понятны и были загрязнены дублированием и скрытой зависимостью. После нескольких шагов изменений мы получили две строки кода, но мы нарушили зависимость от числовых идентификационных номеров. Это круто или как?

Мы закончили с RunnerFunctions.php ? Хорошо, если я вижу if() это означает логику. Если я вижу логику, это означает, что для проверки необходим юнит-тест. И у нас есть цикл if() в run() do-while() нашего метода run() . Пришло время использовать наш инструмент рефакторинга IDE, чтобы извлечь метод, а затем протестировать его.

Но какой кусок кода мы должны извлечь? На первый взгляд взятие только условного утверждения кажется хорошей идеей. Это приводит к приведенному ниже коду.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function run() {
    $notAWinner;
 
    $aGame = new Game();
 
    $aGame->add(«Chet»);
    $aGame->add(«Pat»);
    $aGame->add(«Sue»);
 
    do {
        $dice = rand(0, 5) + 1;
        $aGame->roll($dice);
 
        $notAWinner = getNotWinner($aGame);
 
    } while ($notAWinner);
}
 
function getNotWinner($aGame) {
    if (isCurrentAnswerCorrect()) {
        $notAWinner = $aGame->wasCorrectlyAnswered();
        return $notAWinner;
    } else {
        $notAWinner = $aGame->wrongAnswer();
        return $notAWinner;
    }
}

Хотя это выглядит довольно прилично и сгенерировано простым выбором нужного пункта меню в нашей IDE, есть проблема, которая беспокоит меня. Объект aGame используется как в цикле do-while while, так и в извлеченном методе. Что насчет этого?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function run() {
    $notAWinner;
 
    $aGame = new Game();
 
    $aGame->add(«Chet»);
    $aGame->add(«Pat»);
    $aGame->add(«Sue»);
 
    do {
        $dice = rand(0, 5) + 1;
        $notAWinner = getNotWinner($aGame, $dice);
 
    } while ($notAWinner);
}
 
function getNotWinner($aGame, $dice) {
    $aGame->roll($dice);
 
    if (isCurrentAnswerCorrect()) {
        $notAWinner = $aGame->wasCorrectlyAnswered();
        return $notAWinner;
    } else {
        $notAWinner = $aGame->wrongAnswer();
        return $notAWinner;
    }
}

Это решение удаляет объект aGame из цикла. Однако это приводит к другому типу проблем. Наш счетчик параметров увеличивается. Теперь нам нужно отправить в $dice . Хотя количество параметров, два, достаточно мало, чтобы не вызывать беспокойства, мы также должны подумать о том, как эти параметры используются в самом методе. $dice используется только тогда, когда метод roll() вызывается в aGame . Хотя метод roll() имеет большое значение в классе Game , он не решает, есть у нас победитель или нет. Анализируя код в Game , мы можем сделать вывод, что состояние победителя может быть истинным, только вызвав wasCorrectlyAnswered() . Это странно и подчеркивает некоторые серьезные проблемы с именами в классе Game мы рассмотрим на следующем уроке.

Основываясь на всех вышеупомянутых наблюдениях, наиболее вероятно, что лучше пойти с первой версией нашего извлеченного метода.

1
2
3
4
5
6
7
8
9
function getNotWinner($aGame) {
    if (isCurrentAnswerCorrect()) {
        $notAWinner = $aGame->wasCorrectlyAnswered();
        return $notAWinner;
    } else {
        $notAWinner = $aGame->wrongAnswer();
        return $notAWinner;
    }
}

Мы можем верить в нашу IDE, и, просто взглянув на код, мы можем быть уверены, что ничего не сломалось. Если вы чувствуете себя неуверенно, просто проведите свои золотые мастер тесты Теперь давайте сосредоточимся на создании некоторых тестов для этого приятного метода.

1
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {}

Я придумал это имя, преобразовав то, что я хочу проверить, в имя метода тестирования. Очень важно назвать ваши тесты о том, какое поведение они должны тестировать, а не о том, что они будут делать. Это поможет другим или вам самим через шесть месяцев понять, что на самом деле должен делать этот небольшой фрагмент кода.

Но у нас есть проблема. Наш проверенный метод нуждается в объекте. Нам нужно запустить его так:

1
2
3
4
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
    $aGame = ???
    getNotWinner($aGame);
}

Нам нужен объект $aGame типа Game . Но мы проводим модульное тестирование, мы не хотим использовать настоящий, сложный и плохо понятый класс Game . Это приводит нас к новой главе по тестированию, о которой мы поговорим на следующем уроке: издевательства, окурки и подделки . Это все методы создания и тестирования объекта с использованием других объектов, которые ведут себя предопределенным образом. Хотя использование фреймворка или даже собственных встроенных возможностей PHPUnit может помочь, для наших текущих знаний о нашем очень простом тесте мы можем сделать то, что многие люди забывают.

Мы можем просто создать класс, похожий на Game в нашем тестовом файле и определить в нем только два интересующих нас метода. Это очень просто.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase {
 
    // … //
 
    function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
        $aGame = new FakeGame();
        getNotWinner($aGame);
    }
 
    // … //
 
}
 
class FakeGame {
 
    function wasCorrectlyAnswered() {
 
    }
 
    function wrongAnswer() {
 
    }
}

Это делает наши тесты успешными, и мы все еще в миллисекундной зоне. Обратите внимание, что два пропущенных теста — это тесты золотого мастера.

1
2
3
4
Time: 43 ms, Memory: 3.00Mb
 
OK, but incomplete or skipped tests!
Tests: 5, Assertions: 10, Skipped: 2.

Несмотря на то, что нам пришлось называть наш класс не так, как в Game потому что мы не можем объявить один и тот же класс дважды, код довольно прост. Мы только что определили два метода, которые нас интересуют. Следующий шаг — вернуть что-то и проверить его. Но это может быть сложнее, чем мы ожидали из-за этой строки кода:

1
if (isCurrentAnswerCorrect())

Наш метод вызывает isCurrentAnswerCorrect() без каких-либо параметров. Это плохо для нас. Мы не можем контролировать его вывод. Это будет просто генерировать случайные числа. Нам нужно немного реорганизовать наш код, прежде чем мы сможем продолжить. Нам нужно переместить вызов этого метода в цикл и передать его результат в качестве параметра getNotWinner() . Это позволит нам контролировать результат выражения в приведенном выше операторе if, таким образом контролируя путь, по которому будет идти наш код. Для нашего первого теста нам нужно ввести if и вызвать wasCorrectlyAnswered() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
function run() {
 
    // … //
 
    do {
        $dice = rand(0, 5) + 1;
        $aGame->roll($dice);
 
        $notAWinner = getNotWinner($aGame, isCurrentAnswerCorrect());
 
    } while ($notAWinner);
}
 
function getNotWinner($aGame, $isCurrentAnswerCorrect) {
    if ($isCurrentAnswerCorrect) {
        $notAWinner = $aGame->wasCorrectlyAnswered();
        return $notAWinner;
    } else {
        $notAWinner = $aGame->wrongAnswer();
        return $notAWinner;
    }
}

Теперь у нас есть контроль, все зависимости сломаны. Пришло время для тестирования.

1
2
3
4
5
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
    $aGame = new FakeGame();
    $isCurrentAnswerCorrect = true;
    $this->assertTrue(getNotWinner($aGame, $isCurrentAnswerCorrect));
}

Это проходное испытание, довольно мило. Конечно, мы вернули true из нашего переопределенного метода.

1
2
3
function wasCorrectlyAnswered() {
    return true;
}

Нам нужно также проверить другой путь через if() .

1
2
3
4
5
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() {
    $aGame = new FakeGame();
    $isCurrentAnswerCorrect = false;
    $this->assertFalse(getNotWinner($aGame, $isCurrentAnswerCorrect));
}

На этот раз мы просто решили проверить значение false, поэтому проще различать два случая.

01
02
03
04
05
06
07
08
09
10
class FakeGame {
 
    function wasCorrectlyAnswered() {
        return true;
    }
 
    function wrongAnswer() {
        return false;
    }
}

И наша FakeGame была изменена соответственно.

Мы почти закончили. Извините за то, что вы получили этот урок так долго, надеюсь, он вам понравился и не заснул. Окончательные изменения перед завершением файла RunnerFunctions.php и его тестов.

1
2
3
4
5
6
7
8
9
function getNotWinner($aGame, $isCurrentAnswerCorrect) {
    if ($isCurrentAnswerCorrect) {
        $notAWinner = $aGame->wasCorrectlyAnswered();
        return $notAWinner;
    } else {
        $notAWinner = $aGame->wrongAnswer();
        return $notAWinner;
    }
}

В нашем методе есть некоторые ненужные назначения, мы должны его очистить. Наши юнит-тесты сделают это изменение очень безопасным.

1
2
3
4
5
6
7
function getNotWinner($aGame, $isCurrentAnswerCorrect) {
    if ($isCurrentAnswerCorrect) {
        return $aGame->wasCorrectlyAnswered();
    } else {
        return $aGame->wrongAnswer();
    }
}

Мы применили тот же встроенный рефакторинг переменной, и это привело к его исчезновению. Тесты все еще проходят, и мы все еще не достигли 100 мс для всех модульных тестов вместе. Я говорю, что это довольно мило.

Да, да, мы можем сделать наш тест немного лучше. Поскольку у нас всего несколько строк кода, наши рефакторинги будут простыми. Проблема в коде ниже.

01
02
03
04
05
06
07
08
09
10
11
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
    $aGame = new FakeGame();
    $isCurrentAnswerCorrect = true;
    $this->assertTrue(getNotWinner($aGame, $isCurrentAnswerCorrect));
}
 
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() {
    $aGame = new FakeGame();
    $isCurrentAnswerCorrect = false;
    $this->assertFalse(getNotWinner($aGame, $isCurrentAnswerCorrect));
}

У нас есть дублирующий код, вызывая new FakeGame() в каждом методе. Время для метода извлечения.

01
02
03
04
05
06
07
08
09
10
11
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
    $aGame = $this->aFakeGame();
    $isCurrentAnswerCorrect = true;
    $this->assertTrue(getNotWinner($aGame, $isCurrentAnswerCorrect));
}
 
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() {
    $aGame = $this->aFakeGame();
    $isCurrentAnswerCorrect = false;
    $this->assertFalse(getNotWinner($aGame, $isCurrentAnswerCorrect));
}

Теперь это делает переменную $aGame довольно бесполезной. Время для встроенной переменной.

1
2
3
4
5
6
7
8
9
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
    $isCurrentAnswerCorrect = true;
    $this->assertTrue(getNotWinner($this->aFakeGame(), $isCurrentAnswerCorrect));
}
 
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() {
    $isCurrentAnswerCorrect = false;
    $this->assertFalse(getNotWinner($this->aFakeGame(), $isCurrentAnswerCorrect));
}

Это сделало наш код короче и выразительнее одновременно. Когда мы читаем утверждение, оно читается как проза. Утверждаем, что мы получаем true мы вызываем, и пытаемся получить не победителя, используя наш поддельный класс с правильным ответом. Что мне до сих пор не нравится, так это то, что мы используем одну и ту же переменную и присваиваем ей true или false зависимости от теста. Я думаю, что должен быть более выразительный способ сделать это.

1
2
3
4
5
6
7
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
    $this->assertTrue(getNotWinner($this->aFakeGame(), $this->aCorrectAnswer()));
}
 
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() {
    $this->assertFalse(getNotWinner($this->aFakeGame(), $this->aWrongAnswer()));
}

Вот это да! Наши тесты стали единичными, и они действительно отражают то, что мы тестируем. Все детали скрыты в приватных методах, в конце теста. В 99% случаев вы не будете заботиться об их реализации, и когда вы это сделаете, вы можете просто нажать CTRL + щелкнуть имя метода, и среда IDE перейдет к реализации.

Если мы посмотрим на наш цикл, то увидим, что существует переменная, от которой мы можем избавиться в мгновение ока.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
function run() {
    $notAWinner;
 
    $aGame = new Game();
 
    $aGame->add(«Chet»);
    $aGame->add(«Pat»);
    $aGame->add(«Sue»);
 
    do {
        $dice = rand(0, 5) + 1;
        $aGame->roll($dice);
 
        $notAWinner = getNotWinner($aGame, isCurrentAnswerCorrect());
 
    } while ($notAWinner);
}

Это превратится в это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function run() {
 
    $aGame = new Game();
 
    $aGame->add(«Chet»);
    $aGame->add(«Pat»);
    $aGame->add(«Sue»);
 
    do {
        $dice = rand(0, 5) + 1;
        $aGame->roll($dice);
 
    } while (getNotWinner($aGame, isCurrentAnswerCorrect()));
}

Пока, пока переменная $notAWinner . Но название нашего метода ужасно. Мы знаем, что мы всегда должны отдавать предпочтение положительным именам и поведению и отрицать их в условных выражениях. Как насчет этого наименования?

1
2
3
4
5
do {
    $dice = rand(0, 5) + 1;
    $aGame->roll($dice);
 
} while (didSomebodyWin($aGame, isCurrentAnswerCorrect()));

Но с этим именем нам нужно отрицать его в while() и изменить его поведение. Мы начинаем с изменения наших тестов.

01
02
03
04
05
06
07
08
09
10
class FakeGame {
 
    function wasCorrectlyAnswered() {
        return false;
    }
 
    function wrongAnswer() {
        return true;
    }
}

На самом деле лучше менять только нашу фальшивую игру. Это делает тесты действительно читаемыми, с новыми именами методов.

1
2
3
4
5
6
7
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() {
    $this->assertTrue(didSomebodyWin($this->aFakeGame(), $this->aCorrectAnswer()));
}
 
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() {
    $this->assertFalse(didSomebodyWin($this->aFakeGame(), $this->aWrongAnswer()));
}

Конечно, тесты сейчас не проходят. Мы должны изменить реализацию метода.

1
2
3
4
5
6
7
function didSomebodyWin($aGame, $isCurrentAnswerCorrect) {
    if ($isCurrentAnswerCorrect) {
        return !
    } else {
        return !
    }
}

Юнит тесты проходят, но работа нашего золотого мастера сломается Нам нужно отрицать логин в операторе while .

1
2
3
4
5
do {
    $dice = rand(0, 5) + 1;
    $aGame->roll($dice);
 
} while (!didSomebodyWin($aGame, isCurrentAnswerCorrect()));

Теперь это заставляет золотого мастера снова пройти, и наше do-while читается как хорошо написанная проза. Теперь настало время остановиться. Спасибо за чтение.