Este artículo es el último de una serie sobre la aplicación del patrón arquitectónico MVI en Kotlin Multiplatform. En las dos partes anteriores ( parte 1 y parte 2 ), recordamos qué es MVI, creamos un módulo de Kittens genérico para cargar imágenes de gatos y lo integramos en aplicaciones iOS y Android.
En esta parte, cubriremos el módulo Kittens con pruebas unitarias y de integración. Aprenderemos sobre las limitaciones actuales de las pruebas en Kotlin Multiplatform, descubriremos cómo superarlas e incluso hacer que funcionen a nuestro favor.
Un proyecto de muestra actualizado está disponible en nuestro GitHub .
Prólogo
No hay duda de que las pruebas son un paso importante en el desarrollo de software. Por supuesto, ralentiza el proceso, pero al mismo tiempo:
- le permite comprobar los casos extremos que son difíciles de detectar manualmente;
- Reduce la posibilidad de regresión al agregar nuevas funciones, corregir errores y refactorizar;
- te obliga a descomponer y estructurar tu código.
A primera vista, el último punto puede parecer una desventaja, porque lleva tiempo. Sin embargo, hace que el código sea más legible y beneficioso a largo plazo.
“De hecho, la proporción de tiempo dedicado a leer versus escribir es más de 10 a 1. Estamos constantemente leyendo código antiguo como parte del esfuerzo por escribir código nuevo. ... [Por lo tanto,] facilitar la lectura facilita la escritura ". - Robert C. Martin, "Código limpio: un manual de artesanía de software ágil"
Kotlin Multiplatform amplía las capacidades de prueba. Esta tecnología agrega una característica importante: cada prueba se realiza automáticamente en todas las plataformas compatibles. Si, por ejemplo, solo se admiten Android e iOS, la cantidad de pruebas se puede multiplicar por dos. Y si en algún momento se agrega soporte para otra plataforma, automáticamente se cubre en las pruebas.
Las pruebas en todas las plataformas compatibles son importantes porque puede haber diferencias en el comportamiento del código. Por ejemplo, Kotlin / Native tiene un modelo de memoria especial , Kotlin / JS a veces también da resultados inesperados.
Antes de continuar, vale la pena mencionar algunas de las limitaciones de prueba en Kotlin Multiplatform. El más grande es la falta de una biblioteca de burla para Kotlin / Native y Kotlin / JS. Esto puede parecer una gran desventaja, pero personalmente lo considero una ventaja. Probar en Kotlin Multiplatform fue bastante difícil para mí: tuve que crear interfaces para cada dependencia y escribir sus implementaciones de prueba (falsificaciones). Me llevó mucho tiempo, pero en algún momento me di cuenta de que dedicar tiempo a las abstracciones es una inversión que conduce a un código más limpio.
También noté que las modificaciones posteriores a este código toman menos tiempo. ¿Porqué es eso? Porque la interacción de una clase con sus dependencias no está clavada (burlada). En la mayoría de los casos, basta con actualizar sus implementaciones de prueba. No necesita profundizar en cada método de prueba para actualizar sus simulacros. Como resultado, dejé de usar bibliotecas burlonas incluso en el desarrollo estándar de Android. Recomiendo leer el siguiente artículo: " Burlarse no es práctico - Use falsificaciones " de Pravin Sonawane .
Plan
Recordemos lo que tenemos en el módulo Gatitos y lo que debemos probar.
- KittenStore es el componente principal del módulo. Su implementación KittenStoreImpl contiene la mayor parte de la lógica empresarial. Esto es lo primero que vamos a probar.
- KittenComponent es la fachada del módulo y el punto de integración para todos los componentes internos. Cubriremos este componente con pruebas de integración.
- KittenView es una interfaz pública que representa la dependencia de la interfaz de usuario de KittenComponent.
- KittenDataSource es una interfaz de acceso web interna que tiene implementaciones específicas de plataforma para iOS y Android.
Para una mejor comprensión de la estructura del módulo, daré su diagrama UML:
El plan es el siguiente:
- Prueba KittenStore
- Creando una implementación de prueba de KittenStore.Parser
- Creando una implementación de prueba de KittenStore.Network
- Pruebas unitarias de escritura para KittenStoreImpl
- Creando una implementación de prueba de KittenStore.Parser
- Prueba del componente Kitten
- Creando una implementación de prueba de KittenDataSource
- Cree una implementación de KittenView de prueba
- Pruebas de integración de escritura para KittenComponent
- Creando una implementación de prueba de KittenDataSource
- Ejecutando pruebas
- conclusiones
Prueba de la unidad KittenStore
La interfaz KittenStore tiene su propia clase de implementación: KittenStoreImpl. Esto es lo que vamos a probar. Tiene dos dependencias (interfaces internas), definidas directamente en la propia clase. Comencemos escribiendo implementaciones de prueba para ellos.
Prueba de implementación de KittenStore.Parser
Este componente es responsable de las solicitudes de red. Así es como se ve su interfaz:
Antes de escribir una implementación de prueba de una interfaz de red, debemos responder una pregunta importante: ¿qué datos devuelve el servidor? La respuesta es que el servidor devuelve un conjunto aleatorio de enlaces de imágenes, cada vez un conjunto diferente. En la vida real, se usa el formato JSON, pero como tenemos una abstracción del analizador, no nos importa el formato en las pruebas unitarias.
Una implementación real puede cambiar de transmisión, por lo que los suscriptores pueden congelarse en Kotlin / Native. Sería genial modelar este comportamiento para asegurarse de que el código maneja todo correctamente.
Por lo tanto, nuestra implementación de prueba de Network debería tener las siguientes características:
- debe devolver un conjunto no vacío de filas diferentes para cada solicitud;
- el formato de respuesta debe ser común para la red y el analizador;
- debería poder simular errores de red (tal vez debería completarse sin una respuesta);
- debería ser posible simular un formato de respuesta no válido (para comprobar si hay errores en el analizador);
- debería ser posible simular retrasos en la respuesta (para comprobar la fase de arranque);
- debería poder congelarse en Kotlin / Native (por si acaso).
La implementación de la prueba en sí misma podría verse así:
TestKittenStoreNetwork tiene almacenamiento de cadenas (como un servidor real) y puede generar cadenas. Para cada solicitud, la lista actual de líneas se codifica en una línea. Si la propiedad "imágenes" es cero, entonces Maybe simplemente terminará, lo que debería considerarse un error.
También usamos TestScheduler . Este programador tiene una función importante: congela todas las tareas entrantes. Por lo tanto, el operador observeOn, utilizado junto con TestScheduler, congelará el flujo descendente, así como todos los datos que pasan a través de él, como en la vida real. Pero al mismo tiempo, el multihilo no estará involucrado, lo que simplifica las pruebas y las hace más confiables.
Además, TestScheduler tiene un modo especial de "procesamiento manual" que nos permitirá simular la latencia de la red.
Prueba de implementación de KittenStore.Parser
Este componente es responsable de analizar las respuestas del servidor. Aquí está su interfaz:
Por lo tanto, todo lo que se descargue de la web debe convertirse en una lista de enlaces. Nuestra red simplemente concatena cadenas usando un separador de punto y coma (;), así que use el mismo formato aquí.
Aquí hay una implementación de prueba:
Al igual que con Network, TestScheduler se usa para congelar a los suscriptores y verificar su compatibilidad con el modelo de memoria Kotlin / Native. Los errores de procesamiento de respuesta se simulan si la cadena de entrada está vacía.
Pruebas unitarias para KittenStoreImpl
Ahora tenemos implementaciones de prueba de todas las dependencias. Es hora de las pruebas unitarias. Todas las pruebas unitarias se pueden encontrar en el repositorio , aquí solo daré la inicialización y algunas pruebas.
El primer paso es crear instancias de nuestras implementaciones de prueba:
KittenStoreImpl usa mainScheduler, por lo que el siguiente paso es anularlo:
Ahora podemos ejecutar algunas pruebas. KittenStoreImpl debería cargar las imágenes inmediatamente después de la creación. Esto significa que se debe cumplir una solicitud de red, se debe procesar su respuesta y se debe actualizar el estado con el nuevo resultado.
Lo que hicimos:
- imágenes generadas en la Red;
- creó una nueva instancia de KittenStoreImpl;
- se aseguró de que el estado contenga la lista correcta de cadenas.
Otro escenario que debemos considerar es obtener KittenStore.Intent.Reload. En este caso, la lista debe recargarse desde la red.
Pasos de prueba:
- generar imágenes fuente;
- cree una instancia de KittenStoreImpl;
- generar nuevas imágenes;
- enviar Intent.Reload;
- asegúrese de que la condición contenga nuevas imágenes.
Finalmente, probemos el siguiente escenario: cuando el indicador isLoading se establece mientras se cargan las imágenes.
Hemos habilitado el procesamiento manual para TestScheduler; ahora las tareas no se procesarán automáticamente. Esto nos permite verificar el estado mientras esperamos una respuesta.
Pruebas de integración de componentes de Kitten
Como mencioné anteriormente, KittenComponent es el punto de integración de todo el módulo. Podemos cubrirlo con pruebas de integración. Echemos un vistazo a su API:
Hay dos dependencias, KittenDataSource y KittenView. Necesitaremos implementaciones de prueba para estos antes de que podamos comenzar a probar.
Para completar, este diagrama muestra el flujo de datos dentro del módulo:
Implementación de prueba de KittenDataSource
Este componente es responsable de las solicitudes de red. Tiene implementaciones separadas para cada plataforma y necesitamos otra implementación para las pruebas. Así es como se ve la interfaz KittenDataSource:
TheCatAPI admite la paginación, así que agregué los argumentos apropiados de inmediato. De lo contrario, es muy similar a KittenStore.Network, que implementamos anteriormente. La única diferencia es que tenemos que usar el formato JSON ya que estamos probando código real en integración. Así que solo estamos tomando prestada la idea de implementación:
Como antes, generamos diferentes listas de cadenas que se codifican en una matriz JSON en cada solicitud. Si no se generan imágenes o los argumentos de la solicitud son incorrectos, Maybe simplemente terminará sin una respuesta.
La biblioteca kotlinx.serialization se usa para formar una matriz JSON . Por cierto, el KittenStoreParser probado lo usa para decodificar.
Prueba de implementación de KittenView
Este es el último componente para el que necesitamos una implementación de prueba antes de que podamos comenzar a probar. Aquí está su interfaz:
Es una vista que solo toma modelos y activa eventos, por lo que su implementación de prueba es muy simple:
Solo necesitamos recordar el último modelo aceptado; esto nos permitirá verificar la exactitud del modelo mostrado. También podemos enviar eventos en nombre de KittenView utilizando el método dispatch (Event), que se declara en la clase AbstractMviView heredada.
Pruebas de integración para KittenComponent
El conjunto completo de pruebas se puede encontrar en el repositorio , aquí daré solo algunas de las más interesantes.
Como antes, comencemos creando instancias de dependencias e inicializando:
Actualmente se utilizan dos programadores para el módulo: mainScheduler y computationScheduler. Necesitamos anularlos:
Ahora podemos escribir algunas pruebas. Primero revisemos el script principal para asegurarnos de que las imágenes estén cargadas y mostradas al inicio:
Esta prueba es muy similar a la que escribimos cuando miramos las pruebas unitarias para KittenStore. Solo ahora está involucrado todo el módulo.
Pasos de prueba:
- generar enlaces a imágenes en TestKittenDataSource;
- crear y ejecutar KittenComponent;
- asegúrese de que los enlaces lleguen a TestKittenView.
Otro escenario interesante: las imágenes deben recargarse cuando KittenView activa el evento RefreshTriggered.
Etapas:
- generar enlaces de origen a imágenes;
- crear y ejecutar KittenComponent;
- generar nuevos enlaces;
- enviar Event.RefreshTriggered en nombre de KittenView;
- asegúrese de que los nuevos enlaces lleguen a TestKittenView.
Ejecutando pruebas
Para ejecutar todas las pruebas, debemos realizar la siguiente tarea de Gradle:
./gradlew :shared:kittens:build
Esto compilará el módulo y ejecutará todas las pruebas en todas las plataformas compatibles: Android e iosx64.
Y aquí está el informe de cobertura de JaCoCo:
Conclusión
En este artículo, cubrimos el módulo Kittens con pruebas unitarias y de integración. El diseño propuesto del módulo nos permitió cubrir las siguientes partes:
- KittenStoreImpl: contiene la mayor parte de la lógica empresarial;
- KittenStoreNetwork: responsable de las solicitudes de red de alto nivel;
- KittenStoreParser: responsable de analizar las respuestas de la red;
- todas las transformaciones y conexiones.
El último punto es muy importante. Es posible cubrirlo gracias a la función MVI. La única responsabilidad de la vista es mostrar datos y enviar eventos. Todas las suscripciones, conversiones y enlaces se realizan dentro del módulo. Así, podemos cubrir todo con pruebas generales, excepto la propia pantalla.
Tales pruebas tienen las siguientes ventajas:
- no utilice las API de la plataforma;
- realizado muy rápidamente;
- confiable (no parpadear);
- se ejecuta en todas las plataformas compatibles.
También pudimos probar la compatibilidad del código con el complejo modelo de memoria Kotlin / Native. Esto también es muy importante debido a la falta de seguridad en el tiempo de compilación: el código simplemente falla en tiempo de ejecución con excepciones que son difíciles de depurar.
Espero que esto te ayude en tus proyectos. ¡Gracias por leer mis artículos! Y no olvides seguirme en Twitter .
...
Ejercicio extra
Si desea trabajar con implementaciones de prueba o jugar con MVI, aquí hay algunos ejercicios prácticos.
Refactorización de KittenDataSource
Hay dos implementaciones de la interfaz KittenDataSource en el módulo: una para Android y otra para iOS. Ya mencioné que son responsables del acceso a la red. Pero en realidad tienen otra función: generan la URL para la solicitud basándose en los argumentos de entrada "límite" y "página". Al mismo tiempo, tenemos una clase KittenStoreNetwork que no hace nada más que delegar la llamada a KittenDataSource.
Asignación: mueva la lógica de generación de solicitudes de URL de KittenDataSourceImpl (en Android e iOS) a KittenStoreNetwork. Necesita cambiar la interfaz KittenDataSource de la siguiente manera:
Una vez que lo haya hecho, deberá actualizar sus pruebas. La única clase que necesita tocar es TestKittenDataSource.
Añadiendo carga de página
TheCatAPI admite la paginación, por lo que podemos agregar esta funcionalidad para una mejor experiencia de usuario. Puede comenzar agregando un nuevo evento Event.EndReached para KittenView, después de lo cual el código dejará de compilarse. Luego, deberá agregar el Intent.LoadMore apropiado, convertir el nuevo Evento en Intent y procesar este último en KittenStoreImpl. También deberá modificar la interfaz KittenStoreImpl.Network de la siguiente manera:
Finalmente, deberá actualizar algunas implementaciones de prueba, corregir una o dos pruebas existentes y luego escribir algunas nuevas para cubrir la paginación.