Как написать unit тест php

Время на прочтение
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:

  1. Тесты для класса Class содержатся в классе ClassTest.

  2. ClassTest наследуется (чаще всего) от PHPUnitFrameworkTestCase.

  3. Тесты — общедоступные методы с именами test*.

    Кроме того, вы можете использовать аннотацию @test в докблоке метода, чтобы пометить его как метод тестирования.

  4. Внутри тестовых методов для проверки того, соответствует ли фактическое значение ожидаемому используются методы-утверждения, такие как 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):

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

Зависимости тестов

Адриан Кун (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
показывает доступные методы для тестирования вывода

Таблица 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 — это техника, используемая при разработке программного обеспечения. Основная идея этой техники заключается в том, что сначала пишутся тесты и только после написания тестов пишется код приложения, который пройдет эти тесты.

На этом все! Следующая статья.

How to Test PHP Code With PHPUnit

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.

Screenshot-2022-03-08-at-11.08.39

Composer init prompt

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": {}
}
Composer generated compose.json

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 named UserTest.
  • The test class, UserTest, will usually inherit the PHPUnitFrameworkTestCase class.
  • Individual tests on the class are public methods named with test as a prefix. For example, to test a sayHello method on the User class, the method will be named testSayHello.
  • Inside the test method, say testSayHello, you use PHPUnit’s method like assertSame 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;
    }
}
Sample User Class

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);
}
Test for __construct method

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:

Screenshot-2022-03-08-at-13.17.54

Test 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:

Screenshot-2022-03-08-at-13.24.20

Failed Assertion 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

How to Test PHP Code With PHPUnit

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.

Screenshot-2022-03-08-at-11.08.39

Composer init prompt

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": {}
}
Composer generated compose.json

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 named UserTest.
  • The test class, UserTest, will usually inherit the PHPUnitFrameworkTestCase class.
  • Individual tests on the class are public methods named with test as a prefix. For example, to test a sayHello method on the User class, the method will be named testSayHello.
  • Inside the test method, say testSayHello, you use PHPUnit’s method like assertSame 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;
    }
}
Sample User Class

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);
}
Test for __construct method

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:

Screenshot-2022-03-08-at-13.17.54

Test 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:

Screenshot-2022-03-08-at-13.24.20

Failed Assertion 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 в нашей системе:

  1. Загрузите его: PHPUnit распространяется в PHAR(PHp ARhive) файле. Скачать можно здесь.
  2. Добавьте путь к нему в системную переменную $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-приложении.

Понравилась статья? Поделить с друзьями:
  • Как написать trash talk
  • Как написать trap beat
  • Как написать telegram bot python
  • Как написать tab
  • Как написать synthwave