PhpUnit

Не смотря на то, что у PHPUnit хорошая документация, есть моменты, по которым хочется видеть больше примеров.

Конфигурирование php.ini

Мы хотим видеть все ошибки PHP, предупреждения, уведомления и т.д., когда мы проводим наши тесты.

error_reporting=-1

Когда что-то идет действительно не так, мы хотим видеть все сообщение об ошибке (которое по умолчанию усечено до 1024 символов).

log_errors_max_len=0

Мы не хотим, чтобы Xdebug печатал свои следы исключений во время выполнения наших тестов.

xdebug.show_exception_trace=0

Чтобы операторы assert() оценивались и вызывали исключения:

zend.assertions=1
assert.exception=1

Сбор данных о покрытии кода и генерация отчета о покрытии кода иногда требуют больше памяти, чем PHP может использовать по умолчанию.

memory_limit=-1

Как замокать весь класс, кроме определенного метода

$mockWithOneRealMethod = $this->getMockBuilder(My::class)
			->disableOriginalConstructor()
			->setMethodsExcept(['myRealMethod'])
			->getMock();

self::assertEquals($expected, $mockWithOneRealMethod->myRealMethod());

Если myRealMethod вызывает другой публичный замоканный метод (например getPhone), который возвращает значение, то эмулировать возвращаемое им значение можно так: 

$mockWithOneRealMethod->method('getPhone')->willReturn('8-800-123-4567');

Как выбросить эксепшен из замоканного метода:

$mockWithOneRealMethod->method('getPhone')->willThrowException(new Exception('msg'));

Важные изменения в версиях

  • ранее метод setMethodsExcept назывался setMethods
  • ранее можно было мокать Protected-методы, кажется начиная с PHPUnit v.7 нельзя, только публичные.

В PHPUnit 10:

1. setMethods и setMethodsExcept окончательно удалены, поэтому setMethodsExcept нужно написать самостоятельно с использованием onlyMethods

2. setMethods() разделили на части:

  • onlyMethods() может только заменять методы, существующие в оригинальном классе
  • addMethods() — только добавлять новые (которых в оригинальном классе нет)

3. Теперь createPartialMock умеет заменять только существующие методы мокаемого класса, и нужно заменить использование createPartialMock на getMockBuilder()->addMethods().

4. Вместо собственной реализации подмены, теперь используется библиотека https://github.com/phpspec/prophecy - с использованием философии пророчеств (prophecies) и откровений (revelations). Подробнее: https://phpunit.readthedocs.io/ru/latest/test-doubles.html#prophecy

Как замокать приватный метод

К сожалению, PHPUnit не позволяет менять доступ метода, поэтому у Вас 2 выхода:

  1. использовать AspectMock
  2. заменить private на public

Как замоканый метод заставить возвращать переданное в него значение

$mock = $this->createMock(My::class);
$mock->method('mockMethod')->willReturnArgument(0); // 0 - первый аргумент

Как подменять значение, возвращаемое замоканым методом

$mock = $this->createMock(My::class);
$mock->method('mockMethod')->willReturnCallback(function ($value) {
    return [ $value ];
});

Как проверить, что метод вызывался + значения переданных аргументов

Прежде чем читать примеры, важно знать про следующее:

$mock->expects($this->once()) - метод должен быть вызван (не менее 1 раза и не более 1 раза), есть кстати :

$mock->expects($this->exactly(2)) - метод должен быть вызван указанное кол-во раз

$mock->expects($this->any()) - метод может быть вызван сколько угодно раз (а может и не быть вызван, тут нет требования), в итоге даже, если метод был вызван, а аргумент был успешно проверен, phpunit все равно скажет "This test did not perform any assertions", но если аргумент не прошел проверку, то phpunit ругнется, словно был сделан assert.

Важный момент:

  • сначала пишется ожидание
  • потом пишется вызов метода

например обычно мы вызываем myRealMethod а затем ожидаем совпадение результата:

$mock = $this->getMockBuilder(My::class)
			->disableOriginalConstructor()
			->setMethodsExcept(['myRealMethod'])
			->getMock();
$result = $mock->myRealMethod();
$this->assertSame('expected value', $result);

а в данном случае надо писать ожидание, а затем вызов метода:

$mock = $this->getMockBuilder(My::class)
			->disableOriginalConstructor()
			->setMethodsExcept(['myRealMethod'])
			->getMock();
$mock->expects($this->once())->method('mockMethod')->with('expectedValue1', 'expectedValue2');
$mock->myRealMethod();

Таким образом проверяет значение которое будет отправлено в mockMethod при вызове метода myRealMethod.

Еще примеры:

// Метод update() должен вызваться только один раз со строкой 'something' в качестве своего параметра.
$mock->expects($this->once())->method('update')->with('something');
// или
$mock->expects($this->once())->method('update')->with($this->equalTo('something'));

// Метод update() должен вызваться только два раза с определенными аргументами
$mock->expects($this->exactly(2)->method('update')->withConsecutive(
    [$this->equalTo('foo'), $this->greaterThan(0)],
    [$this->equalTo('bar'), $this->greaterThan(0)]
);

Проверка через кэлбэк:

// Метод writeln должен быть вызыван с указанным значением аргумента
$phpFpmWrapper = $this->getMockBuilder(PhpFpmWrapper::class)
            ->disableOriginalConstructor()
            ->setMethods(['writeln'])
            ->getMock();
$result = '';
$phpFpmWrapper->method('writeln')->with($this->callback(function ($argument) use (&$result) {
            $result = $argument;
            return true; // иначе phpunit ругается
}));
$phpFpmWrapper->exec();
$this->assertSame('expected value', $result);

Как проверить, что метод ни разу не вызывался

$handler = $this->getMockBuilder(MyHandler::class)->setMethodsExcept(['realMethod'])->getMock();
$handler->expects(self::never())->method('someMethod');
$handler->realMethod();

Более сложные варианты описаны тут »

Как проверить, что иногда выбрасывается Exception

if ($expected === UnexpectedValueException::class) {
	$this->expectException($expected);
	$handler->realMethod();
} else {
	$result = $handler->realMethod();
	$this->assertEquals($expected, $result, $message);
}

Проблемы с памятью в Doctrine

Когда мы пишем функциональные тесты, то по окончанию теста нужно чистить EntityManager:

$entityManager->close();

внутри вызывается $entityManager->clear();

Казалось бы этого достаточно, но память все равно утекает, поэтому нужно делать:

self::$dbObjectManager = null;

Итоговая версия выглядит так:

    public static function tearDownAfterClass(): void
    {
        parent::tearDownAfterClass();
        self::$dbObjectManager->close();
        self::$dbObjectManager = null;
    }

Как подменить сервис в DI-контейнере Symfony

    /**
     * @var ContainerInterface
     */
    private $container;

    protected function setUp()
    {
        self::bootKernel();
        $this->container = self::$kernel->getContainer();
        $mock = $this->getMockBuilder(My::class)->disableOriginalConstructor()->getMock();
        $this->container->set('my_service', $mock);
    }

PHPUnit Best Practices: 1

Белый список файлов

Какие файлы исходного кода следует включить в отчёт о покрытии кода (это можно сделать либо используя опцию командной строки --whitelist, либо через файл конфигурации). Сразу стоит сказать, что речь идет только о файлах из <whitelist>...</whitelist>

  • addUncoveredFilesFromWhitelist="false" - в отчёт попадают только whitelist-файлы содержащие хотя бы одну строку выполненного кода
  • addUncoveredFilesFromWhitelist="true" (по умолчанию) - в отчёт попадают все whitelist-файлы, даже если ни одна строка кода такого файла не была выполнена
  • processUncoveredFilesFromWhitelist="true" - PHPUnit выполнит include каждого whitelist-файла, таким образом комментарии в коде не будут влиять на % покрытия кода тестами. ВНИМАНИЕ: данный режим может вызвать проблемы, например, когда файл исходного кода содержит код вне области класса или функции.
  • processUncoveredFilesFromWhitelist="false" (по умолчанию) - PHPUnit не будет делать include, таким образом комментарии в коде будут расценены как код и это скажется на уменьшении % покрытия кода тестами. Внимание: должно быть установлено addUncoveredFilesFromWhitelist="true"

Источник: 1


30.12.2010 09:08