codeception symfony functional Функциональные тесты - заметки, mock

Наброски того, как можно организовать функциональное тестирование API без браузера/сервера

Проверялось на: Symfony 6, Codeception 5

Базовый класс для тестовых наборов может выглядеть напр. так:

<?php

namespace App\Tests\_support;

use App\Tests\FunctionalTester;
use Doctrine\ORM\EntityManagerInterface;

class BaseFunctionalCest
{
    use CommonFunctionalTrait;

    protected $inited = false;

    protected EntityManagerInterface $em;

    protected FunctionalTester $functionalTester;

    public function _before(FunctionalTester $I, )
    {
        $this->initMe($I);
    }


    protected function initMe(FunctionalTester $I)
    {
        if (!$this->inited) {
            $this->functionalTester = $I;
            $this->getEntityManager();
            $this->runInitConsoleCommands();
        }
    }

    protected function getApiVersion() {
        return 'v1';
    }

    protected function getEntityManager()
    {
        $this->em = $this->functionalTester->grabService('doctrine')->getManager();
    }
}

Получаем клиент из ядра симфони:

    protected function getUnauthedClient(): KernelBrowser
    {
        $client = $this->functionalTester->grabService('kernel')->getContainer()->get('test.client');
        $client->catchExceptions(false); // выключаем перехват исключений
        return $client;
    }

-- можно выставить какие-то опции, если требуется сделать так, чтобы клиент работал от доменного имени отличного от localhost-а, то в config/services.yaml добавим:

parameters:
    test.client.parameters:
        HTTP_HOST: '%env(APP_COOKIE_DOMAIN)%' # с использованием .env, можно и явно задать строку

Используя этот клиент можно авторизоваться напр. так (в т.ч. получить куку):

    protected function createClient(UserAuthData $userCred)
    {
        $client = $this->getUnauthedClient();
        if (!empty($userCred->username) && !empty($userCred->password)) {
            $client->request(
                'POST',
                "/api/" . $this->getApiVersion() . "/login",
                [],
                [],
                ['CONTENT_TYPE' => 'application/json'],
                json_encode($userCred)
            );

            if ($client->getResponse()->getStatusCode() !== Response::HTTP_OK) {
                throw new AccessDeniedHttpException('C авторизацией что-то пошло не так');
            }
        } else {
            throw new \Exception('User data format not valid!');
        }

        $this->functionalTester->assertEquals(200, $client->getResponse()->getStatusCode());

        
        return $client;
    }

В свою очередь "создание клиента" (по сути тут скорее "создание авторизованного клиента", т.к. базовый объект клиентам мы извлекаем из ядра условно называя его "неавторизованным" в коде выше) можно использовать для авторизации под конкретным юзером:

    /**
     * Авторизация под админом
     * 
     * @param FunctionalTester $I
     */
    protected function authAsAdmin(FunctionalTester $I)
    {
        $this->client = $this->createClient($this->getAdminAuthData());
    }

Далее, как обычно в кодэсэпшн можно использовать зависимости типа before, чтобы сначала авторизоваться, а потом, напр. запросить данные пользователя:

   /**
     * @before authAsAdmin
     */
    public function getAdminInfo(FunctionalTester $I)
    {
        $client = $this->client;
        $client->request(
            'GET',
            '/api/' . $this->getApiVersion() . '/user_info',
            [],
            [],
            [],
        );

        $rsContent = json_decode($client->getResponse()->getContent(), true);


        $I->assertEquals(200, $client->getResponse()->getStatusCode());
        $I->assertArrayHasKey('roles', $rsContent);
        $I->assertContains('ROLE_ADMIN', $rsContent['roles']);
    }

Как подменить объект пользователя (полезно при изоляции авторизации, напр. в микросервисе)

Опишем класс, с теми ролями, что нам нужны, и можно описать методы для проведения авторизации:

/**
    * Авторизация под обычным пользователем 
    * 
    * @param FunctionalTester $I
    */
protected function authAsSimpleUser(FunctionalTester $I)
{
    $this->client = $this->getUnauthedClient();
    $client = $this->client;
    $simpleUser = new SimpleUser();
    $client->loginUser($simpleUser);
}

protected function getUnauthedClient(): KernelBrowser
{
    $client = $this->functionalTester->grabService('kernel')->getContainer()->get('test.client');
    $client->catchExceptions(false); // выключаем перехват исключений
    return $client;
}

-- тут мы используем loginUser(), опираясь на authAsSimpleUser() (с помощью аннотаций codeception) мы можем использовать уже авторизованного пользователя для выполнения к запроса к методу, для которого нужно обладать ролью:

/**
    * @before authAsSimpleUser
    */
public function test(FunctionalTester $I)
{
    $client = $this->client;
    $client->request(
        'GET',
        '/api/' . $this->getApiVersion() . '/test',
        [],
        [],
        [],
    );

    $rsContent = json_decode($client->getResponse()->getContent(), true);

    $I->pre($rsContent);
}

Видео-материалы

  • О функциональных тестах в целом: https://youtu.be/1vKDqzuMVeU
  • Как организовать функциональные тесты на Symfony с использованием Codeception https://youtu.be/RN2_1x__-bQ
  • Подмена пользователя, авторизация с указанной ролью (в т.ч. для несуществующего пользователя): https://youtu.be/j4-ttbmaOFM

Источники