Link Search Menu Expand Document
18 Июля 2023 г.

PHP Unit

Содержание

Вводная статья про unit тестирование https://lexusalex.ru/5-php-phpunit

Установка

# Установка стандартная через composer
composer require --dev phpunit/phpunit
# Проверка версии
./vendor/bin/phpunit --version # PHPUnit 10.2.6 by Sebastian Bergmann and contributors.

Настройка

Запускать phpunit можно с ключами в cli, что менее удобно, гораздо удобнее создать конфигурационный файл phpunit.xml


# Генерирование конфига в xml
./vendor/bin/phpunit --generate-configuration
# Указание конфигурационного файла
./vendor/bin/phpunit --colors=always --configuration=etc/phpunit.xml

Основные ключи конфигурационного файла

phpunit
    bootstrap - Сценарий начальной загрузки, как правило это путь до autoload.php
    cacheDirectory - Каталог для кеша
    executionOrder - В каком порядке выполнять тесты, так как у нас независимость, ставлю random
    beStrictAboutOutputDuringTests - Помечать тест как рискованный когда он выводит что-то на печать
testsuites
    testsuite - Где искать тесты, список каталогов, так же можно указать имя    

Пример базовой конфигурации, переделанной под мои нужды:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="../vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="../tests/bootstrap.php"
         cacheDirectory="../var/cache/.phpunit.result.cache"
         executionOrder="random"
         beStrictAboutOutputDuringTests="true"
         beStrictAboutTodoAnnotatedTests="true">
    <testsuites>
        <testsuite name="default">
            <directory>../tests</directory>
        </testsuite>
    </testsuites>
    <php>
        <ini name="error_reporting" value="-1"/>
        <env name="APPLICATION_ENVIRONMENT" value="test" force="true"/>
        <env name="APPLICATION_DEBUG" value="1" force="true"/>
    </php>
</phpunit>

Написание тестов

// Название файла должно совпадать с названием класса в таком виде [Название]Test
// Тогда phpunit распознает что это тест
<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class SolutionTest extends TestCase
{
    
}

// Внутри добавляются тестовые методы
// Обычно один метод проверяет один case
public function testSolution1()
{

}

public function testSolution2()
{

}

public function testSolution3()
{

}

Утверждения

Внутри методов пишут утверждения, некоторые примеры:

public function testSolution1()
{
    // Наличие key в массиве
    self::assertArrayHasKey('key',['key' => 123]);
    // Указанного ключа нет в массиве
    self::assertArrayNotHasKey('key',['key2' => 123]);
    // Имеется ли значение в массиве
    self::assertContains(1, [1,2,3,4]);
    // Значения в массиве нет
    self::assertNotContains(1, [2,3,4]);
    // Количество элементов в массиве
    self::assertCount(1, [2]);
    // Сколько элементов нет в массиве
    self::assertNotCount(2, [2]);
    // Пустое значение
    self::assertEmpty('');
    // Эквивалентные значения
    self::assertEquals(1,1);
    // Не эквивалентные значения
    self::assertNotEquals(3,1);
    // true
    self::assertTrue(print_r(''));
    // false
    self::assertFalse(false);
    // тип и значение должны совпадать
    self::assertSame([1],[1]);
    // тип или значение не должны совпадать
    self::assertNotSame('1',1);
    // Сравнить две строки, без учета регистра
    self::assertStringContainsStringIgnoringCase('STR','str');
    // Сравнить размеры двух массивов
    self::assertSameSize([1],[1]);
    // Больше чем
    self::assertGreaterThan(1,2);
}

Документация с полным перечнем https://docs.phpunit.de/en/10.2/assertions.html

Исключения

// Если код выбрасывает исключения - это тоже можно тестировать
$this->expectException(DomainException::class); // Указываем класс которое исключение ожидается
throw new DomainException();

// Так же есть возможность проверить сообщение, возвращаемое исключением
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('`stdClass` must implement Psr\Container\ContainerInterface');

Печать строки в поток

Если код выдает побочный эффект как строка, это тоже можно отловить, таким образом

$this->expectOutputString('String');
echo 'String';

Незавершенные тесты

// Этим можно управлять указав, что тест незавершенный
$this->markTestIncomplete('Завершу позже'); // I

Пропущенные тесты

// Тест можно пропускать, при каких - то условиях
$this->markTestSkipped('Пропускаю'); // S
// Тесты могут быть зависимы друг от друга, хоть это не приветствуется

Stub

Объект, который заменяет реальный объект и создает его заглушку.

В стаб передаются название класса или название интерфейса.

Посмотрим на примере:

<?php
// Класс который нужно заменить
namespace Test;

class Original
{
    public function send(string $y)
    {
       return false;
    }
}

// Тестовый класс
namespace Test;

use PHPUnit\Framework\TestCase;

class SolutionTest extends TestCase
{
    public function testSolution()
    {
        // Если это интерфейс он на лету создает реализацию этого интерфейса и объект этого класса
        // Если это класс создает объект этого класса примерно с таким названием Mock_Original_23a89a53. Переопределяет все его методы
       $stub = $this->createStub(Original::class);
       // Помимо всех оригинальных методов, phpunit добавляет свои
       // Например наш метод send должен вернуть true, делается это так
       $stub->method('send')->willReturn(true);
       // Теперь метод send возвращает true
       $stub->send(4));
       
       // Так же метод может бросить нужное нам исключение, но в исходно коде все останется так же
       $stub->method('send')->willThrowException(new \RuntimeException());
       
       // Еще имеем возможность вернуть аргумент по порядковому номеру
       $stub->method('send')->willReturnArgument(0);
       $stub->send(44) // Здесь вернется 44 как первый агрумент метода
       
       // Можно возарщать функцию
       $stub->method('send')->willReturnCallback(function (){return 55;});
       $stub->send() // 55
       
       // Можно вернуть сам объект стаба, если это необходимо
       $stub->method('send')->willReturnSelf();
       print_r($stub->send());
       
       // В итоге мы на лету генерируем то, что должен возвращать метод
       // При этом очень удобно работать с классами с небольшим набором методов
       // Стаб это просто заглушка которая возвращает результат
    }
}

Mock

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

// Объект заменяемого класса, это и есть мок
// __construct() __clone() не выполняются
// Все методы исходного класса возвращают null, без его вызова
//методы с правом доступа final, private, protected и static не могут быть подменены
$mock = $this->createMock(\Test\Original::class);

$mock->expects($this->any())->method('send')->with($this->equalTo('4'))->will($this->returnValue(false));
$mock->method('send')->willReturn(false);

Фикстуры

Обобщенно - это настройка окружения для теста. Нужно для подготовительной работы для начала выполнения теста и окончания.

Метод tearDown - запускается перед setUp

Метод setUp - запускается перед тестом.

class SolutionTest extends TestCase
{
    protected function setUp(): void
    {
        echo 'setUp';
    }

    protected function tearDown(): void
    {
        echo 'tearDown';
    }


    public function testSolution0()
    {
        echo 'testSolution0';
    }

    public function testSolution1()
    {
        echo 'testSolution1';
    }

    public function testSolution2()
    {
        echo 'testSolution2';
    }
}

// Результат получается такой
//tearDownsetUptestSolution0RtearDownsetUptestSolution2RtearDownsetUptestSolution1R

Data Providers

Дата провайдеры - это такая штука, когда нужно прогнать несколько значений через один assert

Удобно тестировать например почтовые адреса


<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class SolutionTest extends TestCase
{

    public static function Solution(): array
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 2, 3],
        ];
    }
    #[DataProvider('Solution')]
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }
    
    // Если провайдер слишком большой можно вынести его в отдельный класс
    // Для каждого сета можно указать пояснение, так будет яснее какой тест не пройдет
    //'one' => [0, 0, 0],
    //'two' => [0, 1, 1],
    //'three' => [1, 0, 1],
    //'four' => [1, 2, 3],
}

Возник вопрос или предложение пиши на почту alexsey_89@bk.ru или в Телеграмм канал

Дата публикации: 18 Июля 2023 г.

Содержание


Наверх

Последнее изменение страницы: 22 Июля 2023 г.