homeASCIIcasts

261: Tests de JavaScript con Jasmine 

(view original Railscast)

Other translations: En Ja

Written by Juan Lupión

La mayoría de desarrolladores Rails entienden la ventaja que supone escribir tests del código Ruby de su aplicación, pero con frecuencia las pruebas de JavaScript consisten en ejecutar la aplicación en el navegador e ir haciendo pruebas en busca de errores. Dado que las aplicaciones van siendo cada vez más complejas e incorporan más funcionalidad surge la necesidad de tener una forma automática de probar el código JavaScript. En este episodio Vamos a ver una forma de hacerlo.

A continuación se muestra la aplicación con la que vamos a trabajar. Se trata de un formulario básico de pedido con un campo de texto para un número de tarjeta de crédito. Queremos añadir validación en el lado de cliente a este campo de forma que se valide el número introducido cuando el campo pierde el foco.

Página de pedido con el campo de tarjeta de crédito.

Aunque no queremos (aún) programar una validación completa de la tarjeta de crédito la lógica tiene cierta complejidad así que queremos asegurarnos de que el código se encuentra completamente cubierto por los tests. Podríamos considerar el uso de tests de aceptación, que ya vimos en el episodio 257 [verlo, leerlo], y que nos permitirían escribir tests de alto nivel de la aplicación mediante Capybara pudiendo apoyarnos en Selenium para soportar JavaScript.

Pero hay veces en que el JavaScript es tan complejo que tenemos que acercarnos más al código, algo así como los tests unitarios para JavaScript. Aquí es donde encaja Jasmine. Se trata de un framework de testing para JavaScript que funciona de manera muy similar a RSpec con funciones útiles para organizar los tests como describe e it. Por supuesto, y a diferencia de RSpec, los tests se escriben en JavaScript y no en Ruby.

Instalación de Jasmine

Existe una gema que facilita el uso de Jasmine en las aplicaciones Rails, así que empezaremos añadiéndola a nuestra aplicación y luego ejecutando bundle para instalarla. Vamos a restringir el uso de Jasmine a los entornos de development y test para impedir que se active en producción. Nótese que ya estamos usando jQuery en la aplicación: su uso no es obligatorio con Jasmine pero es de mucha ayuda.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'
gem 'jquery-rails'
gem 'jasmine', :group => [:development, :test]

Tras la instalación de la gema debemos ejecutar un generador para instalar los archivos de Jasmine.

$ rails g jasmine:install
      create  spec
      create  spec/javascripts/helpers/.gitkeep
      create  spec/javascripts/support/jasmine.yml
      create  spec/javascripts/support/jasmine_config.rb
      create  spec/javascripts/support/jasmine_runner.rb

Los ficheros se instalan en el directorio spec. Jasmine se usa por lo general junto con RSpec aunque podemos utilizarla de forma independiente.

Nuestra primera especificación

Los ficheros de especificación se guardan en la carpeta /spec/javascripts y en nuestro caso el nombre del archivo será credit_card_spec.js (por cierto, si usamos Textmate hay un paquete disponible que proporciona atajos útiles como des para crear una nueva descripción e it para crear un nuevo bloque). Nuestra primera especificación comprobará que el número introducido se sanea quitando los guiones y espacios.

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function() {
  it("cleans the number by removing spaces and dashes", function() {
    expect(CreditCard.cleanNumber("123 4-5")).toEqual("12345");
  });
});

Jasmine utiliza expect en lugar de assert para comprobar que un objeto tiene cierto valor. En nuestro test comprobamos que el método cleanNumber de un objeto CreditCard devuelve “12345” cuando recibe “123 4-5”. Obviamente todavía no existen ni el objeto CreditCard ni el método cleanNumber porque estamos siguiendo la filosofía TDD.

Ya podemos lanzar el servidor de pruebas de Jasmine con rake jasmine. Esto lanzará un servidor en el puerto 8888, y si visitamos dicha página veremos los resultados de la ejecución de los tests.

Nuestra primera especificación que falla.

Como era de esperar vamos a tener que escribir el código para que los tests pasen. Este código lo pondremos en un fichero llamado credit_card.js, en el directorio public/javascripts, donde crearemos el objeto, así como un intento de escribir la función cleanNumber.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/, "");
  }
}

Si ahora recargamos la página de Jasmine veremos un error diferente. Jasmine ya no se queja de que le falta el objeto creditCard pero parece que el método cleanNumber no elimina todos los espacios y guiones.

El test sigue fallando.

El problema reside en la expresión regular, a la que le falta la opción g para que haga una sustitución global en toda la cadena.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/g, "");
  }
}

Con esto podemos comprobar que el error queda corregido recargando la página, tras lo que veremos que nuestro test ya pasa.

El test pasa.

A continuación mejoraremos la validación validando la suma de control con el algoritmo del resto de dividir por 10. Escribamos un test con dos resultados esperados para probar este código, uno que espera un número válido de tarjeta de crédito y otro que espera uno no válido y que debería fallar. Estos tests invocarán a una nueva función llamada validNumber, en un caso enviarán un número válido de Visa y otro que pasará un número incorrecto que debería fallar. Nótese que ponemos espacios y guinones en los números para también probar que la validación los tiene en cuenta.

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function () {
  it("cleans the number by removing spaces and dashes", function() {
    expect(CreditCard.cleanNumber("123 4-5")).toEqual("12345");
  });
  
  it("validates based on mod 10", function () {
    expect(CreditCard.validNumber("4111 1111 1111-1111")).toBeTruthy();
    expect(CreditCard.validNumber("4111 1111 1111-1121")).toBeFalsy();
  });
});

Si volvemos a cargar la página de Jasmine veremos un error porque no se puede encontrar la función validNumber.

El test que falla con la validación del módulo 10.

Escribamos la función en el fichero credit_card.js.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/g, "");
  },
  
  validNumber: function(number) {
    var total = 0;
    number = this.cleanNumber(number);
    for (var i=number.length-1; i >= 0; i--) {
      var n = parseInt(number[i]);
      if ((i+number.length) % 2 == 0) {
        n = n*2 > 9 ? n*2 - 9 : n*2;
      }
      total += n;
    };
    return total % 10 == 0;
  }
}

La próxima vez que carguemos la página de Jasmine veremos que los tests ya pasan.

The mod10 validation now passes.

Uso del navegador para comprobar la validación

Ahora que funciona la validación ya podemos añadirla a nuestra página web. Para comprobar que la validación sigue funcionando en la web podemos utilizar Capybara y Selenium. Si la página tiene mucho código JavaScript sería difícil de probar el código de validación de manera aislada, pero en este caso podemos hacerlo (y a continuación veremos cómo) mediante el uso de fixturas de HTML.

Jasmine-jquery es, como su nombre indica, una extensión de jQuery para utilizar con Jasmine. Permite utilizar fixturas, lo que quiere decir que podemos probar el JavaScript contra un fragmento de HTML sencillo. También incluye una larga lista de matchers que se pueden utilizar con expect. Se puede instalar esta extensión con curl descargando el archivo en el directorio spec/javascripts/helpers para que se incluya automáticamente.

$ curl http://cloud.github.com/downloads/velesin/jasmine-jquery/jasmine-jquery-1.2.0.js > spec/javascripts/helpers/jasmine_jquery-1.2.0.js

Tenemos que hacer un cambio en la sección src_files del fichero jasmine.yml en el directorio spec/javascripts/support porque viene configurado para utilizar Prototype por defecto:

/spec/javascripts/support/jasmine.yml

src_files:
  - public/javascripts/prototype.js
  - public/javascripts/effects.js
  - public/javascripts/controls.js
  - public/javascripts/dragdrop.js
  - public/javascripts/application.js
  - public/javascripts/**/*.js

Cambiemos estas referencias por las correspondientes a jQuery. Por defecto cualquier fichero del directorio javascripts será incluído pero esto puede dar problemas de ahí que sea preferible irlos incluyendo manualmente, lo que además garantiza que se cargan en el orden adecuado.

/spec/javascripts/support/jasmine.yml

src_files:
  - public/javascripts/jquery.js
  - public/javascripts/credit_card.js

Nos hará falta un directorio para los ficheros de las fixturas, por lo que crearemos uno en /spec/javascript/fixtures. En él crearemos un fichero llamado order_form.html, que contendrá un sencillo formulario de HTML que nos servirá para probar nuestra validación de tarjetas de crédito. Sólo hace falta un campo de texto con un id para poder referenciarlo desde JavaScript, y un div para mostrar los errores.

/spec/javascripts/fixtures/order_form.html
<form>
  <input type="text" id="card_number">
  <div id="card_number_error"></div>
</form>

Para que nuestro código de validación de tarjeta de crédito sea más sencillo lo vamos a convertir en un plugin de jQuery que llamaremos validateCreditCardNumber. Este número validará el valor de cualquier campo de texto al que se adjunte, y se activará cuando dicho campo pierda el foco. Si existen errores de validación establecerá el texto del elemento cuyo id sea el mismo que el campo de texto con el sufijo _error.

Antes de crear el plugin tenemos que escribir una especificación que utilizará nuestra nueva fixtura:

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function () {
  // Se omiten las otras especificaciones
  it("validates when text field loses focus", function() {
    loadFixtures("order_form.html");
    $("#card_number").validateCreditCardNumber();
    $("#card_number").val("123");
    $("#card_number").blur();
    expect($("#card_number_error")).toHaveText("Invalid credit ↵
      card number.");
  });
});

En esta especificación cargamos la fixtura invocando a loadFixtures y luego invocamos a nuestro nuevo plugin sobre el campo de texto. Luego establecemos el valor del campo de texto a un número de tarjeta de crédito incorrecto y llamamos a blur() sobre él para hacer que el plugin entre en acción. Por último comprobamos que el elemento que muestra el mensaje de error contiene el mensaje correcto utilizando la función toHaveText que viene con jasmine-jquery. Como es de esperar cuando cargamos la página vemos que el test falla porque jQuery no conoce el método validateCreditCardNumber.

El test fallido del <em>plugin</em> de jQuery.

A continuación escribiremos el plugin validateCreditCardNumber. Este plugin atenderá el evento blur en cualquier elemento al que se adjunte y cuando se active validará el valor de dicho elemento. A continuación usará la función validNumber que escribimos anteriormente para comprobar que el número de tarjeta es correcto y si no lo es mostrar un error.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/g, "");
  },
  
  validNumber: function(number) {
    var total = 0;
    number = this.cleanNumber(number);
    for (var i=number.length-1; i >= 0; i--) {
      var n = parseInt(number[i]);
      if ((i+number.length) % 2 == 0) {
        n = n*2 > 9 ? n*2 - 9 : n*2;
      }
      total += n;
    };
    return total % 10 == 0;
  }
}

(function ($){
  $.fn.validateCreditCardNumber = function () {
    return this.each(function () {
      $(this).blur(function () {
        if (!CreditCard.validNumber(this.value)) {
          $("#" + this.id + "_error").text("Invalid credit card number.");
        }
      });
    });
  };
})(jQuery);

Si recargamos la página de tests veremos que este test pasa y nuestro plugin parece funcionar.

Ya pasan las especificaciones del <em>plugin</em>.

Integración del plugin

Ahora que ya tenemos las pruebas aisladas de nuestro plugin vamos a integrarlo en nuestra aplicación. Hay tres ficheros que debemos cambiar. Primero incluyamos nuestro nuevo plugin de jQuery en el fichero de layout.

/app/views/layouts/application.html.erb

<%= javascript_include_tag :defaults, "credit_card" %>

A continuación actualizaremos el formulario que tiene el campo de la tarjeta de crédito añadiendo un elemento para mostrar mensajes de error. Como el formulario es para Order y el campo se llamada credit_card_number el elemento tiene que tener un id igual a order_credit_card_number_error.

/app/views/orders/_form.html.erb

<%= form_for @order do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :credit_card_number %><br />
    <%= f.text_field :credit_card_number %>
    <span id="order_credit_card_number_error">
  </p>
  <p>
    <%= f.label :credit_card_expires_on %><br />
    <%= f.date_select :credit_card_expires_on, :add_month_numbers => true, :start_year => Time.now.year, :order => [:month, :year] %>
  </p>
  <p><%= f.submit %></p>
<% end %>

Finalmente añadiremos el siguiente código en application.js para añadir la validación al campo de texto del número de tarjeta de crédito.

/public/javascripts/application.js

$(function () {
  $("#order_credit_card_number").validateCreditCardNumber();
});

Podemos hacer la prueba visitando la página de nuevo pedido e introduciendo un número incorrecto de tarjeta de créidto. Cuando salgamos del campo de texto aparecerá el mensaje de error.

El mensaje de error que aparece cuando se introduce un número incorrecto de tarjeta de crédito.

Pero hay todavía un error en la página. Si cambiamos el número de la tarjeta de crédito y ponemos uno que sea válido el mensaje de error permanece en la página. Cuando se introduzca el código correcto se debería borrar el mensaje de error si es que se estaba mostrando.

Lo primero que tenemos que hacer es escribir un test que falle. En este caso no tenemos que escribir una nueva especificación, sino que podemos añadir un nuevo expect en el código de especificación ya existente para el caso en que no se debe mostrar un error para los números válidos.

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function () {
  // Other specs omitted.  
  it("validates when text field loses focus", function() {
    loadFixtures("order_form.html");
    $("#card_number").validateCreditCardNumber();
    $("#card_number").val("123");
    $("#card_number").blur();
    expect($("#card_number_error")).toHaveText("Invalid credit card number.");

    $("#card_number").val("4111 1111 1111-1111");
    $("#card_number").blur();
    expect($("#card_number_error")).toHaveText("");
  });
});

Cuando carguemos la página de Jasmine veremos que hemos reproducido correctamente el error y que se muestra el test que falla. Esperamos que el div no tenga texto pero todavía está mostrando el mensaje.

El test que falla comprueba que no se muestran errores para un número válido.

Es fácil corregir este error. Podemos modificar el código de nuestro plugin de jQuery para que se oculte el mensaje de error si el número de tarjeta introducido es válido.

/public/javascripts/credit_card.js

(function ($){
  $.fn.validateCreditCardNumber = function () {
    return this.each(function () {
      $(this).blur(function () {
        if (!CreditCard.validNumber(this.value)) {
          $("#" + this.id + "_error").text("Invalid credit ↵
  card number.");
        }
        else {
          $("#" + this.id + "_error").text("");
        }
      });
    });
  };
})(jQuery);

Si volvemos a cargar la página de Jasmine veremos que ya pasan todos los tests.

Ya pasa el test.

Por último lo probaremos en el navegador una vez más y esta vez cuando introduzcamos un número incorrecto para que aparezca el error y luego introduzcamos un número válido veremos que el mensaje desaparece.

Ya se oculta el mensaje de error al introducir un número correcto.

Un consejo

En lugar de recargar la página que muestra los tests cada vez que hacemos un cambio podemos ejecutar rake jasmine:ci. Cuando lo hagamos se abrirá Firefox, se ejecutarán los tests con Selenium y a continuación se mostrará la salida en la ventana de terminal.

Con esto concluye este episodio en el que hemos estudiado Jasmine. Es una forma muy interesante de escribir los tests del JavaScript en nuestra aplicación con el mismo rigor que nuestro código Ruby.