Este documento se refiere a cómo hacer tests de canicas sobre el funcionamiento interno del repositorio de RxJS, y está orientado a todo el que quiera ayudar con el mantenimiento del repositorio de RxJS. Los usuarios de RxJS deberían leer la guía para hacer tests de canicas para aplicaciones en lugar de este documento. La mayor diferencia es que el comportamiento del
TestScheduler
difiere entre el uso manual y el uso de la función auxiliartestScheduler.run(callback)
.
Los "Tests de Canicas" son tests que utilizan un Planificador Virtual especializado llamado TestScheduler
. Nos permiten hacer test a operaciones asíncronas de forma síncrona y fiable. La "sintaxis de canicas" es un concepto que se ha adaptado a partir de enseñanzas y/o documentos de personas tal y como @jhusain, @headinthebox, @mattpodwysocki y @andrestaltz. De hecho, André Staltz inicialmente la recomendó como un DSL para crear tests unitarios, y desde entonces, se ha alterado y adoptado.
Los tests unitarios tienen métodos auxiliares que se han añadido para facilitar la labor de creación de tests.
hot(marbles: string, values?: object, error?: any)
- crea un Observable 'caliente' (un Sujeto) que se comportará como si ya se estuviese "ejecutando" al comenzar el test. Una diferencia interesante es que las canicas hot
permiten un carácter ^
para señalar la posición del 'frame cero'. Este es el punto en el que comienza la suscripción a los Observables a los que se realiza el test.cold(marbles: string, values?: object, error?: any)
- crea un Observable 'frío' cuya suscripción empieza al comenienzo del test.expectObservable(actual: Observable<T>).toBe(marbles: string, values?: object, error?: any)
- planifica un aserto para el momento en el que se llame a la función flush()
del TestScheduler
. El TestScheduler
llamará a esta función automáticamente al final del bloque it
de Jasmine.expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string)
- al igual que expectObservable
, planifica un aserto para el momento en el que se llame a la función flush()
del TestScheduler
. Tanto cold()
como hot()
retornan un Observable con una propiedad subscriptions
del tipo SubscriptionLog[]
. Se proporciona el parámetro subscriptions
a expectSubscriptions
para aseverar si este coincide con el diagrama de canicas subscriptionsMarbles
proporcionado en toBe()
. Los diagramas de canicas de suscripción son ligeramente diferentes a los diagramas de canicas de observables.En ambos métodos hot
y cold
, los caracteres que corresponden a valores emitidos que se especifican en los diagramas de canicas se emiten como cadenas, a no ser que se le proporcione un argumento values
al método. Por tanto:
hot('--a--b')
emitirá 'a'
y 'b'
mientras quehot('--a--b', { a: 1, b: 2 })
emitirá 1
y 2
.De la misma forma, los errores que no se especifiquen llevarán por defecto la cadena 'error'
:
hot('---#')
emitirá el error 'error'
mientras quehot('---#', null, new SpecialError('test'))
emitirá new SpecialError('test')
.La sintaxis de canicas es una cadena que representa eventos que ocurren a lo largo del 'tiempo'. El primer carácter de cualquier cadena de canicas siempre representa el 'frame cero'. Un 'frame' es, de cierta manera, análogo a un milisegundo virtual.
'-'
tiempo: Representa 10 'frames' de paso de tiempo.'|'
compleción: Representa la compleción con éxito de un Observable. Se trata del Productor del Observable señalando complete()
.'#'
error: Representa un error finalizando el Observable. Se trata del Productor del Observable señalando error()
.'a'
cualquier carácter: Todos los demás caracteres representan un valor emitido por el Productor señalando next()
.'()'
agrupaciones síncronas: cuando varios eventos tienen que ocurrir en el mismo frame de forma síncrona, se utilizan los paréntesis para agrupar dichos eventos. Se pueden agrupar valores next
, complete
o error
de esta manera. La posición de la (
inicial determina el momento en el tiempo en el que estos valores se emiten.'^'
punto de suscripción: (solo para Observables calientes) muestra el punto en el que los Observables a los que se les hace el test se suscriben al Observable caliente. Es el "frame cero" para el OBservable, cada frame anterior al ^
será negativo.'-'
o '------'
: Equivale a never()
o a un Observable que nunca emite ni llega a completarse.
|
: Equivale a Observable.empty()
.
#
: Equivale a Observable.throwError()
.
'--a--'
: Un Observable que espera 20 frames, emite un valor a
y nunca llega a completarse.
'--a--b--|'
: En el frame 20 emite a
, en el frame 50 emite b
y en el frame 80, complete
.
'--a--b--#'
: En el frame 20 emite a
, en el frame 50 emite b
y en el frame 80, error
.
'-a-^-b--|'
: Un Observable caliente, en el frame -20 emite a
, en el frame 20 emite b
y en el frame 50, complete
.
'--(abc)-|'
: En el frame 20 emite a
, b
y c
, en el frame 80, complete
.
'-----(a|)'
: En el frame 50 emite a
y complete
.
La sintaxis de las canicas de suscripción es ligeramente distinta a la sintaxis convencional de canicas. Representa los puntos de suscripción y de cancelación de suscripción que ocurren en el tiempo. No debería haber ningún otro tipo de evento representado en este tipo de diagrama.
'-'
tiempo: Representa 10 'frames' de tiempo.'^'
punto de suscripción: Muestra el punto en el tiempo en el que se realiza una suscripción.'!'
punto de cancelación de suscripción: Muestra el punto en el tiempo en el que se cancela una suscripción.En cada diagrama de canicas de suscripción debería haber como mucho un punto ^
y como mucho un punto !
. Además de estos dos caracteres, el carácter -
es el único permitido en un diagrama de canicas de suscripción.
'-'
or '------'
: No se ha realizado ninguna suscripción.
'--^--'
: Se ha realizado una suscripción después de haber pasado 20 frames de tiempo, y dicha suscripción no se ha llegado a cancelar.
'--^--!-'
: En el frame 20 se ha realizado una suscripción, que se ha cancelado en el frame 50.
Un test básico podría tener el siguiente aspecto:
const e1 = hot("----a--^--b-------c--|");
const e2 = hot("---d-^--e---------f-----|");
const expected = "---(be)----c-f-----|";
expectObservable(e1.merge(e2)).toBe(expected);
^
de un Observable hot
siempre deben estar alineados.cold
o de un Observable expected
siempre deben estar alineados entre ellos, y también con el ^
de un Observable caliente.values
cuando sea necesario.Un test de ejemplo con valores especificados:
const values = {
a: 1,
b: 2,
c: 3,
d: 4,
x: 1 + 3, // a + c
y: 2 + 4, // b + d
};
const e1 = hot("---a---b---|", values);
const e2 = hot("-----c---d---|", values);
const expected = "-----x---y---|";
expectObservable(
e1.zip(e2, function (x, y) {
return x + y;
})
).toBe(expected, values);
x: 1 + 3, // a + c
es mejor que simplemente x: 4
. El primero transmite por qué el resultado es 4, mientras que el segundo no.Un test de ejemplo con asertos de suscripción:
const x = cold( '--a---b---c--|');
const xsubs = '------^-------!';
const y = cold( '---d--e---f---|');
const ysubs = '--------------^-------------!';
const e1 = hot( '------x-------y------|', { x: x, y: y });
const expected = '--------a---b----d--e---f---|';
expectObservable(e1.switch()).toBe(expected);
expectSubscriptions(x.subscriptions).toBe(xsubs);
expectSubscriptions(y.subscriptions).toBe(ysubs);
xsubs
y ysubs
con el diagrama expected
.x
al mismo tiempo que e1
emite el valor y
.En la mayoría de los casos será innecesario comprobar los puntos de suscripción y de cancelación de suscripción, ya que o serán obvios o bien estarán implícitos en el diagrama expected
. En estos casos no es necesario realizar asertos de suscripción. En los casos en los que haya suscripciones internas u Observables fríos con varios suscriptores, los asertos de suscripción sí que resultarán útiles.
Normalmente, cada caso de test en Jasmine se escribe como it('should do something', function () { /* ... */ })
. Para indicar que se quiere generar un diagrama PNG en un caso de test concreto, se debe utilizar la función asDiagram(label)
, de la siguiente manera:
it.asDiagram('zip')('should zip by concatenating', function () {
const e1 = hot('---a---b---|');
const e2 = hot('-----c---d---|');
const expected = '-----x---y---|';
const values = { x: 'ac', y: 'bd' };
const result = e1.zip(e2, function(x, y) { return String(x) + String(y); });
expectObservable(result).toBe(expected, values);
});
Al ejecutar npm run tests2png
, este caso de test se analizará sintácticamente y se generará un fichero PNG zip.png
(el nombre del fichero viene determinado por ${operatorLabel}.png
) en la carpeta img/
.