Diagramas de Marble para testear operadores RxJs

Adrián Martínez, Software Engineer en EDICOM, nos explica cómo poder testear Observables RxJs usando los diagramas de Marble. Descubre un nuevo enfoque para verificar el comportamiento de la programación reactiva mediante una representación gráfica de la funcionalidad esperada.

    Artículo elaborado por:

    Adrián Martínez

    Software Engineer

    Desarrollador full-stack apasionado del mundo de la programación. Gran interés en Desarrollo de aplicaciones, Material Design y Machine Learning.

Introducción

La programación reactiva es uno de los muchos paradigmas de programación que existen en la actualidad. Está enfocado al manejo de flujos de datos, ya sean finitos o infinitos, de manera asíncrona. Estos datos se propagan en la aplicación produciendo cambios en la ella y nuestro código reacciona ante estos cambios ejecutando una serie de eventos de manera asíncrona. Este paradigma está muy relacionado con el patrón de diseño de software observador.

Una de las tecnologías que usamos en EDICOM para nuestras aplicaciones es Angular, framework para desarrollar aplicaciones SPA (Single Page Application) usando HTML y TypeScript. Este framework hace uso de la librería RxJs que implementa el patrón observador y proporciona una serie de operadores para trabajar con los flujos de datos de una manera más declarativa.

En este artículo, se va a explicar en qué consiste el patrón observador, se hablará de la librería RxJS y de una representación visual para entender su funcionamiento, los diagramas de Marble. A continuación, se expondrá cómo desde EDICOM sacamos el máximo provecho a esta librería creando operadores personalizados y realizaremos testeos sobre ellos.

Patrón observador

El patrón observador es un patrón de diseño de software de la categoría de patrones de comportamiento donde se establece una relación de 1 a n de forma que cuando el objeto cambia (llamado sujeto o notificador), todos los objetos que dependen de él (llamados suscriptores) se

actualicen automáticamente. Esta dependencia se llama suscripción.

El objeto notificador cuenta con un mecanismo para que los suscriptores puedan realizar o cancelar la suscripción. Cuando ocurre un evento, el notificador recorre la lista de suscriptores actuales y ejecuta el método de notificación que se indicó en el momento de la suscripción.

RxJS

RxJS (Reactive Extensions for JavaScript) es una librería que realiza una implementación del patrón observador para JavaScript/TypeScript y proporciona una serie de tipos y operadores para trabajar más cómodamente.

Lo realmente potente de esta librería son sus operadores, ya que permiten usar código asíncrono complejo, pero usando un paradigma de programación declarativo. En este paradigma el programador se centra más en el “qué” que en el “cómo”. Es decir, nos centramos en lo que queremos que el código realice, pero sin decirle los pasos exactos que debe seguir. Las instrucciones que debe realizar vienen ya correctamente programadas en la librería.

Al tratarse de código reactivo asíncrono, muchos de los operadores aportados por la librería están relacionados con el tiempo, por lo que tratar de explicarlos mediante una descripción textual puede no ser siempre suficiente. Es por ello que surgieron los diagramas de Marble, que explicamos a continuación.

¿Qué son los diagramas de Marble?

Los diagramas de Marble son una representación visual de cómo funciona el operador. Se presenta de manera visual la entrada o las entradas al operador y la salida que se espera, pero haciendo especial hincapié en el tiempo, es decir, en cuándo esa entrada ocurre y cuándo esa salida es emitida.

Estos diagramas pueden ser representados en ASCII con los siguientes caracteres:

  • ‘ ‘: es ignorado, se usa para alinear los distintos diagramas en un ejemplo.
  • ‘-‘: representa el paso de una unidad virtual de tiempo llamada ‘frame’. Por defecto un frame corresponde a 1ms.
  • [a-z0-9]: valor alfanumérico usado para representar un valor emitido.
  • [0-9]+[ms|s|m]: permite representar el paso de un número determinado de frames sin tener que poner n ‘-‘.
  • ‘|’: simboliza el finalizado satisfactorio de un observable.
  • ‘#’: señala un error en un observable.
  • ‘()’: grupo síncrono, usado cuando múltiples eventos ocurren de manera síncrona en el mismo frame. El frame donde se ubica el símbolo ‘(‘ es donde se emitirá el grupo. Una vez se emite el grupo, pasan tantos frames como longitud del grupo, es decir, cuando el grupo “(abc)” se emita, pasarán 5 frames hasta procesar el siguiente carácter. Esto es así para ayudar a alinear verticalmente los distintos diagramas.
  • ‘^’: indica el punto en el que el observable a ser testeado es suscrito al observable. Este símbolo solo se usa en observables ‘hot’, concepto que se explicará más adelante.
  • ‘!’: punto en el que se cancela la suscripción.

Sin embargo, por motivos estéticos no se suele usar la representación en ASCII si no ilustraciones basadas en las mismas.

Diagramas de Marble para testear operadores RxJs

Observables cold y hot
Según cómo se produzcan los datos que emite un observable, se puede catalogar en:

  • Cold: cuando el productor es creado y activado en el momento de realizar la suscripción. Por ejemplo, una llamada al http en Angular se realiza mediante observables para poder reaccionar cuando se obtiene respuesta, pero no es hasta el momento de la suscripción cuando se realiza la propia petición.
  • Hot: lo son aquellos observables cuyo productor al que están asociados es creado e inicializado fuera de la suscripción. Al realizarlo de esta manera el mismo valor puede ser emitido a múltiples observers. Esto se consigue mediante un Subject para hacer multicast del valor generado. Lo sería por ejemplo el valor de un formulario, cuando dicho valor cambio, todos sus suscriptores son notificados al instante y con el mismo valor.

Operadores personalizados y cómo testearlos

Un aspecto muy potente de la librería RxJs es que permite extender los operadores que aportan con los que se necesiten en casos de usos más concretos. De esta forma, podemos crear un operador adicional y usarlo en distintas partes de nuestras aplicaciones siempre que queramos resolver la misma problemática, lo que también mejora la legibilidad de nuestro código. Además, al tratarse de un nuevo operador RxJs, podemos usar la propia librería RxJs para realizar testeos gracias a los diagramas de Marble.

Para crear un operador personalizado usaremos el método estático ‘pipe’ pasándole la serie de acciones que realiza el operador.

Una vez tengamos el operador creado, usaremos el TestScheduler de la paquetería rxjs/testing para validar el comportamiento del nuevo operador mediante un diagrama. Esto nos permite ejecutar operadores RxJs en un contexto donde el scheduler que se usa es remplazado por uno en el que se simula el paso del tiempo con lo que se llama ‘tiempo virtual’. Nos aporta además una serie de funciones helpers para escribir los testeos.

Las funciones que se nos ofrecen son:

  • cold y hot: para crear observables de los distintos tipos
  • expectObservable: planifica la comprobación de que el observable indicado sea igual al diagrama de Marble proporcionado.
  • expectSubscriptions: similar, pero para verificar cuando ocurre una suscripción o se cancela la misma.
  • flush: fuerza el inicio del ‘tiempo virtual’. En caso de no llamarse, se invoca automáticamente.
  • time: crea un observable que solo simula el paso del tiempo indicado.

Los valores emitidos en los diagramas se indican con [a-z0-9] sin embargo, suele hacer falta indicar un mapeo entre esos identificadores y el valor real a ser usado para el test. Esto lo veremos en más profundidad en los ejemplos.

Cabe destacar que el scheduler de testeo es totalmente agnóstico del framework de testeo que se use, por lo que cuando se instancia hay que indicarle cómo validar que dos objetos sean iguales. En EDICOM usamos Jasmine y su configuración sería:

Diagramas de Marble para testear operadores RxJs

Llegados a este punto del artículo, pasaremos a explicar uno de los casos de uso de programación reactiva que tenemos en EDICOM. Primero explicaremos el problema a resolver, se creará un operador RxJs que lo resuelva y validaremos su comportamiento con varios testeos usando el diagrama de Marble.

Caso de uso: búsqueda con retardo

En muchas aplicaciones tenemos un listado de entidades que el usuario puede consultar (como facturas, mensajes, artículos etc.) donde se cuenta además con una caja de búsqueda para realizar búsquedas rápidas mediante un texto libre introducido por el usuario. Cuando se introduce dicho texto, la aplicación web realizará una petición al servidor y devolverá el listado de entidades que cumple con la búsqueda.

Los requisitos que debe cumplir son:

  • Inicialmente se mostrarán todas las entidades
  • Cuando el usuario haya parado de escribir se realizará la búsqueda. Esto, con la velocidad de escritura de un usuario medio, suele ser cuando hayan transcurrido 500ms desde la última vez que se modificó el texto.
  • Si la búsqueda a ser realizada es la misma que la última efectuada se omitirá la llamada.
  • Si se modifica la caja de búsqueda mientras hay una petición en marcha se deberá cancelar y realizar la nueva petición.
  • Cuando se realice la llamada se debe dar un feedback visual de que hay una petición en marcha.
  • Si se produce un error en el servidor, se asumirá que no hay elementos que cumplan con la petición realizada.

Visto desde un enfoque reactivo, el problema se puede explicar de la siguiente forma:

  • Contamos con un flujo de datos, el texto que introduce el usuario.
  • La aplicación debe reaccionar ante este flujo realizando peticiones al servidor siguiendo los requisitos explicados anteriormente.
  • Las peticiones que se mandan al server son en sí otro flujo de datos, conectado con el primero. La aplicación debe reaccionar ante ellos y mostrar los resultados al usuario.

Código

En primer lugar, creamos un servicio que simula la llamada a realizar al servidor. Gracias al operador ‘delay’ se simula que el servidor tarda 2s en contestar.

Diagramas de Marble para testear operadores RxJs

Para programar lo descrito anteriormente se ha procedido de la siguiente forma. En el controlador:

Diagramas de Marble para testear operadores RxJs
  • Creamos un FormControl llamado ‘searchControl’ para obtener la entrada del usuario de una manera reactiva.
  • Creamos ‘filteredList$’ el flujo de datos a ser mostrados al usuario a partir de su entrada. Esto lo hacemos con el operador personalizado ‘delayedSearch’ que explicamos más adelante.

En la vista:

Diagramas de Marble para testear operadores RxJs
  • Contamos con un input al que enlazamos el ‘searchControl’.
  • Escuchamos con el pipe ‘async’ los cambios en ‘filteredList$’ para iterar sobre la lista y mostrar el resultado.
  • Si el resultado de ‘filteredList$’ no está definido es porque la llamada está en curso, y en ese caso se le indicará al usuario.

Y por último, creamos el operador para obtener el flujo de datos a mostrar al usuario. Este operador necesita ser configurado con el valor inicial de la búsqueda y la función a invocar para realizar la búsqueda.

Diagramas de Marble para testear operadores RxJs

Este operador lo creamos concatenando varios operadores de RxJs ya existentes:

  • ‘debounceTime(500)’: emite el último valor emitido por la fuente cuando hayan pasado 500ms sin nuevas emisiones. De esta forma esperamos a que el usuario haya acabado de escribir.
  • ‘startWith’: sirve para emitir un valor nada más haya una suscripción, aunque la fuente aún no haya emitido nada. Gracias a esto podremos mostrar el listado inicial sin que el usuario efectúe ninguna búsqueda. Es importante poner este operador después del ‘debounceTime’ para no introducir un retardo inicial innecesario de 500ms.
  • ‘distinctUntilChanged’: emite el valor recibido siempre y cuando sea distinto al último emitido. De esta forma evitaremos realizar 2 veces la misma llamada al servidor. Destacar que este operador se debe poner también después del ‘debounceTime’ para que la comprobación la haga con el valor después de aplicar el retardo.
  • ‘switchMap’: gracias a este operador (formado por los operadores ‘map’ y ‘switch’) podremos realizar un mapeo entre el texto a buscar y el observable que se resuelve con la lista a mostrar, seguidamente se suscribe al nuevo operador y emitirá dicho valor. Además, si llega un nuevo valor sin que el previo se haya emitido, se cancelará la suscripción previa y se iniciará la nueva.

Esto lo usaremos en conjunción con el operador ‘concat’, operador que permite concatenar una serie de flujo de datos de manera ordenada conforme los observables se vayan completando.

  • ‘of(undefined)’: observable que se resuelve inmediatamente con un valor no definido y se completa para dar paso al siguiente. Este valor sirve para saber cuándo se inicia una petición al servidor.
  • fn con ‘catchError’: función pasada como parámetro para realizar la llamada al servidor. Se le añade el operador ‘catchError’ para que ante un error se siga con el flujo normal, pero suponiendo que se haya devuelto una lista vacía.

Si ejecutamos la aplicación podemos ver cómo funciona todo en conjunción.

Testeo: Configuración

Por último, pasaremos a la fase de testeo para verificar el comportamiento del operador creado. Primero creamos una serie de variables que usaremos en todos los testeos.

Diagramas de Marble para testear operadores RxJs

Creamos las variables ‘LIST’ y ‘LIST_FILTERED_BY_APP’ con la respuesta ficticia del servidor.
Y creamos también las variables ‘SOURCE_VALUES’ y ‘EXPECTED_VALUES’ donde se encuentran los valores reales a ser usados en los diagramas que realizaremos. Este mapeo lo pasaremos al crear los observables para que se emitan los valores reales.

A continuación, configuramos el TestScheduler:

Diagramas de Marble para testear operadores RxJs

Es importante realizar esto en el beforeEach para contar con una instancia distinta del scheduler para cada test.

Test 1: Valor inicial y búsqueda con retardo

Diagramas de Marble para testear operadores RxJs
Diagramas de Marble para testear operadores RxJs

En este test se simula lo siguiente:

  • Primero transcurre 1s y luego se emite ‘a’.
  • Después se emiten los valores ‘ap’ y ‘app’ cada 50ms, lo que simula la escritura del usuario.

El resultado esperado es:

  • Al instante se espera que se emita un valor no definido.
  • 200ms segundos después se obtiene la respuesta del servidor y transcurren otros 800ms.
  • Una vez hayan pasado 500ms desde que el flujo de datos de entrada haya dejado de cambiar se volverá a emitir un valor no definido.
  • Y en 600ms se tendrá la respuesta del servidor.

* NOTA: el uso de, por ejemplo, 199ms en lugar de 200ms es debido a emitir ‘u’ tarda 1ms por lo que la suma da los 200ms esperados.

Para el test se han usado:

  • El método hot para el flujo de datos principal, este observable se tiene que declarar de esta forma debido a que el productor de datos es externo a la suscripción.
  • El método cold para los observables que están conectados con el servidor. Estos deben de ser así ya que hasta que no ocurra la suscripción no se instancia el productor de datos. Estos observables se indican en el ‘returnValues’ de la ‘searchFn’ para que se vayan devolviendo según nuestro operador vaya invocando a la función.

Cabe destacar también la llamada manual realizado al método ‘flush’ para forzar en ese punto la ejecución de los observables y poder realizar a continuación comprobaciones adicionales.

En este test se puede comprobar como los operadores ‘startWith’ y ‘debounceTime’ están correctamente configurados.

Test 2: Cancelar peticiones previas ante nuevos datos

Diagramas de Marble para testear operadores RxJs
Diagramas de Marble para testear operadores RxJs

Para el siguiente test contamos con el siguiente escenario:

  • Primero transcurren 99ms
  • En el frame del tiempo 100ms se emite ‘app’, evento que ocurre antes de que se resuelva la primera petición que se hace al servidor.

El resultado que se espera es:

  • Primero se emite el valor no definido y transcurren los primeros 99ms.
  • Seguidamente transcurren los 500ms del tiempo de retardo.
  • Por último, se emite el valor no definido de nuevo y a los 600ms se emite la respuesta del servidor.

* NOTA: Para este ejemplo y los siguientes no simulamos las emisiones letra a letra ya que no son relevante.

Destacamos en este testeo que se ha aumentado el tiempo que tarda el servidor en contestar para simular el cancelado.

En este segundo test se verifica que el operador ‘switchMap’ en conjunción con el ‘concat’ funcionen como esperamos.

Test 3: No realizar la llamada de nuevo si se va a buscar por lo mismo

Diagramas de Marble para testear operadores RxJs
Diagramas de Marble para testear operadores RxJs

Para este tercer test planteamos este escenario:

  • Primero transcurre 1s hasta que el flujo de entrada emita ‘app’.
  • 1s y 100ms después se emite el valor ‘appl’ pero antes de que transcurran los 500ms se vuelve a emitir ‘app’.

En este caso, lo que se espera es:

  • Se emite de nuevo el valor no definido, a los 200ms la lista con los datos y transcurren otros 800ms.
  • Después transcurren los 500ms y se emite el valor no definido y, por último, a los 600ms, se emite la lista con los valores.

Cabe remarcar que en ya no se emite más valores, si se quitase el operador ‘distinctUntilChanged’ sí que veríamos como se produce de nuevo la misma llamada ya realizada. Por lo que en este test validamos que funcione como esperamos.

Test 4: Se debe recuperar ante errores

Diagramas de Marble para testear operadores RxJs
Diagramas de Marble para testear operadores RxJs

Para el último testeo que vamos a explicar planteamos la situación:

  • Primero transcurren 999ms.
  • Seguidamente se emite el valor ‘1’, valor con el que vamos a simular que el servidor falla.

El comportamiento esperado es:

  • En el primer segundo se emite el valor no definido y la lista respuesta del servidor.
  • Seguidamente transcurren 500ms y se emite de nuevo el valor no definido.
  • A los 300ms obtendremos un listado de datos vacío.

En este test verificamos que el operador ‘catchError’ esté correctamente configurado ya que ante errores devuelve un listado vacío.

Destacamos el carácter ‘#’ en el observable para simular el error.

El código se puede consultar en nuestro GitLab.

Conclusiones

A lo largo de este artículo hemos hablado de la programación reactiva de la mano de la librería RxJs y de cómo la estamos usando en EDICOM para nuestras aplicaciones web que usan Angular. Nos centrado sobre todo en el aspecto de cómo testear las transformaciones de los flujos de datos que son necesarias.

Se ha dado a conocer un enfoque distinto para poder testear los observables de una manera más visual gracias a una representación de su comportamiento con los diagramas de Marble.