Статьи

Тестирование вашей PHP Codebase с EnhancePHP

Ты это знаешь; Я знаю это. Мы должны тестировать наш код больше, чем мы. Я думаю, что отчасти это объясняется тем, что мы не знаем точно, как. Что ж, сегодня я избавляюсь от этого оправдания: я учу вас тестировать PHP с помощью инфраструктуры 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
}

Теперь мы снова запускаем тесты и:


Время для дополнительных тестов:

Давайте создадим несколько тестов для имен пользователей. Наши требования заключаются в том, что это должна быть строка длиной от 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(""))

Есть также несколько других методов утверждения; Вы можете проверить документы для полного списка и примеров.

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. Дайте нам всем знать, что вы используете в комментариях; В конце концов, мы все здесь, чтобы учиться друг у друга! Огромное спасибо, что заглянули!