Як пишуться інтеграційні тести для взаємодії із зовнішнім API?


79

По-перше, там, де знання:

Юніт-тести - це ті, що перевіряють невеликий фрагмент коду (переважно одиночні методи).

Інтеграційні тести - це тести , які перевіряють взаємодію між різними областями коду (які, сподіваємось, уже мають власні модульні тести). Іноді частини тестованого коду вимагають, щоб інший код діяв певним чином. Тут з’являються Mocks & Stubs. Отже, ми висміюємо / заглушаємо частину коду, щоб виконати дуже конкретно. Це дозволяє нашому інтеграційному тесту працювати передбачувано без побічних ефектів.

Всі тести повинні мати можливість працювати самостійно без обміну даними. Якщо необхідний спільний доступ до даних, це ознака недостатньої розв’язки системи.

Далі, ситуація, з якою я стикаюся:

При взаємодії із зовнішнім API (зокрема, RESTful API, який буде модифікувати дані в реальному часі за допомогою запиту POST), я розумію, що ми можемо (повинні?) Висміювати взаємодію з цим API (красномовніше зазначено в цій відповіді ) для тесту інтеграції . Я також розумію, що ми можемо Unit Test перевіряти окремі компоненти взаємодії з цим API (побудова запиту, синтаксичний аналіз результату, помилки тощо). Я не розумію, як насправді робити це.

Отже, нарешті: моє запитання.

Як перевірити свою взаємодію із зовнішнім API, який має побічні ефекти?

Прекрасним прикладом є API вмісту Google для покупок . Для того, щоб мати змогу виконати поставлене завдання, потрібен пристойний обсяг підготовчої роботи, то виконання фактичного запиту, то аналіз поверненого значення. Деякі з них не мають середовища "пісочниці" .

Код для цього, як правило, має досить багато шарів абстракції, приблизно на зразок:

<?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, який має побічний ефект зміни даних в реальному часі.

Це прийнятно? Які альтернативи я маю? Я не бачу способу висміяти об’єкт Request для тесту. І навіть якби я це зробив, це означало б налаштування результатів / точок входу для кожного можливого шляху коду, який приймає API Google (який у цьому випадку потрібно було б знайти методом спроб і помилок), але дозволило б мені використовувати пристосування.

Подальше розширення - коли певні запити покладаються на певні дані, які вже є в реальному часі. Використовуючи Google Content API як приклад ще раз, щоб додати канал даних до субрахунку, він повинен уже існувати.

Одним із підходів, які я можу придумати, є наступні кроки;

  1. В testCreateAccount
    1. Створіть субрахунок
    2. Стверджуємо, що субрахунок створено
    3. Видаліть субрахунок
  2. Have testCreateDataFeedзалежать від testCreateAccountне мають жодних - або помилок
    1. У testCreateDataFeedстворіть новий обліковий запис
    2. Створіть канал даних
    3. Переконайтеся, що канал даних створено
    4. Видаліть стрічку даних
    5. Видаліть субрахунок

Потім це піднімає подальше питання; як протестувати видалення облікових записів / каналів даних? testCreateDataFeedмені здається брудним - Що робити, якщо створення фіду даних не вдається? Тест не вдається, тому підрахунок ніколи не видаляється ... Я не можу протестувати видалення без створення, тому я пишу інший тест ( testDeleteAccount), на який покладається testCreateAccountперед створенням, а потім видалення власного облікового запису (оскільки дані не повинні бути спільним між тестами).

Коротко

  • Як протестувати взаємодію із зовнішнім API, який впливає на дані в реальному часі?
  • Як я можу знущатись / заглушати об’єкти в тесті інтеграції, коли вони заховані за шарами абстракції?
  • Що робити, якщо тест невдалий і дані в режимі реального часу залишаються у несумісному стані?
  • Як я в коді насправді займаюся цим?

Пов’язані:


Це кілька широких питань, а не одне конкретне питання.
Редвальд,

Крім того, пов'язані з : stackoverflow.com/questions/28069535 / ...
andrewdotn

Відповіді:


10

Це більше додаткова відповідь до вже наведеної :

Переглядаючи ваш код, class GoogleAPIRequest файл має жорстко закодовану залежність class Request. Це заважає вам протестувати його незалежно від класу запитів, тому ви не можете знущатися над запитом.

Вам потрібно зробити запит ін’єкційним, щоб ви могли змінити його на фіктивний під час тестування. Після цього не надсилаються реальні API-запити HTTP, дані в реальному часі не змінюються, і ви можете тестувати набагато швидше.


2
Погодився на 100%. Ось чому я відтоді змінив дизайн коду, щоб дозволити саме це. Сказавши це, однак, можливо, class GoogleAPIRequestщоб метод, з getNewRequest()якого можна було знущатися, повернув знущаний Requestоб'єкт (як одну з багатьох можливих альтернатив).
Джес Телфорд,

1

Нещодавно мені довелося оновити бібліотеку, оскільки api, до якого вона підключається, оновився.

Моїх знань недостатньо для детального пояснення, але я багато чого навчився, переглядаючи код. https://github.com/gridiron-guru/FantasyDataAPI

Ви можете подати запит, як зазвичай, на api, а потім зберегти цю відповідь як json-файл, а потім використовувати його як макет.

Погляньте на тести в цій бібліотеці, яка підключається до API за допомогою Guzzle.

Він висміює відповіді api, у документації є багато інформації про те, як працює тестування, це може дати вам уявлення про те, як це зробити.

але в основному ви здійснюєте ручний виклик api разом з будь-якими потрібними вам параметрами і зберігаєте відповідь як файл json.

Коли ви пишете тест для виклику api, надсилаєте ті самі параметри і отримуєте його для завантаження в макеті, а не за допомогою активного api, ви можете протестувати дані в створеному вами макеті, що містить очікувані значення.

Мою оновлену версію розглянутого api можна знайти тут. Оновлений Repo


0

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

Іноді люди називають цей тип тестування "тестуванням на основі контракту", де ви можете писати тести проти API на основі поведінки, яку ви спостерігали та кодували, а коли ці тести починають давати збій, "контракт порушується". Якщо це прості тести на основі REST з використанням фіктивних даних, ви також можете надати їх зовнішньому провайдеру для запуску, щоб вони могли виявити, де / коли вони можуть змінювати API настільки, що це повинна бути нова версія, або видавати попередження про те, що не буде назад сумісні.

Посилання: https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing


0

Спираючись на те, що говорить відповідь з високим рейтингом .... Ось як я це зробив і працює спокійно.

  1. Створено фіктивний об'єкт curl
  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);
    
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.