¿Cómo se escriben las pruebas de integración para interactuar con API externas?

79

Primero, donde está mi conocimiento:

Las pruebas unitarias son aquellas que prueban una pequeña parte de código (métodos únicos, en su mayoría).

Las pruebas de integración son aquellas que prueban la interacción entre múltiples áreas de código (que con suerte ya tienen sus propias pruebas unitarias). A veces, partes del código bajo prueba requieren que otro código actúe de una manera particular. Aquí es donde entran Mocks & Stubs. Por lo tanto, simulamos / eliminamos una parte del código para que funcione de manera muy específica. Esto permite que nuestra prueba de integración se ejecute de manera predecible sin efectos secundarios.

Todas las pruebas deben poder ejecutarse de forma independiente sin compartir datos. Si es necesario compartir datos, esto es una señal de que el sistema no está lo suficientemente desacoplado.

A continuación, la situación a la que me enfrento:

Al interactuar con una API externa (específicamente, una API RESTful que modificará los datos en vivo con una solicitud POST), entiendo que podemos (¿deberíamos?) Simular la interacción con esa API (que se indica con más elocuencia en esta respuesta ) para una prueba de integración . También entiendo que podemos realizar una prueba unitaria de los componentes individuales de la interacción con esa API (construir la solicitud, analizar el resultado, generar errores, etc.). Lo que no entiendo es cómo hacer esto.

Entonces, finalmente: Mi pregunta (s).

¿Cómo pruebo mi interacción con una API externa que tiene efectos secundarios?

Un ejemplo perfecto es la API de contenido de Google para compras . Para poder realizar la tarea en cuestión, se requiere una cantidad decente de trabajo de preparación, luego realizar la solicitud real y luego analizar el valor de retorno. Parte de esto es sin ningún entorno de "caja de arena" .

El código para hacer esto generalmente tiene bastantes capas de abstracción, algo como:

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

Nota: Este es un ejemplo de PHP5 / PHPUnit

Dado que testTheRequestes el método llamado por la suite de pruebas, el ejemplo ejecutará una solicitud en vivo.

Ahora, esta solicitud en vivo (con suerte, siempre que todo haya ido bien) realizará una solicitud POST que tiene el efecto secundario de alterar los datos en vivo.

¿Es esto aceptable? ¿Qué alternativas tengo? No veo una forma de simular el objeto Solicitud para la prueba. E incluso si lo hiciera, significaría configurar resultados / puntos de entrada para cada ruta de código posible que acepte la API de Google (que en este caso tendría que encontrarse por prueba y error), pero me permitiría el uso de accesorios.

Una extensión adicional es cuando ciertas solicitudes dependen de que ciertos datos ya estén en vivo. Utilizando la API de contenido de Google como ejemplo nuevamente, para agregar una fuente de datos a una subcuenta, la subcuenta ya debe existir.

Un enfoque en el que puedo pensar son los siguientes pasos;

  1. En testCreateAccount
    1. Crea una subcuenta
    2. Confirmar que se creó la subcuenta
    3. Eliminar la subcuenta
  2. Tenga testCreateDataFeeden depend testCreateAccountno tener ningún error
    1. En testCreateDataFeed, crea una nueva cuenta
    2. Crea el feed de datos
    3. Confirmar que se creó el feed de datos
    4. Eliminar la fuente de datos
    5. Eliminar la subcuenta

Esto entonces plantea la pregunta adicional; ¿Cómo pruebo la eliminación de cuentas / feeds de datos? testCreateDataFeedme parece sucio - ¿Qué pasa si falla la creación de la fuente de datos? La prueba falla, por lo tanto, la subcuenta nunca se elimina ... No puedo probar la eliminación sin creación, así que escribo otra prueba ( testDeleteAccount) en la que se basa testCreateAccountantes de crear y luego eliminar una cuenta propia (ya que los datos no deberían compartirse entre pruebas).

En resumen

  • ¿Cómo pruebo la interacción con una API externa que afecta a los datos en vivo?
  • ¿Cómo puedo simular / apuntar objetos en una prueba de integración cuando están ocultos detrás de capas de abstracción?
  • ¿Qué hago cuando falla una prueba y los datos en vivo se dejan en un estado inconsistente?
  • ¿Cómo en el código realmente hago todo esto?

Relacionado:

Jess Telford
fuente
Son varias preguntas amplias, no una pregunta específica.
Raedwald
También relacionado: stackoverflow.com/questions/28069535/…
andrewdotn

Respuestas:

10

Esta es más una respuesta adicional a la ya dada :

Mirando su código, class GoogleAPIRequesttiene una dependencia codificada de class Request. Esto le impide probarlo independientemente de la clase de solicitud, por lo que no puede burlarse de la solicitud.

Debe hacer que la solicitud sea inyectable, para que pueda cambiarla a una simulación durante la prueba. Hecho esto, no se envían solicitudes HTTP de API reales, los datos en vivo no se cambian y puede probar mucho más rápido.

hakre
fuente
2
De acuerdo al 100%. Es por eso que desde entonces he modificado el diseño del código para permitir eso. Dicho esto, sin embargo, es posible class GoogleAPIRequesttener un método getNewRequest()que pueda ser burlado para devolver un Requestobjeto simulado (como una de las muchas alternativas posibles).
Jess Telford
1

Recientemente tuve que actualizar una biblioteca porque se actualizó la API a la que se conecta.

Mi conocimiento no es suficiente para explicarlo en detalle, pero aprendí mucho mirando el código. https://github.com/gridiron-guru/FantasyDataAPI

Puede enviar una solicitud como lo haría normalmente a la api y luego guardar esa respuesta como un archivo json, luego puede usarla como una simulación.

Eche un vistazo a las pruebas en esta biblioteca que se conecta a una API usando Guzzle.

Se burla de las respuestas de la API, hay una gran cantidad de información en los documentos sobre cómo funcionan las pruebas, lo que podría darle una idea de cómo hacerlo.

pero básicamente haces una llamada manual a la API junto con cualquier parámetro que necesites, y guardas la respuesta como un archivo json.

Cuando escribe su prueba para la llamada a la API, envía los mismos parámetros y consigue que se cargue en el simulacro en lugar de usar la API en vivo, puede probar los datos en el simulacro que creó y que contiene los valores esperados.

Mi versión actualizada de la API en cuestión se puede encontrar aquí. Repo actualizado

Dizzy Bryan High
fuente
0

Una de las formas de probar las API externas es, como mencionaste, creando una simulación y trabajando contra eso con el comportamiento codificado de forma rígida tal como lo has entendido.

A veces, la gente se refiere a este tipo de pruebas como pruebas "basadas en contrato", donde puede escribir pruebas contra la API en función del comportamiento que ha observado y codificado, y cuando esas pruebas comienzan a fallar, el "contrato se rompe". Si son pruebas simples basadas en REST que utilizan datos ficticios, también puede proporcionarlas al proveedor externo para que las ejecute para que puedan descubrir dónde / cuándo podrían estar cambiando la API lo suficiente como para que sea una nueva versión o producir una advertencia sobre no estar al revés compatible.

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

dragón788
fuente
0

Sobre la base de lo que dice la respuesta más votada ... Así es como lo he hecho y funciona bastante bien.

  1. Creó un objeto de rizo simulado
  2. Dígale al simulacro qué parámetros esperaría
  3. Burlarse de cuál será la respuesta de la llamada curl dentro de su función
  4. Deja que tu código haga su trabajo

    $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);
    
Reza S
fuente