Ответы пользователя по тегу PHPUnit
  • Как замокать приватное свойство в классе phpunit?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Имитация вашего объекта:
    class Relay
    {
        private $privateProperty;
    
        public function __construct()
        {
            $this->privateProperty = new \stdClass();
        }
    
        // метод, который работает с приватным свойством
        public function call()
        {
            return $this->privateProperty;
        }
    }


    Способ 1: Нативный PhpUnit + Reflection API
    use PHPUnit\Framework\TestCase;
    
    class RelayTest extends TestCase
    {
        public function testCall(): void
        {
            $reflectionClass = new \ReflectionClass(Relay::class);
            $reflectionProperty = $reflectionClass->getProperty('privateProperty');
            $reflectionProperty->setAccessible(true);
    
            // создаем наш объект БЕЗ конструктора
            $relay = $reflectionClass->newInstanceWithoutConstructor();
    
            // Меняем свойство и вызываем метод, работающий с этим приватным полем
            $reflectionProperty->setValue($relay, 1111);
            self::assertEquals(1111, $relay->call());
    
            // Меняем свойство и вызываем метод, работающий с этим приватным полем
            $reflectionProperty->setValue($relay, 'aaaa');
            self::assertEquals('aaaa', $relay->call());
        }
    }


    Способ 2: Через Codeception Stub
    class RelayTest extends TestCase
        public function testCall(): void
        {
            /** @var Example $stub */
            $stub = Stub::make(Relay::class, [
                'privateProperty' => 1111,
            ]);
            self::assertEquals(1111, $stub->call());
    
            $stub = Stub::make(Relay::class, [
                'privateProperty' => 'aaaa',
            ]);
            self::assertEquals('aaaa', $stub->call());
        }
    }

    Отступления и полезные советы:
    • Почему-то стандартный, давно используемый мною, способ с инъекцией в мок приватного филда не зашел и выдавал null всегда.
    • Юзайте Inversion of Control Principle, например инъекцию зависимости через конструктор или инъекцию через метод с присвоением в конструкторе NullObject. Оба способа будут хороши для тестирования.
    Ответ написан
  • PHPUNIT: Как возвращать разные значения в методе мока?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Так точно работает:
    $this->anyMock
        ->expects($this->exactly(4))
        ->method('doSomething')
        ->withConsecutive(...$args)
        ->willReturnOnConsecutiveCalls(...$results)

    Кол-во $args и $results и цифра в $this->exactly(4) одинаково,
    $args - массив массивов


    UPD: Сейчас глянул код, по идее ваш способ абсолютно эквивалентный и должен работать, либо приведенный мной будет ругаться также. переменные $response1 и $response2 точно имплементят ResponseInterface?

    Причина проблемы:
    Вы просто настраиваете мок на одно число вызовов, а в ассертах вызываете меньшее число раз (в примере -- настроили на 4 ответа, а ассертов всего 3, то есть 1 остался заряженный) и в моке остается значение, которое потом вызывается каким-либо образом в других ситцациях!
    Классический сайд эффект. Нужно после использованного мока удостовериться, что он пустой или в новом тесте заново его создать, скорее всего у вас мок в приватной переменной класса и потому состояние шарится между методами теста...

    Именно поэтому вам expects() скорее всего и подсказала, в чем дело.

    Решения:
    1. Создавать мок именно в методе, не злобоупотреблять setUp()
    2. Контроллировать число вызовов в методе и аргументов для Consecutive Calls, в этом поможет expects($this->exactly(N))
    Ответ написан
  • Как замокать метод phpunit?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Скорее всего вам это не нужно, так как приватные методы не нужно мокать, в общем случае. Мокают методы ВНЕШНИХ зависимостей

    Но если прямо нужно, то текущий объект (метод которого тестите) тоже создаете через мок-билдер. То есть тестируемый класс становится моком. И делаете тестируемый метод исключением, то есть его мокать у мока не нужно и он будет отрабатывать как есть...

    Пример (не забываем про методологию AAA):
    // Arrange
    $provider = $this
         ->getMockBuilder(UserProvider::class)
         ->setMethodsExcept(['delete']) // перечисленные тут методы будут настоящими, хоть и мок
         ->setConstructorArgs([])       // сюда зависимости конструктора передать
         ->getMock();
    $provider = $provider
         ->expects($this->once())
         ->method('canDelete') // мокаем приватный метод
         ->willReturn(true);
    
    // Action
    $result = $provider->delete();
    
    // Assert
    $this->assertEquals(true, $result);
    Ответ написан
  • Как сделать юнит тест для метода отправки сообщений на Email?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Юнит-тест:
    Юнит-тест невозможен в данном случае, тк нет этого самого юнита в виде чистой функции (без побочных эффектов). Тут сплошной сайд-эффект: отправка на SMTP, прослушивание HTTP, то есть особо не потестируешь юнитами.
    Объектом тестирования юнит-тестом служит или функция или объект. А у вас и нет ни функции/процедуры даже хотя бы.

    Это будет функциональный (если будете из кода вызывать) или приемочный (если свое приложение через HTTP дергать) в любом случае.

    Как по итогу все же протестировать:
    Такой функционал обычно тестируют так:
    • для начала настройки отправки почты переносят в конфиг
    • в тестовом окружении в конфиг подсовывают настройки своего SMTP сервера
    • поднимают фейковый почтовый сервер, например Mailhog на Go (очень легко ставится)
    • выполняют работу скрипта, чтобы почта ушла, почта летит на ваш подмененный сервер, и у него через АПИ проверяют, что письмо пришло


    Если возьмете Codeception, установите Mailhog на машину, где будут гоняться тесты, или докер-контейнер (вообще парой строк, например если в Gitlab CI гоняете), и поставите модуль Codeception + Mailhog. То легко все проделаете.
    Ответ написан
  • Как правильно писать глубокие интеграционные тесты?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Эти тесты функциональные строят обычно на фикстурах:
    - загружается состояние системы (например БД) до необходимого, например загружается категория КА и товар ТА
    - поднимается приложение или достаточное число сервисов для теста
    - проводится тест (например добавляется товар дублер товар ТА и система должна ругнуться или удаляется товар, которого нет, например товар ТБ)

    И так для каждого кейса — для него подгоняется состояние. Необходимо, чтобы тест все же должен быть относительно изолированным. Для некоторых тестов состояние одно и инструкции его поднятия будут одинаковы. Кроме того это быстро и проще.
    Для скорости тестов обычно берут in memory БД — это еще одна важная причина использовать сложные хорошие ORM, которые могут работать и с примитивными базами в памяти и с полноценными, отделяя код и работу с данными. Разница во времени по сравнению с реальным БД сильно выше.

    Вообще эти тесты сложные и потому их число как правило сильно меньше простых юнитов... Кроме того иногда уже имеет смысл делать это через приемочные (браузерные) тесты
    Ответ написан
  • PHPUnit: можно ли одновременно замокапить и класс, и интерфейс?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Не надо плодить классы. Воспользуйтесь тогда анонимным классом (для таких задач их и придумали) прямо в коде:
    new class implement YourInterface {
        // тут методы, которые интерфейс содержит
        // тут методы, которые вы будете вызывать
    }
    Ответ написан
  • Как проверить, что массив содержит только значения типа integer или float?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Костыльно, но работает, ищу более элегантное решение
    Кратко -- генерим из массива значений массив массивов, в каждом из которых лежит тип элементов начального массива и итерируем тест по каждому типу, сравнивая с необходимыми типами

    Примечание 1: тип float единственный отдается как double: php.net/manual/ru/function.gettype.php

    Примечание 2: Если в проверяемом массиве много значений, то нужно сделать так, чтобы провайдер возвращал только массивы с уникальными типами

    /**
         * @dataProvider providerTypesOfArraysElements
         */
        public function testTypeInArray($type)
        {
            $typesCorrect = ['integer', 'double'];
            $this->assertContains($type, $typesCorrect);
        }
    
        public function providerTypesOfArraysElements()
        {
            $arr = [28, 1, 7.3];
    
            return array_map(function($val) {
                return [gettype($val)];
            }, $arr);
        }

    Скрин успешного
    5b4c72b0aee63855546159.png
    Скрин неуспешного
    5b4c75ba049b8949436369.png
    Скрин неуспешного
    5b4c76202ce06299115373.png
    Ответ написан
  • Как правильно настроить тестовое окружение в Symfony 4 с .env файлами?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Можете подглянуть у Ларавел, они давно с .env

    In addition, you may create a.env.testing file in the root of your project. This file will override the .env file's variables when running PHPUnit tests or executing Artisan commands with the --env=testing switch.

    https://laravel.com/docs/5.6/testing#environment
    Ответ написан
  • Как вы поддерживаете фикстуры актуальными?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Есть дополнение к Gii от Елисеева,
    в частности оно умеет из актуальной базы формировать файл с массивом данных для модели
    https://github.com/ElisDN/yii2-gii-fixture-generator

    Подробнее -- делаешь фикстуры, потом доработал БД и руками че-то наполнил... и все фикстура выдохлась
    тогда еще раз через gii выкатил актуальную инфу в файл и готово :)
    Ответ написан