Programing

통합 테스트는 외부 API와 상호 작용하기 위해 어떻게 작성 되나요?

lottogame 2020. 11. 6. 07:48
반응형

통합 테스트는 외부 API와 상호 작용하기 위해 어떻게 작성 되나요?


먼저, 내 지식은 다음과 같습니다.

단위 테스트 는 작은 코드 조각 (대부분 단일 메서드)을 테스트하는 것입니다.

통합 테스트 는 여러 코드 영역 (이미 자체 단위 테스트가 있기를 바랍니다) 간의 상호 작용을 테스트하는 테스트입니다. 때로는 테스트중인 코드의 일부에서 특정 방식으로 작동하기 위해 다른 코드가 필요합니다. 이것은 Mocks & Stubs가 들어오는 곳입니다. 그래서 우리는 매우 구체적으로 수행하기 위해 코드의 일부를 조롱 / 스텁합니다. 이를 통해 통합 테스트가 부작용없이 예측 가능하게 실행됩니다.

모든 테스트는 데이터 공유없이 독립적으로 실행될 수 있어야합니다. 데이터 공유가 필요한 경우 이는 시스템이 충분히 분리되지 않은 신호입니다.

다음으로 내가 직면 한 상황 :

외부 API (특히 POST 요청으로 라이브 데이터를 수정하는 RESTful API)와 상호 작용할 때 통합 테스트를 위해 해당 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 요청을 수행합니다.

Is this acceptable? What alternatives do I have? I can't see a way to mock out the Request object for the test. And even if I did, it would mean setting up results / entry points for every possible code path that Google's API accepts (which in this case would have to be found by trial and error), but would allow me the use of fixtures.

A further extension is when certain requests rely on certain data being Live already. Using the Google Content API as an example again, to add a Data Feed to a Sub Account, the Sub Account must already exist.

One approach I can think of is the following steps;

  1. In testCreateAccount
    1. Create a sub-account
    2. Assert the sub-account was created
    3. Delete the sub-account
  2. Have testCreateDataFeed depend on testCreateAccount not having any errors
    1. In testCreateDataFeed, create a new account
    2. Create the data feed
    3. Assert the data feed was created
    4. Delete the data feed
    5. Delete the sub-account

This then raises the further question; how do I test the deletion of accounts / data feeds? testCreateDataFeed feels dirty to me - What if creating the data feed fails? The test fails, therefore the sub-account is never deleted... I can't test deletion without creation, so do I write another test (testDeleteAccount) that relies on testCreateAccount before creating then deleting an account of its own (since data shouldn't be shared between tests).

In Summary

  • How do I test interacting with an external API that effects live data?
  • How can I mock / stub objects in an Integration test when they're hidden behind layers of abstraction?
  • What do I do when a test fails and the live data is left in an inconsistent state?
  • How in code do I actually go about doing all this?

Related:


This is more an additional answer to the one already given:

Looking through your code, the class GoogleAPIRequest has a hard-encoded dependency of class Request. This prevents you from testing it independently from the request class, so you can't mock the request.

You need to make the request injectable, so you can change it to a mock while testing. That done, no real API HTTP requests are send, the live data is not changed and you can test much quicker.


I've recently had to update a library because the api it connects to was updated.

My knowledge isn't enough to explain in detail, but i learnt a great deal from looking at the code. https://github.com/gridiron-guru/FantasyDataAPI

You can submit a request as you would normally to the api and then save that response as a json file, you can then use that as a mock.

Have a look at the tests in this library which connects to an api using Guzzle.

It mocks responses from the api, there's a good deal of information in the docs on how the testing works it might give you an idea of how to go about it.

but basically you do a manual call to the api along with any parameters you need, and save the response as a json file.

When you write your test for the api call, send along the same parameters and get it to load in the mock rather than using the live api, you can then test the data in the mock you created contains the expected values.

My Updated version of the api in question can be found here. Updated Repo


One of the ways to test out external APIs is as you mentioned, by creating a mock and working against that with the behavior hard coded as you have understood it.

Sometimes people refer to this type of testing as "contract based" testing, where you can write tests against the API based on the behavior you have observed and coded against, and when those tests start failing, the "contract is broken". If they are simple REST based tests using dummy data you can also provide them to the external provider to run so they can discover where/when they might be changing the API enough that it should be a new version or produce a warning about not being backwards compatible.

Ref: https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing


Building on top of what the high voted answer says.... Here's how I've done it and works quiet well.

  1. Created a mock curl object
  2. Tell the mock what parameters it would be expecting
  3. Mock out what the response of the curl call within your function will be
  4. Let your code do it's thing

    $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);
    

참고URL : https://stackoverflow.com/questions/7564038/how-are-integration-tests-written-for-interacting-with-external-api

반응형