1. Code
  2. Coding Fundamentals
  3. Testing

Probando tu JavaScript con Jasmine

Scroll to top

Spanish (Español) translation by Luis Chiabrera (you can also view the original English article)

Todos sabemos que deberíamos estar probando nuestro código, pero en realidad no lo hacemos. Supongo que es justo decir que la mayoría de nosotros lo posponemos porque, nueve de cada diez veces, significa aprender otro concepto. En este tutorial, te presentaré un pequeño marco excelente para probar tu código JavaScript con facilidad.


Paso 0: entendiendo BDD

Hoy, vamos a aprender sobre el marco de prueba de Jasmine BDD. Pero nos detendremos aquí para hacer un desvío primero, para hablar muy brevemente, sobre BDD y TDD. Si no estás familiarizado con estos acrónimos, significan desarrollo impulsado por el comportamiento y desarrollo impulsado por pruebas. Estoy en medio de aprender qué es cada uno de estos en la práctica y en qué se diferencian, pero estas son algunas de las diferencias básicas:

BDD y TDD… son sinónimo de desarrollo impulsado por el comportamiento y desarrollo impulsado por pruebas.

TDD en su forma más simple es solo esto:

  1. Escribe tus pruebas
  2. Observa como fallan
  3. Hazlas pasar
  4. Refactoriza
  5. Repite

Eso es bastante fácil de entender, ¿eh?

El BDD es un poco más complejo: tal como lo entiendo ahora mismo, no creo que tú o yo, como desarrollador individual, podamos practicarlo completamente; es más una cosa de equipo. Estas son algunas de las prácticas de BDD:

  • Establecer las metas de diferentes partes interesadas necesarias para implementar una visión.
  • Involucrar a las partes interesadas en el proceso de implementación a través del desarrollo de software de afuera hacia adentro
  • Usar ejemplos para describir el comportamiento de la aplicación o de unidades de código
  • Automatizar esos ejemplos para proporcionar retroalimentación rápida y pruebas de regresión.

Para obtener más información, puedes leer el extenso artículo de Wikipedia (del que se tomaron esos puntos).

Todo esto para decir que, si bien Jasmine se anuncia a sí mismo como un marco BDD, lo vamos a usar de una manera más al estilo TDD. Eso no significa que lo estemos usando mal. Una vez que hayamos terminado, podrás probar tu JavaScript con facilidad ... ¡y espero que lo hagas!


Paso 1: aprender la sintaxis

Jasmine toma muchas señales de Rspec.

Si estás familiarizado con Rspec, el marco BDD de facto, verás que Jasmine toma muchas señales de Rspec. Las pruebas de jazmín constan principalmente de dos partes: bloques describe y bloques it Veamos cómo funciona esto.

Veremos algunas pruebas más cercanas a la vida real en algunas, pero por ahora, lo mantendremos simple:

1
describe('JavaScript addition operator', function () {
2
    it('adds two numbers together', function () {
3
        expect(1 + 2).toEqual(3);
4
    });
5
});

Tanto las funciones describe como it toman dos parámetros: una cadena de texto y una función. La mayoría de los marcos de prueba intentan leer tanto inglés como sea posible, y puedes ver esto con Jasmine. En primer lugar, observa que la cadena que se pasa para describe y la cadena que se pasa a it forman una oración (de algún tipo): "El operador de suma de JavaScript suma dos números". Luego, pasamos a mostrar cómo.

Dentro de ese bloque it, puedes escribir todo el código de configuración que necesitas para tu prueba. No necesitamos ninguno para este simple ejemplo. Una vez que esté listo para escribir el código de prueba real, comenzarás con la función expect, pasándole lo que sea que estés probando. Observe cómo esto también forma una oración: "esperamos que 1 + 2 sea igual a 3."

Pero me estoy adelantando a nosotros mismos. Como dije, cualquier valor que pases a expect sera probado. El método que llames, fuera del valor devuelto de expect, será determinado por la prueba que se ejecute. Este grupo de métodos se denomina "comparadores" y veremos varios de ellos hoy. En este caso, estamos usando el comparador toEqual, que verifica que el valor pasado a expect y el valor pasado a toEqual sean el mismo valor.

Creo que estás listo para llevar esto al siguiente nivel, así que configuremos un proyecto simple con Jasmine.


Paso 2: configurar un proyecto

El jazmín se puede usar solo; o puedes integrarlo con un proyecto de Rails. Nosotros haremos lo primero. Si bien Jasmine puede ejecutarse fuera del navegador (piensa en Node, entre otros lugares), podemos obtener una pequeña plantilla realmente agradable con la descarga.

Entonces, dirígete a la página de descarga independiente y obtén la última versión. Deberías obtener algo como esto:

Jasmine DownloadJasmine DownloadJasmine Download

Encontrarás los archivos del marco Jasmine en la carpeta lib. Si prefieres estructurar tus proyectos de manera diferente, házlo; pero vamos a mantener esto por ahora.

En realidad, hay un código de muestra conectado en esta plantilla de proyecto. El JavaScript "actual" (el código que queremos probar) se puede encontrar en el subdirectorio src; pondremos el nuestro allí en breve. El código de prueba, las especificaciones, van en la carpeta spec. No te preocupes por el archivo SpecHelper.js todavía; volveremos a eso.

Ese archivo SpecRunner.html es el que ejecuta las pruebas en un navegador. Ábrelo (y marca la casilla de verificación "aprobado" en la esquina superior derecha), y deberías ver algo como esto:

Sample TestsSample TestsSample Tests

Esto nos muestra que todas las pruebas para el proyecto de muestra están pasando. Una vez que hayas completado este tutorial, te recomiendo que abra el archivo spec/PlayerSpec.js y lee detenidamente ese código. Pero ahora mismo, probemos este material de escritura de prueba.

  • Crea convert.js en la carpeta src.
  • Crea convertSpec.js en la carpeta spec,
  • Copia el archivo SpecRunner.html y cámbiale el nombre SpecRunner.original.html.
  • Elimina los enlaces a los archivos de proyecto de muestra en SpecRunner.html y agrega estas líneas:

    1
    <script src="src/convert.js"><>/script>
    
    
    2
    <script src="spec/convertSpec.js"></script>
    

Ahora estamos listos para crear una mini biblioteca que convertirá entre unidades de medida. Comenzaremos escribiendo las pruebas para nuestra mini biblioteca.


Paso 3: escribir las pruebas

Entonces, escribamos nuestras pruebas, ¿de acuerdo?

1
describe( "Convert library", function () {
2
    describe( "distance converter", function () {
3
4
        });
5
6
        describe( "volume converter", function () {
7
8
    });
9
});

Empezamos con esto; estamos probando nuestra biblioteca Convert. Notarás que estamos anidando declaraciones describe aquí. Esto es perfectamente legal. En realidad, es una excelente manera de probar partes de funcionalidad separadas de la misma base de código. En lugar de dos llamadas describe separadas para las conversiones de distancia y las conversiones de volumen de la biblioteca de Convert, podemos tener un conjunto de pruebas más descriptivo como este.

Ahora, en las pruebas reales. Repetiré las llamadas describe internas aquí para tu conveniencia.

1
describe( "distance converter", function () {
2
    it("converts inches to centimeters", function () {
3
        expect(Convert(12, "in").to("cm")).toEqual(30.48);
4
    });
5
6
    it("converts centimeters to yards", function () {
7
        expect(Convert(2000, "cm").to("yards")).toEqual(21.87);
8
    });
9
});

Aquí están nuestras pruebas para conversiones de distancia. Es importante notar algo aquí: todavía no hemos escrito ni una pizca de código para nuestra biblioteca Convert, por lo que en estas pruebas estamos haciendo más que solo verificar si funciona: en realidad estamos decidiendo cómo se usará (y por lo tanto implementado). Así es como decidimos realizar nuestras conversiones:

1
Convert(<number>, <from unit string>).to(<to unit string>);

Sí, sigo el ejemplo de la forma en que Jasmine implementó sus pruebas, pero creo que es un formato agradable. Entonces, en estas dos pruebas, hice las conversiones yo mismo (ok, con una calculadora) para ver cuáles deberían ser los resultados de nuestras llamadas. Estamos usando el comparador toEqual para ver si nuestras pruebas pasan.

Aquí están las pruebas de volumen:

1
describe( "volume converter", function () {
2
    it("converts litres to gallons", function () {
3
        expect(Convert(3, "litres").to("gallons")).toEqual(0.79);
4
    });
5
6
    it("converts gallons to cups", function () {
7
        expect(Convert(2, "gallons").to("cups")).toEqual(32);
8
    });
9
});

Y voy a agregar dos pruebas más en nuestra llamada describe de nivel superior:

1
it("throws an error when passed an unknown from-unit", function () {
2
    var testFn = function () {
3
        Convert(1, "dollar").to("yens");
4
    }
5
    expect(testFn).toThrow(new Error("unrecognized from-unit"));
6
});
7
8
it("throws an error when passed an unknown to-unit", function () {
9
    var testFn = function () {
10
        Convert(1, "cm").to("furlongs");
11
    }
12
    expect(testFn).toThrow(new Error("unrecognized to-unit"));
13
});

Estos comprueban los errores que se deben generar cuando se pasan unidades desconocidas a la función Convert o al método to. Notarás que estoy ajustando la conversión real en una función y la paso a la función expect. Eso es porque no podemos llamar a la función como parámetro expect; tenemos que darle una función y dejar que llame a la función en sí. Como necesitamos pasar un parámetro a esa función to podemos hacerlo de esta manera.

La otra cosa a tener en cuenta es que estoy presentando un nuevo comparador: toThrow, que toma un objeto de error. Pronto veremos algunos comparadores más.

Ahora, si abres SpecRunner.html en un navegador, obtendrás esto:

Convert failed testsConvert failed testsConvert failed tests

¡Excelente! Nuestras pruebas estan fallando. Ahora, abramos nuestro archivo convert.js y trabajemos un poco:

1
function Convert(number, fromUnit) {
2
    var conversions = {
3
            distance : {
4
                meters : 1,
5
                cm     : 0.01,
6
                feet   : 0.3048,
7
                inches : 0.0254,
8
                yards  : 0.9144
9
            },
10
            volume : {
11
                liters : 1,
12
                gallons: 3.785411784,
13
                cups   : 0.236588236 
14
            }
15
        },
16
        betweenUnit = false,
17
        type, unit;
18
19
    for (type in conversions) {
20
        if (conversions(type)) {
21
            if ( (unit = conversions[type][fromUnit]) ) {
22
                betweenUnit = number * unit * 1000;
23
            }
24
        }
25
    }
26
27
    return {
28
        to : function (toUnit) {
29
            if (betweenUnit) {
30
                for (type in conversions) {
31
                    if (conversions.hasOwnProperty(type)) {
32
                        if ( (unit = conversions[type][toUnit]) ) {
33
                            return fix(betweenUnit / (unit * 1000));
34
                        }
35
                    }
36
                }
37
                throw new Error("unrecognized to-unit");
38
            } else {
39
                throw new Error("unrecognized from-unit");
40
            }  
41
42
            function fix (num) {
43
                return parseFloat( num.toFixed(2) );
44
            }
45
        }
46
    };
47
}

Realmente no vamos a discutir esto, porque estamos aprendiendo sobre Jasmine aquí. Pero aquí están los puntos principales:

  • Realizamos las conversiones almacenando la conversión en un objeto; los números de conversión se clasifican por tipo (distancia, volumen, agrega el tuyo). Para cada campo de medición, tenemos un valor base (metros o litros, aquí) al que todo se convierte. Entonces, cuando ves yardas: 0.9144, sabes que esa es la cantidad de yardas que hay en un metro. Luego, para convertir yardas a, digamos, centímetros, multiplicamos yards por el primer parámetro (para obtener el número de metros) y luego dividimos el producto por cm, el número de metros en un centímetro. De esta manera, no tenemos que almacenar las tasas de conversión para cada par de valores. Esto también facilita la adición de nuevos valores más adelante.
  • En nuestro caso, esperamos que las unidades transferidas sean las mismas que las claves que estamos usando en la "tabla" de conversión. Si se tratara de una biblioteca real, nos gustaría admitir varios formatos, como "in", "inch" y "inches", y por lo tanto, querríamos agregar algo de lógica para hacer coincidir fromUnit con la clave correcta.
  • Al final de la función Convert, almacenamos el valor intermedio en betweenUnit, que se inicializa en false. De esa manera, si no tenemos fromUnit, betweenUnit será false al ingresar al método to, por lo que se lanzará un error.
  • Si no tenemos toUnit, se lanzará un error diferente. De lo contrario, dividiremos según sea necesario y devolveremos el valor convertido.

Ahora, vuelve a SpecRunner.html y vuelve a cargar la página. Ahora deberías ver esto (después de marcar "Show passed"):

¡Ahí tienes! Nuestras pruebas están pasando. Si estuviéramos desarrollando un proyecto real aquí, escribiríamos pruebas para una cierta parte de la funcionalidad, las haríamos pasar, escribiríamos pruebas para otra verificación, las haríamos aprobadas, etc. Pero como se trataba de un ejemplo sencillo, lo hemos hecho todo de una sola vez.

Y ahora que has visto este sencillo ejemplo del uso de Jasmine, veamos algunas funciones más que te ofrece.


Paso 4: aprendiendo los comparadores.

Hasta ahora, hemos utilizado dos comparadores: toEqual y toThrow. Hay, por supuesto, muchos otros. A continuación, presentamos algunos que probablemente te resulten útiles; puedes ver la lista completa en la wiki.

toBeDefined / toBeUndefined

Si solo deseas asegurarte de que una variable o propiedad esté definida, hay un comparador para eso. También hay uno para confirmar que una variable o propiedad sea undefined.

1
it("is defined", function () {
2
    var name = "Andrew";
3
    expect(name).toBeDefined();
4
})
5
6
it("is not defined", function () {
7
    var name;
8
    expect(name).toBeUndefined();
9
});

toBeTruthy / toBeFalsy

Si algo fuera verdadero o falso, estos comparadores lo harán.

1
it("is true", function () {
2
    expect(Lib.isAWeekDay()).toBeTruthy();
3
});
4
it("is false", function () {
5
    expect(Lib.finishedQuiz).toBeFalsy();
6
});

toBeLessThan / toBeGreaterThan

Para todos ustedes que les gustan los números Saben cómo funcionan estos:

1
it("is less than 10", function () {
2
    expect(5).toBeLessThan(10);
3
});
4
it("is greater than 10", function () {
5
    expect(20).toBeGreaterThan(10);
6
});

toMatch

¿Tienes algún texto de salida que debería coincidir con una expresión regular? El comparador toMatch está listo y dispuesto.

1
it("outputs the right text", function () {
2
    expect(cart.total()).toMatch(/\$\d*.\d\d/);
3
});

toContain

Este es bastante útil. Comprueba si una matriz o cadena contiene un elemento o subcadena.

1
it("should contain oranges", function () {
2
    expect(["apples", "oranges", "pears"]).toContain("oranges");
3
});

También hay algunos otros comparadores que puedes encontrar en la wiki. Pero, ¿qué pasa si quieres un comparador que no existe? Realmente, deberías poder hacer casi cualquier cosa con algún código de configuración y los comparadores que proporciona Jasmine, pero a veces es mejor abstraer algo de esa lógica para tener una prueba más legible. Casualmente (bueno, en realidad no), Jasmine nos permite crear nuestros propios comparadores. Pero para hacer esto, primero tendremos que aprender algo más.

Paso 5: cubriendo antes y después

A menudo, al probar una base de código, querrás realizar algunas líneas de código de configuración para cada prueba de una serie. Sería doloroso y detallado tener que copiar eso para cada llamada it, por lo que Jasmine tiene una pequeña característica útil que nos permite designar código para que se ejecute antes o después de cada prueba. Veamos cómo funciona esto:

1
describe("MyObject", function () {
2
    var obj = new MyObject();
3
4
    beforeEach(function () {
5
        obj.setState("clean");
6
    });
7
8
    it("changes state", function () {
9
        obj.setState("dirty");
10
        expect(obj.getState()).toEqual("dirty");
11
    })
12
    it("adds states", function () {
13
        obj.addState("packaged");
14
        expect(obj.getState()).toEqual(["clean", "packaged"]);
15
    })
16
});

En este ejemplo artificial, puedes ver cómo, antes de ejecutar cada prueba, el estado de obj se establece a "clean". Si no hicimos esto, los cambios realizados en un objeto en una prueba anterior persisten en la siguiente prueba de forma predeterminada. Por supuesto, también podríamos hacer algo similar con la función AfterEach:

1
describe("MyObject", function () {
2
    var obj = new MyObject("clean"); // sets initial state
3
4
    afterEach(function () {
5
        obj.setState("clean");
6
    });
7
8
    it("changes state", function () {
9
        obj.setState("dirty");
10
        expect(obj.getState()).toEqual("dirty");
11
    })
12
    it("adds states", function () {
13
        obj.addState("packaged");
14
        expect(obj.getState()).toEqual(["clean", "packaged"]);
15
    })
16
});

Aquí, estamos configurando el objeto para empezar y luego lo corregimos después de cada prueba. Si deseas la función MyObject para poder probar este código, puedes obtenerlo aquí en un gist de GitHub.

Paso 6: escribir comparadores personalizados

Como dijimos anteriormente, los comparadores personalizados probablemente serían útiles en ocasiones. Así que escribamos uno. Podemos agregar un comparador en una llamada BeforeEach o en una llamada it (bueno, supongo que podrías hacerlo en una llamada AfterEach, pero eso no tendría mucho sentido). Así es como empiezas:

1
beforeEach(function () {
2
    this.addMatchers({
3
4
    });
5
});

Bastante sencillo, ¿eh? Llamamos this.addMatchers, pasándole un parámetro de objeto. Cada clave de este objeto se convertirá en el nombre de un comparador, y la función asociada (el valor) será cómo se ejecute. Supongamos que queremos crear un comparador que compruebe si un número está entre otros dos. Esto es lo que escribirías:

1
beforeEach(function () {
2
    this.addMatchers({
3
        toBeBetween: function (rangeFloor, rangeCeiling) {
4
            if (rangeFloor > rangeCeiling) {
5
                var temp = rangeFloor;
6
                rangeFloor = rangeCeiling;
7
                rangeCeiling = temp;
8
            }
9
            return this.actual > rangeFloor && this.actual < rangeCeiling;
10
        }
11
    });
12
});

Simplemente tomamos dos parámetros, nos aseguramos de que el primero sea más pequeño que el segundo y devolvemos una declaración boolean que se evalúa como verdadera si se cumplen nuestras condiciones. Lo importante a tener en cuenta aquí es cómo obtenemos el valor que se pasó a la función expect: this.actual.

1
it("is between 5 and 30", function () {
2
    expect(10).toBeBetween(5, 30);
3
});
4
5
it("is between 30 and 500", function () {
6
    expect(100).toBeBetween(500, 30);
7
});

Esto es lo que hace el archivo SpecHelper.js; tiene una llamada beforeEach que agrega el comparador tobePlaying(). ¡Echale un vistazo!


Conclusión: ¡Diviértete!

Hay mucho más que puedes hacer con Jasmine: comparadores relacionados con funciones, espías, especificaciones asincrónicas y más. Te recomiendo que explores la wiki si estás interesado. También hay algunas bibliotecas adjuntas que facilitan las pruebas en el DOM: Jasmine-jQuery y Jasmine-fixture (que depende de Jasmine-jQuery).

Asi que, si no estás probando tu JavaScript hasta ahora, ahora es un excelente momento para comenzar. Como hemos visto, la sintaxis rápida y sencilla de Jasmine hace que las pruebas sean bastante sencillas. No hay ninguna razón para que no lo hagas, ¿verdad?