Unidad de prueba de un cliente API y envoltorios

14

He estado dando vueltas en círculos tratando de encontrar la mejor manera de probar la unidad de una biblioteca de cliente API que estoy desarrollando. La biblioteca tiene una Clientclase que básicamente tiene una asignación 1: 1 con la API, y una Wrapperclase adicional que proporciona una interfaz más fácil de usar en la parte superior de la Client.

Wrapper --> Client --> External API

La primera vez que escribí un montón de pruebas en contra de ambos Clienty Wrapper, efectivamente haciendo una prueba de que siempre que presenten a las funciones apropiadas de cualquiera que sea la vez operan en ( Wrapperopera en Client, y Clientopera en una conexión HTTP). Sin embargo, comencé a sentirme incómodo con esto porque siento que estoy probando la implementación de estas clases, en lugar de la interfaz. En teoría, podría cambiar las clases para tener otra implementación perfectamente válida, pero mis pruebas fallarían porque no se llaman las funciones que esperaba que se llamaran. Eso me suena a pruebas frágiles.

Después de esto, pensé en la interfaz de las clases. Las pruebas deben verificar que las clases realmente hacen el trabajo que deben hacer, en lugar de cómo lo hacen. Entonces, ¿cómo puedo hacer esto? Lo primero que viene a la mente es tropezar las solicitudes de API externas. Sin embargo, estoy nervioso por simplificar demasiado el servicio externo. Muchos de los ejemplos de API apagadas que he visto solo dan respuestas enlatadas, lo que parece una forma realmente fácil de probar que su código se ejecuta correctamente contra su API falsa. La alternativa es burlarse del servicio, que no es factible y que debería mantenerse actualizado cada vez que cambie el servicio real, lo que se siente como una exageración y una pérdida de tiempo.

Finalmente, leí esto de otra respuesta en los programadores SE :

El trabajo de un cliente API remoto es emitir ciertas llamadas, ni más ni menos. Por lo tanto, su prueba debe verificar que emite esas llamadas, ni más ni menos.

Y ahora estoy más o menos convencido: cuando pruebo Client, todo lo que necesito probar es que realiza las solicitudes correctas a la API (Por supuesto, siempre existe la posibilidad de que la API cambie, pero mis pruebas continúan aprobadas, pero eso es donde las pruebas de integración serían útiles). Dado que Clientes solo un mapeo 1: 1 con la API, mi preocupación antes de cambiar de una implementación válida a otra realmente no se aplica: solo hay una implementación válida para cada método Client.

Sin embargo, todavía estoy atrapado con la Wrapperclase. Veo las siguientes opciones:

  1. Termino la Clientclase y solo pruebo que se llamen los métodos apropiados. De esta manera, estoy haciendo lo mismo que antes pero tratando el Clientcomo un sustituto de la API. Esto me devuelve a donde comencé. Una vez más, esto me da la sensación incómoda de probar la implementación, no la interfaz. El Wrapperpodría muy bien implementarse usando un cliente completamente diferente.

  2. Yo creo un simulacro Client. Ahora tengo que decidir hasta dónde llegar con burlarse de él: crear una burla completa del servicio requeriría mucho esfuerzo (más trabajo del que se ha dedicado a la biblioteca). La API en sí es simple, pero el servicio es bastante complejo (es esencialmente un almacén de datos con operaciones sobre esos datos). Y nuevamente, tendré que mantener mi simulacro sincronizado con lo real Client.

  3. Solo pruebo que se están realizando las solicitudes HTTP adecuadas. Esto significa que Wrapperllamará a través de un Clientobjeto real para hacer esas solicitudes HTTP, por lo que en realidad no lo estoy probando de forma aislada. Esto lo convierte en una prueba unitaria terrible.

Así que no estoy particularmente contento con ninguna de estas soluciones. ¿Qué harías? ¿Hay una manera correcta de hacer esto?

Joseph Mansfield
fuente
Tiendo a evitar las pruebas unitarias en estos escenarios donde hay una biblioteca de terceros que hace la mayor parte del trabajo duro y simplemente tengo un contenedor (principalmente porque no tengo idea de cómo hacer esto de una manera que pruebe algo realmente significativo). Generalmente hago pruebas de integración en esos casos, posiblemente con un servicio simulado. Tal vez alguien sepa cómo hacer una prueba unitaria realmente significativa para estos: tiendo a priorizar los componentes más críticos del sistema bajo nuestro control. Aquí la parte crítica está fuera de nuestro control. :-(

Respuestas:

10

TLDR : a pesar de la dificultad, debe desactivar el servicio y utilizarlo para las pruebas de la unidad del cliente.


No estoy tan seguro de que "el trabajo de un cliente API remoto sea emitir ciertas llamadas, ni más, ni menos ...", a menos que la API solo consista en puntos finales que siempre devuelvan un estado fijo, y no consuman ni produzcan cualquier dato Esta no sería la API más útil ...

También querrá verificar que el cliente no solo envíe las solicitudes correctas, sino que maneje adecuadamente el contenido de respuesta, errores, redirecciones, etc. Y pruebe todos estos casos.

Como observa, debe tener pruebas de integración que cubran la pila completa desde el contenedor -> cliente -> servicio -> DB y más, pero para responder a su pregunta principal, a menos que tenga un entorno donde las pruebas de integración se puedan ejecutar como parte de cada Creación de CI sin muchos dolores de cabeza (bases de datos de prueba compartidas, etc.), debe invertir el tiempo en crear un trozo de la API.

El código auxiliar le permitirá crear una implementación funcional del servicio, pero sin tener que implementar ninguna capa debajo del servicio en sí.

Podría considerar usar una solución basada en DI para lograr esto, con una implementación del patrón de Repositorio debajo de los recursos REST:

  • Reemplace todo el código funcional en los controladores REST con llamadas a un IWhateverRepository.
  • Cree un ProductionWhateverRepository con el código que se extrajo de los recursos REST y un TestWhateverRespository que devuelve respuestas enlatadas para su uso durante las pruebas unitarias.
  • Use el contenedor DI para inyectar ya sea la DLL / clase TestWhateverRepository o TestWhateverRepository, etc., según la configuración.

De todos modos, a menos que descartar y / o refactorizar el servicio esté fuera de discusión, ya sea política o prácticamente, probablemente emprendería algo similar a lo anterior. Si no es posible, me aseguraría de tener una cobertura de integración realmente buena y ejecutarlas con la frecuencia que sea posible dada la configuración de prueba disponible.

HTH

Dan1701
fuente