Saltar al contenido

Cómo testear tus modelos con RSpec en Ruby on Rails

Una introducción a TDD en Ruby on Rails; el ciclo red-green-refactor y cómo testear las validaciones de un modelo con RSpec.

6 min de lectura
  • ruby
  • rails
  • testing
  • tdd

Publicado originalmente en Medium

En este post hablaremos sobre TDD y sus ventajas, del mismo modo mostraremos un ejemplo de cómo hacer TDD para los modelos en Ruby on Rails haciendo uso de la famosa gema RSpec, y finalmente explicaré, desde mi punto de vista, por qué los tests son importantes.

Muchas veces hemos escuchado sobre el famoso "Test-Driven Development (TDD)", que no es nada más que, primero hacer los tests y correrlos esperando que estos fallen (RED), escribir el código y volver a correr los tests esperando que estos pasen (GREEN) y finalmente refactorizar el código.

La primera vez que escuché sobre TDD pensé:

¿Escribir tests antes que el código? ¿Cómo pruebas algo que aún no has codeado? (ノ ಥ ウಥ )ノ

Luego entendí por qué me miraban así cuando olvidaba hacer los tests:

Do you even TDD, bro?

Con todas esas dudas y más, me sumergí en el oscuro abismo de TDD, 'Test-first', etc. [en sí existen muchos conceptos y 'teorías' respecto al desarrollo guiado por tests, no explicaré todos porque nos tomaría todo el día, noche, mes, año…]. Aunque al inicio me costó entender cómo funcionaba, ahora agradezco a Dios, Buda, Alá, Zeus, Mahoma y todos los dioses habidos y por haber ╰( ಠ ω ಠ )╯. Con el TDD pude reemplazar los 'clicks' por 'código'; me sorprendió mucho el poder probar/testear interfaces completas de webs a 'punta de código'.

I know TDD

Ok, entonces dejémonos de parla y ¡¡vayamos a la acción!! (┛◉Д◉)┛

Actualmente programo en Ruby y trabajo con su famoso framework "Ruby on Rails".

RoR es un entorno de desarrollo web de código abierto que está optimizado para la satisfacción de los programadores y para la productividad sostenible.

Ruby on Rails también obliga a no repetir partes de código (Don't repeat yourself) y permite que el desarrollador pueda comenzar su trabajo sin tener que meterse en las tortuosas configuraciones que muchas veces tenemos que hacer en nuestro entorno de trabajo (Convention over configuration).

Ruby on Rails

Ruby on Rails, por defecto, tiene integrada la funcionalidad para los tests. Al crear tu aplicación notarás que esta tendrá una carpeta "test". Dentro de esta carpeta podrás colocar todas las pruebas que quieras. Casi siempre se tiene una carpeta 'models' en donde colocas los tests para tus modelos, otra llamada 'controllers' en donde colocas los tests para los controllers y así; y digo casi siempre porque este tipo de estructura depende del tipo de aplicación así como del tipo de desarrollador [por ejemplo, para las API's algunos desarrolladores prefieren crear una carpeta llamada 'requests' y dentro de ella colocar los tests para los controllers, pues consideran que de alguna manera ellos no están testeando los controllers sino las peticiones o 'requests'].

# código extraído de: http://guides.rubyonrails.org/testing.html
 
require 'test_helper'
 
class PostTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  test "the truth" do
    assert true
  end
end

El código de arriba [por fin algo de código (´⌣ˋ)] es un ejemplo de cómo RoR crea por defecto los tests para los modelos. Pero para esta ocasión nosotros usaremos la gema RSpec.

# código extraído de: https://github.com/rspec/rspec-rails
require "rails_helper"
 
RSpec.describe User, :type => :model do
  it "orders by last name" do
    lindeman = User.create!(first_name: "Andy", last_name: "Lindeman")
    chelimsky = User.create!(first_name: "David", last_name: "Chelimsky")
 
    expect(User.ordered_by_last_name).to eq([chelimsky, lindeman])
  end
end

RSpec usa una sintaxis un tanto diferente; puedes revisar la documentación en los links que dejaré al final del post. Mientras que la sintaxis por defecto que nos da RoR nos obliga a definir los tests directamente (test "the truth" do…), RSpec nos permite poder agruparlos usando 'describe', que describe, valga la redundancia, lo que hará el conjunto de tests [en RSpec un test es un 'ejemplo' y estos son definidos usando 'it'].

# fuente: https://github.com/rspec/rspec-rails
 
require "rails_helper"
 
RSpec.describe PostsController, :type => :controller do
  describe "GET #index" do
    it "responds successfully with an HTTP 200 status code" do
      get :index
      expect(response).to be_success
      expect(response).to have_http_status(200)
    end
 
    it "renders the index template" do
      get :index
      expect(response).to render_template("index")
    end
 
    it "loads all of the posts into @posts" do
      post1, post2 = Post.create!, Post.create!
      get :index
 
      expect(assigns(:posts)).to match_array([post1, post2])
    end
  end
end

En el ejemplo de arriba podemos ver cómo la sintaxis es mucho más clara y entendible: tenemos un 'describe' que agrupa a 3 ejemplos (tests). Por cierto, si lo notaron, en este (línea 5) y en el anterior ejemplo (línea 4) se define lo que se está testeando con ":type => controller do"; no es necesario, pero es una buena práctica. Veamos un ejemplo sencillo de cómo testear los modelos junto a sus validaciones.

Primero, esta será la estructura de nuestro modelo, que en este caso se llamará contact.rb y que contiene los campos para firstname, lastname y email:

# ./app/models/contact.rb
 
class Contact < ActiveRecord::Base
  has_many :phones
  accepts_nested_attributes_for :phones
 
  validates :firstname, presence: true
  validates :lastname, presence: true
  validates :email, presence: true, uniqueness: true
 
end

Estos serían nuestros tests para nuestro modelo 'contact' usando la gema RSpec. Nótese que el nombre del archivo que contiene los tests tiene el formato de 'model_spec.rb':

# ./spec/models/contact_spec.rb
 
require 'rails_helper'
 
describe Contact do
  it "is valid with a firstname, lastname and email" do
    contact = Contact.new(
      firstname: 'Aaron',
      lastname: 'Sumner',
      email: 'tester@example.com')
    expect(contact).to be_valid
  end
 
  it "is invalid without a firstname" do
    contact = Contact.new(firstname: nil)
    contact.valid?
    expect(contact.errors[:firstname]).to include("can't be blank")
  end
 
  it "is invalid without a lastname" do
    contact = Contact.new(lastname: nil)
    contact.valid?
    expect(contact.errors[:lastname]).to include("can't be blank")
  end
 
  it "is invalid without an email address" do
    contact = Contact.new(email: nil)
    contact.valid?
    expect(contact.errors[:email]).to include("can't be blank")
  end
end

Lo primero que hacemos es un 'describe' para agrupar todos los ejemplos (tests) pertenecientes al modelo. Dentro de un 'describe' también podemos hacer otro 'describe' [podríamos usar uno para agrupar los ejemplos (tests) pertenecientes a algún método de nuestro modelo Contact, como por ejemplo un def name que nos devuelva el nombre completo de un contacto en específico]. Tenemos 4 'it' o ejemplos, todos enfocados a probar las validaciones definidas en nuestro modelo contact.rb.

...
  it "is valid with a firstname, lastname and email" do
    contact = Contact.new(
      firstname: 'Aaron',
      lastname: 'Sumner',
      email: 'tester@example.com')
    expect(contact).to be_valid
  end
...

El primer ejemplo (test) es 'is valid with a firstname, lastname, and email'. Dentro de nuestro modelo tenemos la validación "presence: true" para nuestros campos. Lo que hacemos para testear esto es, primero, crear un nuevo Contacto (líneas 3–6) con cualquier dato de ejemplo que deseemos [existe una gema llamada Faker que nos permite crear y usar estos objetos o datos de prueba de manera más sencilla].

Luego especificamos lo que esperamos que suceda con 'expect' (línea 7): le pasamos como parámetro nuestro objeto contact recién creado y con 'be_valid' le decimos que este objeto debería ser válido. Ser válido porque se cumple con el presence: true ლ(╹◡╹ლ).

En el segundo test…

...
  it "is invalid without a firstname" do
    contact = Contact.new(firstname: nil)
    contact.valid?
    expect(contact.errors[:firstname]).to include("can't be blank")
  end
...

…trataremos de testear que un contact deba tener obligatoriamente un firstname, esto por obvias razones. Lo primero (línea 3) es crear nuestro objeto contact con un firstname nil y lo que esperaremos (línea 5) es que nuestro objeto contact, dentro de su método errors, debería tener incluido ('include') el mensaje de 'can't be blank'. Y lo mismo hacemos para los tests restantes: en uno lo hacemos para corroborar que un modelo sea inválido si no tiene un lastname (it "is invalid without a lastname" do) y en el otro, para corroborar que sea inválido si no tiene un email (it "is invalid without an email" do). Entonces podemos decir que de cierta forma creamos el test necesario para nuestro modelo Contact. We did it!!

RSpec nos permite escribir las pruebas unitarias así como las pruebas de integración.

I know TDD

Para terminar…

…quizás al inicio te sea complicado o incluso innecesario escribir los tests primero y luego el código, pero una de las ventajas más grandes es que al final del proyecto no necesitarás utilizar algún depurador o debugger, o hacer 'clicks' como un crazy-tester en las interfaces. Además, TDD agrega valor añadido a las aplicaciones, ya que se crean con mucha más calidad. Y no solo esto, sino que también permite detectar algún error que genera el agregar un nuevo componente, módulo o funcionalidad a la aplicación. Algunos dicen que TDD está muerto, pero no es cierto; creo que solo se está cambiando el enfoque al que va dirigido.

TDD is dead…

Y sí, lo sé, quizás te tome mucho más tiempo desarrollar una aplicación haciendo uso de TDD [y esa es la principal razón por la que algunos desarrolladores no escriben tests], pero créeme, al final agradecerás haberlo hecho ya que, como lo mencioné anteriormente, la calidad del software será mayor. Espero que este post le sea de ayuda y le sirva como una pequeña introducción a TDD y a la fabulosa gema RSpec.

Puedes revisar otros posts:

Links:

¡Gracias por leer! ( ゚,_ゝ゚) Si te gustó, puedes recomendar el post a tus amigos o compartirlo.

Gracias por leer