Как пишутся интеграционные тесты для взаимодействия с внешним API? - PullRequest
68 голосов
/ 27 сентября 2011

Прежде всего, где мои знания:

Модульные тесты - это тесты, которые тестируют небольшой фрагмент кода (в основном, одиночные методы).

Интеграционные тесты - это тесты, которые проверяют взаимодействие между несколькими областями кода (которые, мы надеемся, уже имеют свои собственные модульные тесты).Иногда части тестируемого кода требуют, чтобы другой код действовал определенным образом.Вот тут-то и появляются Mocks & Stubs. Итак, мы макетируем / заглушаем часть кода, чтобы выполнить его очень специфично.Это позволяет нашему Интеграционному тесту работать предсказуемо без побочных эффектов.

Все тесты должны быть в состоянии выполняться автономно без совместного использования данных.Если обмен данными необходим, это признак того, что система недостаточно отделена.

Далее возникает ситуация, с которой я сталкиваюсь:

При взаимодействии с внешним API (в частности, RESTful)API, который будет изменять действительные данные с помощью запроса POST), я понимаю, что мы можем (должны?) Смоделировать взаимодействие с этим API (более красноречиво указано в этот ответ ) для интеграционного теста.Я также понимаю, что мы можем проводить модульное тестирование отдельных компонентов взаимодействия с этим API (создание запроса, анализ результата, выдача ошибок и т. Д.).Чего я не понимаю, так это как на самом деле поступить.

Итак, наконец: мой вопрос (ы).

Как проверить взаимодействие с внешним API, имеющим побочные эффекты?

Прекрасным примером является Google Content API для покупок .Чтобы иметь возможность выполнить поставленную задачу, требуется приличный объем подготовительной работы, затем выполнение фактического запроса, а затем анализ возвращаемого значения.Отчасти это без какой-либо среды «песочницы» .

Код для этого обычно содержит несколько уровней абстракции, что-то вроде:

<?php
class Request
{
    public function setUrl(..){ /* ... */ }
    public function setData(..){ /* ... */ }
    public function setHeaders(..){ /* ... */ }
    public function execute(..){
        // Do some CURL request or some-such
    }   
    public function wasSuccessful(){
        // some test to see if the CURL request was successful
    }   
}

class GoogleAPIRequest
{
    private $request;
    abstract protected function getUrl();
    abstract protected function getData();

    public function __construct() {
        $this->request = new Request();
        $this->request->setUrl($this->getUrl());
        $this->request->setData($this->getData());
        $this->request->setHeaders($this->getHeaders());
    }   

    public function doRequest() {
        $this->request->execute();
    }   
    public function wasSuccessful() {
        return ($this->request->wasSuccessful() && $this->parseResult());
    }   
    private function parseResult() {
        // return false when result can't be parsed
    }   

    protected function getHeaders() {
        // return some GoogleAPI specific headers
    }   
}

class CreateSubAccountRequest extends GoogleAPIRequest
{
    private $dataObject;

    public function __construct($dataObject) {
        parent::__construct();
        $this->dataObject = $dataObject;
    }   
    protected function getUrl() {
        return "http://...";
    }
    protected function getData() {
        return $this->dataObject->getSomeValue();
    }
}

class aTest
{
    public function testTheRequest() {
        $dataObject = getSomeDataObject(..);
        $request = new CreateSubAccountRequest($dataObject);
        $request->doRequest();
        $this->assertTrue($request->wasSuccessful());
    }
}
?>

Примечание: это пример PHP5 / PHPUnit

Учитывая, что testTheRequest является методом, вызываемым набором тестов, пример выполнит живой запрос.

Теперь,этот живой запрос (надеюсь, при условии, что все прошло хорошо) выполнит POST-запрос, имеющий побочный эффект изменения живых данных.

Является ли это приемлемым?Какие у меня есть альтернативы?Я не вижу способа макетировать объект запроса для теста.И даже если бы я это сделал, это означало бы настройку результатов / точек входа для каждого возможного пути кода, который принимает API Google (который в этом случае должен был бы быть найден методом проб и ошибок), но позволил бы мне использовать фиксаторы.

Дальнейшее расширение - это когда определенные запросы зависят от того, что определенные данные уже являются живыми.Используя API контента Google в качестве примера снова, чтобы добавить фид данных к дополнительной учетной записи, дополнительная учетная запись уже должна существовать.

Один из подходов, о которых я могу подумать, - это следующие шаги:

  1. В testCreateAccount
    1. Создать субсчет
    2. Подтвердить, что субсчет был создан
    3. Удалить субсчет
  2. * testCreateDataFeed зависит от testCreateAccount без ошибок
    1. В testCreateDataFeed создайте новый аккаунт
    2. Создайте фид данных
    3. Подтвердитеканал данных был создан
    4. Удалить канал данных
    5. Удалить субсчет

Затем возникает еще один вопрос;Как проверить удаление учетных записей / каналов данных?testCreateDataFeed мне кажется грязным - Что, если создать фид данных не удастся?Тест не пройден, поэтому дополнительная учетная запись никогда не удаляется ... Я не могу проверить удаление без создания, поэтому я пишу другой тест (testDeleteAccount), который опирается на testCreateAccount перед созданием и удалением собственной учетной записи.(поскольку данные не должны совместно использоваться тестами).

Вкратце

  • Как проверить взаимодействие с внешним API, который влияет на живые данные?
  • Какможно ли макетировать / заглушки объекты в тесте интеграции, когда они скрыты за слоями абстракции?
  • Что мне делать, если тест не пройден и текущие данные остаются в несогласованном состоянии?
  • Как в коде действительно ли я все это делаю?

Связано:

Ответы [ 4 ]

8 голосов
/ 30 сентября 2011

Это больше дополнительный ответ на уже заданный ответ :

При просмотре вашего кода class GoogleAPIRequest имеет жестко закодированную зависимость class Request.Это не позволяет вам тестировать его независимо от класса запроса, поэтому вы не можете смоделировать запрос.

Вам нужно сделать запрос инъекционным, чтобы вы могли изменить его на макет во время тестирования.После этого никакие реальные HTTP-запросы API не отправляются, текущие данные не изменяются, и вы можете тестировать намного быстрее.

1 голос
/ 31 августа 2015

Мне недавно пришлось обновить библиотеку, потому что был обновлен API, к которому она подключается.

Моих знаний недостаточно для подробного объяснения, но я многому научился, глядя на код.https://github.com/gridiron-guru/FantasyDataAPI

Вы можете отправить запрос, как обычно, в API, а затем сохранить этот ответ в виде файла JSON, затем вы можете использовать его как макет.

Посмотрите натесты в этой библиотеке, которая подключается к API с помощью Guzzle.

Она проверяет ответы API, в документах содержится много информации о том, как работает тестирование, она может дать вам представление о том, как идтиоб этом.

но в основном вы выполняете вызов API вручную вместе с любыми необходимыми параметрами и сохраняете ответ в виде файла json.

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

Моя обновленная версия API вВопрос можно найти здесь. Обновленный репо

0 голосов
/ 06 июня 2018

Опираясь на то, что говорит ответ с высоким голосом ... Вот как я это сделал и работает тихо и хорошо.

  1. Создан фиктивный локон
  2. Скажите макету, какие параметры он будет ожидать
  3. Смоделируйте, как ответ на вызов curl в вашей функции будет
  4. Пусть твой код делает свое дело

    $curlMock = $this->getMockBuilder('\Curl\Curl')
                     ->setMethods(['get'])
                     ->getMock();
    
    $curlMock
        ->expects($this->once())
        ->method('get')
        ->with($URL .  '/users/' . urlencode($userId));
    
    $rawResponse = <<<EOL
    {
         "success": true,
         "result": {
         ....
         }
    }
    EOL;
    
    $curlMock->rawResponse = $rawResponse;
    $curlMock->error = null;
    
    $apiService->curl = $curlMock;
    
    // call the function that inherently consumes the API via curl
    $result = $apiService->getUser($userId);
    
    $this->assertTrue($result);
    
0 голосов
/ 05 февраля 2018

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

Иногда люди называют этот тип тестирования «тестированием на основе контракта», где вы можете написать тесты по API на основе поведения, которое вы наблюдали и кодировали, а когда эти тесты начинают давать сбой, «контракт нарушается» , Если это простые тесты на основе REST с использованием фиктивных данных, вы также можете предоставить их внешнему провайдеру для запуска, чтобы они могли выяснить, где и когда они могут изменить API, настолько, что это должна быть новая версия, или выдать предупреждение о невозможности возврата совместимы.

Ссылка: https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing

...