it-roy-ru.com

Как пишутся интеграционные тесты для взаимодействия с внешним API?

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

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

Интеграционные тесты - это тесты, которые проверяют взаимодействие между несколькими областями кода (которые, мы надеемся, уже имеют свои собственные модульные тесты). Иногда части тестируемого кода требуют, чтобы другой код действовал определенным образом. Вот тут-то и приходят 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, который влияет на живые данные?
  • Как я могу макетировать/заглушки объекты в интеграционном тесте, когда они скрыты за слоями абстракции?
  • Что мне делать, если тест не пройден и текущие данные остаются в несогласованном состоянии?
  • Как в коде я действительно делаю все это?

Связанные с:

66
Jess Telford

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

Вы не Вы должны действительно верить, что фактический API действительно работает.

Вы можете - и должны - использовать API с живыми данными, чтобы убедиться, что вы его понимаете. 

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

Как я могу макетировать/заглушки объекты в интеграционном тесте, когда они скрыты за слоями абстракции?

В этом-то и дело. Проверьте абстракцию. Вы должны верить, что реализация работает. Вы тестируете ваш код. Не их код.

Что мне делать, если тест не пройден и текущие данные остаются в несогласованном состоянии?

Какие? Почему вы тестируете живые API, чтобы убедиться, что они работают? Ты им не доверяешь? Если вы им не доверяете, не проверяйте. Найдите продавца, которому вы можете доверять.

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


Как ты это делаешь.

  1. Поиграйте с API. Отправляйте запросы. Получите ответы.

  2. Поиграйте с вашим приложением. Выясните, какие запросы вы собираетесь отправлять.

  3. Вернитесь к API. Отправить известный хороший запрос. Получите ответ. Сохранить этот ответ . Это ваш золотой стандарт ответа на хороший запрос. Canonize это в тестовом случае. 

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

  5. После проработки нескольких вариантов использования (хороший запрос, неправильный запрос) вы сможете получить хороший ответ и некоторые типичные сообщения об ошибках от API. Сохраните добро и сообщения об ошибках. Они полезны для модульного тестирования, чтобы убедиться, что вы правильно обрабатываете некоторые ответы.

64
S.Lott

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

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

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

8
hakre

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

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

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

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

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

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

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

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

1
Dizzy Bryan High

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

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

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

0
dragon788

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

  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
Reza S