Pruebas de Ember.js
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
Cuando comencé a jugar con Ember.js hace casi un año, la historia de las comprobaciones dejaba algo que desear. Podría probar un objeto en una unidad sin ningún problema, pero una prueba unitaria es solo una forma de obtener retroalimentación cuando crea un producto de software. Además de las pruebas unitarias, quería una forma de verificar la integración de múltiples componentes. Entonces, como la mayoría de las personas que prueban aplicaciones ricas de JavaScript, busqué a la madre de todas las herramientas de prueba, Selenium.
Ahora, antes de golpearlo, sin una introducción adecuada, vale la pena mencionar que Selenium es una excelente manera de verificar que toda su aplicación web funciona con una base de datos completa de producción y todas sus dependencias de producción, etc. Y desde una perspectiva de control de calidad, esta herramienta puede ser un gran recurso para equipos que necesitan pruebas de aceptación de IU de extremo a extremo.
Pero con el tiempo, un conjunto de pruebas aparentemente pequeño basado en Selenium puede comenzar a arrastrar la velocidad de su equipo a un ritmo de caracoles. Una forma fácil de reducir este dolor es evitar la creación de una aplicación grande en primer lugar. Si construyes un puñado de aplicaciones web más pequeñas en su lugar, podría ayudarte a mantenerte a flote por un poco más de tiempo porque ninguna compilación individual aplastará al equipo, a medida que creces.
Pero incluso en un proyecto pequeño, el problema real con Selenium es que no es parte del proceso de desarrollo basado en pruebas. Cuando estoy haciendo rojo / verde / refactor no tengo tiempo para comentarios lentos en ninguna forma. Necesitaba una forma de escribir pruebas tanto de unidad como de integración que me proporcionaran retroalimentación rápida para ayudarme a configurar el software que estaba escribiendo de una manera más iterativa. Si está utilizando una versión de Ember.js> = RC3, tiene suerte porque escribir una prueba de unidad o de integración es un paso en la parte.
Instalación del corredor de prueba
Ahora que podemos escribir pruebas de JavaScript para nuestra aplicación, ¿cómo las ejecutamos? La mayoría de los desarrolladores comienzan a usar el navegador directamente, pero como quería algo que pudiera ejecutar desde la línea de comandos en un entorno de CI con un rico ecosistema lleno de complementos, miré Karma.
Lo que me gustó de Karma es que solo quiere ser tu corredor de prueba. No importa qué marco de prueba de JavaScript utilice o qué marco de MVC del lado del cliente utilice. Es sencillo comenzar y escribir pruebas que se ejecutan en la aplicación de producción Ember.js es solo unas pocas líneas de configuración.
Pero antes de que podamos configurar Karma, necesitamos instalarlo usando npm. Recomiendo instalarlo localmente para que pueda mantener sus módulos npm aislados por proyecto. Para hacer esto, agregue un archivo llamado package.json 'a la raíz de su proyecto que se parece a lo siguiente.
1 |
|
2 |
{
|
3 |
"dependencies": { |
4 |
"karma-qunit": "*", |
5 |
"karma": "0.10.2" |
6 |
}
|
7 |
}
|
Este ejemplo requerirá Karma y un complemento para QUnit. Después de guardar el archivo package.json anterior, vuelva a la línea de comandos y escriba npm install para desplegar los módulos de Node requeridos.
Después de que se complete la instalación de npm, ahora verá una nueva carpeta con el nombre node_modules en la raíz de su proyecto. Esta carpeta contiene todo el código JavaScript que acabamos de bajar con npm, incluidos Karma y el complemento QUnit. Si profundizas aún más en node_modules/karma/bin/ verás el ejecutable Karma. Usaremos esto para configurar el corredor de prueba, ejecutar pruebas desde la línea de comandos, etc.
Configurar el corredor de prueba
Luego necesitamos configurar el karma para que sepa cómo ejecutar las pruebas QUnit. Escriba karma init desde la raíz del proyecto. Se le pedirá una lista de preguntas. El primero preguntará qué marco de prueba desea usar, presione Tab hasta que vea qunit, luego presione Enter. Luego responda no a la pregunta Require.js, ya que no la usaremos para esta aplicación de muestra. Presione Tab hasta que vea PhantomJS para la tercera pregunta y tendrá que presionar Enter dos veces, ya que permite múltiples opciones aquí. En cuanto al resto, simplemente déjalos en su opción por defecto.
Cuando hayas terminado, deberías ver que Karma ha generado un archivo de configuración llamado karma.conf.js en la raíz o en tu proyecto. Si desea leer más sobre las diversas opciones que admite Karma, puede encontrar los comentarios útiles. Por el bien de este ejemplo, tengo una versión simplificada del archivo de configuración para mantener las cosas para principiantes.
Si desea seguir adelante, elimine el archivo de configuración generado y reemplácelo con este.
1 |
|
2 |
module.exports = function(karma) { |
3 |
karma.set({ |
4 |
basePath: 'js', |
5 |
|
6 |
files: [ |
7 |
"vendor/jquery/jquery.min.js", |
8 |
"vendor/handlebars/handlebars.js", |
9 |
"vendor/ember/ember.js", |
10 |
"vendor/jquery-mockjax/jquery.mockjax.js", |
11 |
"app.js", |
12 |
"tests/*.js" |
13 |
],
|
14 |
|
15 |
logLevel: karma.LOG_ERROR, |
16 |
browsers: ['PhantomJS'], |
17 |
singleRun: true, |
18 |
autoWatch: false, |
19 |
|
20 |
frameworks: ["qunit"] |
21 |
});
|
22 |
};
|
Esto debería ser bastante similar a lo que generó Karma anteriormente, acabo de eliminar todos los comentarios y eliminar algunas opciones que no nos interesan en este momento. Para escribir la primera prueba de unidad, tuve que contarle un poco más a Karma sobre la estructura del proyecto.
En la parte superior del archivo de configuración, verá que he establecido la basePath en js porque todos los activos de JavaScript se encuentran en esta carpeta en el proyecto. A continuación, le dije a Karma dónde puede encontrar los archivos JavaScript necesarios para probar nuestra sencilla aplicación. Esto incluye jQuery, Handlebars, Ember.js y el propio archivo app.js.
Escribiendo la primera Unit Test
Ahora podemos agregar el primer archivo de prueba de unidad al proyecto. Primero haga una nueva carpeta llamada tests y anídelo bajo la carpeta js. Agregue un archivo en este nuevo directorio llamado unit_tests.js que se parece a esto.
1 |
|
2 |
test('hello world', function() { |
3 |
equal(1, 1, ""); |
4 |
});
|
Esta prueba no está haciendo nada valioso todavía, pero nos ayudará a verificar que tenemos todo conectado con Karma para ejecutarlo correctamente. Note que en la sección files Karma, ya agregamos el directorio js/tests. De esta manera, Karma obtendrá todos los archivos JavaScript que usamos para probar nuestra aplicación, en el futuro.
Ahora que tenemos Karma configurado correctamente, ejecute las pruebas qunit desde la línea de comandos usando ./node_modules/karma/bin/karma start.
Si tiene todo configurado correctamente, debería ver Karma ejecutar una prueba y tener éxito. Para verificar que ejecutó la prueba que acabamos de escribir, haga que falle modificando la instrucción igual. Por ejemplo, podrías hacer lo siguiente:
1 |
|
2 |
test('hello world', function() { |
3 |
equal(1, 2, "boom"); |
4 |
});
|
Si puede fallar esto y hacerlo pasar de nuevo, es hora de escribir una prueba con un poco más de propósito.
La aplicación de muestra
Pero antes de comenzar, analicemos la aplicación de ejemplo utilizada en esta publicación. En la siguiente captura de pantalla, ves que tenemos una grilla de usuarios muy simple. En la tabla HTML, se muestra a cada usuario por su nombre junto con un botón para eliminar a ese usuario. En la parte superior de la aplicación, verá una entrada para el nombre, el apellido y finalmente un botón que agregará otro usuario a la tabla cuando se haga clic.
https://dl.dropboxusercontent.com/u/716525/content/images/2013/pre-tuts.png
La aplicación de ejemplo tiene tres problemas. Primero, queremos mostrar el nombre y apellido del usuario, no solo el nombre. A continuación, al hacer clic en un botón de eliminar, en realidad no eliminará al usuario. Y finalmente, cuando agrega un nombre, un apellido y hace clic en agregar, no pondrá a otro usuario en la tabla.
En la superficie, el cambio de nombre completo parece ser el más simple. También resultó ser un gran ejemplo que muestra cuándo debes escribir una prueba de unidad, una prueba de integración o ambas. En este ejemplo, la forma más rápida de obtener retroalimentación es escribir una prueba unitaria simple que afirme que el modelo tiene una propiedad computada fullName.
Unidad de prueba de la propiedad calculada
La prueba unitaria de un objeto de ember es fácil, simplemente cree una nueva instancia del objeto y solicite el valor de fullName.
1 |
|
2 |
test('fullName property returns both first and last', function() { |
3 |
var person = App.Person.create({firstName: 'toran', lastName: 'billups'}); |
4 |
var result = person.get('fullName'); |
5 |
equal(result, 'toran billups', "fullName was " + result); |
6 |
});
|
A continuación, si regresa a la línea de comandos y ejecuta ./node_modules/karma/bin/karma start, debería mostrar una prueba fallida con un mensaje útil que describa fullName como indefinido actualmente. Para solucionar esto, necesitamos abrir el archivo app.js y agregar una propiedad computada al modelo que devuelve una cadena de los valores de nombre y apellido combinados.
1 |
|
2 |
App.Person = Ember.Object.extend({ |
3 |
firstName: '', |
4 |
lastName: '', |
5 |
fullName: function() { |
6 |
var firstName = this.get('firstName'); |
7 |
var lastName = this.get('lastName'); |
8 |
return firstName + ' ' + lastName; |
9 |
}.property() |
10 |
});
|
Si vuelves a la línea de comandos y ejecutas ./node_modules/karma/bin/karma start deberías ver una prueba de unidad de aprobación. Puede extender este ejemplo escribiendo algunas otras pruebas de unidad para mostrar que la propiedad calculada debe cambiar cuando se actualice el nombre o el apellido en el modelo.
1 |
|
2 |
test('fullName property returns both first and last', function() { |
3 |
var person = App.Person.create({firstName: 'toran', lastName: 'billups'}); |
4 |
var result = person.get('fullName'); |
5 |
equal(result, 'toran billups', "fullName was " + result); |
6 |
});
|
7 |
|
8 |
test('fullName property updates when firstName is changed', function() { |
9 |
var person = App.Person.create({firstName: 'toran', lastName: 'billups'}); |
10 |
var result = person.get('fullName'); |
11 |
equal(result, 'toran billups', "fullName was " + result); |
12 |
person.set('firstName', 'wat'); |
13 |
result = person.get('fullName'); |
14 |
equal(result, 'wat billups', "fullName was " + result); |
15 |
});
|
16 |
|
17 |
test('fullName property updates when lastName is changed', function() { |
18 |
var person = App.Person.create({firstName: 'toran', lastName: 'billups'}); |
19 |
var result = person.get('fullName'); |
20 |
equal(result, 'toran billups', "fullName was " + result); |
21 |
person.set('lastName', 'tbozz'); |
22 |
result = person.get('fullName'); |
23 |
equal(result, 'toran tbozz', "fullName was " + result); |
24 |
});
|
Si agrega estas dos pruebas adicionales y ejecuta las tres desde la línea de comandos, debe tener dos errores. Para que las tres pruebas pasen, modifique la propiedad calculada para escuchar los cambios tanto en el nombre como en el apellido. Ahora, si ejecuta ./node_modules/karma/bin/karma start desde la línea de comandos, debe realizar tres pruebas de aprobación.
1 |
|
2 |
App.Person = Ember.Object.extend({ |
3 |
firstName: '', |
4 |
lastName: '', |
5 |
fullName: function() { |
6 |
var firstName = this.get('firstName'); |
7 |
var lastName = this.get('lastName'); |
8 |
return firstName + ' ' + lastName; |
9 |
}.property('firstName', 'lastName') |
10 |
});
|
Agregue el preprocesador Karma-Ember y configúrelo
Ahora que tenemos una propiedad calculada en el modelo, debemos observar la plantilla en sí misma porque actualmente no usamos la nueva propiedad fullName. En el pasado, necesitarías conectar todo por ti mismo o usar Selenium para verificar que la plantilla se procesa correctamente. Pero con la prueba de ember, ahora puede realizar una prueba de integración agregando algunas líneas de JavaScript y un complemento para Karma.
Primero abra el archivo package.json y agregue la dependencia karma-ember-preprocessor. Después de actualizar el archivo package.json, realice npm install desde la línea de comandos para desplegar esto.
1 |
|
2 |
{
|
3 |
"dependencies": { |
4 |
"karma-ember-preprocessor": "*", |
5 |
"karma-qunit": "*", |
6 |
"karma": "0.10.2" |
7 |
}
|
8 |
}
|
Ahora que tiene el preprocesador instalado, necesitamos que Karma esté al tanto de los archivos de plantilla. En la sección files de su archivo karma.conf.js agregue lo siguiente para informarle a Karma acerca de las plantillas de manillares.
1 |
|
2 |
module.exports = function(karma) { |
3 |
karma.set({ |
4 |
basePath: 'js', |
5 |
|
6 |
files: [ |
7 |
"vendor/jquery/jquery.min.js", |
8 |
"vendor/handlebars/handlebars.js", |
9 |
"vendor/ember/ember.js", |
10 |
"vendor/jquery-mockjax/jquery.mockjax.js", |
11 |
"app.js", |
12 |
"tests/*.js", |
13 |
"templates/*.handlebars" |
14 |
],
|
15 |
|
16 |
logLevel: karma.LOG_ERROR, |
17 |
browsers: ['PhantomJS'], |
18 |
singleRun: true, |
19 |
autoWatch: false, |
20 |
|
21 |
frameworks: ["qunit"] |
22 |
});
|
23 |
};
|
Luego debemos decirle a Karma qué hacer con estos archivos de manubrios, porque técnicamente queremos que cada plantilla se compile previamente antes de que se la entregue a PhantomJS. Agregue la configuración del preprocesador y señale cualquier cosa con una extensión de archivo de *.handlebars en el preprocesador de ember. También debe agregar la configuración de los complementos para registrar el preprocesador de ember (junto con algunos otros que normalmente se incluyen con la configuración predeterminada de Karma).
1 |
|
2 |
module.exports = function(karma) { |
3 |
karma.set({ |
4 |
basePath: 'js', |
5 |
|
6 |
files: [ |
7 |
"vendor/jquery/jquery.min.js", |
8 |
"vendor/handlebars/handlebars.js", |
9 |
"vendor/ember/ember.js", |
10 |
"vendor/jquery-mockjax/jquery.mockjax.js", |
11 |
"app.js", |
12 |
"tests/*.js", |
13 |
"templates/*.handlebars" |
14 |
],
|
15 |
|
16 |
logLevel: karma.LOG_ERROR, |
17 |
browsers: ['PhantomJS'], |
18 |
singleRun: true, |
19 |
autoWatch: false, |
20 |
|
21 |
frameworks: ["qunit"], |
22 |
|
23 |
plugins: [ |
24 |
'karma-qunit', |
25 |
'karma-chrome-launcher', |
26 |
'karma-ember-preprocessor', |
27 |
'karma-phantomjs-launcher' |
28 |
],
|
29 |
|
30 |
preprocessors: { |
31 |
"**/*.handlebars": 'ember' |
32 |
}
|
33 |
});
|
34 |
};
|
Prueba de integración de la plantilla enlazada a datos
Ahora que tenemos la configuración de configuración de Karma para las pruebas de integración, agregue un nuevo archivo llamado integration_tests.js en la carpeta tests. Dentro de esta carpeta, debemos agregar una prueba simple para demostrar que podemos hacer frente a toda la aplicación Ember.js sin error. Agregue una prueba de qunit simple para ver si podemos alcanzar la ruta '/' y obtener el HTML básico devuelto. Para la prueba inicial, solo estamos afirmando que la etiqueta table existe en el HTML que se generó.
1 |
|
2 |
test('hello world', function() { |
3 |
App.reset(); |
4 |
visit("/").then(function() { |
5 |
ok(exists("table")); |
6 |
});
|
7 |
});
|
Tenga en cuenta que estamos utilizando algunos ayudantes que están integrados en las pruebas de ember, como visit y find. El asistente visit es una forma amigable de brasas de decirle a la aplicación en qué estado se encuentra durante la ejecución. Esta prueba comienza en la ruta '/' porque ahí es donde los modelos de People se unen a la plantilla y se genera nuestra tabla HTML. El ayudante find es una forma rápida de buscar elementos en el DOM usando selectores de CSS como lo haría con jQuery para verificar algo sobre el marcado.
Antes de que podamos ejecutar esta prueba, necesitamos agregar un archivo auxiliar de prueba que inyectará a los asistentes de prueba y establecerá un elemento raíz genérico. Agregue el código a continuación, a un archivo llamado integration_test_helper.js en el mismo directorio tests. Esto asegurará que nuestra aplicación tenga los ayudantes de prueba en el momento de la ejecución.
1 |
|
2 |
document.write('<div id="ember-testing-container"><div id="ember-testing"></div></div>'); |
3 |
|
4 |
App.rootElement = '#ember-testing'; |
5 |
App.setupForTesting(); |
6 |
App.injectTestHelpers(); |
7 |
|
8 |
function exists(selector) { |
9 |
return !!find(selector).length; |
10 |
}
|
Ahora desde la línea de comandos debería poder ejecutar la prueba de integración anterior. Si obtuvo una prueba de aprobación, elimine la tabla de la plantilla de manillares para que falle (solo para ayudar a demostrar que Ember estaba generando el HTML utilizando esa plantilla).
Ahora que tenemos la configuración de las pruebas de integración, es hora de escribir la que afirma que mostramos el fullName de cada usuario en lugar de su firstName. Primero queremos afirmar que obtenemos dos filas, una para cada persona.
1 |
|
2 |
test('hello world', function() { |
3 |
App.reset(); |
4 |
visit("/").then(function() { |
5 |
var rows = find("table tr").length; |
6 |
equal(rows, 2, rows); |
7 |
});
|
8 |
});
|
Nota: la aplicación actualmente está devolviendo datos codificados para mantener todo simple en este momento. Si tiene curiosidad de por qué tenemos dos personas, aquí está el método find en el modelo:
1 |
|
2 |
App.Person.reopenClass({ |
3 |
people: [], |
4 |
find: function() { |
5 |
var first = App.Person.create({firstName: 'x', lastName: 'y'}); |
6 |
var last = App.Person.create({firstName: 'x', lastName: 'y'}); |
7 |
this.people.pushObject(first); |
8 |
this.people.pushObject(last); |
9 |
return this.people; |
10 |
}
|
11 |
});
|
Si realizamos las pruebas ahora, todavía deberíamos pasar todo porque se devuelven dos personas como es de esperar. Luego, necesitamos obtener la celda de la tabla que muestra el nombre de la persona y afirmar que está usando la propiedad fullName en lugar de solo el firstName.
1 |
|
2 |
test('hello world', function() { |
3 |
App.reset(); |
4 |
visit("/").then(function() { |
5 |
var rows = find("table tr").length; |
6 |
equal(rows, 2, rows); |
7 |
var fullName = find("table tr:eq(0) td:eq(0)").text(); |
8 |
equal(fullName, "x y", "the first table row had fullName: " + fullName); |
9 |
});
|
10 |
});
|
Si ejecuta la prueba anterior, debería ver una prueba fallida porque todavía no hemos actualizado la plantilla para usar fullName. Ahora que tenemos una prueba fallida, actualice la plantilla para usar fullName y ejecute las pruebas usando ./node_modules/karma/bin/karma start. Ahora debería tener una serie de pruebas de unidad e integración que pasan.
¿Debo escribir pruebas de unidad o de integración?
Si se está preguntando, "¿cuándo debo escribir una prueba unitaria frente a una prueba de integración?", La respuesta es simple: ¿qué será menos doloroso? Si escribir una prueba de unidad es más rápido y explica el problema mejor que una prueba de integración mucho más grande, entonces le digo que escriba la prueba de unidad. Si las pruebas de unidad parecen menos valiosas porque está haciendo CRUD básico y el comportamiento real está en la interacción entre los componentes, le digo que escriba la prueba de integración. Debido a que las pruebas de integración escritas con las pruebas de brasas son increíblemente rápidas, son parte del ciclo de retroalimentación del desarrollador y deben usarse de manera similar a una prueba de unidad cuando tenga sentido.
Para mostrar una prueba de integración similar a CRUD en acción, escriba la siguiente prueba para probar que el botón add coloca a la persona en la colección y que se representa una nueva fila en la plantilla de manillares.
1 |
|
2 |
test('add will append another person to the html table', function() { |
3 |
App.Person.people = []; |
4 |
App.reset(); |
5 |
visit("/").then(function() { |
6 |
var rows = find("table tr").length |
7 |
equal(rows, 2, "the table had " + rows + " rows"); |
8 |
fillIn(".firstName", "foo"); |
9 |
fillIn(".lastName", "bar"); |
10 |
return click(".submit"); |
11 |
}).then(function() { |
12 |
equal(find("table tr").length, 3, "the table of people was not complete"); |
13 |
equal(find("table tr:eq(2) td:eq(0)").text(), "foo bar", "the fullName for the person was incorrect"); |
14 |
});
|
15 |
});
|
Comience diciéndole a la prueba con qué estado desea trabajar, luego use el asistente fillIn, agregue un nombre y un apellido. Ahora, si hace clic en el botón submit, debería agregar a esa persona a la tabla HTML, por lo que en la devolución then podemos afirmar que existen tres personas en la tabla HTML. Ejecute esta prueba y debería fallar porque el controlador Ember no está completo.
Para obtener la aprobación de la prueba, agregue la siguiente línea al PeopleController
1 |
|
2 |
App.PeopleController = Ember.ArrayController.extend({ |
3 |
actions: { |
4 |
addPerson: function() { |
5 |
var person = { |
6 |
firstName: this.get('firstName'), |
7 |
lastName: this.get('lastName') |
8 |
};
|
9 |
App.Person.add(person); |
10 |
}
|
11 |
}
|
12 |
});
|
Ahora, si ejecuta las pruebas utilizando ./node_modules/karma/bin/karma start debería mostrar tres personas en el HTML renderizado.
La última prueba es la eliminación, observe que encontramos el botón para una fila específica y lo presionamos. A continuación, then simplemente verificamos que se muestra una persona menos en la tabla HTML.
1 |
|
2 |
test('delete will remove the person for a given row', function() { |
3 |
App.Person.people = []; |
4 |
App.reset(); |
5 |
visit("/").then(function() { |
6 |
var rows = find("table tr").length; |
7 |
equal(rows, 2, "the table had " + rows + " rows"); |
8 |
return click("table .delete:first"); |
9 |
}).then(function() { |
10 |
equal(find("table tr").length, 1, "the table of people was not complete |
11 |
});
|
12 |
});")})}) |
Para obtener esta aprobación, simplemente agregue la siguiente línea al PeopleController:
1 |
|
2 |
App.PeopleController = Ember.ArrayController.extend({ |
3 |
actions: { |
4 |
addPerson: function() { |
5 |
var person = { |
6 |
firstName: this.get('firstName'), |
7 |
lastName: this.get('lastName') |
8 |
};
|
9 |
App.Person.add(person); |
10 |
},
|
11 |
deletePerson: function(person) { |
12 |
App.Person.remove(person); |
13 |
}
|
14 |
}
|
15 |
});
|
Ejecute las pruebas desde la línea de comandos y una vez más debe tener un conjunto de pruebas que pasan.
Conclusión
Así que eso envuelve nuestra aplicación de muestra. Siéntase libre de hacer cualquier pregunta en los comentarios.
Bonus: Pero ya estoy usando Grunt...
Si prefiere utilizar Grunt en lugar del karma-ember-preprocessor, simplemente elimine los complementos y la configuración de los preprocesadores. También elimine templates/*.handlebars de la sección de archivos ya que Karma no necesitará precompilar las plantillas. Aquí hay un karma.conf.js simplificado que funciona cuando se usa grunt para precompilar las plantillas de manillares.
1 |
|
2 |
module.exports = function(karma) { |
3 |
karma.set({ |
4 |
basePath: 'js', |
5 |
|
6 |
files: [ |
7 |
"lib/deps.min.js", //built by your grunt task |
8 |
"tests/*.js" |
9 |
],
|
10 |
|
11 |
logLevel: karma.LOG_ERROR, |
12 |
browsers: ['PhantomJS'], |
13 |
singleRun: true, |
14 |
autoWatch: false, |
15 |
|
16 |
frameworks: ["qunit"] |
17 |
});
|
18 |
};
|
¡Y eso es!



