Probar una App de Backbone y Marionette con Jasmine y Karma

Como Aislar Pruebas Unitarias Cuando se Utiliza el Agregador de Eventos

Raul Reynoso
6 min readJul 14, 2014

Read in English

Agregación de Eventos con Marionette

Es importante que las pruebas no interaccionen. Evitar interacción hace el código de prueba más fácil de mantener y ampliar, ademas evita largas sesiones de depuración. Errores sutiles en el código de prueba pueden causar la perdida de much tiempo. Y, al fin y al cabo, mejora la comprensión de lo bien que funciona el código de producción.

Marionett.js es una biblioteca para Backbone que ayuda a desarrolladores crear aplicaciones de gran escala bien organizadas. Marionette proporciona el Agregador de Eventos para facilitar un acoplamiento más débil.

App = new Backbone.Marionette.Application(); // Crear App.vent
App.vent.on('my-event',callback); // Receptor de Evento
App.vent.trigger('my-event'); // Dispara evento

Cada objeto de la aplicación puede utilizar App.vent para disparar o recibir eventos. Los objetos no tienen que saber el uno del otro para interaccionar. Sólo necesitan saber que eventos hay que detectar y cuales parámetros serán pasados a la retrollamada.

Esto es ideal para la construcción de una aplicación débilmente acoplada, pero lo hace mas difícil probarla.

Pruebas Unitarias con el Agregador de Eventos

El Agregador de Eventos de la aplicación complica dos metas claves de las pruebas unitarias: 1) la prevención de interacciones entre las pruebas unitarias; y 2) aislar el sistema en prueba ( o SUT del inglés “System Under Test)

Varias pruebas unitarias pueden registrar retrollamadas para el mismo evento. Todas las pruebas unitarias utilizan App.vent. Así que pruebas que se ejecutan mas tarde ejecutarán también retrollamadas de las pruebas que ya se ejecutaron. Esto crea una interacción entre las pruebas unitaria que podría resultar en una situación desagradable. Por ejemplo, podría hacer que las pruebas unitarias fallan aunque el código de producción funciona correctamente.

Si usas un ejecutor de pruebas como Karma, un método común es incluir todo el código en producción y el código de prueba, luego configurar Karma para ejecutar todas las pruebas cada vez que se actualiza uno de los archivos. Así Karma te avisa inmediatamente cuando un cambio hace que las pruebas fallen. Pero, también hay que asegurar que el sistema en prueba está bastante bien asilada.

En una app que utiliza el Agregador de Eventos código externo puede interaccionar con el sistema en prueba a través de detectar eventos que dispara el sistema en prueba.

El ejemplo que sigue prueba que el sistema en prueba dispara un evento particular y pasa el objeto correcto a la retrollamada.

describe(“NonIsolatedSUTSpec”, function() {
it(“Fires an event and passes an order object ”,function(){
var returnedOrder= null;
var testedObject = new Orders();
App.vent.on('orders:new:order',function(orderObj){
returnedOrder=orderObj;
});
testedObject.order(333,20);
expect(returnedOrder).toEqual({ amount:20,
itemId:333,
price:19.99 });
});
});

Suponga que la aplicación incluye la clase Discount que detecta el evento “orders:new:order” y rebaja el precio. Discount cambia el atributo price del objecto orderObj de su valor original. Esto causará que la prueba falla o obligará tomar en cuenta el descuento en la prueba. En este último caso, la prueba unitaria no estará correctamente aislada . Sería frágil y podría fallar cuando se realizan cambios a Discount.

La solución más sencilla es desenlazar los eventos antes de ejecutar cada prueba. Luego para realizar una acción cuando se dispara un evento, incluye el receptor del evento dentro de la prueba. Asi:

describe(“IsolatedSUTSpec”, function() {
beforeEach(function(){
App.vent.unbind();
});
it(“Fires an event and passes an order object ”,function(){
var returnedOrder= null;
var testedObject = new Orders();
App.vent.on(‘orders:new:order’,function(orderObj){
returnedOrder=orderObj;
});
testedObject.order(333,20);
expect(returnedOrder).toEqual({ amount:20,
itemId:333,
price:19.99 });
});
});

El unbind evitará que el objeto Discount dispare el evento y cambie el resultado. Asi que puedes probar la clase Order de forma aislada. Esto funciona muy bien para las pruebas unitarias, pero ¿qué pasa con las pruebas de integración?

Pruebas de Integración con el Agregador de Eventos

Ahora supongamos que realmente quieres probar que Order y Discount trabajan juntos correctamente. Discount tendría que detectar el evento que dispara Order y realizar las acciones apropiadas. Así que no se puede desenlazar todos los eventos antes de cada prueba.

Esto nos presenta con dos retos:

  • Se ejecutará todos los receptores de App.vent que fueron enlazados en las pruebas
  • Se ejecutará todos los receptores de App.vent que fueron enlazados en el código de producción

El primer problema tiene una solución bastante sencilla que toma ventaja de los contextos de evento. Todos los receptores que se agregan en el código de prueba deben utilizar el contexto “test”. Antes de cada prueba se desenlaza todos los receptores en el contexto ‘test’, así:

describe(“IntegrationTestSpec”, function() {
beforeEach(function(){
App.vent.unbind(‘test’);
});
it(“Tracks the order in which orders were made”,function(){
var returnedOrder= null;
var testedObject = new Orders();
App.vent.on(‘orders:new:order’,function(orderObj){
returnedOrder = orderObj;
},’test’);
testedObject.order(333,1);
expect(returnedOrder).toEqual({ amount:20,
itemId:333,
price:19.99 });
});
});

El segundo problema es más espinoso. Imagina que la aplicación tiene los objetos Order, Discount, y Bonus. Discount cambia el precio cuando un orden nuevo se realice, y Bonus cambia la cantidad. Las dos acciones se realizan cuando se dispara el evento de orden nuevo.

Suponga que quiere probar la integración Order y Discount. Podría probar que el costo total de un pedido (precio por cantidad) sea precisa después de aplicar un descuento. Sin embargo, si Bonus ha cambiado la cantidad, la prueba fallará, incluso si el código de producción funciona correctamente. Hay un par de maneras de abordar este problema.

Crear Mocks de los Objetos que no vas a Probar

La forma más directa para evitar interacciones es crear Mocks de los objetos no sometidos a prueba. Jasmine spies (espías ) pueden hacer precisamente eso. En nuestra situación nos gustaría espiar al objeto Bonus y evitar ejecute su código.

describe(“IntegrationTestSpec”, function() {
beforeEach(function(){
App.vent.unbind(‘test’);
});
it(“Discounts the total cost of the order”,function(){
var returnedOrder= null;
spyOn(App.Bonus,”giveBonus”);
App.vent.on(‘orders:new:order’,function(orderObj){
returnedOrder = orderObj;
},’test’);
App.OrderService.order(333,1);
expect(returnedOrder.totalCost).toEqual(100);
});
});

En el código anterior accedemos a los objetos de nivel de aplicación en el espacio de nombres App. Así es como podemos evitar que el objeto Bonus agregue a la cantidad de la orden.

Este método es sencillo y funciona bien. El único inconveniente es que puede que tenga que añadir espías adicionales si otros objetos luego empiezan a detectar el evento de orden nuevo y hacer cambios a la orden.

Re-implementar la Lógica del Receptor en la Prueba

La mayoría de las aplicaciones incluirían un controlador para componer y coordinar Order, Discount and Bonus. El controlador detecta eventos e invoca los métodos correspondientes de los objetos que colaboran.

Una manera de prevenir las interacciones impredecibles es desenlazar de nuevo todos los eventos antes de cada prueba, con: App.vent.unbind();. Entonces reimplementar el receptor del controlador dentro de la prueba. Esto funciona mejor cuando el receptor es sencillo y no guarda datos de estado del sistema.

En este caso, se puede implementar un receptor que simplemente llama al método de Discount apropiada. Por ejemplo:

describe(“IntegrationTestSpec”, function() {
beforeEach(function(){
App.vent.unbind();
});
it(“Discounts the Total Cost of the order”,function(){
var returnedOrder= null;
var ordersService = new Orders();
var discountService = new Discount();
App.vent.on(‘orders:new:order’,function(orderObj){
discountService.giveDiscount(orderObj);
returnedOrder = orderObj;
},’test’);
ordersService.order(333,1);
expect(returnedOrder.totalCost).toEqual(100);
});
});

La ventaja es que la adición de nuevos objetos que detectan evento de nuevo orden no afectará a esta prueba. Hay desventajas también.

En primer lugar, esta técnica no puede ser fácilmente utilizado con los receptores más complejas. Imagina un receptor que rastrea el número de ordenes y sólo da descuentos para los tres primeros pedidos. Muy pronto usted podría volver a implementar un método bastante complejo que podría fácilmente volverse no sincronizado con la versión de producción.

En segundo lugar, puede que tenga que cambiar la prueba, cuando el receptor del controlador cambia, a pesar de que la clase del controlador no se está en prueba.

Pensamientos Adicionales

Backbone y Marrionette.js son herramientas fantásticas y cada vez más populares para desarrollo de aplicaciones JavaScript. Espero que este post ayuda a otros desarrolladores a crear pruebas eficaces para sus aplicaciones, así como pensar en estos temas me ha ayudado.

Hay sin duda muchas otras maneras de probar aplicaciones Marionette.js y mucho más que decir acerca de las técnicas descritas anteriormente. Por favor, hágame saber sus pensamientos.

--

--

Raul Reynoso

Software Engineer, Entrepreneur, Blockchain Enthusiast