Saltar al contenido

¿No sabes cómo testear un controlador en Rails? ¡Este post es para ti!

Pruebas funcionales en Rails con RSpec y FactoryGirl; qué testear en un controlador —estados HTTP, plantillas, redirecciones y persistencia— cubriendo un CRUD completo.

9 min de lectura
  • ruby
  • rails
  • testing

Publicado originalmente en Medium

En el post pasado expliqué sobre lo que trata TDD y cómo testeamos nuestros modelos en Rails, así que esta vez veremos la forma en que podemos realizar pruebas funcionales a nuestra aplicación haciendo uso de la gema RSpec.

¿Pruebas funcionales? (ㆆ_ㆆ). Así es, pruebas funcionales. Esto porque en Rails hacer testing a nuestros controladores es una forma de hacer pruebas funcionales. Me enteré de esto semanas atrás, cuando buscaba ejemplos de pruebas funcionales; sin darme cuenta ya escribía este tipo de pruebas.

En Rails, las pruebas unitarias son para nuestros modelos, mientras que las pruebas funcionales son para nuestros controladores.

Lo primero que debemos tener en cuenta es: ¿qué vamos a testear? [aunque parezca tonto, este objetivo muchas veces es olvidado; al final se crean tests para probar cualquier cosa, menos para las que fueron creados (≖_≖ )]. Lo que testearemos en nuestros controladores será:

El estado de las peticiones. Ya que nuestros controladores manejan las peticiones que llegan a nuestra aplicación, debemos testear que estas tienen el estado esperado. Si nuestra petición es exitosa, esta debería tener un 'status: 200'.

La plantilla o vista que se debe renderizar. Muchos de nuestros métodos, en nuestro controlador, tendrán vistas asociadas, por lo que debemos asegurarnos de que estas son mostradas correctamente. Por ejemplo, nuestro método 'new' debe mostrar una vista que contenga un formulario, 'index' una vista que contenga todos los registros, etc.

¿Se redirigió a la página correcta? Puede sonar confuso con el punto anterior, pero esto generalmente se aplica para aquellos métodos que no renderizan alguna vista, sino que luego de realizar ciertos procesos nos redirigen a una vista en específico. Un claro ejemplo puede ser el método 'create', que luego de crear algún recurso (usuario, artículo, etc.) hace un 'redirect_to' a la URL del recurso.

¿Se guardó o actualizó correctamente el recurso? Probaremos si realmente nuestro recurso fue guardado en la base de datos o actualizado correctamente. Este punto va específicamente para los métodos 'create' y 'update'.

Existen muchos puntos más a tener en cuenta, como por ejemplo probar si el usuario ha sido autentificado correctamente o si luego de crear algún recurso se mostró el mensaje esperado en la página, etc. Sin embargo, solo tomé estos 4, pues considero que son los puntos más importantes dentro de una aplicación [obviamente esto puede variar dependiendo del tipo de aplicación].

Pasemos a ver algo de código. Este será nuestro controlador 'Users':

class UsersController < ApplicationController
 
  before_action :find_user, only: [:show, :edit, :update, :destroy]
 
  def index
    @users = User.all
  end
 
  def show
    # find_user
  end
 
  def new
    @user = User.new
  end
 
  def create
    @user = User.new(parameters)
    if @user.save
      redirect_to user_path(@user), notice: 'Se ha creado el usuario.'
    else
      render :new
    end
  end
 
  def edit
    # find_user
  end
 
  def update
    if @user.update(parameters)
      redirect_to user_path(@user), notice: 'Se actualizó el usuario.'
    else
      render :edit
    end
  end
 
  def destroy
  # find_user
    redirect_to action: :index, alert: 'Se eliminó el usuario' if @user.destroy
  end
 
  private
 
  def parameters
    params.require(:user).permit(:name, :last_name, :email)
  end
 
  def find_user
    if User.find_by(id: params[:id])
      @user = User.find(params[:id])
    else
      redirect_to users_path, alert: "No se encontró el usuario"
    end
  end
end

Nuestro users_controller tiene los métodos básicos para hacer nuestro CRUD (Create, Read, Update, Delete). Teniendo todo esto en cuenta, pasemos a ver nuestros tests para nuestro users_controller.

Aclaración: sé que te debes estar preguntando por qué no usé TDD (͡° ͜ʖ ͡°) como lo hice en los posts pasados, y esto se debe a que el post se haría demasiado largo con TDD ( ˘︹˘ ). Sin embargo, ¡usted sí debería aplicar TDD a todo lo que se mueva! (≖᷆︵≖). Bueno, literalmente a todo lo que se mueva, usted me entiende.

Bien, pasemos a ver nuestros tests:

describe UsersController do
 
  describe 'GET index' do
    before do
      @user = FactoryGirl.create(:user)
    end
 
    it 'ok' do
      get :index
      expect(response).to have_http_status(:ok)
      expect(response).to render_template('index')
      expect(assigns(:users)).to eq([@user])
    end
  end
 
  describe 'GET show' do
    it 'ok' do
      get :show, id: @user.id
      expect(response).to have_http_status(:ok)
      expect(response).to render_template('show')
      expect(assigns(:user)).to eq(@user)
    end
 
    it 'when user doesn\'t exist' do
      get :show, id: 69_696
      expect(response).to have_http_status(:redirect)
    end
  end
 
  describe 'GET new' do
    it 'ok' do
      get :new
      expect(assigns(:user)).to be_a_new(User)
    end
  end
 
  describe 'GET edit' do
    it 'ok' do
      get :edit, id: @user.id
      expect(assigns(:user)).to eq(@user)
    end
  end
 
  describe 'POST create' do
    before do
      @attributes = { name: 'Giancarlos', last_name: 'Isasi', email: 'giancarlos@devacademy.la' }
    end
 
    context 'with valid params' do
      it 'creates a new user' do
        User.new
        expect { post :create, user: @attributes }.to change(User, :count).by(1)
      end
 
      it 'assigns a newly created user as @user' do
        post :create, user: @attributes
        expect(assigns(:user)).to be_a(User)
      end
 
      it 'redirects to the created user' do
        post :create, user: @attributes
        expect(response).to redirect_to(user_path(User.last))
      end
    end
  end
 
  describe 'PUT update' do
    before do
      @attributes = { name: 'update Giancarlos' }
    end
 
    context 'with valid params' do
      it 'assigns the user as @user' do
        put :update, id: @user.id, user: @attributes
        expect(assigns(:user).name).to eq @attributes[:name]
      end
 
      it 'redirects' do
        put :update, id: @user.id, user: @attributes
        expect(response).to redirect_to(user_path(@user))
      end
    end
  end
end

Tranquilo, no se asuste; puede parecer magia negra, pero no lo es. Trataré de explicar cada uno de los tests lo mejor que pueda (҂◡̀_◡́)ᕤ

Primero tenemos un 'describe UsersController' que nos servirá para encerrar todos los tests de nuestro controlador. Como primeros tests tenemos los referentes a nuestro método INDEX, pero antes de todo hacemos un 'before' para crear un usuario antes de que se ejecuten todas las pruebas.

FactoryGirl.define do
  factory :user do
    name 'Giancarlos'
    last_name 'Isasi'
    email 'giancarlos@devacademy.la'
  end
end

[El usuario lo creé haciendo uso de una funcionalidad de la gema FactoryGirl; esta gema nos permite crear 'factories' (que contienen datos de prueba) de todos nuestros modelos y luego poder crearlos de forma muy sencilla]. Entonces, como expliqué al inicio, tendremos que testear el estado de la petición y la plantilla o vista que será mostrada.

describe 'GET index' do
  before do
    @user = FactoryGirl.create(:user)
  end
 
  it 'ok' do
    get :index
    expect(response).to have_http_status(:ok)
    expect(response).to render_template('index')
    expect(assigns(:users)).to eq([@user])
  end
end

Dentro de nuestro test, que de por sí tiene el nombre 'ok', testeamos aquello con 'have_http_status(:ok)' [en vez de ':ok' puede ir 200; después de todo es lo mismo] y con 'render_template('index')' [sí, index es el nombre de nuestra vista, por si no lo notaron] indicamos qué vista debe ser mostrada; y por último comprobamos que nuestro usuario creado (@user) sea igual a nuestra colección 'users' [assigns es un hash, accesible dentro de las pruebas de Rails, que contiene todas las variables de instancia que estarían disponibles, en nuestra vista o 'view', en este punto].

describe 'GET show' do
  it 'ok' do
    get :show, id: @user.id
    expect(response).to have_http_status(:ok)
    expect(response).to render_template('show')
    expect(assigns(:user)).to eq(@user)
  end
 
  it 'when user doesn\'t exist' do
    get :show, id: 69_696
    expect(response).to have_http_status(:redirect)
  end
end

Ahora veamos los tests para nuestro método SHOW. Tenemos dos tests (líneas 18 y 24): el primero nos servirá para comprobar que efectivamente, al buscar un usuario, este nos arroja un http_status de '200' (línea 19), se renderiza la plantilla o vista 'show' (línea 20) y al final comprobamos que nuestro @user sea igual a la variable 'user' que se tiene hasta ese punto [la forma en como funciona assigns puede parecer confusa, pero no es nada más que un hash que nos devuelve el valor que la variable tiene hasta ese punto]. Luego, nuestro segundo test (línea 24) es para comprobar que cuando un usuario no exista, se redirija automáticamente a la URL que especificamos en nuestro método privado 'find_user' [este método se encuentra en nuestro users_controller; pueden revisarlo en el código que coloqué más arriba]. Ahora, nótese que tanto en el primer como en el segundo ejemplo tenemos algo como 'get :show, id: @user.id…' y sí, es lo que piensas: con ello estamos haciendo una petición 'GET' a nuestro método 'SHOW' para luego pasarle parámetros; en este caso le estamos pasando el id del usuario a buscar.

describe 'GET new' do
  it 'ok' do
    get :new
    expect(assigns(:user)).to be_a_new(User)
  end
end

Sigamos con nuestro método NEW, el cual solo tiene un test. Este hace una petición 'GET' a nuestro método 'NEW' para luego comprobar que nuestra variable 'user' ha sido instanciada correctamente [en nuestro método 'NEW' casi siempre tenemos algo como '@user = User.new', el cual servirá para instanciar la variable con la que crearemos nuestro formulario o 'form'].

describe 'POST create' do
  before do
    @attributes = { name: 'Giancarlos', last_name: 'Isasi', email: 'giancarlos@devacademy.la' }
  end
 
  context 'with valid params' do
    it 'creates a new user' do
      User.new
      expect { post :create, user: @attributes }.to change(User, :count).by(1)
    end
 
    it 'assigns a newly created user as @user' do
      post :create, user: @attributes
      expect(assigns(:user)).to be_a(User)
    end
 
    it 'redirects to the created user' do
      post :create, user: @attributes
      expect(response).to redirect_to(user_path(User.last))
    end
  end
end

Luego del NEW, continuemos con el CREATE. Para ello, antes especificaremos (líneas 45–47) los parámetros correspondientes a nuestro usuario a crear. Creamos un contexto llamado 'with valid params' [para agrupar las pruebas que se llevarán a cabo cuando nuestro método 'CREATE' reciba parámetros válidos (que no tiene un name 'nil', el email cumple con el formato especificado, etc.)]; dentro de este contexto tenemos 3 pruebas.

'it creates a new user' primeramente instancia nuestra variable user para que esta sea creada (no es necesario colocar @user = User.new, basta con User.new), y luego hacemos un 'POST' pasándole un json 'user' que contiene los parámetros de nuestro usuario a crear, y esperamos que este responda con un cambio de 1 en el número de registros que hay en nuestro modelo User.

'assigns a newly created user as @user': aquí hacemos casi lo mismo, hacemos el 'POST' junto a los parámetros respectivos, y al final esperaremos que nuestra variable 'user', llegado a este punto, tenga la misma estructura que nuestro modelo User [el 'be_a(User)' comprobará que nuestro usuario sea un 'User'; esto lo hará comprobando que el tipo de dato de cada uno de los valores que tiene nuestra variable 'user' coincida con los tipos de dato que tienen las columnas de nuestro modelo User].

'redirects to the created user': sí, es lo que piensas, hacemos lo mismo, el 'POST' junto a los parámetros, y al final esperamos que nos redirija a la página de ese usuario creado. Por si lo notaron, a la URL 'user_path' tenemos que pasarle como parámetro el último usuario creado [obviamente el último usuario de nuestro modelo User debería coincidir con nuestro usuario creado].

Luego tenemos el EDIT [no seguí el orden que está en el archivo de tests, pues considero que tanto 'NEW' y 'POST' deberían ir juntos, y lo mismo para el 'EDIT' y 'UPDATE'].

describe 'GET edit' do
  it 'ok' do
    get :edit, id: @user.id
    expect(assigns(:user)).to eq(@user)
  end
end

Entonces volvemos a hacer una petición 'GET' a nuestro método 'EDIT', pasándole el id del usuario a editar, y luego, con el test, esperamos que nuestra variable 'user' (la encontrada haciendo uso del id que le pasamos), hasta este punto, sea igual a nuestra variable @user (que fue creada al inicio).

describe 'PUT update' do
  before do
    @attributes = { name: 'update Giancarlos' }
  end
 
  context 'with valid params' do
    it 'assigns the user as @user' do
      put :update, id: @user.id, user: @attributes
      expect(assigns(:user).name).to eq @attributes[:name]
    end
 
    it 'redirects' do
      put :update, id: @user.id, user: @attributes
      expect(response).to redirect_to(user_path(@user))
    end
  end
end

Y finalmente testearemos nuestro método UPDATE. Para ello, con el 'before do…' especificamos los datos que deseamos actualizar; en este caso se trata del nombre. Creamos un contexto para agrupar nuestros tests que tendrán parámetros válidos [te debes estar haciendo la pregunta: ¿si existe un 'with valid params' debe existir un 'with invalid params', verdad? Y sí, tienes razón: básicamente con parámetros inválidos tendríamos que testear que se muestra algún mensaje o 'alert' de que el user no fue creado o actualizado, comprobar el 'http_status', etc., pero lastimosamente para esta oportunidad no entraré en ello].

Entonces hacemos una petición 'PUT' a nuestro método 'UPDATE'; en los dos tests (líneas 73 y 79) le pasamos como parámetro el id del usuario a actualizar, junto a los datos que se actualizarán. En el primer test esperamos que nuestra variable que tenemos hasta ese momento, 'assigns(:user).name', sea igual al 'name' de nuestra variable @attributes, para comprobar que efectivamente nuestro usuario ha sido actualizado con el nuevo 'name' que le especificamos. En el segundo test, luego de hacer la petición y pasarle los parámetros, esperamos que nos redirija a la URL ('user_path') del usuario actualizado; no olvidemos pasarle como parámetro nuestro objeto @user.

They didn't use TDD

Espero que el post le sirva como una buena introducción a los tests con RSpec en Ruby on Rails, así como también para crear los tests necesarios con los cuales pueda cubrir un CRUD básico en un controlador. Si tiene alguna duda, no dude en escribirla en la caja de comentarios, estaré gustoso de ayudarlo (◡̀_◡́҂). Y si aún no tiene idea de qué es TDD, le recomiendo leer mi post en donde explico de qué trata (͡° ͜ʖ ͡°), así como también leer cómo es que se puede aplicar TDD al crear una API con Ruby on Rails (͡° ͜ʖ ͡°).

Puedes seguir leyendo otros posts:

Gracias por leer; si le gustó, ¡¡no olvide recomendar y compartir el post!! (•◡•)\ (•◡•) /

Gracias por leer

Links: