Wie werden Integrationstests für die Interaktion mit externen APIs geschrieben?


77

Zunächst einmal, wo mein Wissen ist:

Unit-Tests sind solche, die einen kleinen Teil des Codes testen (meistens einzelne Methoden).

Integrationstests sind solche, die die Interaktion zwischen mehreren Codebereichen testen (die hoffentlich bereits eigene Unit-Tests haben). Manchmal erfordern Teile des zu testenden Codes, dass anderer Code auf eine bestimmte Weise funktioniert. Hier kommen Mocks & Stubs ins Spiel. Also verspotten wir einen Teil des Codes, um eine sehr spezifische Leistung zu erzielen. Dadurch kann unser Integrationstest vorhersehbar und ohne Nebenwirkungen ausgeführt werden.

Alle Tests sollten ohne Datenaustausch eigenständig ausgeführt werden können. Wenn Datenaustausch erforderlich ist, ist dies ein Zeichen dafür, dass das System nicht ausreichend entkoppelt ist.

Als nächstes die Situation, mit der ich konfrontiert bin:

Bei der Interaktion mit einer externen API (insbesondere einer RESTful-API, die Live-Daten mit einer POST-Anforderung ändert) können (sollten?) Wir die Interaktion mit dieser API (in dieser Antwort beredter angegeben ) für einen Integrationstest verspotten . Ich verstehe auch, dass wir die einzelnen Komponenten der Interaktion mit dieser API testen können (Erstellen der Anforderung, Analysieren des Ergebnisses, Auslösen von Fehlern usw.). Was ich nicht verstehe, ist, wie man das tatsächlich macht.

Also endlich: Meine Frage (n).

Wie teste ich meine Interaktion mit einer externen API, die Nebenwirkungen hat?

Ein perfektes Beispiel ist die Content-API von Google zum Einkaufen . Um die vorliegende Aufgabe ausführen zu können, ist ein angemessener Vorbereitungsaufwand erforderlich. Anschließend wird die eigentliche Anforderung ausgeführt und anschließend der Rückgabewert analysiert. Ein Teil davon ist ohne Sandbox-Umgebung .

Der Code dafür hat im Allgemeinen einige Abstraktionsebenen, etwa:

<?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());
    }
}
?>

Hinweis: Dies ist ein PHP5 / PHPUnit-Beispiel

Da dies testTheRequestdie von der Testsuite aufgerufene Methode ist, führt das Beispiel eine Live-Anforderung aus.

Jetzt wird diese Live-Anfrage (hoffentlich vorausgesetzt, dass alles gut gegangen ist) eine POST-Anfrage ausführen, die den Nebeneffekt hat, Live-Daten zu ändern.

Ist das akzeptabel? Welche Alternativen habe ich? Ich kann keine Möglichkeit finden, das Request-Objekt für den Test zu verspotten. Und selbst wenn ich dies tun würde, würde dies bedeuten, Ergebnisse / Einstiegspunkte für jeden möglichen Codepfad einzurichten, den die Google-API akzeptiert (was in diesem Fall durch Ausprobieren gefunden werden müsste), aber mir die Verwendung von Fixtures erlauben würde.

Eine weitere Erweiterung ist, wenn bestimmte Anforderungen davon abhängen, dass bestimmte Daten bereits aktiv sind. Wenn Sie erneut die Google Content-API als Beispiel verwenden, um einem Unterkonto einen Datenfeed hinzuzufügen, muss das Unterkonto bereits vorhanden sein.

Ein Ansatz, den ich mir vorstellen kann, sind die folgenden Schritte:

  1. Im testCreateAccount
    1. Erstellen Sie ein Unterkonto
    2. Stellen Sie sicher, dass das Unterkonto erstellt wurde
    3. Unterkonto löschen
  2. Habe darauf testCreateDataFeedangewiesen testCreateAccount, keine Fehler zu haben
    1. In testCreateDataFeedein neues Konto erstellen
    2. Erstellen Sie den Datenfeed
    3. Stellen Sie sicher, dass der Datenfeed erstellt wurde
    4. Löschen Sie den Datenfeed
    5. Unterkonto löschen

Dies wirft dann die weitere Frage auf; Wie teste ich das Löschen von Konten / Datenfeeds? testCreateDataFeedfühlt sich für mich schmutzig an - Was ist, wenn das Erstellen des Datenfeeds fehlschlägt? Der Test schlägt fehl, daher wird das Unterkonto nie gelöscht. Ich kann das Löschen nicht ohne Erstellung testen. Daher schreibe ich einen weiteren Test ( testDeleteAccount), auf den testCreateAccountvor dem Erstellen zurückgegriffen wird, und lösche dann ein eigenes Konto (da Daten dies nicht sollten zwischen Tests geteilt werden).

In Summe

  • Wie teste ich die Interaktion mit einer externen API, die sich auf Live-Daten auswirkt?
  • Wie kann ich Objekte in einem Integrationstest verspotten / stubben, wenn sie hinter Abstraktionsebenen versteckt sind?
  • Was mache ich, wenn ein Test fehlschlägt und die Live-Daten in einem inkonsistenten Zustand verbleiben?
  • Wie mache ich das alles im Code ?

Verbunden:


Das sind mehrere allgemeine Fragen, keine spezifische Frage.
Raedwald

Antworten:


9

Dies ist eher eine zusätzliche Antwort auf die bereits gegebene :

Wenn Sie Ihren Code durchsehen, class GoogleAPIRequesthat der eine fest codierte Abhängigkeit von class Request. Dies verhindert, dass Sie es unabhängig von der Anforderungsklasse testen können, sodass Sie die Anforderung nicht verspotten können.

Sie müssen die Anforderung injizierbar machen, damit Sie sie beim Testen in ein Modell ändern können. Damit werden keine echten API-HTTP-Anforderungen gesendet, die Live-Daten werden nicht geändert und Sie können viel schneller testen.


2
Einverstanden 100%. Deshalb habe ich seitdem das Design des Codes geändert, um genau das zu ermöglichen. Allerdings ist es möglich class GoogleAPIRequest, eine Methode zu haben, getNewRequest()die verspottet werden kann, um ein verspottetes RequestObjekt zurückzugeben (als eine von vielen möglichen Alternativen).
Jess Telford

1

Ich musste kürzlich eine Bibliothek aktualisieren, da die API, mit der sie verbunden ist, aktualisiert wurde.

Mein Wissen reicht nicht aus, um es im Detail zu erklären, aber ich habe viel gelernt, indem ich mir den Code angesehen habe. https://github.com/gridiron-guru/FantasyDataAPI

Sie können eine Anfrage wie gewohnt an die API senden und diese Antwort dann als JSON-Datei speichern. Sie können sie dann als Mock verwenden.

Schauen Sie sich die Tests in dieser Bibliothek an, die mit Guzzle eine Verbindung zu einer API herstellen.

Es verspottet die Antworten der API. In den Dokumenten finden Sie zahlreiche Informationen zur Funktionsweise der Tests. Dadurch erhalten Sie möglicherweise eine Vorstellung davon, wie Sie vorgehen müssen.

Grundsätzlich rufen Sie die API zusammen mit den erforderlichen Parametern manuell auf und speichern die Antwort als JSON-Datei.

Wenn Sie Ihren Test für den API-Aufruf schreiben, dieselben Parameter mitsenden und ihn in das Modell laden, anstatt die Live-API zu verwenden, können Sie die Daten in dem von Ihnen erstellten Modell testen, das die erwarteten Werte enthält.

Meine aktualisierte Version der betreffenden API finden Sie hier. Repo aktualisiert


0

Eine der Möglichkeiten, externe APIs zu testen, besteht darin, wie bereits erwähnt, ein Modell zu erstellen und dem mit dem Verhalten, das Sie so verstanden haben, hart zu codieren.

Manchmal wird diese Art von Tests als "vertragsbasiertes" Testen bezeichnet, bei dem Sie Tests für die API basierend auf dem beobachteten und codierten Verhalten schreiben können. Wenn diese Tests fehlschlagen, wird der "Vertrag gebrochen". Wenn es sich um einfache REST-basierte Tests mit Dummy-Daten handelt, können Sie diese auch dem externen Anbieter zur Ausführung bereitstellen, damit dieser feststellen kann, wo / wann die API möglicherweise so weit geändert wird, dass es sich um eine neue Version handelt, oder eine Warnung angezeigt wird, dass keine Rückwärtsdaten vorliegen kompatibel.

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


0

Aufbauend auf dem, was in der Antwort mit den hohen Stimmen steht ... So habe ich es gemacht und funktioniert ruhig gut.

  1. Erstellt ein Mock-Curl-Objekt
  2. Sagen Sie dem Mock, welche Parameter er erwarten würde
  3. Verspotten Sie die Reaktion des Curl-Aufrufs in Ihrer Funktion
  4. Lassen Sie Ihren Code das tun

    $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);
    
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.