Avillions
@Avillions
PHP Developer at Genesis

Как в Symfony3.4 + Codeception при написании функциональных тестов замокать сервис в DI?

Добрый день, есть приложение на Symfony3.4 которое предоставляет REST API обертку над сторонним сервисом.

Работа со сторонним сервисом происходит в классе который инициализируется в DI как сервис:

AppBundle/Resources/config/services.yml
app.service.third_party_service:
        class: AppBundle\Service\ThirdPartyService
        arguments:
            - "%third_party_service.url%"
            - "@event_dispatcher"


Код самого класса AppBundle\Service\ThirdPartyService.php
/**
 * Class ThirdPartyService
 * @package AppBundle\Service
 */
class ThirdPartyService implements ThirdPartyInterface
{
    /**
     * @param string $username
     * @param string $password
     * @return bool
     */
    public function login(string $username, string $password): bool
    {
        //some php code
    }

    /**
     * @param string $email
     * @param string $username
     * @param string $password
     * @return bool
     */
    public function register(string $email, string $username, string $password): bool
    {
        //some php code
    }
}


Нужно написать функциональные Codeception тесты на REST API приложения, только что бы сервис app.service.third_party_service не делал запросов на стороннее приложение, т.е. нужно замокать методы этого сервиса на определенные ответы.

Есть конфиг функциональных тестов tests/functional.suite.yml:
actor: FunctionalTester
modules:
    enabled:
        - Symfony:
            app_path: 'app'
            environment: 'test'
            debug: true
            part: SERVICES
        - Doctrine2:
            depends: Symfony
        - REST:
            depends: PhpBrowser
            url: http://localhost/app_dev.php/api/v1
        - \Helper\Functional


Есть сам тест tests/functional/AccountLoginCest.php:
class AccountLoginCest
{
    /**
     * @param FunctionalTester $I
     */
    public function testSuccessfulLogin(FunctionalTester $I)
    {
        $thirdPartyService = $I->grabService('app.service.third_party_service');

        dump($thirdPartyService);

        $I->sendPOST('/account/login', [
            'login'    => 'test',
            'password' => 'test',
        ]);

        $I->seeResponseCodeIs(HttpCode::OK);
    }
}


В тесте я получил доступ к сервису app.service.third_party_service, но я не могу понять как сделать так что бы метод login этого сервиса возвращал true для тестовых данных.

Я вижу тут два варианта:
1) Хардкодом в сервисе делать проверки на тестовые данные, и в случае если это они, то не делать запрос на сторонний веб сервис а сразу вернуть true;
2) Создать environment test, и в конфиге app/config/config_test.yml сделать декоратор app.service.third_party_service. Сам environment в тестах передавать как параметр в url.

Оба эти варианта хоть и подходят но с моей точки зрения очень убоги, помогите плиз совет как можно человеческим образом замокать app.service.third_party_service?
  • Вопрос задан
  • 508 просмотров
Решения вопроса 1
prototype_denis
@prototype_denis
Symfony
Не сервис надо подменять в другом окружении, а клиента.
Вы ведь тестируете же сервис, верно же?

<?php

namespace App\Http {
    interface HttpClientInterface {}
    class FakeHttpClient implements HttpClientInterface {}
    class RealHttpClient implements HttpClientInterface {}
}

namespace App\Service {
    class ThirdPartyService {
        private $client;
        public function __construct(\App\Http\HttpClientInterface $client) {
            $this->client = $client;
        }
    }
}

?>


// prod
app.http_client:
    class: App\Http\RealHttpClient

// test
app.http_client:
    class: App\Http\FakeHttpClient

app.third_party_service:
    class: App\Service\ThirdPartyService
    arguments: [app.http_client]


Под HttpClientInterface можно подогнать что угодно, от curl до soap, а так же логгировать и собирать данные того что отправляется и что приходит, а так же вешаться на события, которые там же можно и генерить.

Оставьте сервис в покое, пусть делает свою работу.

Так же можете добавить и "псевдореальное окружение" и время от времени запускать тесты с реальными севисами, собирая данные и предохраняясь от внезапного изменения API. Но это другая история.

Если не хотите заморачиватся с клиентом, то замените класс сервиса

// parameters.yml
app.third_party_service_class: App\Service\ThirdPartyService

// config_test.yml
app.third_party_service_class: App\Service\MockThirdPartyService

app.third_party_service:
    class: "%app.third_party_service_class%"

class MockThirdPartyService extends ThirdPartyService {
    public function foo($ignoredArguments) {
        return true;
    }
}


Но в этом случае, если ваш реальный сервис поломается - то увы, тесты будут зеленее травы и врать вам в глаза.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы