Модульное тестирование класса обслуживания Symfony - PullRequest
2 голосов
/ 06 августа 2020

Я ищу руководство по написанию модульного теста для Symfony класса обслуживания. Весь день ищу в Интернете, но в основном я нахожу устаревшие вопросы и ответы о старых версиях phpunit и старых Symfony версиях.

Я использую Symfony 4 и имею класс обслуживания ApiService. php. Этот класс подключается к внешней службе API, я не смотрю на тестирование этой внешней службы API, а скорее смотрю на мои собственные методы с фиксированным набором данных.

Очень урезанная версия класса похожа на эту и живет в папка src / Service / ApiService. php:

<?php

namespace App\Service;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Uri;
use JsonException;

class ApiService
{
    /**
     * Set if test environment is enabled
     *
     * @var    bool
     * @since  1.0.0
     */
    private bool $test;

    /**
     * User key for API authentication
     *
     * @var    string
     * @since  1.0.0
     */
    private string $userKey;

    /**
     * Construct the class.
     *
     * @param   bool    $test  Set API mode
     * @param   string  $key   Set the API token
     *
     * @since   1.0.0
     */
    public function __construct(bool $test, string $key)
    {
        $this->userKey = $key;
        $this->test    = $test;
    }

    /**
     * Search companies.
     *
     * @param   array  $params     Parameters to filter the query on
     * @param   array  $companies  List of retrieved companies
     *
     * @return  array  List of companies.
     *
     * @since   1.0.0
     * @throws  JsonException
     * @throws  GuzzleException
     */
    public function getCompanies(array $params, array $companies = []): array
    {
        $results = $this->callApi('search/companies', $params);

        if (isset($results['data']['items'])) {
            $companies = array_merge(
                $companies,
                $results['data']['items']
            );
        }

        $nextLink = $results['data']['nextLink'] ?? null;

        if ($nextLink) {
            $uri = new Uri($nextLink);
            parse_str($uri->getQuery(), $params);
            $companies = $this->getCompanies($params, $companies);
        }

        return $companies;
    }

    /**
     * Call the API.
     *
     * @param   string  $destination  The endpoint to call
     * @param   array   $params       The parameters to pass to the API
     *
     * @return  array  API details.
     *
     * @since   1.0.0
     * @throws  GuzzleException|JsonException
     */
    private function callApi(string $destination, array $params = []): array
    {
        $client = new Client(['base_uri' => 'https://test.com/']);

        if ($this->test) {
            $destination = 'test' . $destination;
        }

        if ($this->userKey) {
            $params['user_key'] = $this->userKey;
        }

        $response = $client->get($destination, ['query' => $params]);

        return json_decode(
            $response->getBody()->getContents(),
            true,
            512,
            JSON_THROW_ON_ERROR
        );
    }
}

Вот тестовый класс, с которым я до сих пор работал, но он не работает:

<?php

namespace App\Tests\Service;

use App\Service\ApiService;
use PHPUnit\Framework\TestCase;

class ApiServiceTest extends TestCase
{
    public function testGetCompanies()
    {
        $result = ['data' => [
            'items' => [
                1 => 'first',
                2 => 'second'
            ]
        ];

        $apiService = $this->getMockBuilder(ApiService::class)
            ->disableOriginalConstructor()
            ->getMock();
        $apiService->method('callApi')
            ->with($result);

        $result = $apiService->getCompanies([]);

       print_r($result);
    }
}

То, что я не могу понять, это несколько вещей.

Первый, какой класс я должен расширить:

  • TestCase
  • WebTestCase
  • KernelTestCase

Во-вторых, как мне настроить фиктивные данные, чтобы я не использовал внешний API, а скорее передал $ результат , который я определил.

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

Любые советы будут очень признательны.

Ответы [ 2 ]

2 голосов
/ 06 августа 2020

Вы должны перейти от PHPUnit TestCase. WebTestCase и KernelTestCase полезны, если вы хотите провести функциональные тесты. Ваш случай представляет собой модульный тест classi c: вы хотите протестировать свой ApiService изолированно.

ApiService на самом деле сейчас выполняет две вещи:

  • Выполнение вызовов
  • Обработка данных

Хорошая идея - отделить один от другого, представив специальный клиент API:

interface ApiClient
{
    public function call(string $destination, array $params = []): array;
}

Для вашего производственного кода , вы можете создать реализацию с помощью Guzzle. Вы можете написать интеграционные тесты для GuzzleApiClient, которые выполняют фактические HTTP-запросы, чтобы гарантировать, что он обрабатывает ответы ожидаемым образом.

Ваш ApiService теперь сводится к следующему:

final class ApiService
{
    private ApiClient $apiClient;

    public function __construct(ApiClient $apiClient)
    {
        $this->apiClient = $apiClient;
    }

    public function getCompanies(array $params, array $companies = []): array
    {
        $results = $this->apiClient->call('search/companies', $params);

        if (isset($results['data']['items'])) {
            $companies = array_merge(
                $companies,
                $results['data']['items']
            );
        }

        $nextLink = $results['data']['nextLink'] ?? null;

        if ($nextLink) {
            parse_str(parse_url($nextLink, PHP_URL_QUERY), $params);

            $companies = $this->getCompanies($params, $companies);
        }

        return $companies;
    }
}

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

/**
 * @covers \App\Service\ApiService
 */
class ApiServiceTest extends TestCase
{
    /**
     * @var MockObject&ApiClient
     */
    private ApiClient $apiClient;

    private ApiService $subject;

    public function testGetCompanies()
    {
        $this->apiClient->addResponse(
            'search/companies',
            [],
            ['data' => ['items' => [1 => 'first', 2 => 'second']]]
        );

        $result = $this->subject->getCompanies([]);

        self::assertEquals(['first', 'second'], $result);
    }

    public function testGetCompaniesPaginated()
    {
        $this->apiClient->addResponse(
            'search/companies',
            [],
            ['data' => ['items' => [1 => 'first', 2 => 'second'], 'nextLink' => 'search/companies?page=2']]
        );
        $this->apiClient->addResponse(
            'search/companies',
            ['page' => 2],
            ['data' => ['items' => [1 => 'third', 2 => 'fourth'], 'nextLink' => 'search/companies?page=3']]
        );
        $this->apiClient->addResponse(
            'search/companies',
            ['page' => 3],
            ['data' => ['items' => [1 => 'fifth']]]
        );


        $result = $this->subject->getCompanies([]);

        self::assertEquals(['first', 'second', 'third', 'fourth', 'fifth'], $result);
    }

    protected function setUp(): void
    {
        parent::setUp();

        $this->apiClient = new class implements ApiClient {
            private array $responses = [];

            public function call(string $destination, array $params = []): array
            {
                return $this->responses[$this->key($destination, $params)] ?? [];
            }

            public function addResponse(string $destination, array $params, array $response)
            {
                $this->responses[$this->key($destination, $params)] = $response;
            }

            private function key(string $destination, array $params): string
            {
                return $destination . implode('-', $params);
            }
        };

        $this->subject = new ApiService($this->apiClient);
    }
}

Я создал анонимный класс для реализации ApiClient. Это всего лишь пример. Вы, конечно, можете также использовать mocks PHPUnit, Prophecy или любой другой фреймворк mocking, который вам нравится. Но я обнаружил, что часто проще создавать специализированные тестовые двойники.

0 голосов
/ 07 августа 2020

В моих проектах это помогает внедрить HttpClient в службы , например, используя HttpClientInterface $httpClient в конструкторе. После этого у вас есть заменяемый клиент в этой службе, и вы перестаете создавать его в своей службе.

Довольно простой тестовый пример может выглядеть следующим образом. Он проверяет, выполнен ли запрос API все-таки, не более того:

public function testRequestIsExecuted(): void
{
    $callbackWasCalled = false;

    $callback = function ($method, $url, $options) use (&$callbackWasCalled) {
        $callbackWasCalled = true;
        return new MockResponse('[]');
    };

    $mockedClient = new MockHttpClient($callback);

    $apiService = new Apiservice($mockedClient);
    $result = $apiService->getCompanies([]);

    $this->assertTrue($callbackWasCalled);
}

Вы хотите сделать более подробные проверки? Нет проблем, просто поиграйте с параметрами вашего обратного вызова: вы можете сравнить тип метода (GET / POST), вернуть разные ответы на основе вызываемого URL, ....

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...