Saltar al contenido

Tech Blog | Probando las llamadas HTTP

Realizar llamadas HTTP para obtener recursos o llamar a las API es un elemento básico del desarrollo de software, pero si no se abstrae adecuadamente la construcción de peticiones y la gestión de respuestas de la biblioteca HTTP que se utiliza, puede ser extremadamente difícil de probar.

Aquí hay un ejemplo de algún código relacionado con HTTP que podríamos encontrar en C#:

Tech Blog | Probando las llamadas HTTP
Tech Blog | Probando las llamadas HTTP
publicUserGetThatUserData(stringuserId){varhttpClient=newHttpClient();varurl=$"https://example.com/api/users/{userId}";varresponse=httpClient.GetAsync(url).Result;if(response.StatusCode! =HttpStatusCode.OK){thrownewException($"Código de estado inesperado: {response.StatusCode}");}varbody=response.Content.ReadAsStringAsync().Result;returnJsonConvert.DeserializeObject<User;}(body);}

Y un ejemplo similar en JavaScript:

asyncfunctiongetThatUserData(userId){consturl=$0027https://example.com/api/users/$0027+userIdconstresponse=awaitaxios.get(url)if(response.status!==200){thrownewError($0027Código de estado inesperado: $0027+response.statusCode)}returnresponse.data}

A primera vista, estas parecen buenas abstracciones. Todo el manejo relacionado con el HTTP está contenido dentro de la función; ni la entrada ni la salida exponen nada sobre URLs, encabezados, códigos de estado o cuerpos. Pero son difíciles de probar.

A veces elegimos confiar en las pruebas de integración para este tipo de funciones. Desafortunadamente, esto hace que nuestras pruebas dependan de una dependencia externa. Si ese servidor se cae o falla por alguna razón desconocida, nuestras pruebas fallarán aunque nuestro código esté bien. Además, es posible que no pueda probar sus rutas de error, porque no puede controlar la respuesta. Y las cosas se vuelven aún más complejas cuando se nos ocurre la idea de ejecutar nuestro propio servidor con fines de prueba.

Dependiendo del idioma y de las bibliotecas que utilicemos, puede ser posible probar estas funciones de forma unitaria, pero incluso cuando es posible, a menudo es complicado, lo que hace que nuestras pruebas sean más difíciles de entender y mantener.

Así que cuando las pruebas son difíciles, puede que no probemos esas funciones en absoluto. Después de todo, no necesitamos probar la biblioteca HTTP subyacente, ¿verdad? Desafortunadamente, esto deja un hueco sin probar en nuestra gestión de peticiones y respuestas.

Abstrayéndonos en C

Para que el código sea más fácil de probar, necesitamos separar la librería HTTP. Hagamos esto primero en C#.

En el código anterior, la petición y la respuesta HTTP se construyen llamando al método GetAsync con una url.Para abstraer la biblioteca, podemos empezar introduciendo una nueva interfaz para la pieza que realmente ejecuta las llamadas HTTP:

publicinterfaceIHttpExecutor{Task<HttpResponseMessage,}publicUserGetThatUserData(stringuserId,IHttpExecutorexecutor){varurl=$"https://example.com/api/users/{userId}";varresponse=executor.GetAsync(url). Result;if(response.StatusCode!=HttpStatusCode.OK){thrownewException($"Código de estado inesperado: {response.StatusCode}");}varbody=response.Content.ReadAsStringAsync().Result;returnJsonConvert.DeserializeObject<User;}

Para este ejemplo, he decidido utilizar una inyección de dependencia basada en un método para introducir el nuevo IHttpExecutor en el código porque es sencillo de escribir en una entrada de blog.Pero si lo prefieres, puedes utilizar la inyección de nivel de clase.Lo importante es que ahora tenemos una interfaz que separa nuestro código de la biblioteca HTTP.

Ahora podemos empezar a escribir pruebas que utilicen una implementación de doble prueba de esa interfaz. Luego podemos verificar que hemos llamado a la url correcta (interrogando al doble de la prueba) y podemos hacer que devuelva cualquier mensaje de respuesta Http que queramos probar. Incluso podemos lanzar excepciones como se desee para representar los tiempos muertos o los fallos de conexión.

Pero lidiar con el mensaje de respuesta Http es un poco difícil. ¿Cómo configuramos el cuerpo de respuesta? No es obvio cómo configurar nuestro propio objeto de respuesta para que el contenido de respuesta, ReadAsStringAsync, tenga éxito. No nos hemos abstraído lo suficiente de la biblioteca.

clase públicaHttpResponse{publicintStatusCode{get;set;}publictringBody{get;set;}}publicinterfaceIHttpExecutor{Task<HttpResponse{GetAsync(stringurl);}publicUserGetThatUserData(stringuserId,IHttpExecutorexecutor){varurl=$"https://example. com/api/users/{userId}";varresponse=ejecutor.GetAsync(url).Result;if(response.StatusCode!=200){thrownewException($"Código de estado inesperado: {response.StatusCode}");}returnJsonConvert.DeserializeObject<User>(response.Body);}

Ahora podemos probar fácilmente nuestro código mediante dobles de prueba.El nuevo objeto HttpResponse es simple de instanciar con todo lo que necesitamos.Por supuesto, necesitaremos una implementación real de la interfaz si vamos a ejecutar nuestro código fuera de las pruebas:

clase públicaHttpEjecutor:IHttpEjecutor{privadoHttpClienthttpClient=newHttpClient();publicasyncTask<HttpResponse GetAsync(url);varbody=awaitresponse.Content.ReadAsStringAsync();returnnewHttpResponse{StatusCode=(int)response.StatusCode,Body=body};}}

Este método GetAsync es difícil de probar, pero no incluye la construcción de la solicitud y el manejo de la respuesta que era específico para nuestro caso de uso. Y debido a que es una interfaz más genérica (no especifica una url en particular), se hace más fácil escribir pruebas de integración para ella.

Con el tiempo, podemos añadir más a nuestra HttpResponse para tener en cuenta cosas como las cabeceras de respuesta.También podemos encontrar que queremos crear una clase HttpRequest para poder especificar conceptos de peticiones HTTP como métodos, cabeceras y cuerpo.Entonces podríamos tener un único método ExecuteAsync(petición HttpRequest) que maneje todo tipo de peticiones.

Por supuesto, algunas librerías HTTP son mejores que otras.Quizás deberíamos haber elegido algo distinto al HttpClient incorporado… Si hubiéramos elegido algo con una buena interfaz, podríamos haber evitado la creación de todas estas clases adicionales.pero la ventaja de usar estas clases es que ahora podemos cambiar la implementación subyacente según sea necesario sin cambiar ningún otro código.

Funcionando en JavaScript

En el ejemplo de C#, utilicé dobles de inyección y de prueba porque cuando escribo C# tiendo a utilizar un estilo burlón de TDD.El lenguaje también requirió que definiéramos un montón de tipos para modelar HTTP.Modifiquemos nuestro código JavaScript de ejemplo utilizando un estilo más funcional.

Nuestro problema de prueba se reduce fundamentalmente a la necesidad de comprobar que hemos creado correctamente la petición y manejado la respuesta. Pero eso no tiene nada que ver con la ejecución de la llamada HTTP, y por lo tanto podrían ser funciones puras (sin E/S). ¡Es fácil de probar!

functionbuildRequest(userId){retorno{url:$0027https://example.com/api/users/$0027+userId}}}functionhandleResponse(response){if(response.status!==200){thrownewError($0027Código de estado inesperado: $0027+response.statusCode)}retorno-respuesta.data}

Ahora sólo tenemos que componer esas funciones con nuestra biblioteca HTTP para hacer una llamada de extremo a extremo.

asyncfunctiongetThatUserData(userId){returnhandleResponse(awaitaxios.get(buildRequest(userId).url))}

Lo único que no se ha probado en nuestro nuevo método es el axios.get call. Pero no es nuestro código, así que ¿realmente tenemos que probarlo? La forma en que lo llamamos no está probada, pero eso es difícil de hacer sin dar un paso más para mover la llamada axios detrás de otra función:

asyncfunctionexecute(request){returnawaitaxios.get(request.url)}asyncfunctiongetThatUserData(userId){returnhandleResponse(awaitexecute(buildRequest(userId))}

Al igual que en el ejemplo de C#, este método de ejecución es lo suficientemente genérico como para que sea más fácil de probar que la función original que era específica de la API a la que queríamos llamar. Ahora podemos pensar en soportar otros tipos de peticiones (como POST) o sustituir los axios por otra biblioteca según sea necesario.

Conclusión

Con las abstracciones correctas, puedes probar fácilmente tu código relacionado con HTTP. Esto te permite asegurarte de que estás construyendo tus peticiones correctamente, verificando cosas como la construcción de la URL, los encabezados de las peticiones, etc.

Los casos de fracaso que eran difíciles de probar, como los códigos de estado de los niveles 400 y 500, los tiempos de espera de solicitudes e incluso las respuestas malformadas, ahora son fáciles de probar.

Este enfoque de abstraer invocaciones difíciles de probar también puede utilizarse en otras situaciones similares, como las llamadas a bases de datos. Abstraer bibliotecas y otras dependencias puede añadir mucha flexibilidad a su código y también hacerlo más comprobable.

Categorías: technicalTags: testing, c-sharp, javascript