Introducción a las pruebas unitarias de JavaScript
Probablemente sepa que las pruebas son buenas, pero el primer obstáculo que debe superar al intentar escribir pruebas unitarias para código del lado del cliente es la falta de unidades reales; El código JavaScript se escribe para cada página de un sitio web o cada módulo de una aplicación y está estrechamente entremezclado con la lógica de back-end y el HTML relacionado. En el peor de los casos, el código está completamente mezclado con HTML, como controladores de eventos en línea.
Probablemente sepa que las pruebas son buenas, pero el primer obstáculo que debe superar al intentar escribir pruebas unitarias para código del lado del cliente es la falta de unidades reales; El código JavaScript se escribe para cada página de un sitio web o cada módulo de una aplicación y está estrechamente entremezclado con la lógica de back-end y el HTML relacionado. En el peor de los casos, el código está completamente mezclado con HTML, como controladores de eventos en línea.
Este es probablemente el caso cuando no se utiliza ninguna biblioteca JavaScript para alguna abstracción DOM; escribir controladores de eventos en línea es mucho más fácil que usar las API DOM para vincular esos eventos. Cada vez más desarrolladores eligen una biblioteca como jQuery para manejar la abstracción DOM, lo que les permite mover esos eventos en línea a scripts distintos, ya sea en la misma página o incluso en un archivo JavaScript separado. Sin embargo, poner el código en archivos separados no significa que esté listo para ser probado como una unidad.
Lecturas adicionales sobre SmashingMag:
- Escribir JavaScript rápido y con memoria eficiente
- Errores de JavaScript que se deben evitar con un analizador de código estático
- Análisis de las características de la red utilizando JavaScript y DOM
- Encuentre la solución JavaScript adecuada con una prueba de 7 pasos
¿Qué es una unidad de todos modos? En el mejor de los casos, es una función pura con la que puedes lidiar de alguna manera: una función que siempre te da el mismo resultado para una entrada determinada. Esto hace que las pruebas unitarias sean bastante fáciles, pero la mayoría de las veces es necesario lidiar con los efectos secundarios, lo que aquí significa manipulaciones DOM. Sigue siendo útil determinar en qué unidades podemos estructurar nuestro código y crear pruebas unitarias en consecuencia.
Pruebas unitarias de construcción
Teniendo esto en cuenta, obviamente podemos decir que comenzar con pruebas unitarias es mucho más fácil cuando se comienza algo desde cero. Pero de eso no se trata este artículo. Este artículo tiene como objetivo ayudarle con el problema más difícil: extraer el código existente y probar las partes importantes, potencialmente descubriendo y corrigiendo errores en el código.
El proceso de extraer código y ponerlo en una forma diferente, sin modificar su comportamiento actual, se llama refactorización. La refactorización es un método excelente para mejorar el diseño del código de un programa; y debido a que cualquier cambio podría modificar el comportamiento del programa, es más seguro hacerlo cuando existen pruebas unitarias.
Este problema del huevo y la gallina significa que para agregar pruebas al código existente, debes correr el riesgo de romper cosas. Por lo tanto, hasta que tenga una cobertura sólida con pruebas unitarias, debe continuar realizando pruebas manualmente para minimizar ese riesgo.
Esto debería ser suficiente teoría por ahora. Veamos un ejemplo práctico, probando algún código JavaScript que actualmente está mezclado y conectado a una página. El código busca enlaces con title
atributos y utiliza esos títulos para mostrar cuándo se publicó algo, como un valor de tiempo relativo, como "hace 5 días":
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleMangled date examples/title script function prettyDate(time){ var date = new Date(time || ""), diff = ((new Date().getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff 0 || day_diff = 31) { return; } return day_diff == 0 ( diff 60 "just now" || diff 120 "1 minute ago" || diff 3600 Math.floor( diff / 60 ) + " minutes ago" || diff 7200 "1 hour ago" || diff 86400 Math.floor( diff / 3600 ) + " hours ago") || day_diff == 1 "Yesterday" || day_diff 7 day_diff + " days ago" || day_diff 31 Math.ceil( day_diff / 7 ) + " weeks ago"; } window.onload = function(){ var links = document.getElementsByTagName("a"); for (var i = 0; i links.length; i++) { if (links[i].title) { var date = prettyDate(links[i].title); if (date) { links[i].innerHTML = date; } } } }; /script/headbodyulli pblah blah blah…/p small Posted a href="/2008/01/blah/57/"January 28th, 2008/a by a href="/john/"John Resig/a /small/li!-- more list items --/ul/body/html
Si ejecutara ese ejemplo, vería un problema: ninguna de las fechas se reemplaza. Sin embargo, el código funciona. Recorre todos los anclajes de la página y busca una title
propiedad en cada uno. Si hay uno, lo pasa a la prettyDate
función. Si prettyDate
devuelve un resultado, actualiza el innerHTML
enlace con el resultado.
Hacer que las cosas sean comprobables
El problema es que para cualquier fecha mayor a 31 días, prettyDate
simplemente devuelve indefinido (implícitamente, con una sola return
declaración), dejando el texto del ancla como está. Entonces, para ver qué se supone que sucede, podemos codificar una fecha "actual":
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleMangled date examples/title script function prettyDate(now, time){ var date = new Date(time || ""), diff = (((new Date(now)).getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff 0 || day_diff = 31) { return; } return day_diff == 0 ( diff 60 "just now" || diff 120 "1 minute ago" || diff 3600 Math.floor( diff / 60 ) + " minutes ago" || diff 7200 "1 hour ago" || diff 86400 Math.floor( diff / 3600 ) + " hours ago") || day_diff == 1 "Yesterday" || day_diff 7 day_diff + " days ago" || day_diff 31 Math.ceil( day_diff / 7 ) + " weeks ago"; } window.onload = function(){ var links = document.getElementsByTagName("a"); for (var i = 0; i links.length; i++) { if (links[i].title) { var date = prettyDate("2008-01-28T22:25:00Z", links[i].title); if (date) { links[i].innerHTML = date; } } } }; /script/headbodyulli pblah blah blah…/p small Posted a href="/2008/01/blah/57/"January 28th, 2008/a by a href="/john/"John Resig/a /small/li!-- more list items --/ul/body/html
- Ejecute este ejemplo.
Ahora, los enlaces deberían decir "Hace 2 horas", "Ayer", etc. Eso es algo, pero todavía no es una unidad comprobable real. Entonces, sin cambiar más el código, todo lo que podemos hacer es intentar probar los cambios DOM resultantes. Incluso si eso funcionara, cualquier pequeño cambio en el margen probablemente rompería la prueba, lo que resultaría en una relación costo-beneficio realmente mala para una prueba como esa.
Refactorización, etapa 0
En su lugar, refactoricemos el código lo suficiente como para tener algo que podamos probar unitariamente.
Necesitamos hacer dos cambios para que esto suceda: pasar la fecha actual a la prettyDate
función como argumento, en lugar de usarla simplemente new Date
, y extraer la función a un archivo separado para que podamos incluir el código en una página separada para la unidad. pruebas.
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleRefactored date examples/title script src="prettydate.js"/script script window.onload = function() { var links = document.getElementsByTagName("a"); for ( var i = 0; i links.length; i++ ) { if (links[i].title) { var date = prettyDate("2008-01-28T22:25:00Z", links[i].title); if (date) { links[i].innerHTML = date; } } } }; /script/headbodyulli pblah blah blah…/p small Posted a href="/2008/01/blah/57/"January 28th, 2008/a by a href="/john/"John Resig/a /small/li!-- more list items --/ul/body/html
Aquí está el contenido de prettydate.js
:
function prettyDate(now, time){ var date = new Date(time || ""), diff = (((new Date(now)).getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff 0 || day_diff = 31) { return; } return day_diff == 0 ( diff 60 "just now" || diff 120 "1 minute ago" || diff 3600 Math.floor( diff / 60 ) + " minutes ago" || diff 7200 "1 hour ago" || diff 86400 Math.floor( diff / 3600 ) + " hours ago") || day_diff == 1 "Yesterday" || day_diff 7 day_diff + " days ago" || day_diff 31 Math.ceil( day_diff / 7 ) + " weeks ago";}
- Ejecute este ejemplo.
Ahora que tenemos algo que probar, escribamos algunas pruebas unitarias reales:
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleRefactored date examples/title script src="prettydate.js"/script script function test(then, expected) { results.total++; var result = prettyDate("2008-01-28T22:25:00Z", then); if (result !== expected) { results.bad++; console.log("Expected " + expected + ", but was " + result); } } var results = { total: 0, bad: 0 }; test("2008/01/28 22:24:30", "just now"); test("2008/01/28 22:23:30", "1 minute ago"); test("2008/01/28 21:23:30", "1 hour ago"); test("2008/01/27 22:23:30", "Yesterday"); test("2008/01/26 22:23:30", "2 days ago"); test("2007/01/26 22:23:30", undefined); console.log("Of " + results.total + " tests, " + results.bad + " failed, " + (results.total - results.bad) + " passed."); /script/headbody/body/html
- Ejecute este ejemplo. (Asegúrese de habilitar una consola como Firebug o el Inspector web de Chrome).
Esto creará un marco de prueba ad-hoc, utilizando solo la consola para la salida. No tiene ninguna dependencia del DOM, por lo que también puedes ejecutarlo en un entorno JavaScript sin navegador, como Node.js o Rhino, extrayendo el código de la script
etiqueta a su propio archivo.
Si una prueba falla, generará el resultado esperado y real para esa prueba. Al final, generará un resumen de la prueba con el número total de pruebas, reprobadas y aprobadas.
Si todas las pruebas han pasado, como deberían hacerlo aquí, verá lo siguiente en la consola:
De 6 pruebas, 0 reprobaron y 6 aprobaron.
Para ver cómo se ve una afirmación fallida, podemos cambiar algo para romperla:
Se esperaba hace 2 días, pero fue hace 2 días.
De 6 pruebas, 1 falló y 5 aprobaron.
Si bien este enfoque ad-hoc es interesante como prueba de concepto (realmente puedes escribir un ejecutor de pruebas en solo unas pocas líneas de código), es mucho más práctico utilizar un marco de prueba unitario existente que proporcione mejores resultados y más infraestructura para escribir. y organización de pruebas.
El conjunto de pruebas de JavaScript QUnit
La elección del marco es principalmente una cuestión de gustos. Durante el resto de este artículo, usaremos QUnit (pronunciado “q-unit”), porque su estilo de describir pruebas es similar al de nuestro marco de pruebas ad-hoc.
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleRefactored date examples/title link rel="stylesheet" href="qunit.css" / script src="qunit.js"/script script src="prettydate.js"/script script test("prettydate basics", function() { var now = "2008/01/28 22:25:00"; equal(prettyDate(now, "2008/01/28 22:24:30"), "just now"); equal(prettyDate(now, "2008/01/28 22:23:30"), "1 minute ago"); equal(prettyDate(now, "2008/01/28 21:23:30"), "1 hour ago"); equal(prettyDate(now, "2008/01/27 22:23:30"), "Yesterday"); equal(prettyDate(now, "2008/01/26 22:23:30"), "2 days ago"); equal(prettyDate(now, "2007/01/26 22:23:30"), undefined); }); /script/headbody div/div/body/html
- Ejecute este ejemplo.
Aquí vale la pena examinar más de cerca tres secciones. Junto con el texto estándar HTML, tenemos tres archivos incluidos: dos archivos para QUnit ( qunit.css
y qunit.js
) y el anterior prettydate.js
.
Luego, hay otro bloque de script con las pruebas reales. El test
método se llama una vez, pasando una cadena como primer argumento (nombrando la prueba) y pasando una función como segundo argumento (que ejecutará el código real para esta prueba). Luego, este código define la now
variable, que se reutiliza a continuación, luego llama al equal
método varias veces con diferentes argumentos. El equal
método es una de varias afirmaciones que proporciona QUnit. El primer argumento es el resultado de una llamada a prettyDate
, con la now
variable como primer argumento y una date
cadena como segundo. El segundo argumento equal
es el resultado esperado. Si los dos argumentos equal
tienen el mismo valor, entonces la afirmación será válida; de lo contrario, fallará.
Finalmente, en el elemento del cuerpo hay algunas marcas específicas de QUnit. Estos elementos son opcionales. Si están presentes, QUnit los utilizará para generar los resultados de la prueba.
El resultado es este:
Con una prueba fallida, el resultado sería algo como esto:
Debido a que la prueba contiene una afirmación fallida, QUnit no colapsa los resultados de esa prueba y podemos ver inmediatamente qué salió mal. Junto con la salida de los valores esperados y reales, obtenemos una diferencia diff
entre los dos, que puede ser útil para comparar cadenas más grandes. Aquí, es bastante obvio lo que salió mal.
Refactorización, etapa 1
Actualmente, las afirmaciones están algo incompletas porque aún no estamos probando la n weeks ago
variante. Antes de agregarlo, deberíamos considerar refactorizar el código de prueba. Actualmente, solicitamos prettyDate
cada afirmación y pasamos el now
argumento. Podríamos refactorizar esto fácilmente en un método de aserción personalizado:
test("prettydate basics", function() { function date(then, expected) { equal(prettyDate("2008/01/28 22:25:00", then), expected); } date("2008/01/28 22:24:30", "just now"); date("2008/01/28 22:23:30", "1 minute ago"); date("2008/01/28 21:23:30", "1 hour ago"); date("2008/01/27 22:23:30", "Yesterday"); date("2008/01/26 22:23:30", "2 days ago"); date("2007/01/26 22:23:30", undefined);});
- Ejecute este ejemplo.
Aquí hemos extraído la llamada prettyDate
a la date
función, insertando la now
variable en la función. Terminamos con solo los datos relevantes para cada afirmación, lo que facilita su lectura, mientras que la abstracción subyacente sigue siendo bastante obvia.
Probando la manipulación DOM
Ahora que la prettyDate
función se ha probado suficientemente bien, volvamos a centrarnos en el ejemplo inicial. Junto con la prettyDate
función, también seleccionó algunos elementos DOM y los actualizó dentro del window
controlador de eventos de carga. Aplicando los mismos principios que antes, deberíamos poder refactorizar ese código y probarlo. Además, introduciremos un módulo para estas dos funciones, para evitar saturar el espacio de nombres global y poder dar a estas funciones individuales nombres más significativos.
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleRefactored date examples/title link rel="stylesheet" href="qunit.css" / script src="qunit.js"/script script src="prettydate2.js"/script script test("prettydate.format", function() { function date(then, expected) { equal(prettyDate.format("2008/01/28 22:25:00", then), expected); } date("2008/01/28 22:24:30", "just now"); date("2008/01/28 22:23:30", "1 minute ago"); date("2008/01/28 21:23:30", "1 hour ago"); date("2008/01/27 22:23:30", "Yesterday"); date("2008/01/26 22:23:30", "2 days ago"); date("2007/01/26 22:23:30", undefined); }); test("prettyDate.update", function() { var links = document.getElementById("qunit-fixture").getElementsByTagName("a"); equal(links[0].innerHTML, "January 28th, 2008"); equal(links[2].innerHTML, "January 27th, 2008"); prettyDate.update("2008-01-28T22:25:00Z"); equal(links[0].innerHTML, "2 hours ago"); equal(links[2].innerHTML, "Yesterday"); }); test("prettyDate.update, one day later", function() { var links = document.getElementById("qunit-fixture").getElementsByTagName("a"); equal(links[0].innerHTML, "January 28th, 2008"); equal(links[2].innerHTML, "January 27th, 2008"); prettyDate.update("2008-01-28T22:25:00Z"); equal(links[0].innerHTML, "Yesterday"); equal(links[2].innerHTML, "2 days ago"); }); /script/headbody div/div div ul li pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"January 28th, 2008/a/span by spana href="/john/"John Resig/a/span /small /li li pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"January 27th, 2008/a/span by spana href="/john/"John Resig/a/span /small /li /ul /div/body/html
Aquí está el contenido de prettydate2.js
:
var prettyDate = { format: function(now, time){ var date = new Date(time || ""), diff = (((new Date(now)).getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff 0 || day_diff = 31) { return; } return day_diff === 0 ( diff 60 "just now" || diff 120 "1 minute ago" || diff 3600 Math.floor( diff / 60 ) + " minutes ago" || diff 7200 "1 hour ago" || diff 86400 Math.floor( diff / 3600 ) + " hours ago") || day_diff === 1 "Yesterday" || day_diff 7 day_diff + " days ago" || day_diff 31 Math.ceil( day_diff / 7 ) + " weeks ago"; }, update: function(now) { var links = document.getElementsByTagName("a"); for ( var i = 0; i links.length; i++ ) { if (links[i].title) { var date = prettyDate.format(now, links[i].title); if (date) { links[i].innerHTML = date; } } } }};
- Ejecute este ejemplo.
La nueva prettyDate.update
función es un extracto del ejemplo inicial, pero con el now
argumento para pasar a prettyDate.format
. La prueba basada en QUnit para esa función comienza seleccionando todos a
los elementos dentro del #qunit-fixture
elemento. En el marcado actualizado en el elemento del cuerpo, div…/div
es nuevo. Contiene un extracto del marcado de nuestro ejemplo inicial, suficiente para escribir pruebas útiles. Al colocarlo en el #qunit-fixture
elemento, no tenemos que preocuparnos de que los cambios DOM de una prueba afecten a otras pruebas, porque QUnit restablecerá automáticamente el marcado después de cada prueba.
Veamos la primera prueba para prettyDate.update
. Después de seleccionar esos anclajes, dos afirmaciones verifican que estos tengan sus valores de texto iniciales. Posteriormente prettyDate.update
se convoca pasando una fecha fija (la misma que en pruebas anteriores). Luego se ejecutan dos aserciones más, verificando ahora que las innerHTML
propiedades de estos elementos tengan la fecha formateada correctamente, “Hace 2 horas” y “Ayer”.
Refactorización, etapa 2
La siguiente prueba, prettyDate.update, one day later
, hace casi lo mismo, excepto que pasa en una fecha diferente prettyDate.update
y, por lo tanto, espera resultados diferentes para los dos enlaces. Veamos si podemos refactorizar estas pruebas para eliminar la duplicación.
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleRefactored date examples/title link rel="stylesheet" href="qunit.css" / script src="qunit.js"/script script src="prettydate2.js"/script script test("prettydate.format", function() { function date(then, expected) { equal(prettyDate.format("2008/01/28 22:25:00", then), expected); } date("2008/01/28 22:24:30", "just now"); date("2008/01/28 22:23:30", "1 minute ago"); date("2008/01/28 21:23:30", "1 hour ago"); date("2008/01/27 22:23:30", "Yesterday"); date("2008/01/26 22:23:30", "2 days ago"); date("2007/01/26 22:23:30", undefined); }); function domtest(name, now, first, second) { test(name, function() { var links = document.getElementById("qunit-fixture").getElementsByTagName("a"); equal(links[0].innerHTML, "January 28th, 2008"); equal(links[2].innerHTML, "January 27th, 2008"); prettyDate.update(now); equal(links[0].innerHTML, first); equal(links[2].innerHTML, second); }); } domtest("prettyDate.update", "2008-01-28T22:25:00Z:00", "2 hours ago", "Yesterday"); domtest("prettyDate.update, one day later", "2008-01-29T22:25:00Z:00", "Yesterday", "2 days ago"); /script/headbody div/div div ul li pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"January 28th, 2008/a/span by spana href="/john/"John Resig/a/span /small /li li pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"January 27th, 2008/a/span by spana href="/john/"John Resig/a/span /small /li /ul /div/body/html
- Ejecute este ejemplo.
Aquí tenemos una nueva función llamada domtest
, que encapsula la lógica de las dos llamadas anteriores a test, introduciendo argumentos para el nombre de la prueba, la cadena de fecha y las dos cadenas esperadas. Luego lo llaman dos veces.
De vuelta al principio
Una vez establecido esto, volvamos a nuestro ejemplo inicial y veamos cómo se ve ahora, después de la refactorización.
!DOCTYPE htmlhtmlhead meta http-equiv="Content-Type" content="text/html; charset=UTF-8" / titleFinal date examples/title script src="prettydate2.js"/script script window.onload = function() { prettyDate.update("2008-01-28T22:25:00Z"); }; /script/headbodyulli pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanJanuary 28th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/lili pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanJanuary 27th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/lili pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanJanuary 26th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/lili pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanJanuary 25th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/lili pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanJanuary 24th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/lili pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanJanuary 14th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/lili pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanJanuary 4th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/lili pblah blah blah…/p small Posted spana href="/2008/01/blah/57/"spanDecember 15th, 2008/span/a/span by spana href="/john/"John Resig/a/span /small/li/ul/body/html
- Ejecute este ejemplo.
Para un ejemplo no estático, eliminaríamos el argumento de prettyDate.update
. Considerándolo todo, la refactorización es una gran mejora con respecto al primer ejemplo. Y gracias al prettyDate
módulo que presentamos, podemos agregar aún más funciones sin afectar el espacio de nombres global.
Conclusión
Probar código JavaScript no es sólo cuestión de utilizar algún ejecutor de pruebas y escribir algunas pruebas; Por lo general, requiere algunos cambios estructurales importantes cuando se aplica a código que antes solo se ha probado manualmente. Hemos analizado un ejemplo de cómo cambiar la estructura del código de un módulo existente para ejecutar algunas pruebas utilizando un marco de pruebas ad-hoc y luego reemplazarlo con un marco con más funciones para obtener resultados visuales útiles.
QUnit en sí tiene mucho más que ofrecer, con soporte específico para probar código asincrónico como tiempos de espera, AJAX y eventos. Su ejecutor de pruebas visual ayuda a depurar código al facilitar la repetición de pruebas específicas y al proporcionar seguimientos de pila para aserciones fallidas y excepciones detectadas. Para obtener más información, consulte el libro de cocina de QUnit .
(al) (km)
Explora más en
- Codificación
- javascript
- Pruebas
Deja un comentario