Testea tus aplicaciones Xamarin.Forms y no mueras en el intento

El pasado día 30 de Noviembre de 2019 tuve el placer de dar una charla en Monkey Conf 2019 http://www.monkeyconf.es/, la mayor y más importante conferencia Xamarin en España.

Como tuvo buena acogida entre el público, dejo aquí la presentación de la charla, así como los comentarios de cada slide, que escribo siempre antes de dar una charla con las cosas que quiero decir.

También he subido un código de ejemplo a GitHub que contiene los diferentes recursos que se tratan en la charla. Este código podéis encontrarlo aquí:

https://github.com/dhompanera/monkeyconf2019

Además, la grabación de la charla se puede ver en youtube:

Y aquí la presentación con comentarios:

Soy Daniel Hompanera Velasco, desarrollador .NET y líder de equipo, apasionado principalmente por dos cosas, Xamarin y la gestión ágil de equipos.

Desde aproximadamente el año 2007, cuando comencé mi carrera (ya casi 13 años), he desarrollado principalmente en .NET, aunque nunca me ha dado miedo tocar otros lenguajes. Desde 2014 formo parte del equipo de Solid GEAR, un equipo multidisciplinar (25 personas) dedicado principalmente al desarrollo de soluciones móviles mediante un método de desarrollo ágil centrado en el usuario y con el cliente como actor central durante todo el desarrollo.

Nos podéis encontrar en solidgeargroup.com, en twitter, linkedin o directamente en mi dirección de correo electrónico.

MVVM: nos posibilita hacer aplicaciones que sean testeables.

Una aplicación, para ser testeable, necesita estar bajamente acoplada (“Bajo acomplamiento, Alta cohesión”). Esto es por que en los tests necesitamos reemplazar distintos componentes “reales” que actúan bajo sus propias directrices por otros “Mocks” que actúen en su lugar y que nos permitan establecer su comportamiento en base a nuestras necesidades de testeo.

Esto, desde el punto de vista teórico, es más o menos fácilmente entendible. Ahora bien, si lo llevamos a la práctica, encontraremos problemas típicos que necesitamos resolver y que pasan por utilizar diferentes recursos, en forma de librerías o patrones de diseño.

Yo me he encontrado estas situaciones:

  • No hago tests y me defiendo diciendo que nadie me paga para que haga tests.
  • Mi responsable o mi manager o mi cliente me obliga a tener un 100% de cobertura de código. Acabo odiando los tests.
  • No sé por qué lo hago, pero es cool y puedo decirle a los demás que testeo mis aplicaciones.
  • Sé lo que es el TDD, y aunque puedo seguirlo en mayor o menor medida, soy consciente de que los tests mejoran mi código, me hacen ser mejor desarrollador y me hacen tener que repensar los diseños que hago, algo por lo que, en mi opinión, sí que nos están pagando.

Son muchos los tipos de testings que, si buscamos, nos aparecen como posibles Esto no pretende ser una lista exhaustiva sino simplemente una lista resumida con los 3 tipos de testing que, en una aplicación, en general tiene sentido plantearse realizar.

Un repaso rápido nos resaltará la diferencia entre ellos.

Me he encontrado varias veces ya la tendencia a pensar que los UI tests son lo que molan, pero creo que no es cierto. Esta tendencia proviene de la gran publicidad que se han llevado estos últimos años estos tests. Pero yo creo que esto se debe a que, aunque no discuto que esto ha podido ser un gran avance técnico y tecnológico, estos tests suponen una buena forma de monetización de las plataformas móviles. En mi opinión, aunque estos tests pueden ser útiles en ciertas situaciones, no deberían suponer la base fundamental de nuestros tests. El enorme consumo de recursos que necesitan deberían hacer pensarnos bien cuando realmente los necesitamos.

Como veremos ahora, si nuestra aplicación está bien diseñada y, por otro lado, nuestro proceso de desarrollo está centrado en el usuario final y en el cliente, nuestro tiempo de respuesta ante un error en la manera en la que nuestros usuarios disfrutan de nuestra aplicación debería de ser, en la mayoría de casos, aceptable.

Nos queda por lo tanto hablar de tests unitarios y tests de integración. Los test unitarios nos permiten aceptar el comportamiento individual de unidades atómicas de nuestro código, como una función o una clase.

Los tests de integración van un paso más allá y lo que pretenden es aceptar el comportamiento de flujos enteros de nuestra solución. Lo que podría corresponderse, dentro del método SCRUM, con las historias de usuario, y que también podríamos identificar como las diferentes experiencias de usuario dentro de nuestra aplicación. Los tests de integración siempre deben probar la cohesión de nuestro sistema como un conjunto de componentes y siempre dividiendo nuestra arquitectura en forma de “lonchas verticales” no “lonchas horizontales”.

Por ejemplo, a la hora de aplicar tests unitarios, estos deben ayudarnos a validar funciones, clases o algoritmos individuales y muy concretos. Dos ejemplos podrían ser el formulario de introducción de una tarjeta de crédito o un formulario de recepción de un pedido.

En el primer caso quiero validar las diferentes normas que rigen los número de una tarjeta: longitud, si es Visa o Mastercard, si el número es correcto de acuerdo a su proveedor, su fecha de validez, el CVC, etc.

En el segundo caso, por ejemplo, la introducción de un DNI español correcto, un nombre y dos apellidos, una firma simple o compleja, etc.

En ambos casos se trataría de tests que cumplen los principios FIRST. Completo significa que probamos tanto los casos de éxito habituales como los casos más retorcidos o “edge cases”. Es precisamente este tipo de casos los que hacen enriquecer nuestro código.

Como hemos visto anteriormente, Los tests de integración nos permiten probar la cohesión de los componentes de nuestro sistema. Visto este como una división vertical de su arquitectura. Lo que hemos descrito antes como “lonchas verticales”.

El esquema más habitual de creación de tests de integración es de arriba a abajo: 

Dividimos los casos de prueba en base a los diferentes flujos de usuario en la app. 

  • La ventaja es que este enfoque nos permite encontrar grandes fallos de diseño de manera temprana.
  • La principal desventaja es que los módulos de bajo nivel tienden a quedar vagamente testeados

ViewModel: entidad encargada, dentro del patrón MVVM, de gestionar la lógica de presentación y la lógica de negocio

Manager: instancia única que almacena datos compartidos por varios ViewModel en memoria

Service: instancia única que nos permite acceder a servicios externos a la aplicación. Por ejemplo: conectividad, navegación o api rest

La métrica más utilizada para saber si estamos testeando correctamente nuestra aplicación suele ser el % de código cubierto por nuestros tests. En mi experiencia, esto es un error, y a mi me gusta más hablar de % de experiencias de usuario testeadas. Lo que hemos visto antes que en lenguaje SCRUM podría corresponderse con las historias de usuario, pero que puede no ser siempre así, ya que una historia de usuario podría requerir validar varias experiencias de usuario. Un ejemplo de esto sería, para la carga de un listado de hoteles, la carga en condiciones de conectividad buena por un lado, la carga en condiciones de no conectividad (usando datos en caché por ejemplo cuando o no tenemos conectividad o el servidor está caído).

A estas alturas, a poco que hayamos desarrollado alguna aplicación en Xamarin, ya estaremos bastante familiarizados con el patrón MVVM. No obstante, haré un rápido repaso para situarnos todos en el mismo punto.

MVVM plantea tres entidades:

  • ViewModel como pieza central. Contiene la lógica de presentación y la de negocio.
  • Model almacena las distintas entidades de datos
  • View contiene la declaración de la interfaz y su lógica

Lógica de presentación: cúando y cómo se actualizan los datos y reglas, derivadas del modelo, que aplican a su visualización o no.

Lógica de UI: Formato de los datos a visualizar y respuesta ante eventos de usuario.

En la versión extendida de explicación de MVVM podemos apreciar las distintas interacciones entre las clases.

El ViewModel es la pieza central que controla la lógica de negocio y de presentación: 

  • La lógica de presentación a través de las diferentes relaciones de binding, que actualizan los datos de la vista mediante la ejecución de NotifyPropertyChanged, los Commands para atender los eventos iniciados en la vista, ya sea por acciones de usuario como en las distintas etapas del ciclo de vida de la vista y las Notificaciones, algo que en principio no deberíamos tender a usar porque suponen un pequeño hack del modelo, pero que pueden ser útiles en algunas situaciones (como por ejemplo notificar de un evento en segundo plano como la pérdida de conectividad)
  • La lógica de negocio, a través de la llamada a los diferentes Servicios que nos proporcionan los datos del modelo.

Por lo tanto, y teniendo en cuenta el razonamiento expuesto al hablar de los diferentes tipos de tests, que nos descarta probar la vista a través de tests de UI (no generalizar, puede haber casos en los que sea útil), nos vamos a centrar en los tests necesarios para validar la lógica de presentación y negocio, así como los diferentes servicios que proveen de datos e información externa a nuestra lógica.

Ya entrando en los detalles de implementación de los tests, realizar tests de integración de flujo completo sobre esta parte de nuestro código supone resolver principalmente 3 puntos de interacción. Para resolverlo utilizaremos 3 soluciones de mockeado diferentes pero que en el fondo consiguen el mismo objetivo: suplantar una implementación real de un servicio por otra implementación que nos permita modificar a nuestro antojo el resultado que devuelve para validar las diferentes situaciones.

El primer escollo que debemos superar es la interacción de nuestros tests con los diferentes servicios que usa nuestra aplicación. Estos servicios proporcionan diferentes utilidades que permiten interaccionar con recursos externos a nuestra app como la conectividad del dispositivo móvil o su información interna, el sistema de ficheros o una api rest.

He desplegado una app con código de ejemplo en el siguiente enlace: https://github.com/dhompanera/monkeyconf2019

Para mockear estos servicios vamos a utilizar un recurso ya proporcionado por Xamarin.Forms y de sobra conocido por todos: DependencyService. DependencyService nos permite, en su uso habitual, desacoplar las implementaciones por plataforma (Android e iOS habitualmente) cuando accedemos a recursos del sistema (como conectividad, información del dispositivo o sistema de ficheros).

UItilizaremos DependencyService como Service Locator para registrar en él no solo los servicios de acceso a recursos del sistema, si no cualquier servicio que necesitemos mockear en nuestros tests, como un servicio rest.

Antes que eso, y mediante inyección de dependencias, necesitamos que nuestros ViewModel carguen la referencia a los servicios que utilizan a partir de la instancia del ServiceLocator que nosotros le inyectemos. Esto nos permite, cuando creemos un ViewModel en nuestros tests, inyectar la implementación mockeada de los servicios.

Además, con una implementación sencilla como la de DependencyServiceWrapper (que puedes encontrar en el fichero IDependencyService.cs) podemos abstraer esta inyección de dependencias, de modo que en la ejecución real de nuestra app no cambie practicamente nada.

El siguiente punto a resolver es la interacción de nuestros ViewModel con alguna de los métodos que Xamarin.Forms nos provee y que nos facilitan ciertas tareas. Device.BeginInvokeOnMainThread, Navigation o Application.Properties son quizá los principales ejemplos.

Nótese que el código de ejemplo en GitHub he preferido realizar una implementación adhoc de Navigation con el propósito de mostrar cómo podríamos aproximar una solución a un problema similar por nosotros mismo. Además de mostrar cómo podemos realizar un Mock de una interfaz sin usar Moq.

Para resolver este problema, el camino más corto y sencillo es utilizar la librería Xamarin.Forms.Mocks, que precisamente lo que hace es proporcionarnos Mocks para todas estas interfaces de Xamarin.Forms

Como curiosidad:

Xamarin.Forms.Mocks hace accesibles las clases e interfaces definidas como Internals [assembly: InternalsVisibleTo]

Output assembly: Xamarin.Forms.Core.UnitTests.dll

Class: MockForms

Por último, y no por ello menos importante, queda resolver la interacción con el componente de red. Es más, en mi experiencia, esta fue la clave que habilitó que el resto tuviera sentido, pues es lo que permite llevar a cabo las pruebas que realmente validaran que nuestra app responde correctamente a todo tipo de situaciones complejas.

El cliente de red más utilizado es sin duda HttpClient y para mockearlo lo que debemos hacer es inyectar la dependencia de HttpMessageHandler al inicializar HttpClient. En el ejemplo esto podemos encontrarlo en la clase RestServices, en el método SetConnection. Como vemos, si se ha proporcionado una implementación específica de HttpMessageHandler a través de DependencyServices, se utiliza en la inicialización de HttpClient.

Además deberemos sobreescribir (Setup de Moq) el método interno SendAsync, que nos permite acceder al método http y la uri de llamada, y al código y contenido de la respuesta.

Este código podemos encontrarlo en el método SetupHttpMockSend de la clase Tests.

Deja un comentario

¿Necesitas una estimación?

Calcula ahora