Время на прочтение
13 мин
Количество просмотров 180K
Язык PHP очень легок для изучения. Это, а так же обилие литературы «Освой _что_угодно_ за 24 часа» породило большое количество, мягко говоря, некачественного кода. Как следствие, рано или поздно любой программист, который выходит за рамки создания гостевой книги или сайта-визитки сталкивается с вопросом: «а если я здесь немножко добавлю, все остальное не ляжет?» Дать ответ на этот вопрос и на многие другие может юнит-тестирование.
В самом начале хочется оговориться — здесь речь не будет идти о TDD и методологиях разработки ПО. В данной статье я попробую показать начинающему PHP-разработчику основы использования модульного тестирования на базе фреймворка PHPUnit
Вместо предисловия
Вначале возникает вполне резонный вопрос: А зачем, если все и так работает?
Здесь все просто. После очередного изменения что-то перестанет работать так как надо — это я вам обещаю. И тогда поиск ошибки может отнять очень много времени. Модульное тестирование может свести этот процесс к считанным минутам. Не будем так же исключать переезд на другую платформу и связанные с ним «подводные камни». Чего только стоит разность в точности вычислений на 64- и 32-разрядных системах. А когда-то вы в первый раз столкнетесь с нюансами вроде
<?php
print (int)((0.1 + 0.7) * 10);
// думаете 8? проверьте ...
// или лучше так: "Вы еще не используете BC Math? Тогда мы идем к вам!"
Если вопрос необходимости отпал, то можно приступить к выбору инструмента. Здесь ассортимент в общем-то невелик — PHPUnit (http://www.phpunit.de/) и SimpleTest (http://www.simpletest.org/). Поскольку PHPUnit уже стал стандартом де-факто в тестировании PHP приложений, то на нем и остановимся.
Первое знакомство
Вопросы установки рассматривать не будем: все достаточно внятно изложено на сайте. Попробуем лучше понять, как это все работает.
Допустим, есть у нас некий класс «MyClass», одним из методов реализующий возведение числа в степень. (Здесь вынужден извиниться, но все примеры, в общем-то, высосаны из пальца)
MyClass.php
<?php
class MyClass {public function power($x, $y)
{
return pow($x, $y);
}
}
Хочется проверить, правильно ли он работает. О всем классе речь пока не идет — только об одном методе. Напишем для проверки такой тест.
MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php';
require_once 'MyClass.php';class MyClassTest extends PHPUnit_Framework_TestCase {
public function testPower()
{
$my = new MyClass();
$this->assertEquals(8, $my->power(2, 3));
}
}
Небольшое отступление о формате тестов.
- Название класса в тесте складывается из названия тестируемого класса плюс «Test»;
- Класс для тестирования в большинстве случаев наследуется от PHPUnit_Framework_TestCase;
- Каждый тест является паблик-методом, название которого начинается с префикса «test»;
- Внутри теста мы используем один из assert-методов для выяснения соответствует ли результат обработки ожидаемому (подробнее чуть ниже);
Теперь глядя на тест мы можем понять, что тестируя метод возведения в степень мы создаем экземпляр класса, вызываем нужный нам метод с заранее определенными значениями и проверяем правильно ли были проведены вычисления. Для этой проверки был использован метод assertEquals(), который первым обязательным параметром принимает ожидаемое значение, вторым актуальное и проверяет их соответствие. Размяв мозги и освежив в памяти знания таблицы умножения, мы предположили, что 23=8. На этих данных мы и проверим, как работает наш метод.
Запускаем тест:
$ phpunit MyClassTest
.
Time: 0 seconds
OK (1 test, 1 assertion)
Результат выполнения теста «ОК». Казалось бы можно остановиться на этом, но иногда было бы неплохо для проверки скормить методу не один набор данных. PHPUnit предоставляет нам такую возможность — это провайдеры данных. Провайдер данных тоже является паблик-методом (название не существенно), который возвращает массив наборов данных для каждой итеррации. Для использования провайдера необходимо указать его в теге @dataProvider к тесту.
Изменим наш тест следующим образом:
MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php';
require_once 'MyClass.php';class MyClassTest extends PHPUnit_Framework_TestCase {/**
* @dataProvider providerPower
*/
public function testPower($a, $b, $c)
{
$my = new MyClass();
$this->assertEquals($c, $my->power($a, $b));
}public function providerPower ()
{
return array (
array (2, 2, 4),
array (2, 3, 9),
array (3, 5, 243)
);
}
}
После запуска увидим следующую картину:
.F.
Time: 0 seconds
There was 1 failure:
1) testPower(MyClassTest) with data set #1 (2, 3, 9)
Failed asserting that <integer:8> matches expected value <integer:9>.
/home/user/unit/MyClassTest.php:14
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
Опишу подробнее. Точка, которую в первом выводе теста многие могли принять за опечатку на самом деле таковой не является — это успешно пройденный тест. F(ailure) — соответственно тест не пройденный. Значит в данном случае, было проведено 3 теста, один из который завершился неудачно. В расширенном описании нам было сказано, какой именно, с каким набором исходных данных, с каким реальным и каким ожидаемым результатом. Если бы 23 действительно равнялось 9-ти, то мы увидели бы таким образом ошибку в нашем сценарии.
Здесь, как мне кажется, есть смысл отвлечься от несколько абстрактной практики и перейти ко вполне конкретной теории. А именно, описать какими же assert-методами мы располагаем для проверки поведения тестируемых сценариев.
Два самых простых — это assertFalse() и assertTrue(). Проверяют, является ли полученное значение false и true соответственно. Далее идут уже упомянутый assertEquals() и обратный ему assertNotEquals(). В их использовании есть нюансы. Так при сравнении чисел с плавающей точкой есть возможность указать точность сравнения. Так же эти методы используются для сравнения экземпляров класса DOMDocument, массивов и любых объектов (в последнем случае равенство будет установлено, если атрибуты объектов содержат одинаковые значения). Так же следует упомянуть assertNull() и assertNotNull() которые проверяют соответствие параметра типу данных NULL (да-да, не забываем, что в PHP это отдельный тип данных). Этим возможные сравнения не ограничиваются. Нет смысла в рамках этой статьи заниматься перепечаткой документации, потому приведу по возможности структурированный список всех возможных методов. Более детально интересующиеся могут прочитать здесь
Базовые методы сравнения
assertTrue() / assertFalse()
assertEquals() / assertNotEquals()
assertGreaterThan()
assertGreaterThanOrEqual()
assertLessThan()
assertLessThanOrEqual()
assertNull() / assertNotNull()
assertType() / assertNotType()
assertSame() / assertNotSame()
assertRegExp() / assertNotRegExp()
Методы сравнения массивов
assertArrayHasKey() / assertArrayNotHasKey()
assertContains() / assertNotContains()
assertContainsOnly() / assertNotContainsOnly()
ООП специфичные методы
assertClassHasAttribute() / assertClassNotHasAttribute()
assertClassHasStaticAttribute() / assertClassNotHasStaticAttribute()
assertAttributeContains() / assertAttributeNotContains()
assertObjectHasAttribute() / assertObjectNotHasAttribute()
assertAttributeGreaterThan()
assertAttributeGreaterThanOrEqual()
assertAttributeLessThan()
assertAttributeLessThanOrEqual()
Методы сравнения файлов
assertFileEquals() / assertFileNotEquals()
assertFileExists() / assertFileNotExists()
assertStringEqualsFile() / assertStringNotEqualsFile()
Методы сравнения XML
assertEqualXMLStructure()
assertXmlFileEqualsXmlFile() / assertXmlFileNotEqualsXmlFile()
assertXmlStringEqualsXmlFile() / assertXmlStringNotEqualsXmlFile()
assertXmlStringEqualsXmlString() / assertXmlStringNotEqualsXmlString()
Разное
assertTag()
assertThat()
Исключения
Если еще не надоело, то вернемся к практике, а именно к обработке исключений. Для начала модифицируем наш тестируемый класс — введем в него метод, который будет это исключение выбрасывать.
MyClass.php
<?php
class MathException extends Exception {};class MyClass {// ...public function divide($x, $y)
{
if (!(boolean)$y)
{
throw new MathException('Division by zero');
}
return $x / $y;
}
}
Теперь надо создать тест, который будет завершаться успешно в том случае, если при определенном наборе данных будет вызвано это исключение. Задать требуемое исключение можно как минимум двумя способами — добавив к тесту @expectedException либо вызвав в тесте метод setExpectedException().
MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php';
require_once 'MyClass.php';class MyClassTest extends PHPUnit_Framework_TestCase {// .../**
* @expectedException MathException
*/
public function testDivision1()
{
$my = new MyClass();
$my->divide (8, 0);
}public function testDivision2 ()
{
$this->setExpectedException('MathException');
$my = new MyClass();
$my->divide(8, 0);
}
}
Тесты, в общем-то, абсолютно идентичны. Выбор способа остается на ваше усмотрение. Помимо механизмов предоставляемых непосредственно PHPUnit, для тестирования исключений можно воспользоваться стандартным try {…} catch (), к примеру, так:
MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php';
require_once 'MyClass.php';class MyClassTest extends PHPUnit_Framework_TestCase {// ...public function testDivision3()
{
$my = new MyClass();
try {
$my->divide (8, 2);
} catch (MathException $e) {
return;
}
$this->fail ('Not raise an exception');
}
}
В этом примере мы так же видим не рассмотренный ранее способ завершения теста с помощью вызова метода fail(). Вывод теста будет следующим:
F
Time: 0 seconds
There was 1 failure:
1) testDivision3(MyClassTest)
Not raise an exception
/home/user/unit/MyClassTest.php:50
Принадлежности
Базовые методы тестирования мы освоили. Можно ли улучшить наш тест? Да. Написанный c начала этой статьи класс проводит несколько тестов, в каждом из которых создается экземпляр тестируемого класса, что абсолютно излишне, потому как PHPUnit предоставляет в наше пользование механизм принадлежностей теста (fixtures). Установить их можно защищенным методом setUp(), который вызывается один раз перед началом каждого теста. После окончания теста вызывается метод tearDown(), в котором мы можем провести «сборку мусора». Таким образом, исправленный тест может выглядеть так:
MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php';
require_once 'MyClass.php';class MyClassTest extends PHPUnit_Framework_TestCase {protected
$fixture;
protected
function setUp()
{
$this->fixture = new MyClass ();
}protected
function tearDown()
{
$this->fixture = NULL;
}/**
* @dataProvider providerPower
*/
public function testPower($a, $b, $c)
{
$this->assertEquals($c, $this->fixture->power($a, $b));
}public function providerPower()
{
return array(
array(2, 2, 4),
array(2, 3, 8),
array(3, 5, 243)
);
}// …}
Наборы тестов
После того, как код нескольких классов будет покрыт тестами, становится довольно таки неудобно запускать каждый тест по отдельности. Здесь нам на помощь могут прийти наборы тестов — несколько связанных единой задачей тестов можно объединить в набор и запускать соответственно его. Наборы реализованы классом PHPUnit_Framework_TestSuite. Необходимо создать экземпляр этого класса и добавить в него необходимые тесты с помощью метода addTestSuite(). Так же с помощью метода addTest() возможно добавление другого набора.
SpecificTests.php
<?php
require_once 'PHPUnit/Framework.php';
// подключаем файлы с тестами
require_once 'MyClassTest.php';class SpecificTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('MySuite');
// добавляем тест в набор
$suite->addTestSuite('MyClassTest');
return $suite;
}
}
AllTests.php
<?php
require_once 'PHPUnit/Framework.php';
// подключаем файл с набором тестов
require_once 'SpecificTests.php';class AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('AllMySuite');
// добавляем набор тестов
$suite->addTest(SpecificTests::suite());
return $suite;
}
}
А теперь представим себе набор тестов для сценария, работающего с БД. Неужели нам в каждом тесте придется подключаться к базе? Нет — не придется. Для этого можно создать свой класс унаследованный от PHPUnit_Framework_TestSuite, определить его методы setUp() и tearDown() для инициализации интерфейса БД и просто передать его в тест атрибутом sharedFixture. Базы данных мы оставим на потом, а пока попробуем создать собственный набор тестов для уже имеющегося класса.
MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php';
require_once 'MyClass.php';class MyClassTest extends PHPUnit_Framework_TestCase {
protected$fixture;
protected
function setUp()
{
$this->fixture = $this->sharedFixture;
}protected
function tearDown()
{
$this->fixture = NULL;
}/**
* @dataProvider providerPower
*/
public function testPower ($a, $b, $c)
{
$this->assertEquals($c, $this->fixture->power($a, $b));
}public function providerPower()
{
return array(
array(2, 2, 4),
array(2, 3, 8),
array(3, 5, 243)
);
}// …}
MySuite.php
<?php
require_once 'MyClassTest.php';class MySuite extends PHPUnit_Framework_TestSuite {protected
$sharedFixture;public static function suite()
{
$suite = new MySuite('MyTests');
$suite->addTestSuite('MyClassTest');
return $suite;
}protected
function setUp()
{
$this->sharedFixture = new MyClass();
}protected
function tearDown()
{
$this->sharedFixture = NULL;
}}
Здесь мы в sharedFixture положили экземпляр тестируемого класса, а в тесте просто его использовали — решение не слишком красивое (я бы даже сказал, вообще не красивое), но общее представление о наборах тестов и передаче принадлежностей между тестами оно дает. Если наглядно изобразить очередность вызова методов, то получится нечто вроде такого:
MySuite::setUp()
MyClassTest::setUp()
MyClassTest::testPower()
MyClassTest::tearDown()
MyClassTest::setUp()
MyClassTest::testDivision()
MyClassTest::tearDown()
...
MySuite::tearDown()
Дополнительные возможности
Помимо всего вышеизложенного может возникнуть необходимость проверить не только расчеты и поведение сценария, но так же вывод и скорость отработки. Для этого в наше распоряжение предоставлены расширения PHPUnit_Extensions_OutputTestCase и PHPUnit_Extensions_PerformanceTestCase соответственно. Добавим в наш тестируемый класс еще один метод, и проверим правильно ли он работает.
MyClass.php
<?php
class MyClass {// ...public function square($x)
{
sleep(2);
print $x * $x;
}}
MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/Extensions/OutputTestCase.php';
require_once 'PHPUnit/Extensions/PerformanceTestCase.php';
require_once 'MyClass.php';class MyClassOutputTest extends PHPUnit_Extensions_OutputTestCase {protected
$fixture;
protected
function setUp()
{
$this->fixture = $this->sharedFixture;
}protected
function tearDown()
{
$this->fixture = NULL;
}public function testSquare()
{
$this->expectOutputString('4');
$this->fixture->square(2);
}
}class MyClassPerformanceTest extends PHPUnit_Extensions_PerformanceTestCase {protected
$fixture;
protected
function setUp()
{
$this->fixture = $this->sharedFixture;
}protected
function tearDown()
{
$this->fixture = NULL;
}public function testPerformance()
{
$this->setMaxRunningTime(1);
$this->fixture->square(4);
}
}class MyClassTest extends PHPUnit_Framework_TestCase {// …}
Задать ожидаемый вывод сценария можно с помощью методов expectOutputString() или expectOutputRegex(). А для метода setMaxRunningTime() планируемое время отработки указывается в секундах. Для того, что бы эти тесты запускались вместе с уже написанными их всего лишь надо добавить к нашему набору:
MySuite.php
<?php
require_once 'MyClassTest.php';class MySuite extends PHPUnit_Framework_TestSuite {protected
$sharedFixture;public static function suite()
{
$suite = new MySuite('MyTests');
$suite->addTestSuite('MyClassTest');
$suite->addTestSuite('MyClassOutputTest');
$suite->addTestSuite('MyClassPerformanceTest');
return $suite;
}// ...}
Пропускаем тесты
И напоследок рассмотрим ситуацию, в которой некоторые тесты необходимо пропускать по каким либо причинам. К примеру в том случае, когда на тестируемой машине отсутствует какое-либо расширение PHP, можно убедившись в его отсутствии пометить тест, как пропущенный добавив к его коду следующее:
if (!extension_loaded('someExtension')) {
$this->markTestSkipped('Extension is not loaded.');
}
Либо в том случае, когда тест написан для кода, которого еще нет в сценарии (не редкая для TDD ситуация) его можно пометить как не реализованный с помощью метода markTestIncomplete()
Напоследок
Наверное, на этом пока можно остановиться. Тема модульного тестирования данной статьей далеко не завершена — осталось еще использование mock-объектов, тестирование работы с БД, анализ покрытия кода и многое другое. Но надеюсь, что поставленная цель — ознакомить начинающих с базовыми возможностями PHPUnit и подтолкнуть к использованию юнит-тестов, как одному из средств для достижения большей эффективности — была достигнута.
Удачи вам, и стабильных приложений.
Пример 2.1 показывает,
как мы можем писать тесты, используя PHPUnit, которые выполняют операции с массивом PHP.
В этом примере представлены основные соглашения и шаги для написания тестов с помощью PHPUnit:
-
Тесты для класса
Class
содержатся в классеClassTest
. -
ClassTest
наследуется (чаще всего) отPHPUnitFrameworkTestCase
. -
Тесты — общедоступные методы с именами
test*
.Кроме того, вы можете использовать аннотацию
@test
в докблоке метода, чтобы пометить его как метод тестирования. -
Внутри тестовых методов для проверки того, соответствует ли фактическое значение ожидаемому используются методы-утверждения, такие как
assertSame()
(см. Утверждения).
Пример 2.1 Тестирование операций с массивами с использованием PHPUnit
<?php use PHPUnitFrameworkTestCase; class StackTest extends TestCase { public function testPushAndPop() { $stack = []; $this->assertSame(0, count($stack)); array_push($stack, 'foo'); $this->assertSame('foo', $stack[count($stack)-1]); $this->assertSame(1, count($stack)); $this->assertSame('foo', array_pop($stack)); $this->assertSame(0, count($stack)); } }
Мартин Фаулер (Martin Fowler):
Всякий раз, когда возникает соблазн что-то распечатать, используя
вместо этого.
Зависимости тестов
Адриан Кун (Adrian Kuhn) и другие:
Модульные тесты главным образом пишутся в качестве хорошей практики,
помогающей разработчикам выявлять и исправлять баги, проводить рефакторинг кода
и служить в качестве документации для тестируемого программного модуля (программы).
Для достижения этих преимуществ модульные тесты
в идеале должны охватывать все возможные пути исполнения программе. Один модульный тест
обычно покрывает один конкретный путь в одной функции или метода. Однако тестовые методы
необязательно должны быть инкапсулированными и независимыми. Часто
существуют неявные зависимости между тестовыми методами, скрытые в сценарии
реализации теста.
PHPUnit поддерживает объявление явных зависимостей между тестовыми методами.
Эти зависимости не определяют порядок, в котором должны выполняться тестовые методы,
но они позволяют возвращать экземпляр (данные) фикстуры теста, созданные поставщиком (producer)
для передачи его зависимым потребителям (consumers).
- Поставщик — тестовый метод, который предоставляет свой тестируемый модуль в качестве возвращаемого значения.
- Потребитель — тестовый метод, который зависит от одного или более поставщиков и их возвращаемых значений.
Пример 2.2 показывает,
как использовать аннотацию @depends
для представления зависимостей
между тестовыми методами.
Пример 2.2 Использование аннотации @depends
для описания зависимостей
<?php use PHPUnitFrameworkTestCase; class StackTest extends TestCase { public function testEmpty() { $stack = []; $this->assertEmpty($stack); return $stack; } /** * @depends testEmpty */ public function testPush(array $stack) { array_push($stack, 'foo'); $this->assertSame('foo', $stack[count($stack)-1]); $this->assertNotEmpty($stack); return $stack; } /** * @depends testPush */ public function testPop(array $stack) { $this->assertSame('foo', array_pop($stack)); $this->assertEmpty($stack); } }
В вышеприведённом примере первый тест, testEmpty()
,
создаёт новый массив и утверждает, что он пуст. Затем тест возвращает фикстуру
в качестве результата. Второй тест, testPush()
,
зависит от testEmpty()
и ему передаётся результат этого зависимого теста
в качестве аргумента. Наконец, testPop()
зависит от testPush()
.
Примечание
Возвращаемое значение, предоставленное поставщиком, по умолчанию передаётся потребителям «как есть».
Это означает, что когда поставщик возвращает объект, ссылка на этот объект передаётся потребителям.
Вместо ссылки возможна, либо (а) (глубокая) копия через @depends clone
или (б)
(поверхностная) копия (на основе ключевого слова PHP clone
) через
@depends shallowClone
.
Чтобы быстро находить дефекты, нам нужно сосредоточить внимание
на соответствующих неудачных тестах. Вот почему PHPUnit пропускает выполнение теста,
когда зависимый тест (тест с зависимостью) провалился (не прошёл).
Это помогает локализовать дефекты за счёт использования зависимостей между тестами, как это показано
в Пример 2.3.
Пример 2.3 Использование зависимостей между тестами
<?php use PHPUnitFrameworkTestCase; class DependencyFailureTest extends TestCase { public function testOne() { $this->assertTrue(false); } /** * @depends testOne */ public function testTwo() { } }
$ phpunit --verbose DependencyFailureTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. FS Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) DependencyFailureTest::testOne Failed asserting that false is true. /home/sb/DependencyFailureTest.php:6 There was 1 skipped test: 1) DependencyFailureTest::testTwo This test depends on "DependencyFailureTest::testOne" to pass. FAILURES! Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.
У теста может быть несколько аннотаций @depends
.
PHPUnit не изменяет порядок выполнения тестов, поэтому вы должны убедиться,
что все зависимости действительно могут быть выполнены до запуска теста.
Тест, содержащий более одной аннотации @depends
,
получит фикстуру от первого поставщика в качестве первого аргумента, фикстуру
от второго поставщика вторым аргументом и т.д.
См. Пример 2.4
Пример 2.4 Тест с несколькими зависимостями
<?php use PHPUnitFrameworkTestCase; class MultipleDependenciesTest extends TestCase { public function testProducerFirst() { $this->assertTrue(true); return 'first'; } public function testProducerSecond() { $this->assertTrue(true); return 'second'; } /** * @depends testProducerFirst * @depends testProducerSecond */ public function testConsumer($a, $b) { $this->assertSame('first', $a); $this->assertSame('second', $b); } }
$ phpunit --verbose MultipleDependenciesTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. ... Time: 0 seconds, Memory: 3.25Mb OK (3 tests, 3 assertions)
Провайдеры данных
Тестовый метод может принимать произвольное количество аргументов. Эти аргументы могут быть
предоставлены одним или несколькими методами провайдеров данных (data provider)
(см. additionProvider()
в Пример 2.5).
Метод, который будет использован в качестве провайдера данных, обозначается с помощью аннотации @dataProvider
.
Метод провайдера данных должен быть объявлен как public
и возвращать либо
массив массивов, либо объект, реализующий интерфейс Iterator
и возвращать массив при каждой итерации. Для каждого массива, являющегося
частью коллекции, будет вызываться тестовый метод с элементами массива в качестве его аргументов.
Пример 2.5 Использование провайдера данных, который возвращает массив массивов
<?php use PHPUnitFrameworkTestCase; class DataTest extends TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertSame($expected, $a + $b); } public function additionProvider() { return [ [0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 3] ]; } }
$ phpunit DataTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set #3 (1, 1, 3) Failed asserting that 2 is identical to 3. /home/sb/DataTest.php:9 FAILURES! Tests: 4, Assertions: 4, Failures: 1.
При использовании большого количества наборов данных полезно указывать для каждого из них строковый ключ,
вместо использования числового ключа по умолчанию.
Вывод станет более подробным, так как он будет содержать имя набора данных, не прошедший тест.
Пример 2.6 Использование провайдера данных с наборами данных
<?php use PHPUnitFrameworkTestCase; class DataTest extends TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertSame($expected, $a + $b); } public function additionProvider() { return [ 'adding zeros' => [0, 0, 0], 'zero plus one' => [0, 1, 1], 'one plus zero' => [1, 0, 1], 'one plus one' => [1, 1, 3] ]; } }
$ phpunit DataTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set "one plus one" (1, 1, 3) Failed asserting that 2 is identical to 3. /home/sb/DataTest.php:9 FAILURES! Tests: 4, Assertions: 4, Failures: 1.
Пример 2.7 Использование провайдера данных, который возвращает объект Iterator
<?php use PHPUnitFrameworkTestCase; require 'CsvFileIterator.php'; class DataTest extends TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertSame($expected, $a + $b); } public function additionProvider() { return new CsvFileIterator('data.csv'); } }
$ phpunit DataTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set #3 ('1', '1', '3') Failed asserting that 2 is identical to 3. /home/sb/DataTest.php:11 FAILURES! Tests: 4, Assertions: 4, Failures: 1.
Пример 2.8 Класс CsvFileIterator
<?php use PHPUnitFrameworkTestCase; class CsvFileIterator implements Iterator { protected $file; protected $key = 0; protected $current; public function __construct($file) { $this->file = fopen($file, 'r'); } public function __destruct() { fclose($this->file); } public function rewind() { rewind($this->file); $this->current = fgetcsv($this->file); $this->key = 0; } public function valid() { return !feof($this->file); } public function key() { return $this->key; } public function current() { return $this->current; } public function next() { $this->current = fgetcsv($this->file); $this->key++; } }
Когда тест получает входные данные как из метода с @dataProvider
,
так и от одного или более методов с аннотацией @depends
,
первыми будут приходить аргументы от провайдера данных, а после от зависимых тестов.
Аргументы от зависимых тестов будут одинаковыми для каждого набора данных.
См. Пример 2.9
Пример 2.9 Комбинация @depends и @dataProvider в одном тесте
<?php use PHPUnitFrameworkTestCase; class DependencyAndDataProviderComboTest extends TestCase { public function provider() { return [['provider1'], ['provider2']]; } public function testProducerFirst() { $this->assertTrue(true); return 'first'; } public function testProducerSecond() { $this->assertTrue(true); return 'second'; } /** * @depends testProducerFirst * @depends testProducerSecond * @dataProvider provider */ public function testConsumer() { $this->assertSame( ['provider1', 'first', 'second'], func_get_args() ); } }
$ phpunit --verbose DependencyAndDataProviderComboTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 3.50Mb There was 1 failure: 1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2') Failed asserting that two arrays are identical. --- Expected +++ Actual @@ @@ Array &0 ( - 0 => 'provider1' + 0 => 'provider2' 1 => 'first' 2 => 'second' ) /home/sb/DependencyAndDataProviderComboTest.php:32 FAILURES! Tests: 4, Assertions: 4, Failures: 1.
Пример 2.10 Использование нескольких провайдеров данных для одного теста
:name: writing-tests-for-phpunit.data-providers.examples.DataTest.php
<?php use PHPUnitFrameworkTestCase; class DataTest extends TestCase { /** * @dataProvider additionWithNonNegativeNumbersProvider * @dataProvider additionWithNegativeNumbersProvider */ public function testAdd($a, $b, $expected) { $this->assertSame($expected, $a + $b); } public function additionWithNonNegativeNumbersProvider() { return [ [0, 1, 1], [1, 0, 1], [1, 1, 3] ]; } public function additionWithNegativeNumbersProvider() { return [ [-1, 1, 0], [-1, -1, -2], [1, -1, 0] ]; } }
Примечание
Когда тест зависит от теста, который использует провайдеры данных, зависимый
тест начнёт выполняться, когда тест, от которого от зависит, успешно выполнится
хотя бы для одного набора данных. Результат теста, который использует провайдеры данных,
не может быть внедрён в зависимый тест.
Примечание
Все провайдеры данных выполняются как перед вызовом статического метода setUpBeforeClass()
,
так и перед первым вызовом метода setUp()
.
Из-за этого вы не сможете получить доступ к переменным, определённым внутри провайдера данных.
Это требуется для того, чтобы PHPUnit смог вычислить общее количество тестов.
Тестирование исключений
Пример 2.11
показывает, как использовать метод expectException()
для проверки того,
было ли выброшено исключение в тестируемом коде.
Пример 2.11 Использование метода expectException()
<?php use PHPUnitFrameworkTestCase; class ExceptionTest extends TestCase { public function testException() { $this->expectException(InvalidArgumentException::class); } }
$ phpunit ExceptionTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) ExceptionTest::testException Failed asserting that exception of type "InvalidArgumentException" is thrown. FAILURES! Tests: 1, Assertions: 1, Failures: 1.
В дополнение к методу expectException()
существуют методы
expectExceptionCode()
,
expectExceptionMessage()
и
expectExceptionMessageRegExp()
, чтобы установить ожидания для
исключений, вызванных тестируемым кодом.
Примечание
Обратите внимание, что метод expectExceptionMessage, утверждает,
что фактическое сообщение в $actual
содержит ожидаемое сообщение $expected
без выполнения точного сравнения строк.
Кроме того, вы можете использовать аннотации @expectedException
,
@expectedExceptionCode
,
@expectedExceptionMessage
и
@expectedExceptionMessageRegExp
, чтобы установить
ожидания для исключений, вызванных тестируемым кодом.
Пример 2.12
демонстрирует пример использования.
Пример 2.12 Использование аннотации @expectedException
<?php use PHPUnitFrameworkTestCase; class ExceptionTest extends TestCase { /** * @expectedException InvalidArgumentException */ public function testException() { } }
$ phpunit ExceptionTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) ExceptionTest::testException Failed asserting that exception of type "InvalidArgumentException" is thrown. FAILURES! Tests: 1, Assertions: 1, Failures: 1.
Тестирование ошибок PHP
По умолчанию PHPUnit преобразует ошибки, предупреждения и уведомления, вызываемые PHP
во время выполнения теста, в исключения.
Используя эти исключения, вы можете, например, ожидать, что тест вызовет ошибку
PHP, как показано в Пример 2.13.
Примечание
Конфигурация времени выполнения PHP error_reporting
может
ограничивать, какие ошибки PHPUnit будет конвертировать в исключения. Если у вас
возникли проблемы с этой настройкой, убедитесь, что PHP не настроен на подавление
типов ошибок, которые вы тестируете.
Пример 2.13 Ожидание ошибки PHP в тесте, используя @expectedException
<?php use PHPUnitFrameworkTestCase; class ExpectedErrorTest extends TestCase { /** * @expectedException PHPUnitFrameworkErrorError */ public function testFailingInclude() { include 'not_existing_file.php'; } }
$ phpunit -d error_reporting=2 ExpectedErrorTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. . Time: 0 seconds, Memory: 5.25Mb OK (1 test, 1 assertion)
Классы PHPUnitFrameworkErrorNotice
PHPUnitFrameworkErrorWarning
представляют уведомления и предупреждения PHP
соответственно.
Примечание
Вы должны быть как можно более конкретно указывать исключения при тестировании. Тестирование
слишком общих классов исключений может привести к нежелательным побочным
эффектам. Поэтому проверка исключения на соответствие классу Exception
с помощью @expectedException
или expectException()
больше не разрешена.
При тестировании кода, использующего функции PHP, которые вызывают ошибки, например,
fopen
, иногда бывает полезно использовать подавление ошибок во время тестирования.
Таким образом, это позволит вам проверять возвращаемые значения, подавляя уведомления, которые
преобразуются в объекты PHPUnit PHPUnitFrameworkErrorNotice
.
Пример 2.14 Тестирование возвращаемых значений в коде, в котором возникают ошибки PHP
<?php use PHPUnitFrameworkTestCase; class ErrorSuppressionTest extends TestCase { public function testFileWriting() { $writer = new FileWriter; $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff')); } } class FileWriter { public function write($file, $content) { $file = fopen($file, 'w'); if ($file == false) { return false; } // ... } }
$ phpunit ErrorSuppressionTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. . Time: 1 seconds, Memory: 5.25Mb OK (1 test, 1 assertion)
Без подавления ошибки тест завершится неудачей с сообщением
fopen(/is-not-writeable/file): failed to open stream: No such file or directory
.
Тестирования вывода
Иногда вам нужно проверить, что выполнение метода, например,
генерирует ожидаемый вывод (к примеру, через echo
или
print
). Класс
PHPUnitFrameworkTestCase
использует возможности
буферизации вывода PHP
для предоставления такой функциональности.
Пример 2.15
показывает, как использовать метод expectOutputString()
для установки
ожидаемого вывода. Если этот ожидаемый вывод не будет сгенерирован, тест
будет считаться проваленным.
Пример 2.15 Тестирование вывода функции или метода
<?php use PHPUnitFrameworkTestCase; class OutputTest extends TestCase { public function testExpectFooActualFoo() { $this->expectOutputString('foo'); print 'foo'; } public function testExpectBarActualBaz() { $this->expectOutputString('bar'); print 'baz'; } }
$ phpunit OutputTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. .F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) OutputTest::testExpectBarActualBaz Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'bar' +'baz' FAILURES! Tests: 2, Assertions: 2, Failures: 1.
Таблица 2.1
показывает доступные методы для тестирования вывода
Метод | Описание |
---|---|
void expectOutputRegex(string $regularExpression) |
Проверить, что вывод соответствует регулярному выражению $regularExpression . |
void expectOutputString(string $expectedString) |
Проверить, что вывод соответствует строке $expectedString . |
bool setOutputCallback(callable $callback) |
Устанавливает функцию обратного вызова, используемую, например, для нормализации фактического вывода. |
string getActualOutput() |
Получить фактический вывод. |
Примечание
Тест, который генерирует вывод, не будет работать в строгом режиме.
Вывод ошибки
Всякий раз, когда тест терпит неудачу, PHPUnit изо всех сил пытается предоставить вам
максимально возможный контекст, который может помочь выявить проблему.
Пример 2.16 Вывод ошибки, сгенерированный при неудачном сравнении массива
<?php use PHPUnitFrameworkTestCase; class ArrayDiffTest extends TestCase { public function testEquality() { $this->assertSame( [1, 2, 3, 4, 5, 6], [1, 2, 33, 4, 5, 6] ); } }
$ phpunit ArrayDiffTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) ArrayDiffTest::testEquality Failed asserting that two arrays are identical. --- Expected +++ Actual @@ @@ Array ( 0 => 1 1 => 2 - 2 => 3 + 2 => 33 3 => 4 4 => 5 5 => 6 ) /home/sb/ArrayDiffTest.php:7 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
В этом примере только одно из значений массива отличается, а остальные значения показаны,
для обеспечения контекста, где произошла ошибка.
Когда сгенерированный вывод будет длинным для чтения, PHPUnit разделит его
и отобразит несколько строк контекста вокруг каждого несоответствия (разницы).
Пример 2.17 Вывод ошибки при неудачном сравнении длинного массива
<?php use PHPUnitFrameworkTestCase; class LongArrayDiffTest extends TestCase { public function testEquality() { $this->assertSame( [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6] ); } }
$ phpunit LongArrayDiffTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) LongArrayDiffTest::testEquality Failed asserting that two arrays are identical. --- Expected +++ Actual @@ @@ 11 => 0 12 => 1 13 => 2 - 14 => 3 + 14 => 33 15 => 4 16 => 5 17 => 6 ) /home/sb/LongArrayDiffTest.php:7 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
Крайние случаи
Когда сравнение терпит неудачу, PHPUnit создаёт текстовые представления
входных значений и сравнивает их. Благодаря этой реализации результат сравнения изменений (формат diff)
может показать больше проблем, чем существуют на самом деле.
Это происходит только при использовании assertEquals()
или „слабых“ („weak“) функций
сравнения массивов или объектов.
Пример 2.18 Крайний случай в генерации сравнения при использовании слабого сравнения
<?php use PHPUnitFrameworkTestCase; class ArrayWeakComparisonTest extends TestCase { public function testEquality() { $this->assertEquals( [1, 2, 3, 4, 5, 6], ['1', 2, 33, 4, 5, 6] ); } }
$ phpunit ArrayWeakComparisonTest PHPUnit |version|.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) ArrayWeakComparisonTest::testEquality Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( - 0 => 1 + 0 => '1' 1 => 2 - 2 => 3 + 2 => 33 3 => 4 4 => 5 5 => 6 ) /home/sb/ArrayWeakComparisonTest.php:7 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
В этом примере сообщается о различии в первом индексе между
1
и '1'
,
хотя метод assertEquals()
считает, что эти значения совпадают.
Знакомая ситуация: вы разрабатываете приложение, решаете проблемы и, иногда, возникает ощущение, что вы ходите по кругу. Правите один баг и сразу появляется другой. Иногда это тот, который вы поправили 30 минут назад, иногда — новый. Отладка становится очень сложной, но есть хороший и простой выход из этой ситуации. Юнит тесты могут не только уменьшить боль при разработке, но и помогут писать код, который легче сопровождать и легче изменять.
Для понимания того, что такое модульное тестирование, необходимо определить понятие «модуля». Модуль (или unit) — это часть функционала приложения результат работы которой мы можем проверить (или протестировать). Модульное тестирование — это, собственно проверка, что данный модуль работает именно так как ожидается.Написав один раз тесты, всякий раз когда вы внесете изменения в код, вам останется только запустить тесты, для проверки, что всё правильно. Таким образом вы всегда будете уверены, что своими изменениями вы не сломаете систему.
Мифы о юнит тестировании
Не смотря на всю пользу юнит тестирования, не все разработчики им пользуются. Почему? Есть несколько ответов на этот вопрос, но все они — не слишком хорошие оправдания. Рассмотрим распространенные причины и попытаемся разобраться почему они не оправданы.
Написание тестов занимает слишком много времени
Самая распространенная причина: написание тестов — это долго. Конечно, некоторые среды разработки сгенерируют для вас набор простейших тестов, но написание качественных, нетривиальных тестов для вашего кода занимает время. Это нормальная практика — уделить некоторое врмя написанию юнит тестов, в результате это поможет сэкономить гораздо больше времени при сопровождении проекта. Если вы разрабатывали сайт, то наверняка, при добавлении нового функционала, вы тестировали его просто кликая по всевозможным ссылкам на сайте. Простой запуск набора тестов будет гораздо быстрее, нежели ручная проверка всех функций.
Не надо тестов — код и так работает!
Еще одно оправдание разработчиков: приложение работает — нет необходимости в тестировании. Они знают приложение и знают его слабые места, и смогут поправить все, что надо, иногда за несколько секунд. Но представьте, что для разработки приложения привлеки нового разработчика, который понятия не имеет как устроен код. Новичок может сделать какие-либо изменения, которые могут сломать что угодно. Юнит тесты помогут избежать подобных ситуаций.
Это неинтересно
И еще одина причина почему разработчики не любят тесты — это неинтересно. Разработчики по натуре своей любят решать проблемы. Написание кода — это как создание чего-то из пустоты, создание порядка из хаоса, создание чего-то полезного. В результате написание тестов становится скучным занятием, делом которое можно сделать после основной работы. И тестирование отходит на задний план. Но посмотрите на это с другой стороны: ловить какой-либо неприятный баг часами — тоже невесело.
Пример
Приступим к практике. В наших примерах мы будем использовать библиотеку PHPUnit. Самый простой способ установить PHPUnit — затянуть с PEAR канала.
pear config-set auto_discover 1 pear install pear.phpunit.de/PHPUnit
Если все пройдет хорошо, то все необходимые инструменты будут установлены. Если вы хотите установить PHPUnit вручную — инструкцию вы найдете здесь.
Первый тест
Используя PHPUnit, вы будете писать тестовые классы, содержащие тестовые методы и всё это должно удовлетворять следующим соглашениям:
- В большинстве случаев вы будете наследовать класс
PHPUnit_Framework_TestCase
, что предоставит вам доступ к встроенным методам, например,setUp()
иtearDown()
. - Имя тестирующего класса образуется добавлением слова Test к имени тестируемого класса. Например, вы тестируете класс
RemoteConnect
, значит имя тестирующего —RemoteConnectTest
. - Имена тестирующих методов всегда должны начинаться с “test” (например,
testDoesLikeWaffles()
). Методы должны быть публичными. Вы можете использовать приватные методы в своих тестах, но они не будут запускаться как тесты через PHPUnit. - Тестирующие методы не принимают параметров. Вы должны писать тестирующие методы максимально независимыми и самодостаточными. иногда это неудобно, но вы получите более чистые и эффективные тесты.
Напишем небольшой класс для тестирования RemoteConnect.php
:
<?php class RemoteConnect { public function connectToServer($serverName=null) { if($serverName==null){ throw new Exception(“That's not a server name!”); } $fp = fsockopen($serverName,80); return ($fp) ? true : false; } public function returnSampleObject() { return $this; } } ?>
Если мы хотим протестировать функционал для соединения с удаленным сервером, то мы должны написать подобный тест:
<?php require_once('RemoteConnect.php'); class RemoteConnectTest extends PHPUnit_Framework_TestCase { public function setUp(){ } public function tearDown(){ } public function testConnectionIsValid() { // проверка валидности соединения с сервером $connObj = new RemoteConnect(); $serverName = 'www.google.com'; $this->assertTrue($connObj->connectToServer($serverName) !== false); } } ?>
Тестирующий класс наследует базовый PHPUnit класс, а значит и всю необходимую функциональность. Первые два метода — setUp и tearDown — пример этой встроенной функциональности. Это вспомогательные функции, которые являются частью каждого теста. Они выполняются до запуска всех тестов и после соответственно. Но сейчас нас интересует метод testConnectionIsValid
. Этот метод создает объект типа RemoteConnect
, и вызывает метод connectToServer
.
Мы вызываем еще одну вспомогательную функцию assertTrue
в нашем тесте. Эта функция определяет простейшее утверждение (assertion): она проверяет является ли переданное значение истиной. Другие вспомогательные функции выполняют проверки свойств объектов, существования файлов, наличия ключей в массиве, или соответствия регулярному выражению. В нашем случае мы хотим убедиться в правильности подключения к удаленному серверу, т.е. в том, что функция connectToServer
возвращает true
.
Запуск тестов
Запускаются тесты простым вызовом команды phpunit
с указанием вашего php файла с тестами:
phpunit /path/to/tests/RemoteConnectTest.php
PHPUnit запускает все тесты и собирает немного статистики: успешно завершился тест или нет, количество тестов и утверждений (assertions) и выводит все это. Пример вывода:
PHPUnit 3.4 by Sebastian Bergmann . Time: 1 second Tests: 1, Assertions: 1, Failures 0
Для каждого выполненного теста будет выведен результат: «.» если тест завершился успешно, “F” если тест не пройден, “I” если тест невозможно завершить и “S” если тест был пропущен.
В нашем примере тест завершился успешно, значит тестируемая функция работает как ожидается. Но проверки функции только на правильность работы недостаточно, необходимо так же проверить работу функции при неправильном использовании.
В PHPUnit предусмотрен набор базовых проверок, которые покрывают большинство возможных ситуаций. Конечно, иногда придется писать хитрые тесты, которые тестируют нетривиальную функциональность вашего приложения. Но в основном используются базовые функции PHPUnit:
AssertTrue /AssertFalse |
Проверка переданных значений на равенство true/false |
AssertEquals |
Проверка переданных значений на равенство |
AssertGreaterThan |
Сравнивает две переменные (есть так же LessThan, GreaterThanOrEqual, and LessThanOrEqual) |
AssertContains |
Содержит ли переданная переменная заданное значение |
AssertType |
Проверка типа переменной |
AssertNull |
Проверка на равенство null |
AssertFileExists |
Проверка существования файла |
AssertRegExp |
Провка по регулярному выражению |
Например есть функция, которая возвращает объект (returnSampleObject
) и мы хотим убедиться в том, что возвращаемый объект будет нужного нам типа:
<?php function testIsRightObject() { $connObj = new RemoteConnect(); $returnedObject = $connObj->returnSampleObject(); $this->assertType('remoteConnect', $returnedObject); } ?>
Один тест — одно утверждение (assert)
Как и во всех областях разработки программного обеспечения, в тестировании есть лучшие практики. Одна из них — «один тест — одно утверждение» (one test, one assertion). Это правило поможет писать небольшие и легко читаемые тесты. Но иногда возникают мысли: «Раз уж мы здесь проверяем это, то и кое-что другое заодно проверим!». Например:
<?php public function testIsMyString(){ $string = “Mostly Harmless”; $this->assertGreaterThan(0,strlen($string)); $this->assertContains(“42”,$string); } ?>
Наш тест testIsMyString
проводит два разных теста. Сначала тест на пустую строку (длина должна быть > 0), затем тест на содержание в строке подстроки «42». Но этот тест может провалиться как в первом так и во втором случае, а сообщение об ошибке в обоих случаях будет одинаковым. Поэтому стоит придерживаться принципа «один тест — одно утверждение».
Test-driven Development (разработка через тестирование)
Было бы нехорошо, говоря о тестировании не упомянуть о распространенной технике разработки — разработке через тестирование (test driven development). TDD — это техника, используемая при разработке программного обеспечения. Основная идея этой техники заключается в том, что сначала пишутся тесты и только после написания тестов пишется код приложения, который пройдет эти тесты.
На этом все! Следующая статья.
There are many different ways to test your software application, and unit testing is an important one.
So what is unit testing and how can you do it? You’ll learn that and more in this article.
What is Unit Testing?
Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinised for process operation. — SearchSoftwareQuality
In basic terms, unit testing means that you break your application down to its simplest pieces and test these small pieces to ensure that each part is error free (and secure).
This testing is automated and written by software engineers as part of their development process. This is a very important step during development as it helps developers build better applications with fewer bugs.
What is PHPUnit?
You can perform unit testing in PHP with PHPUnit, a programmer-oriented testing framework for PHP. PHPUnit is an instance of the xUnit architecture for unit testing frameworks. It is very easy to install and get started with.
PHPUnit Installation
You can install PHPUnit globally on your server. You can also install it locally, on a per-project, development-time basis as a dependency to your project using composer. This article will explain how to use it on a per project basis.
To get started, create and initiate a new project with composer using these commands:
$ mkdir test-project
$ cd test-project
$ composer init
The first command creates a folder in your current directory, test-project
and the second command moves into it. The last command starts an interactive shell.
Follow the prompt, filling in the details as required (the default values are fine). You can set the project description, author name (or contributors’ names), minimum stability for dependencies, project type, license, and define your dependencies.
You can skip the dependencies part, as we are not installing any dependencies. PHPUnit is supposed to be a dev-dependency
because testing as a whole should only happen during development.
Now, when the prompt asks Would you like to define your dev dependencies (require-dev) interactively [yes]?
, press enter to accept. Then type in phpunit/phpunit
to install PHPUnit as a dev-dependency
.
Accept the other defaults and proceed to generating the composer.json
file. The generated file should look like this currently:
{
"name": "zubair/test-project",
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"autoload": {
"psr-4": {
"Zubair\TestProject\": "src/"
}
},
"authors": [
{
"name": "Idris Aweda Zubair",
"email": "zubairidrisaweda@gmail.com"
}
],
"require": {}
}
To learn how to install PHPUnit globally on your server, read here.
How to Write Tests in PHPUnit
Writing tests in PHPUnit is quite simple. Here are a few conventions to get you started:
- To test a class in PHP, you’ll create a test class named after that class. For example, if I had some sort of
User
class, the test class would be namedUserTest
. - The test class,
UserTest
, will usually inherit thePHPUnitFrameworkTestCase
class. - Individual tests on the class are public methods named with
test
as a prefix. For example, to test asayHello
method on theUser
class, the method will be namedtestSayHello
. - Inside the test method, say
testSayHello
, you use PHPUnit’s method likeassertSame
to see that some method returns some expected value.
A popular convention is to have all tests in a tests
directory, and all source code in the src
directory.
PHPUnit Testing Example
To help understand this article, here’s a sample User
class with simple methods that will be tested:
<?php
namespace ZubairTestProject;
use InvalidArgumentException;
class User
{
public int $age;
public array $favorite_movies = [];
public string $name;
/**
* @param int $age
* @param string $name
*/
public function __construct(int $age, string $name)
{
$this->age = $age;
$this->name = $name;
}
public function tellName(): string
{
return "My name is " . $this->name . ".";
}
public function tellAge(): string
{
return "I am " . $this->age . " years old.";
}
public function addFavoriteMovie(string $movie): bool
{
$this->favorite_movies[] = $movie;
return true;
}
public function removeFavoriteMovie(string $movie): bool
{
if (!in_array($movie, $this->favorite_movies)) throw new InvalidArgumentException("Unknown movie: " . $movie);
unset($this->favorite_movies[array_search($movie, $this->favorite_movies)]);
return true;
}
}
This user class could be the User
class in your movie streaming application. The user has a name, age, and a list of favourite movies that can be updated. For the rest of the article we will test that all these features work as they’re expected to.
Create a UserTest
class in the tests
folder. Paste this in to start:
<?php
namespace ZubairTestProject;
use PHPUnitFrameworkTestCase;
final class UserTest extends TestCase
{
// Tests will go here
}
Test Constructor
Normally, you wouldn’t be testing the __construct
method. However, since we’re setting values in it, it only makes sense to be sure that the values are being set correctly.
This seems like a very small thing to test, but that’s the whole point of unit tests – to ensure that the smallest parts of your application function as expected.
Create a testClassConstructor
method to test the constructor:
public function testClassConstructor()
{
$user = new User(18, 'John');
$this->assertSame('John', $user->name);
$this->assertSame(18, $user->age);
$this->assertEmpty($user->favorite_movies);
}
Let’s take a quick break now, to see how to run the tests.
How to Run Tests in PHPUnit
You can run all the tests in a directory using the PHPUnit binary installed in your vendor folder.
$ ./vendor/bin/phpunit --verbose tests
You can also run a single test by providing the path to the test file.
$ ./vendor/bin/phpunit --verbose tests/UserTest.php
You use the --verbose
flag to get more information on the test status.
Now, we can run the test and see the output:
The output shows that we ran 1 test, and made 3 assertions in it. We also see how long it took to run the test, as well as how much memory was used in running the test.
These assertions are what PHPUnit uses to compare values returned from the methods to their expected value.
This example uses assertSame
to check if the name
and age
properties on the user object match the entered values. It also uses assertEmpty
to check that the favorite_movies
array is empty.
To see a list of all these assertions, you can check out PHPUnit’s docs here.
Edit the code to check if the user age is the same as 21.
public function testClassConstructor()
{
$user = new User(18, 'John');
$this->assertSame('John', $user->name);
$this->assertSame(21, $user->age);
$this->assertEmpty($user->favorite_movies);
}
Running the test again this time gives this output:
The output now shows that we ran 1 test, with 2 successful assertions, and also a failed one. We can see some explanation of the failure, showing the expected value, the gotten value, and the line where the error is from.
Test testName and tellAge
Next, we can test the testName
method. This method tells the name of a user as a sentence. So, we can write the test to check:
- If the returned value is a string.
- If the returned string has the user’s name in it (with or without case sensitivity).
public function testTellName()
{
$user = new User(18, 'John');
$this->assertIsString($user->tellName());
$this->assertStringContainsStringIgnoringCase('John', $user->tellName());
}
The test uses the assertions assertIsString
and assertStringContainsStringIgnoringCase
to check that the return value is a string and that it contains the string John, respectively.
The testAge
method is very similar to testName
and uses the same logic. Its test will be similar to the previous one:
public function testTellAge()
{
$user = new User(18, 'John');
$this->assertIsString($user->tellAge());
$this->assertStringContainsStringIgnoringCase('18', $user->tellAge());
}
Test addFavoriteMovie
We can test this method, too. This method adds a movie to the list of movies. To test it, we can check if the newly added movie is in the list, and that the number of items in the list actually increased.
The latter is for confirming that items are not being displaced. Also, since the function returns some value at the end, we can check that this value is correct too.
public function testAddFavoriteMovie()
{
$user = new User(18, 'John');
$this->assertTrue($user->addFavoriteMovie('Avengers'));
$this->assertContains('Avengers', $user->favorite_movies);
$this->assertCount(1, $user->favorite_movies);
}
Here, we use a few new assertions – assertTrue
, assertContains
, and assertCount
– to check that the returned value is true, that it contains the newly added string, and that the array now has one item in it.
Test removeFavoriteMovie
Finally, we can test that the method to remove a movie works.
public function testRemoveFavoriteMovie()
{
$user = new User(18, 'John');
$this->assertTrue($user->addFavoriteMovie('Avengers'));
$this->assertTrue($user->addFavoriteMovie('Justice League'));
$this->assertTrue($user->removeFavoriteMovie('Avengers'));
$this->assertNotContains('Avengers', $user->favorite_movies);
$this->assertCount(1, $user->favorite_movies);
}
Here, we’re adding some movies to the list. Then, we remove one of them, and confirm that the function returned true. Next, we confirm the removal by checking that the value is no longer in the list. Finally, we confirm that we have only one movie in the list, instead of two.
Conclusion
Now you know how to set up PHPUnit in your projects and how to test and ensure that you’re building world class software. You can find all the code for this article here.
If you have any questions or relevant advice, please get in touch with me to share them.
To read more of my articles or follow my work, you can connect with me on LinkedIn, Twitter, and Github. It’s quick, it’s easy, and it’s free!
Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started
There are many different ways to test your software application, and unit testing is an important one.
So what is unit testing and how can you do it? You’ll learn that and more in this article.
What is Unit Testing?
Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinised for process operation. — SearchSoftwareQuality
In basic terms, unit testing means that you break your application down to its simplest pieces and test these small pieces to ensure that each part is error free (and secure).
This testing is automated and written by software engineers as part of their development process. This is a very important step during development as it helps developers build better applications with fewer bugs.
What is PHPUnit?
You can perform unit testing in PHP with PHPUnit, a programmer-oriented testing framework for PHP. PHPUnit is an instance of the xUnit architecture for unit testing frameworks. It is very easy to install and get started with.
PHPUnit Installation
You can install PHPUnit globally on your server. You can also install it locally, on a per-project, development-time basis as a dependency to your project using composer. This article will explain how to use it on a per project basis.
To get started, create and initiate a new project with composer using these commands:
$ mkdir test-project
$ cd test-project
$ composer init
The first command creates a folder in your current directory, test-project
and the second command moves into it. The last command starts an interactive shell.
Follow the prompt, filling in the details as required (the default values are fine). You can set the project description, author name (or contributors’ names), minimum stability for dependencies, project type, license, and define your dependencies.
You can skip the dependencies part, as we are not installing any dependencies. PHPUnit is supposed to be a dev-dependency
because testing as a whole should only happen during development.
Now, when the prompt asks Would you like to define your dev dependencies (require-dev) interactively [yes]?
, press enter to accept. Then type in phpunit/phpunit
to install PHPUnit as a dev-dependency
.
Accept the other defaults and proceed to generating the composer.json
file. The generated file should look like this currently:
{
"name": "zubair/test-project",
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"autoload": {
"psr-4": {
"Zubair\TestProject\": "src/"
}
},
"authors": [
{
"name": "Idris Aweda Zubair",
"email": "zubairidrisaweda@gmail.com"
}
],
"require": {}
}
To learn how to install PHPUnit globally on your server, read here.
How to Write Tests in PHPUnit
Writing tests in PHPUnit is quite simple. Here are a few conventions to get you started:
- To test a class in PHP, you’ll create a test class named after that class. For example, if I had some sort of
User
class, the test class would be namedUserTest
. - The test class,
UserTest
, will usually inherit thePHPUnitFrameworkTestCase
class. - Individual tests on the class are public methods named with
test
as a prefix. For example, to test asayHello
method on theUser
class, the method will be namedtestSayHello
. - Inside the test method, say
testSayHello
, you use PHPUnit’s method likeassertSame
to see that some method returns some expected value.
A popular convention is to have all tests in a tests
directory, and all source code in the src
directory.
PHPUnit Testing Example
To help understand this article, here’s a sample User
class with simple methods that will be tested:
<?php
namespace ZubairTestProject;
use InvalidArgumentException;
class User
{
public int $age;
public array $favorite_movies = [];
public string $name;
/**
* @param int $age
* @param string $name
*/
public function __construct(int $age, string $name)
{
$this->age = $age;
$this->name = $name;
}
public function tellName(): string
{
return "My name is " . $this->name . ".";
}
public function tellAge(): string
{
return "I am " . $this->age . " years old.";
}
public function addFavoriteMovie(string $movie): bool
{
$this->favorite_movies[] = $movie;
return true;
}
public function removeFavoriteMovie(string $movie): bool
{
if (!in_array($movie, $this->favorite_movies)) throw new InvalidArgumentException("Unknown movie: " . $movie);
unset($this->favorite_movies[array_search($movie, $this->favorite_movies)]);
return true;
}
}
This user class could be the User
class in your movie streaming application. The user has a name, age, and a list of favourite movies that can be updated. For the rest of the article we will test that all these features work as they’re expected to.
Create a UserTest
class in the tests
folder. Paste this in to start:
<?php
namespace ZubairTestProject;
use PHPUnitFrameworkTestCase;
final class UserTest extends TestCase
{
// Tests will go here
}
Test Constructor
Normally, you wouldn’t be testing the __construct
method. However, since we’re setting values in it, it only makes sense to be sure that the values are being set correctly.
This seems like a very small thing to test, but that’s the whole point of unit tests – to ensure that the smallest parts of your application function as expected.
Create a testClassConstructor
method to test the constructor:
public function testClassConstructor()
{
$user = new User(18, 'John');
$this->assertSame('John', $user->name);
$this->assertSame(18, $user->age);
$this->assertEmpty($user->favorite_movies);
}
Let’s take a quick break now, to see how to run the tests.
How to Run Tests in PHPUnit
You can run all the tests in a directory using the PHPUnit binary installed in your vendor folder.
$ ./vendor/bin/phpunit --verbose tests
You can also run a single test by providing the path to the test file.
$ ./vendor/bin/phpunit --verbose tests/UserTest.php
You use the --verbose
flag to get more information on the test status.
Now, we can run the test and see the output:
The output shows that we ran 1 test, and made 3 assertions in it. We also see how long it took to run the test, as well as how much memory was used in running the test.
These assertions are what PHPUnit uses to compare values returned from the methods to their expected value.
This example uses assertSame
to check if the name
and age
properties on the user object match the entered values. It also uses assertEmpty
to check that the favorite_movies
array is empty.
To see a list of all these assertions, you can check out PHPUnit’s docs here.
Edit the code to check if the user age is the same as 21.
public function testClassConstructor()
{
$user = new User(18, 'John');
$this->assertSame('John', $user->name);
$this->assertSame(21, $user->age);
$this->assertEmpty($user->favorite_movies);
}
Running the test again this time gives this output:
The output now shows that we ran 1 test, with 2 successful assertions, and also a failed one. We can see some explanation of the failure, showing the expected value, the gotten value, and the line where the error is from.
Test testName and tellAge
Next, we can test the testName
method. This method tells the name of a user as a sentence. So, we can write the test to check:
- If the returned value is a string.
- If the returned string has the user’s name in it (with or without case sensitivity).
public function testTellName()
{
$user = new User(18, 'John');
$this->assertIsString($user->tellName());
$this->assertStringContainsStringIgnoringCase('John', $user->tellName());
}
The test uses the assertions assertIsString
and assertStringContainsStringIgnoringCase
to check that the return value is a string and that it contains the string John, respectively.
The testAge
method is very similar to testName
and uses the same logic. Its test will be similar to the previous one:
public function testTellAge()
{
$user = new User(18, 'John');
$this->assertIsString($user->tellAge());
$this->assertStringContainsStringIgnoringCase('18', $user->tellAge());
}
Test addFavoriteMovie
We can test this method, too. This method adds a movie to the list of movies. To test it, we can check if the newly added movie is in the list, and that the number of items in the list actually increased.
The latter is for confirming that items are not being displaced. Also, since the function returns some value at the end, we can check that this value is correct too.
public function testAddFavoriteMovie()
{
$user = new User(18, 'John');
$this->assertTrue($user->addFavoriteMovie('Avengers'));
$this->assertContains('Avengers', $user->favorite_movies);
$this->assertCount(1, $user->favorite_movies);
}
Here, we use a few new assertions – assertTrue
, assertContains
, and assertCount
– to check that the returned value is true, that it contains the newly added string, and that the array now has one item in it.
Test removeFavoriteMovie
Finally, we can test that the method to remove a movie works.
public function testRemoveFavoriteMovie()
{
$user = new User(18, 'John');
$this->assertTrue($user->addFavoriteMovie('Avengers'));
$this->assertTrue($user->addFavoriteMovie('Justice League'));
$this->assertTrue($user->removeFavoriteMovie('Avengers'));
$this->assertNotContains('Avengers', $user->favorite_movies);
$this->assertCount(1, $user->favorite_movies);
}
Here, we’re adding some movies to the list. Then, we remove one of them, and confirm that the function returned true. Next, we confirm the removal by checking that the value is no longer in the list. Finally, we confirm that we have only one movie in the list, instead of two.
Conclusion
Now you know how to set up PHPUnit in your projects and how to test and ensure that you’re building world class software. You can find all the code for this article here.
If you have any questions or relevant advice, please get in touch with me to share them.
To read more of my articles or follow my work, you can connect with me on LinkedIn, Twitter, and Github. It’s quick, it’s easy, and it’s free!
Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started
Это первая часть серии «PHPUnit для начинающих». В этом руководстве мы объясним для чего покрывать код unit-тестами и всю мощь инструмента PHPUnit. В конце мы напишем простой тест с использованием PHPUnit.
- PHPUnit для начинающих. Часть 1: Начните использование.
- PHPUnit для начинающих. Часть 2: Data Provider.
- PHPUnit для начинающих. Часть 3: Тестовые «двойники»
Типы тестов
Прежде чем мы погрузимся в PHPUnit давайте разберём различные типы тестов. В зависимости от того, как вы хотите категоризировать их, в PHPUnit применяются любые типы тестов для разработки ПО.
Давайте разделим тесты на категории по уровню их специфичности. По данным Википедии. В целом существует 4 признанных уровня тестов:
- Unit-тестирование (модульное): этот уровень тестирует наименьшую единицу функциональности.
С точки зрения разработчика его задачей является убедиться, что тестируемая функция выполняет именно то, для чего она реализована. Таким образом, она должна быть минимально зависима или совершенно независима от другой функции или класса. Она должна быть написана таким образом, чтобы она полностью выполнялась в памяти, т.е. она не должна коннектиться к БД, не должна обращаться к сети или использовать ФС и т.д. Unit-тестирование должно быть как можно более простым. - Интеграционное тестирование: этот уровень «соединяет» разные единицы кода и тестирует правильно ли работают их комбинации. Он надстраивается сверху над unit-тестированием и способен отловить баги, которые нельзя выявить с помощью unit-тестирования, т.к. интеграционное тестирование проверяет, работает ли класс А с классом Б.
- Системное тестирование: оно создано для воспроизведения работы сценариев в условиях, приближенных к боевым. Оно, в свою очередь, надстраивается сверху над интеграционным тестированием. В то время как интеграционное тестирование обеспечивает слаженную работу различных частей системы. Системное тестирование отвечает за то, что система работает так, как предполагает пользователь, прежде чем отправить её следующий уровень.
- Приёмочное тестирование: в то время как выше приведённые тесты предназначены для разработчиков на стадии разработки, приёмочное тестирование фактически выполняется пользователями ПО. Пользователей не интересуют внутренние особенности ПО, их интересует только как работает это ПО.
Если мы поместим типы тестов в пирамиду, выглядеть это будет так:
Что такое PHPUnit
Глядя на пирамиду вверху, мы можем сказать, что юнит-тестирование — это строительный материал для всех остальных видов тестирования. Когда мы закладываем сильную базу, мы способны построить устойчивое приложение. Однако написание тестов вручную, а также запуск тестов каждый раз когда вы вносите изменения — это процесс трудоёмкий. Если бы существовал инструмент для автоматизации этого процесса, то написание тестов стало бы гораздо более приятным занятием.
Вот где выходит на сцену PHPUnit. В настоящее время PHPUnit наиболее популярный фреймворк для юнит-тестирования в PHP. Кроме наличия таких возможностей, как моки (подделки) объектов, он также может анализировать покрытие кода, логирование и предоставляет тысячи других возможностей.
Давайте установим PHPUnit в нашей системе:
- Загрузите его: PHPUnit распространяется в PHAR(PHp ARhive) файле. Скачать можно здесь.
- Добавьте путь к нему в системную переменную
$PATH
: после скачивания PHAR файла, убедитесь, что он является запускаемым (executable
) и путь, где он находится, прописан в системной переменной$PATH
. Т.о. вы сможете запускать его из любого места.
Если вы работаете на Unix-подобной системе, то это вы можете сделать следующими командами:
$ wget https://phar.phpunit.de/phpunit.phar
$ chmod +x phpunit.phar
$ sudo mv phpunit.phar /usr/local/bin/phpunit
Если вы сделали всё верно, то вы сможете увидеть версию установленного PHPUnit, набрав в вашем терминале команду:
$ phpunit --version
Ваш первый unit-тест
Пришло время написать ваш первый unit-тест! Для начала нам нужен какой-нибудь класс, который мы будем тестировать. Давайте напишем простенький класс под названием Calculator
. И напишем для него тест.
Создайте файл "Calculator.php"
и скопируйте в него нижеприведённый код. Этот класс Calculator
имеет только один метод add
.
class Calculator { public function add($a, $b) { return $a + $b; } }
Теперь создайте файл для тестов "CalculatorTest.php"
и скопируйте в него следующий код. Мы остановимся на каждом методе более детально.
require 'Calculator.php'; class CalculatorTests extends PHPUnit_Framework_TestCase { private $calculator; protected function setUp() { $this->calculator = new Calculator(); } protected function tearDown() { $this->calculator = NULL; } public function testAdd() { $result = $this->calculator->add(1, 2); $this->assertEquals(3, $result); } }
- Line 2: подключаем файл тестируемого класса
Calculator.php
. Так как в этом файле мы собираемся тестировать именно этот класс, убедитесь, что он подключен. - Line 8:
setUp()
это метод, который вызывается перед каждым тестом. Запомните он вызывается перед Каждым тестом, что означает, что если вы добавите ещё один метод теста в этот класс, то он будет вызываться и перед ним тоже. - Line 13: аналогично методу
setUp()
,tearDown()
вызывается после каждого теста. - Line 18:
testAdd()
— это метод-тест для методаadd()
. PHPUnit будет распознавать каждый метод, начинающийся с test, как метод-тест и автоматически запускать его. В действительности этот метод очень прост: сначала мы вызываем методCalculator::add()
чтобы вычислить значение 1 плюс 2, а затем мы проверяем, что этот метод вернул правильное значение, используяassertEquals()
из PHPUnit.
Заключительной частью проделанной работы является запуск PHPUnit и проверка, что все тесты проходят (выполняются без ошибок). В вашем терминале зайдите в директорию где вы создали файл с тестами и запустите следующую команду:
$ phpunit CalculatorTest.php
Если вы всё сделали правильно, то вы должны увидеть что-то вроде этого:
PHPUnit 3.7.32 by Sebastian Bergmann. . Time: 31ms, Memory: 2.25Mb OK (1 test, 1 assertion)
Заключение
Мы завершили первое руководство из серии «PHPUnit для начинающих». В следующей статье мы собираемся показать вам как использовать Data Provider
(поставщик данных) в ваших тестах.
Надеемся это простое руководство поможет вам в вашей разработке и поможет начать использовать unit-тестирование.
Если вам понравился перевод на эту тему читайте нас в Twitter, подписывайтесь на наши группы в Facebook, ВКонтакте, Google+. Мы будем рады продолжить серию.
Мы уже познакомились с тестированием. Узнали о видах и даже разобрались что к чему? Но это всё была теория. Без примеров всё это не то.
Предыдущие статьи:
https://mynrg.ru/zachem-nuzhno-testirovanie
https://mynrg.ru/testirovanie-vidy
Часто привожу и буду приводить примеры на php, потому что язык прост и понятен. И поняв что-то на php, перенести это на другой язык не составит труда.
Я уже касался TDD — Test Driven Development.
Что такое Test Driven Development?
Идея Test Driven Development в том, что ты пишешь код таким образом, что сначала пишешь еще одну часть кода, единственная цель которого заключается в том, чтобы убедиться, что изначально предназначенный код работает, даже если он еще не написан.
Что такое PHPUnit?
PHPUnit — это фреймворк для тестирования на php.
Это набор утилит (классы PHP и исполняемые файлы), который не только упрощает сложный процесс создания тестов (частое написание тестов влечет за собой написание большего количества кода, чем на самом деле оно того стоит), но также позволяет видеть процесс тестирования в наглядном виде и позволяет узнать
- о качестве кода (например, возможно, в классе слишком много условий IF), что отмечено как плохое качество, поскольку для изменения одного условия часто требуется переписывать столько тестов, сколько есть IF)
- охват кода (сколько данного класса или функции были покрыты тестами и сколько осталось непроверенными)
- и ещё много всего интересного
Чтобы не утомлять слишком большим текстом (или уже слишком поздно?), давай попробуем использовать это фреймворк и начнём учиться на примерах.
Каркас приложения
Поисковики выдают кучу однотипных статей для новичков на тему unit-тестирования, где тестируются функции умножения, возведения в степень и т.п.
Да там понятна суть тестирования, синтаксис и инструменты, но в таком виде тяжело понять зачем оно надо. Хотя у меня и есть на эту тему целая статья.
Для того чтобы воссоздать структуру реального php-приложения возьмём https://github.com/php-pds/skeleton.
Пойдём в папку с нашими проектами и склонируем этот каркас:
git clone https://github.com/php-pds/skeleton converter
cd converter
composer require phpunit/phpunit —dev
Обрати внимание, что мы использовали --dev
флаг только для установки PHPUnit в качестве зависимости от разработчика, то есть он не нужен в продакшене, что облегчит проект. Также обрати внимание, что использование с PDS-Skeleton означает, что наша папка tests
уже создана для нас, с двумя демонстрационными файлами, которые мы удалим.
Далее, нам нужен фронт-контроллер для нашего приложения — файл, через который проходят все запросы и маршрутизируются. В папку converter/public
, создаю index.php
со следующим содержимым:
<?php
echo «Hello world»;
Если ты повторяешь все эти действия, то я предполагаю, что ты знаешь php, ООП, composer и как установить и запустить php(хотя бы у себя на компе, хотя бы используя denwer).
Открыв проект в браузере, мы должны увидеть
Нужно удалить лишние файлы. Это можно сделать либо руками, либо из командной строки:
rm -rf bin/* src/* docs/* tests/*
Нам нужен файл конфигурации PHPUnit, который сообщает PHPUnit, где искать тесты, какие шаги подготовки необходимо выполнить перед тестированием и как тестировать. В корне проекта создаю файл phpunit.xml
со следующим содержимым:
<phpunit bootstrap=»tests/autoload.php»>
<testsuites>
<testsuite name=»converter»>
<directory suffix=»Test.php»>tests</directory>
</testsuite>
</testsuites>
</phpunit>
Проект может иметь несколько наборов тестов, в зависимости от контекста. Например, все связанные с учетной записью пользователя могут быть сгруппированы в набор под названием «users» и может иметь свои собственные правила или другую папку для тестирования этого функционала. В нашем случае проект очень мал, поэтому одной группы тестов более чем достаточно, ни будут в папке tests, что мы и говорим тэгом
<directory suffix=»Test.php»>tests</directory>
В phpunit.xml
мы определили аргумент для тэга directory — suffix
— это означает, что PHPUnit будет запускать только те файлы, которые заканчиваются Test.php
. Полезно, когда мы хотим использовать и другие файлы из папки tests
, но не хотим, чтобы они запускались автоматически. Например вспомогательные файлы для тестов.
Прочитать о других аргументах для конфигурации можно здесь .
Значение аругмента bootstrap
указывает PHPUnit , какой файл PHP загрузить перед тестированием. Это полезно при настройке параметров автоматической загрузки или загрузки тестовых переменных, даже тестовой базы данных и т. д. То есть убрать, то что не нужно для тестов, а нужно только на продакшене или добавить то, что не нужно на продакшене, но нужно здесь.
Давайте создадим tests/autoload.php
:
<?php
require_once __DIR__.’/../vendor/autoload.php’;
В этом случае мы просто загружаем автозагрузчик по умолчанию Composer, потому что у PDS-Skeleton уже есть пространство имен Tests, настроенное для нас composer.json
. Если мы заменим значения шаблона в этом файле собственными, в итоге получим composer.json
следующее:
{
«name»: «mynrg.ru/jsonconverter»,
«type»: «standard»,
«description»: «A converter from JSON files to PHP array files.»,
«homepage»: «https://github.com/php-pds/skeleton»,
«license»: «MIT»,
«autoload»: {
«psr-4»: {
«MyNrg\»: «src/MyNrg»
}
},
«autoload-dev»: {
«psr-4»: {
«MyNrg\»: «tests/MyNrg»
}
},
«bin»: [«bin/converter»],
«require-dev»: {
«phpunit/phpunit»: «^6.2»
}
}
После этого мы запускаем composer du
(сокращенно dump-autoload
) для обновления сценариев автозагрузки.
composer du
Первый тест
Помните, что TDD — это искусство сначала делать ошибки, а затем вносить изменения в код, который заставляет их перестать быть ошибками, а не наоборот. Имея это в виду, давайте создадим наш первый тест.
tests/MyNrg/Converter/ConverterTest.php
<?php
namespace MyNrgConverter;
use PHPUnitFrameworkTestCase;
class ConverterTest extends TestCase {
public function testHello() {
$this->assertEquals(‘Hello’, ‘Hell’ . ‘o’);
}
}
Лучше всего, если тесты будут следовать той же структуре, которую мы закладываем в проект. Имея это в виду, мы даем им те же пространства имен и располагаем также в дереве папок. Таким образом, наш ConverterTest.php
файл находится в tests в
подпапке MyNrg, вложенной папке Converter
.
В этом примере «тестовый пример» утверждается, что строка Hello
равна конкатенации Hell
и o
.
Если выполнить:
php vendor/bin/phpunit
запустится тест и мы получим положительный результат.
PHPUnit запускает каждый метод, который начинается с testв файле с тестами, если не указано иное.
Но этот тест не является ни полезным, ни реалистичным. Мы использовали его только для проверки работы нашей установки. Давайте сейчас напишем правильный. Перепишем ConverterTest.php
файл следующим образом:
<?php
namespace MyNrgConverter;
use PHPUnitFrameworkTestCase;
class ConverterTest extends TestCase
{
public function testSimpleConversion()
{
$input = ‘{«key»:»value»,»key2″:»value2″}’;
$output = [
‘key’ => ‘value’,
‘key2’ => ‘value2’
];
$converter = new MyNrgConverterConverter();
$this->assertEquals($output, $converter->convertString($input));
}
}
}
Мы тестируем простое преобразование(testSimpleConversion). Входные данные $input представляют собой JSON объект, который приведён к типу строки (JSON.stringify в JS), а ожидаемый вывод($output) — это массив PHP. Наш тест утверждает , что наш класс конвертер, при обработке с $input
использованием convertString
метода, дает желаемое $output
, так же , как это определено здесь.
Если мы сейчас запустим тест, то, конечно же, будет ошибка.
Естественно, класс же еще не существует.
Отредактируем phpunit.xml
файл, добавив к phpunit
атрибут colors="true"
:
<phpunit colors=»true» bootstrap=»tests/autoload.php»>
Теперь, если мы запустим php vendor/bin/phpunit
, мы получим такой результат:
Создание тестового прохода
Теперь мы начинаем процесс прохождения этого теста.
Наша первая ошибка: Класс MyNrgConverter Converter не найден. Давайте это исправим.
Создадим src/
MyNrg/Converter/Converter.php:
<?php
namespace MyNrgConverter;
class Converter
{
}
Теперь, если мы перезапустим тест
Уже лучше! Нам не хватает метода, который мы вызываем в тесте. Давайте добавим его в наш класс.
<?php
namespace MyNrgConverter;
class Converter
{
public function convertString(string $input): ?array
{
}
}
Мы определили метод, который принимает параметр строкового типа, и возвращает либо массив, либо null, если что-то пошло не так. Если ты не знаком со типизацией в php типами ( string $input
), узнай больше здесь и для понимания возвращаемых значений типа null ( ?array
), см. здесь .
Перезапустим тест
Это ошибка возвращаемго типа — функция ничего не возвращает (void) — потому что она пуста , а ожидается, что она вернет либо null, либо массив. Давайте завершим метод. Мы будем использовать встроенную PHP json_decode
функцию для декодирования строки JSON.
public function convertString(string $input): ?array
{
$output = json_decode($input);
return $output;
}
Ну, вроде, всё должно работать, разве нет?
Функция возвращает объект, а не массив. Ах, ха! Это потому, что мы не активировали режим «ассоциативного массива» для функции json_decode
.
Функция превращает объект JSON в экземпляр класса stdClass
по умолчанию, если не указано иное. Изменим это так:
public function convertString(string $input): ?array
{
$output = json_decode($input, true);
return $output;
}
Наш тест теперь проходит! Он получает тот же результат, который мы ожидаем от него в тесте!
Давайте добавим еще несколько тестовых примеров, чтобы убедиться, что наш метод действительно работает по назначению. Давайте сделаем это немного сложнее, чем простой пример, с которого мы начали. Добавьте следующие методы ConverterTest.php
:
{
$input = ‘{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5]}’;
$output = [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
];
$converter = new MyNrgConverterConverter();
$this->assertEquals($output, $converter->convertString($input));
}
public function testMoreComplexConversion()
{
$input = ‘{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}}’;
$output = [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
];
$converter = new MyNrgConverterConverter();
$this->assertEquals($output, $converter->convertString($input));
}
public function testMostComplexConversion()
{
$input = ‘[{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}},{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}},{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}}]’;
$output = [
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
];
$converter = new MyNrgConverterConverter();
$this->assertEquals($output, $converter->convertString($input));
}
Мы сделали каждый тестовый пример немного более сложным, чем предыдущий. Повторный запуск тестового набора показывает нам, что все в порядке …
но что-то не так, не так ли? Здесь очень много повторений, и если мы когда-либо изменим API класса, нам придется внести изменения в 4 местоположения (и это только пока).
Data Providers
Поставщики данных — это специальные функции в тестовых классах, которые имеют одну конкретную цель: предоставить набор данных тестовой функции, так что вам не нужно будет повторять свою логику в нескольких тестовых функциях, как это было сделано. Это лучше всего объяснить на примере. Давайте переформулируем наш ConverterTest
класс следующим образом:
<?php
namespace MyNrgConverter;
use PHPUnitFrameworkTestCase;
class ConverterTest extends TestCase
{
public function conversionSuccessfulProvider()
{
return [
[
‘{«key»:»value»,»key2″:»value2″}’,
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
[
‘{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5]}’,
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
],
],
[
‘{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}}’,
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
],
[
‘[{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}},{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}},{«key»:»value»,»key2″:»value2″,»some-array»:[1,2,3,4,5],»new-object»:{«key»:»value»,»key2″:»value2″}}]’,
[
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
[
‘key’ => ‘value’,
‘key2’ => ‘value2’,
‘some-array’ => [1, 2, 3, 4, 5],
‘new-object’ => [
‘key’ => ‘value’,
‘key2’ => ‘value2’,
],
],
],
],
];
}
/**
* @param $input
* @param $output
* @dataProvider conversionSuccessfulProvider
*/
public function testStringConversionSuccess($input, $output)
{
$converter = new MyNrgConverterConverter();
$this->assertEquals($output, $converter->convertString($input));
}
}
Сначала мы написали новый метод conversionSuccessfulProvider
. Это указывает на то, что все предоставленные случаи должны возвращать положительный результат, потому что вывод и ввод совпадают. Поставщики данных возвращают массивы (чтобы тестовая функция могла автоматически перебирать элементы). Один элемент массива — это один тестовый пример — в нашем случае каждый элемент представляет собой массив с двумя элементами: первый входные данные, второй — вывод в виде массива.
Затем мы собрали функции тестирования в одну с более общим названием, что свидетельствует о том, что ожидается: testStringConversionSuccess
. Этот метод тестирования допускает два аргумента: ввод и вывод. Остальная логика идентична тому, что было раньше. Кроме того, чтобы убедиться, что метод использует dataprovider, мы объявляем dataProvider с помощью @dataProvider conversionSuccessfulProvider
.
Вот и все. И, запустив, мы получаем тот же результат.
Если мы хотим добавить дополнительные тестовые примеры, нам нужно только добавить дополнительные пары ввода-вывода в провайдер. Не нужно изобретать новые имена методов или повторять код.
Введение в покрытие кода
Покрытие кода — это показатель, указывающий, сколько из нашего кода покрывается тестами. Если наш класс имеет два метода, но только один из них проверяется в тестах, то наш охват кода составляет не более 50% — в зависимости от того, сколько логических веток (IF, switch, циклов и т. д.) имеют методы (каждая ветка должна быть охвачена отдельным тестом). PHPUnit имеет возможность автоматически генерировать отчеты о покрытии кода после запуска данного набора тестов.
Давайте быстро это настроим. Мы будем расширять phpunit.xml
, добавляя тэги и как элементы сразу внутри , так что они получаются элементами первого уровня (если принять, что это коневой элемент и его уровень 0):
<phpunit …>
<filter>
<whitelist>
<directory suffix=».php»>src/</directory>
</whitelist>
</filter>
<logging>
<log type=»tap» target=»tests/build/report.tap»/>
<log type=»junit» target=»tests/build/report.junit.xml»/>
<log type=»coverage-html» target=»tests/build/coverage» charset=»UTF-8″ yui=»true» highlight=»true»/>
<log type=»coverage-text» target=»tests/build/coverage.txt»/>
<log type=»coverage-clover» target=»tests/build/logs/clover.xml»/>
</logging>
Тэг filter настраивает белый список, указывающий PHPUnit, на какие файлы обратить внимание при тестировании. У нас будут проверяться все .php-файлы внутри папки /src на любом уровне .
Logging сообщает PHPUnit какие отчёт(логи) генерировать — различные инструменты могут читать различные логи, поэтому ничего страшного, что мы создаём больше форматов, чем может потребоваться. В нашем случае нас действительно интересует только HTML.
Прежде чем это сработает, нам нужно активировать XDebug. PHPUnit использует XDebug для проверки классов, которые он проходит.
Повторный запуск теста теперь будет информировать нас о сгенерированных отчетах о покрытии. Кроме того, они появятся в дереве каталогов в указанном месте.
Давайте откроем index.html
файл в браузере.
В индексном файле будет отображаться сводка всех тестов.Можно щелкнуть по отдельным классам, чтобы просмотреть их подробные отчеты о покрытии, а наведение на тела методов вызовет подсказки, которые объясняют, насколько данный метод проверен.
Мы поняли принцип TDD и внедрили тесты на простейшем php-приложении.