Ты это знаешь; Я знаю это. Мы должны тестировать наш код больше, чем мы. Я думаю, что отчасти это объясняется тем, что мы не знаем точно, как. Что ж, сегодня я избавляюсь от этого оправдания: я учу вас тестировать PHP с помощью инфраструктуры EnhancePHP .
Познакомьтесь с EnhancePHP
Я не собираюсь пытаться убедить вас проверить ваш код; и мы также не будем обсуждать разработку через тестирование. Это было сделано раньше на Nettuts + . В этой статье Никко Баутиста объясняет, почему тестирование — хорошая вещь, и описывает рабочий процесс TDD. Прочтите это когда-нибудь, если вы не знакомы с TDD. Он также использует библиотеку SimpleTest для своих примеров, поэтому, если вам не нравится внешний вид EnhancePHP, вы можете попробовать SimpleTest в качестве альтернативы.
Как я уже сказал, мы будем использовать EnhancePHP . Это отличная маленькая PHP-библиотека — один файл, который предлагает множество функций тестирования.
Начните с перехода на их страницу загрузки и получения последней версии фреймворка.
Мы собираемся создать действительно простой класс Validation для тестирования. Это не сделает слишком много: просто верните true
если элемент проходит проверку, или false
если нет. Итак, создайте действительно простой маленький проект:
Мы сделаем это полу-TDD, поэтому начнем с нескольких тестов.
Письменные тесты
Наш маленький класс собирается проверить три вещи: адреса электронной почты, имена пользователей и номера телефонов.
Но прежде чем приступить к написанию реальных тестов, нам нужно настроить наш класс:
1
2
3
4
5
6
7
8
|
<?php
class Validation_test extends \Enhance\TestFixture {
public function setUp () {
$this-> val = new Validation();
}
}
|
Это наше начало; обратите внимание, что мы расширяем класс \Enhance\TestFixture
. Таким образом, мы даем EnhancePHP знать, что любые открытые методы этого класса являются тестами, за исключением методов setUp
и tearDown
. Как вы можете догадаться, эти методы выполняются до и после всех ваших тестов (не до и после каждого). В этом случае наш метод setUp
создаст новый экземпляр Validation
и назначит его свойству в нашем экземпляре.
Кстати, если вы относительно новичок в PHP, вы, возможно, не знакомы с синтаксисом \Enhance\TestFixture
: что с косыми чертами? Это пространство имен PHP для вас; Проверьте документы, если вы не знакомы с ним .
Итак, тесты!
Адрес электронной почты
Давайте начнем с проверки адресов электронной почты. Как вы увидите, просто выполнить базовый тест довольно просто:
1
2
3
4
|
public function validates_a_good_email_address () {
$result = $this->val->validate_email(«[email protected]»);
\Enhance\Assert::isTrue($result);
}
|
Мы просто вызываем метод, который хотим протестировать, передавая ему действительный адрес электронной почты и сохраняя $result
. Затем мы передаем $result
в метод isTrue
. Этот метод принадлежит классу \Enhance\Assert
.
Мы хотим убедиться, что наш класс будет отклонять не адреса электронной почты. Итак, давайте проверим это:
01
02
03
04
05
06
07
08
09
10
|
public function reject_bad_email_addresses () {
$val_wrapper = \Enhance\Core::getCodeCoverageWrapper(‘Validation’);
$val_email = $this->get_scenario(‘validate_email’);
$addresses = array(«john», «[email protected]», «john@doe.», «jo*[email protected]»);
foreach ($addresses as $addr) {
$val_email->with($addr)->expect(false);
}
$val_email->verifyExpectations();
}
|
Это вводит довольно классную особенность EnhancePHP: сценарии. Мы хотим протестировать несколько адресов, не связанных с электронной почтой, чтобы убедиться, что наш метод вернет false
. Создавая сценарий, мы, по сути, оборачиваем экземпляр нашего класса в некотором совершенстве EnhancePHP, пишем гораздо меньше кода для проверки всех наших неадресов. Вот что такое $val_wrapper
: модифицированный экземпляр нашего класса Validation
. Затем $val_email
является объектом сценария, чем-то вроде ярлыка для метода validate_email
.
Затем у нас есть массив строк, которые не должны проверяться как адреса электронной почты. Мы зациклим этот массив с помощью цикла foreach
. Обратите внимание, как мы запускаем тест: мы вызываем метод with
для нашего объекта сценария, передавая ему параметры для метода, который мы тестируем. Затем мы вызываем expect
метод и передаем все, что ожидаем получить.
Наконец, мы вызываем метод verifyExpectations
сценария.
Итак, первые тесты написаны; как мы их запускаем?
Запуск тестов
Прежде чем мы на самом деле запустим тесты, нам нужно создать наш класс Validation
. Внутри lib.validation.php
начните с этого:
1
2
3
4
5
6
7
|
<?php
class Validation {
public function validate_email ($address) {
}
}
|
Теперь в test.php
мы соберем все это вместе:
1
2
3
4
5
6
7
|
<?php
require «vendor/EnhanceTestFramework.php»;
require «lib/validation.php»;
require «test/validation_test.php»;
\Enhance\Core::runTests();
|
Для начала нам потребуются все необходимые файлы. Затем мы вызываем метод runTests
, который находит наши тесты.
Далее идет аккуратная часть. Запустите сервер, и вы получите хороший вывод HTML:
Очень мило, правда? Теперь, если у вас есть PHP в вашем терминале, запустите это в терминале:
EnhancePHP замечает, что вы находитесь в другой среде, и соответствующим образом корректирует вывод. Дополнительным преимуществом этого является то, что если вы используете IDE, например PhpStorm , который может запускать модульные тесты, вы можете просмотреть этот вывод терминала прямо внутри IDE.
Вы также можете получить выходные данные XML и TAP , если это то, что вы предпочитаете, просто передайте \Enhance\TemplateType::Xml
или \Enhance\TemplateType::Tap
к методу runTests
чтобы получить соответствующий вывод. Обратите внимание, что запуск его в терминале также приведет к результатам командной строки, независимо от того, что вы передаете runTests
.
Получение тестов для прохождения
Давайте напишем метод, который заставляет наши тесты пройти. Как вы знаете, это validate_email
. Вверху класса Validation
давайте определим открытое свойство:
1
|
public $email_regex = ‘/^[\w+-_\.]+@[\w\.]+\.\w+$/’;
|
Я помещаю это в публичное свойство, чтобы, если пользователь захочет заменить его своим собственным регулярным выражением, он мог это сделать. Я использую эту простую версию регулярного выражения электронной почты, но вы можете заменить ее своим любимым регулярным выражением, если хотите.
Затем есть метод:
1
2
3
|
public function validate_email ($address) {
return preg_match($this->email_regex, $address) == 1
}
|
Теперь мы снова запускаем тесты и:
Написание большего количества тестов
Время для дополнительных тестов:
Usernames
Давайте создадим несколько тестов для имен пользователей. Наши требования заключаются в том, что это должна быть строка длиной от 4 до 20 символов, состоящая только из символов слова или точек. Так:
1
2
3
4
|
public function validates_a_good_username () {
$result = $this->val->validate_username(«some_user_name.12»);
\Enhance\Assert::isTrue($result);
}
|
Теперь, как насчет нескольких имен пользователей, которые не должны проверяться:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
public function rejects_bad_usernames () {
$val_username = $this->get_scenario(‘validate_username’);
$usernames = array(
«name with space»,
«no!exclaimation!mark»,
«ts»,
«thisUsernameIsTooLongItShouldBeBetweenFourAndTwentyCharacters»);
foreach ($usernames as $name) {
$val_username->with($name)->expect(false);
}
$val_username->verifyExpectations();
}
|
Это очень похоже на нашу функцию reject_bad_email_addresses
. Обратите внимание, однако, что мы вызываем этот метод get_scenario
: откуда это get_scenario
? Я абстрагирую функциональность создания сценария в приватный метод, внизу нашего класса:
1
2
3
4
|
private function get_scenario ($method) {
$val_wrapper = \Enhance\Core::getCodeCoverageWrapper(‘Validation’);
return \Enhance\Core::getScenario($val_wrapper, $method);
}
|
Мы можем использовать это в наших reject_bad_usernames
и заменить создание сценариев в reject_bad_email_addresses
. Поскольку это закрытый метод, EnhancePHP не будет пытаться запустить его как обычный тест, как это делается с публичными методами.
Мы сделаем эти тесты проходящими аналогично тому, как мы сделали первый проход набора:
1
2
3
4
5
6
7
|
# At the top .
public $username_regex = ‘/^[\w\.]{4,20}$/’;
# and the method .
public function validate_username ($username) {
return preg_match($this->username_regex, $username) == 1;
}
|
Конечно, это довольно просто, но это все, что нужно для достижения нашей цели. Если мы хотим вернуть объяснение в случае сбоя, вы можете сделать что-то вроде этого:
01
02
03
04
05
06
07
08
09
10
|
public function validate_username ($username) {
$len = strlen($username);
if ($len < 4 || $len > 20) {
return «Username must be between 4 and 20 characters»;
} elseif (preg_match($this->username_regex, $username) == 1) {
return true;
} else {
return «Username must only include letters, numbers, underscores, or periods.»;
}
}
|
Конечно, вы также можете проверить, существует ли имя пользователя.
Теперь запустите тесты, и вы должны увидеть, как они проходят.
Телефонные номера
Я думаю, что вы уже поняли это, поэтому давайте закончим наш пример проверки, проверив номера телефонов:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public function validates_good_phonenumbers () {
$val_phonenumber = $this->get_scenario(«validate_phonenumber»);
$numbers = array(«1234567890», «(890) 123-4567»,
«123-456-7890», «123 456 7890», «(123) 456 7890»);
foreach($numbers as $num) {
$val_phonenumber->with($num)->expect(true);
}
$val_phonenumber->verifyExpectations();
}
public function rejects_bad_phonenumnbers () {
$result = $this->val->validate_phonenumber(«123456789012»);
\Enhance\Assert::isFalse($result);
}
|
Вы можете, вероятно, выяснить метод Validation
:
1
2
3
4
5
|
public $phonenumber_regex = ‘/^\d{10}$|^(\(?\d{3}\)?[ |-]\d{3}[ |-]\d{4})$/’;
public function validate_phonenumber ($number) {
return preg_match($this->phonenumber_regex, $number) == 1;
}
|
Теперь мы можем запустить все тесты вместе. Вот как это выглядит из командной строки (моя предпочтительная среда тестирования):
Другие функциональные возможности теста
Конечно, EnhancePHP может сделать гораздо больше, чем мы видели в этом небольшом примере. Давайте посмотрим на некоторые из них сейчас.
Мы очень кратко встретили класс \Enhance\Assert
в нашем первом тесте. В действительности мы не использовали его иначе, потому что это бесполезно при использовании сценариев. Тем не менее, это где все методы утверждения. Прелесть их в том, что их имена делают их функциональность невероятно очевидной. Следующие тестовые примеры пройдут:
-
\Enhance\Assert::areIdentical("Nettuts+", "Nettuts+")
-
\Enhance\Assert::areNotIdentical("Nettuts+", "Psdtuts+")
-
\Enhance\Assert::isTrue(true)
-
\Enhance\Assert::isFalse(false)
-
\Enhance\Assert::contains("Net", "Nettuts+")
-
\Enhance\Assert::isNull(null)
-
\Enhance\Assert::isNotNull('Nettust+')
-
\Enhance\Assert::isInstanceOfType('Exception', new Exception(""))
-
\Enhance\Assert::isNotInstanceOfType('String', new Exception(""))
Есть также несколько других методов утверждения; Вы можете проверить документы для полного списка и примеров.
Mocks
EnhancePHP также может делать насмешки и заглушки. Разве не слышали о насмешках и заглушках? Ну, они не слишком сложны. Макет — это обертка для объекта, которая может отслеживать, какие методы вызываются, с какими свойствами они вызываются и какие значения возвращаются. У нас будет тест для проверки, как мы увидим.
Вот небольшой пример издевательства. Давайте начнем с очень простого класса, который имеет значение:
01
02
03
04
05
06
07
08
09
10
11
|
<?php
require «vendor/EnhanceTestFramework.php»;
class Counter {
public $num = 0;
public function increment ($num = 1) {
$this->num = $this->num + $num;
return $this->num;
}
}
|
У нас есть одна функция: increment
, которая принимает параметр (но по умолчанию равен 1) и увеличивает значение свойства $num
на это число.
Мы могли бы использовать этот класс, если бы строили табло:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
class Scoreboard {
public $home = 0;
public $away = 0;
public function __construct ($home, $away) {
$this->home_counter = $home;
$this->away_counter = $away;
}
public function score_home () {
$this->home = $this->home_counter->increment();
return $this->home;
}
public function score_away () {
$this->away = $this->away_counter->increment();
return $this->home;
}
}
|
Теперь мы хотим протестировать, чтобы убедиться, что increment
метода экземпляра Counter
работает правильно, когда его вызывают методы экземпляра Scoreboard
. Итак, мы создали этот тест:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
class ScoreboardTest extends \Enhance\TestFixture {
public function score_home_calls_increment () {
$home_counter_mock = \Enhance\MockFactory::createMock(«Counter»);
$away_counter = new Counter();
$home_counter_mock->addExpectation( \Enhance\Expect::method(‘increment’) );
$scoreboard = new Scoreboard($home_counter_mock, $away_counter);
$scoreboard->score_home();
$home_counter_mock->verifyExpectations();
}
}
\Enhance\Core::runTests();
|
Обратите внимание, что мы начинаем с создания $home_counter_mock
: мы используем фабрику макетов EnhancePHP, передавая имя класса, над которым мы работаем. Это возвращает «обернутый» экземпляр Counter
. Затем мы добавляем ожидание, с этой строкой
1
|
$home_counter_mock->addExpectation( \Enhance\Expect::method(‘increment’) );
|
Наше ожидание просто говорит о том, что мы ожидаем increment
метода increment
.
После этого мы продолжаем создавать экземпляр Scoreboard
и вызываем score_home
. Затем мы verifyExpectations
. Если вы запустите это, вы увидите, что наш тест прошел.
Мы также могли бы указать, с какими параметрами мы хотим, чтобы метод в mock-объекте вызывался, какое значение возвращалось или сколько раз метод должен быть вызван, например:
1
2
3
4
5
|
$home_counter_mock->addExpectation( \Enhance\Expect::method(‘increment’)->with(10) );
$home_counter_mock->addExpectation( \Enhance\Expect::method(‘increment’)->times(2) );
$home_counter_mock->addExpectation( \Enhance\Expect::method(‘increment’)->returns(1) );
$home_counter_mock->addExpectation( \Enhance\Expect::method(‘increment’)->with(3)->times(1) );
$home_counter_mock->addExpectation( \Enhance\Expect::method(‘increment’)->with(2)->returns(2) );
|
Я должен отметить, что хотя with
и times
покажут неудачные тесты, если ожидания не подразумеваются, returns
— нет. Вы должны будете сохранить возвращаемое значение и использовать утверждение для этого. Я не уверен, почему это так, но у каждой библиотеки есть свои причуды :). (Вы можете увидеть пример этого в библиотеках примеров в Github .)
Столбики
Тогда есть окурки. Заглушка заменяет реальный объект и метод, возвращая именно то, что вы говорите. Итак, скажем, мы хотим убедиться, что наш экземпляр Scoreboard
правильно использует значение, которое он получает от increment
, мы можем заглушить экземпляр Counter
чтобы мы могли контролировать, какое increment
вернет:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class ScoreboardTest extends \Enhance\TestFixture {
public function score_home_calls_increment () {
$home_counter_stub = \Enhance\StubFactory::createStub(«Counter»);
$away_counter = new Counter();
$home_counter_stub->addExpectation( \Enhance\Expect::method(‘increment’)->returns(10) );
$scoreboard = new Scoreboard($home_counter_stub, $away_counter);
$result = $scoreboard->score_home();
\Enhance\Assert::areIdentical($result, 10);
}
}
\Enhance\Core::runTests();
|
Здесь мы используем \Enhance\StubFactory::createStub
для создания нашего счетчика заглушки. Затем мы добавляем ожидание, что increment
метода вернет 10. Мы можем видеть, что результат — это то, что мы ожидаем, учитывая наш код.
Дополнительные примеры макетов и заглушки с библиотекой EnhancePHP можно найти в Github Repo .
Вывод
Что ж, это взгляд на тестирование в PHP с использованием инфраструктуры EnhancePHP. Это невероятно простая структура, но она предоставляет все необходимое для простого модульного тестирования вашего PHP-кода. Даже если вы выберете другой метод / среду для тестирования вашего PHP (или, возможно, свой собственный!), Я надеюсь, что этот урок вызвал интерес к тестированию вашего кода и его простоте.
Но, возможно, вы уже тестировали свой PHP. Дайте нам всем знать, что вы используете в комментариях; В конце концов, мы все здесь, чтобы учиться друг у друга! Огромное спасибо, что заглянули!