Testeando API
10.1 Definición de API
A lo largo de los últimos años, una de las formas de comunicarse distintos proyectos ha sido mediante un API (Interfaz de programa de aplicación, del inglés Application Program Interface).
En el mundo web, una API no es más que una forma de permitir que un entorno distinto de la aplicación pueda acceder a los datos a través de un lenguaje común. Este tipo de comunicación normalmente de desarrolla sobre el protocolo HTTP, dado que es el más extendido en internet.
Hay varias tecnologías para permitir dicha comunicación, como por ejemplo SOAP (en inglés, Simple Object Access Protocol) o REST (en inglés, Representational State Transfer). Dado que el tipo de API más extendido, tanto para aplicaciones móviles como para entornos web, es REST, este capítulo lo centraremos en este tipo.
Algunos ejemplos claros de estos servicios pueden ser pasarelas de pagos como PayPal, almacenaje de ficheros como Amazon S3, o integración de datos de servicios de analítica como Google Analytics. Dado que es muy común tener este tipo de integraciones, es conveniente tener estas piezas de código bajo test.
10.2 Formas de testear un API
Testear una API pública se puede afrontar de varias formas, dependiendo de las necesidades del proyecto. Una aproximación puede ser mediante tests dobles. Para ello crearíamos objetos mocks que simulen el comportamiento del API y ejercitaríamos nuestro código frente a una simulación.
Otra forma de realizar estos test es ejercitando API de forma completa. Estos tests son de integración o funcionales y no se pueden considerar como tests unitarios, dado que no podríamos controlar el estado de la aplicación y tienen varias complejidades. Es necesario que la máquina que ejecuta los tests tenga conexión a internet, para conectarse con el servicio sobre el se realizarán los tests.
A continuación veremos un ejemplo de una implementación de una clase que pueda conectarse a la API pública de un proyecto y sus correspondientes tests, tanto funcionales como unitarios.
10.3 Testeando la API de Flickr.com
Uno de los servicios de gestión de fotografías más extendidos en internet es Flickr.com. Supongamos que queremos realizar una página web que permite al usuario importar sus fotografías de Flickr.com. Para realizar esta página web necesitamos poder acceder a la API pública de Flickr.com. La documentación oficial de la API de Flickr.com [28] nos muestra todas las posibles interaccionares que podemos realizar con este servicio online.
En nuestro ejemplo vamos a implementar una clase llamada "Flickr" que sea capaz de conectarse con Flickr.com y nos permita interactuar.
Para poder utilizar la API de Flickr.com es necesario tener una cuenta de usuario y registrar una aplicación para conseguir las claves de acceso. En el caso de Flickr.com, y generalmente a cualquier API que permita autenticarse a través del protocolo Oauth [29], necesitaremos una clave de usuario y una clave secreta.
El nuestra implementación de la API de Flickr.com hemos utilizado tres librerías de código abierto:
Guzzle [30], como cliente HTTP.
Config [31], para gestionar las configuraciones de nuestra aplicación.
oauth1-client [32], como cliente del protocolo Oauth 1.0, usado Flickr.com para permitir que aplicaciones de terceros accedan a los datos de usuarios.
Con estas tres librerías solucionamos los distintos problemas que nos encontramos y nos dejan con una sencilla clase para poder gestionar los métodos básicos de la API de Flickr.com. El código de nuestra clase de Flickr.com sería el siguiente:
namespace Flickr;
use Noodlehaus\Config;
use Guzzle\Service\Client as GuzzleClient;
class Flickr
{
protected $client;
protected $config;
private $tokenCredentials;
const URL_FLICKR = 'https://api.flickr.com/services/rest/?method=';
public function __construct($configFile, $client = null, $server = null)
{
if(!is_null($client)) {
$this->client = $client;
} else {
$this->client = new GuzzleClient();
}
$this->config = new Config($configFile);
if(!is_null($server)) {
$this->server = $server;
} else {
$this->server = new \Flickr\Oauth1\Flickr(array(
'identifier' => $this->config->get('flickrKey'),
'secret' => $this->config->get('secretFlickr'),
'callback_uri' => $this->config->get('callbackUriFlickr'),
));
}
}
public function setTokenCredentials($token) {
$this->tokenCredentials = $token;
}
public function callMethod($method, $params, $isWithOauth = false)
{
$url = $this->buildQuery($method, $params, $isWithOauth);
if ($isWithOauth) {
$headers = $this->server->getHeaders($this->tokenCredentials,
"POST",
$url,
$params);
$response = $this->client->post($url, $headers)->send();
} else {
$response = $this->client->get($url)->send();
}
$result = json_decode($response->getBody());
return $result;
}
public function getTemporalCredentialsToken() {
$temporaryCredentials = $this->server->getTemporaryCredentials();
return $temporaryCredentials;
}
public function getTokenCredentials($tempCredential, $oauthToken, $oauthVerifier) {
$this->tokenCredentials = $this->server
->getTokenCredentials($tempCredential, $oauthToken, $oauthVerifier);
return $this->tokenCredentials;;
}
protected function buildQuery($method, $params, $isWithOauth)
{
$defaultOptions = array(
'format' => 'json',
'nojsoncallback' => 1,
);
if ($isWithOauth && empty($this->tokenCredentials)) {
throw new \Exception("Not authenticated");
}
if($isWithOauth) {
$ouathParams = ['auth_token'=> $this->tokenCredentials->getIdentifier(),
'api_sig' => $this->tokenCredentials->getSecret()];
}
$options = array_merge($params, $defaultOptions);
$optionsUrl = http_build_query($options);
return self::URL_FLICKR.
$method.
'&api_key='.
$this->config->get('flickrKey').
'&'.$optionsUrl;
}
}
Además, necesitamos un fichero de configuración donde tengamos guardadas las distintas claves necesarias para acceder a Flickr.com, tal y como lo especificamos en la propia clase Flickr y es leído por la librería hassankhan/config. El contenido de este fichero de configuración debería deter la forma:
flickrKey: CHANGE_WITH_YOUR_KEY
secretFlickr: CHANGE_WITH_YOUR_SECRET
callbackUriFlickr: http://dev.local
Donde, en caso de utilizar esta librería para conectarse con una cuenta de aplicación de Flickr.com, deberíamos de sustituir CHANGE_WITH_YOUR_KEY, CHANGE_WITH_YOUR_SECRET y CHANGE_WITH_YOUR_SECRET por los valores correspondientes.
Esta pequeña clase no implementa todos los métodos de Flickr.com, dado que hay algunos métodos especiales como la carga de fotos que necesitan una acción especial en Flickr.com, tal y como lo indican en la documentación oficial https://up.flickr.com/services/upload/.
Hay dos métodos que merezca la pena comentar. El método __construct, que inicializa las dependencias en el caso de que no vengan como parámetros. Esta es una buena práctica si queremos que nuestra clase pueda ser gestionada por un inyector de dependencias y pueda ser fácilmente testeable, como veremos a continuación.
El siguiente método a comentar es callMethod. Este método es el que realza la petición HTTP a Flickr.com*, utilizando el método de llamada. Los distintos métodos que soporta Flickr son muy variados y permiten tanto acceder como modificar distintos elementos en la cuenta de usuario.
Dado que nuestro método es genérico, vamos a realizar dos tests distintos: Vamos a testear los métodos de la API Flickr.com flickr.photos.search y flickr.galleries.create. Realizaremos dos veces el mismo test. Un test realizando la ejecución de la API y otra vez mockeando todas las dependencias y utilizando un fixture, como si tuviéramos el resultado de la ejecución de la API. De esta forma daremos visibilidad a las dos posibles aproximaciones que podemos encontrar cuando testeamos un API pública de un servicio online.
10.3.1 Test sin autentificación
La implementación del test funcional para una búsqueda en Flickr.com sería:
/**
* Test a query of Flickr without mocks.
*/
public function testQueryDirect()
{
$flickr = new Flickr('config.yml');
$photoCats = $flickr->callMethod('flickr.photos.search', array('text' => 'cats'));
$this->assertEquals(100, count($photoCats->photos->photo));
}
Este test inicializa nuestra implementación del cliente de la API de Flickr.com y realiza una búsqueda con el texto "cats". Como esta búsqueda es muy popular, verificamos que tenemos 100 fotos, que es el número de fotos que devuelve Flickr.com por defecto.
Este test tiene varios problemas y ventajas: Si la máquina en la que se ejecuta no puede conectarse a Flickr.com por algún motivo, el test fallará. Además, al realizar una conexión HTTP y una búsqueda en los sistemas de Flickr.com, será lento en ejecución. Si quisiéramos verificar el contenido de alguna foto, el test fallaría con el paso del tiempo, dado que los resultados para una búsqueda en Flickr.com cambiarán según los usuarios vayan añadiendo más fotos. Por otra parte, este test está ejecutando completamente nuestra implementación y verificando que, tanto nuestro código como Flickr.com se comportan de la forma esperada.
Veamos ahora el mismo test pero implementado con mocks.
/**
* Test a query of Flickr with mocks.
*/
public function testQueryWithMocks()
{
$client = $this->prophesize(Client::class);
$response = $this->prophesize(Response::class);
$response->getBody()->willReturn($this->getFixtureContent());
$request = $this->prophesize(RequestInterface::class);
$request->send()->willReturn($response->reveal());
$client->get(Argument::any())->willReturn($request->reveal());
$server = $this->prophesize(FlickrServer::class);
$flickr = new Flickr('config.yml', $client->reveal(), $server->reveal());
$photoCats = $flickr->callMethod('flickr.photos.search', array('text' => 'cats'));
$this->assertEquals(100, count($photoCats->photos->photo));
}
private function getFixtureContent()
{
return file_get_contents('./tests/fixtures/cats.json');
}
Este test es el mismo que el anterior, pero con grandes diferencias conceptuales. Dado que estamos inyectando mocks en el constructor del cliente HTTP Guzzle, el test nunca llega a conectarse a Flickr.com. Por ello, es un test más rápido en ejecución. Para que funcione el tests hemos necesitado guardar una búsqueda, almacenada en el fichero "./tests/fixtures/cats.json" de nuestro proyecto. Como contrapartida, con este test no llegamos a ver claramente que nuestra implementación está dando el resultado esperado, dado que no estamos ejercitando las dependencias, de la misma forma que un cambio en Flickr.com pasaría sin ser detectado por nuestros tests.
10.3.2 Test con autentificación
Los dos ejemplos anteriores ejecutaban un método por el que no es necesario autentificarse. Ahora veamos un método en el que es necesario estar a autentificado y los distintos problemas que esto conlleva.
/**
* Test create Galery without mocks.
*/
public function testCreateGallery()
{
$token = new TokenCredentials();
$token->setIdentifier('IDENTIFIER');
$token->setSecret('SECRET');
$this->flickr->setTokenCredentials($token);
$gallery = ['title' => 'Galería de ejemplo', 'description' => 'Galería de ejemplo'];
$res = $this->flickr->callMethod('flickr.galleries.create', $gallery, true);
$this->assertEquals('ok', $res->stat);
}
Al igual que el test testQueryDirect, este test se conecta con Flickr.com y realiza la acción de crear una galería. Por ello podemos decir que tiene los mismos problemas que el test testQueryDirect, pero en realidad tiene dos problemas más. Como podemos ver, en el test hemos tenido que crear un objeto TokenCredentials, asignarle los valores Identifier y Secret a través de los setter correspondientes he inyectar este objeto a nuestro objeto Flickr.com. Esto debemos hacerlo por como funciona Oauth.
Los valores IDENTIFIER y SECRET debemos configurarlos realizando una conexión real a Flickr.com, aceptar el uso de la API desde nuestra aplicación y obtener la respuesta. Este proceso es tedioso y complejo, además de inestable, dado que si en algún momento indicamos que ya no damos permiso a nuestra aplicación a conectarse a Flickr.com, el test dejará de funcionar.
Además, este test presenta otro problema. Cada vez que ejecutamos el test estamos creando una galería nueva. Esto se puede comprobar en el perfil de usario asociado a las credenciales. Dado que Flickr.com no soporta un método para el borrado de galerías y automatizar el borrado en el mismo test, la cuenta del usuario quedará con varias galerías con el nombre indicado en el test.
Algunos proyectos ofrecen un interfaz de testeo, generalmente llamado Sandbox, en el cual un desarrollador puede realizar cualquier tipo de acción como si fuera el sistema real, pero en un entorno aislado y periódicamente reseteado. Flickr.com no ofrece este tipo de entorno para poder ejercitar su API, por lo que cualquier acción que se ejecute a nivel de testeo contra los servicios reales quedará reflejada en las cuentas de usuario asociadas.
Hay que destacar que Oauth1 es bastante complejo de testear, debido a la autorización por parte del usuario al acceso de una tercera aplicación a los servicios que permite acceder. En testeo unitario esto es prácticamente imposible, debido a que hay una interacción a tres bandas. En un test de aceptación con herramientas como Behat con Selenium o alguna otra herramienta este tipo de test se podría automatizar, pero de tal y como lo estamos resolviendo, es necesario obtener las credenciales manualmente para poder ejecutar estos tests.
Veamos una implementación utilizando mocks, y por lo tanto, exenta de estos dos últimos problemas:
/**
* Test create Galery with mocks.
*/
public function testCreateGalleryWithMocks()
{
$client = $this->prophesize(Client::class);
$response = $this->prophesize(Response::class);
$response->getBody()->willReturn($this->getFixtureContentForCreateGallery());
$request = $this->prophesize(RequestInterface::class);
$request->send()->willReturn($response->reveal());
$client->post(Argument::any(), Argument::any())->willReturn($request->reveal());
$server = $this->prophesize(FlickrServer::class);
$server->getHeaders(Argument::any(),
Argument::any(),
Argument::any(),
Argument::any())->willReturn([]);
$flickr = new Flickr('config.yml', $client->reveal(), $server->reveal());
$token = $this->prophesize(TokenCredentials::class);
$flickr->setTokenCredentials($token->reveal());
$gallery = ['title' => 'Galería de ejemplo', 'description' => 'Galería de ejemplo'];
$res = $flickr->callMethod('flickr.galleries.create', $gallery, true);
$this->assertEquals("ok", $res->stat);
}
private function getFixtureContentForCreateGallery()
{
return file_get_contents('./tests/fixtures/create-gallery.json');
}
Este caso presenta un escenario muy similar al test testQueryWithMocks, pese a que el método que estamos testeando necesita autenticación. Como todo está mockeado y la respuesta inyectada en otro fixture el test no tiene efectos secundarios en ninguna cuenta de Flickr.com y no hemos necesitado conectarnos y aceptar el uso de la aplicación previa a la creación al test.
Por otra parte, y como comentábamos con el test de la búsqueda, este tipo de test no realiza nada, por lo cual no podemos garantizar que la acción en los servicios de Flickr.com sea válida.
Optar por una u otra forma de realizar test a servicios externos depende de la naturaleza del problema y es decisión del equipo de desarrollo. En la mayor parte de los casos la mejor solución, por simplicidad y tiempo de ejecución sea la de mockear cualquier sistema externo, pero hay situaciones concretas y críticas, como sistemas de pagos, en las cuales la mejor solución es testear el sistema de forma completa. Algunos de estos servicios online, como Paypal.com son conscientes de la necesidad de crear tests validos y ofrecen entornos de pruebas como comentábamos anteriormente, para que los desarrollos sobre esta plataforma estén implementando de la forma más estable y con mejores garantías posibles.