3. Specs de modelos
Con RSpec instalado correctamente, pongámoslo a trabajar y comencemos a construir una suite de pruebas confiables. Empezaremos con los componentes fundamentales de TasteDrivenDishes: sus modelos.
En este capítulo, completaremos las siguientes tareas:
- Primero crearemos specs de modelos para los modelos existentes.
- Luego, escribiremos pruebas que pasen para las validaciones, los métodos de clase y los métodos de instancia de un modelo, y organizaremos nuestras specs en el proceso.
Crearemos nuestros primeros archivos spec para los modelos existentes de forma manual. Más adelante, al agregar nuevos modelos a la aplicación, los prácticos generadores de RSpec que configuramos en el capítulo 2 generarán archivos de plantilla vacíos por nosotros.
Anatomía de un spec de modelo
Creo que la forma más fácil de aprender a hacer pruebas es a nivel del modelo, porque hacerlo te permite examinar y probar los componentes fundamentales de una aplicación. Un código bien probado a este nivel proporciona una base sólida para un código base general confiable.
Para empezar, un spec de modelo debería incluir pruebas para lo siguiente:
- Cuando se instancia con atributos válidos, un objeto del modelo debería ser válido.
- Los objetos que no cumplen con los requisitos de validación no deberían ser válidos.
- Los métodos de clase y de instancia funcionan como se espera.
Este es un buen momento para ver la estructura básica de un spec de modelo de RSpec. Me resulta útil pensarlos como esquemas individuales. Por ejemplo, veamos los requisitos más simples de nuestro modelo User:
describe User do
it "is valid with an email, password, nickname, and API token"
it "requires a nickname"
it "requires a unique nickname"
it "requires an email"
it "requires a unique email"
it "requires a password"
it "requires an API token"
it "sets a new user's API token"
end
Ampliaremos este esquema en unos minutos, pero esto nos da mucho con qué trabajar para empezar. Es un spec sencillo para un modelo ciertamente simple, pero nos lleva a nuestras primeras cuatro buenas prácticas:
- Describe un conjunto de expectativas–en este caso, cómo debería ser el modelo User y cómo debería comportarse.
-
Cada ejemplo (una línea que comienza con
it) espera solo una cosa. Observa que estoy probando cada validación por separado. De esta manera, si un ejemplo falla, sé que es por esa validación específica, y no tengo que revisar a fondo la salida de RSpec en busca de pistas–al menos, no tan profundamente. -
Cada ejemplo es explícito. La cadena descriptiva después de
ites técnicamente opcional en RSpec. Sin embargo, omitirla hace que tus specs sean más difíciles de leer. - La descripción de cada ejemplo comienza con un verbo, no con “should”. Lee las expectativas en voz alta: User requires an API token, User requires an email, User sets a new user’s API token. La legibilidad es importante, ¡y es una característica clave de RSpec!
Con estas buenas prácticas en mente, construyamos un spec para el modelo User.
Creación de un spec de modelo
En el capítulo 2, configuramos RSpec para que genere automáticamente archivos de plantilla cada vez que agregamos nuevos modelos y controladores a la aplicación. Sin embargo, podemos invocar los generadores en cualquier momento. Aquí, usaremos uno para generar un archivo inicial para nuestro primer spec de modelo.
Comienza usando el generador rspec:model en la línea de comandos:
$ bin/rails g rspec:model user
RSpec informa la creación del nuevo archivo:
rails g rspec:model user
create spec/models/user_spec.rb
Abramos el nuevo archivo y echemos un vistazo.
1 require 'rails_helper'
2
3 RSpec.describe User, type: :model do
4 pending "add some examples to (or delete) #{__FILE__}"
5 end
El nuevo archivo nos ofrece un primer vistazo a la sintaxis y las convenciones de RSpec. Primero, hacemos require del archivo rails_helper en este archivo, y lo haremos en prácticamente todos los archivos de nuestra suite de pruebas. Esto le indica a RSpec que necesitamos que se cargue la aplicación Rails para que luego pueda ejecutar las pruebas contenidas en el archivo. A continuación, usamos el método describe para listar un conjunto de cosas que se espera que haga un modelo llamado User. Observa la declaración type: :model–como comentamos en el capítulo anterior, esto le indica a RSpec cómo tratar el spec. Hablaremos más sobre pending en el capítulo 12, cuando comencemos a practicar el desarrollo guiado por pruebas. Por ahora, ¿qué ocurre si ejecutamos esto, usando bin/rspec?
User
add some examples to (or delete) /path/to/spec/models/user_spec.rb (PENDING: Not yet im\
plemented)
Pending: (Failures listed here are expected and do not affect your suite's status)
1) User add some examples to (or delete) /path/to/spec/models/user_spec.rb
# Not yet implemented
# ./spec/models/user_spec.rb:4
Finished in 0.00058 seconds (files took 0.66131 seconds to load)
1 example, 0 failures, 1 pending
Vamos a conservar el bloque describe, pero reemplazaremos su contenido con el esquema que creamos hace unos minutos:
1 require 'rails_helper'
2
3 RSpec.describe User, type: :model do
4 it "is valid with an email, password, nickname, and API token"
5 it "requires a nickname"
6 it "requires a unique nickname"
7 it "requires an email"
8 it "requires a unique email"
9 it "requires a password"
10 it "requires an API token"
11 it "sets a new user's API token"
12 end
Vamos a completar los detalles en un momento, pero si ejecutáramos los specs ahora mismo desde la línea de comandos (escribiendo bin/rspec en la línea de comandos), la salida sería algo así:
User
is valid with an email, password, nickname, and API token (PENDING: Not yet implemented)
requires a nickname (PENDING: Not yet implemented)
requires a unique nickname (PENDING: Not yet implemented)
requires an email (PENDING: Not yet implemented)
requires a unique email (PENDING: Not yet implemented)
requires a password (PENDING: Not yet implemented)
requires an API token (PENDING: Not yet implemented)
sets a new user's API token (PENDING: Not yet implemented)
Pending: (Failures listed here are expected and do not affect your suite's status)
1) User is valid with an email, password, nickname, and API token
# Not yet implemented
# ./spec/models/user_spec.rb:4
2) User requires a nickname
# Not yet implemented
# ./spec/models/user_spec.rb:5
3) User requires a unique nickname
# Not yet implemented
# ./spec/models/user_spec.rb:6
4) User requires an email
# Not yet implemented
# ./spec/models/user_spec.rb:7
5) User requires a unique email
# Not yet implemented
# ./spec/models/user_spec.rb:8
6) User requires a password
# Not yet implemented
# ./spec/models/user_spec.rb:9
7) User requires an API token
# Not yet implemented
# ./spec/models/user_spec.rb:10
8) User sets a new user's API token
# Not yet implemented
# ./spec/models/user_spec.rb:11
Finished in 0.00086 seconds (files took 0.83556 seconds to load)
8 examples, 0 failures, 8 pending
¡Genial! Ocho specs pendientes. RSpec los marca como pending porque aún no hemos escrito ningún código real para realizar las pruebas. Hagamos eso ahora, comenzando con el primer ejemplo.
La sintaxis de RSpec
En 2012, el equipo de RSpec anunció una nueva alternativa preferida al tradicional should, añadida en la versión 2.11. Por supuesto, esto ocurrió apenas unos días después de que publiqué la primera versión completa de este libro — ¡a veces es difícil mantenerse al día con estas cosas!
Este nuevo enfoque soluciona algunos problemas técnicos causados por la antigua sintaxis should. En lugar de decir que algo should o should_not coincidir con la salida esperada, usas expect de que algo to o not_to sea algo distinto.
Como ejemplo, veamos esta prueba de muestra, o expectación. En este ejemplo, 2 + 1 siempre debería ser igual a 3, ¿verdad? En la sintaxis antigua de RSpec, esto se escribiría así:
it "adds 2 and 1 to make 3" do
(2 + 1).should eq 3
end
La nueva sintaxis pasa el valor de prueba a un método expect() y luego encadena un matcher a este:
it "adds 2 and 1 to make 3" do
expect(2 + 1).to eq 3
end
Si estás buscando en Google o Stack Overflow ayuda con alguna pregunta sobre RSpec, o si estás trabajando con una aplicación Rails más antigua, es muy probable que encuentres información que usa la sintaxis antigua con should. Esta sintaxis todavía funciona técnicamente en las versiones actuales de RSpec, pero recibirás una advertencia de deprecación cuando intentes usarla. Puedes configurar RSpec para desactivar estas advertencias, pero en toda honestidad, es mejor que aprendas a usar la sintaxis preferida con expect().
Entonces, ¿cómo luce esa sintaxis en un ejemplo real? Completemos esa primera expectación de nuestro spec para el modelo User:
1 require 'rails_helper'
2
3 RSpec.describe User, type: :model do
4 it "is valid with an email, password, nickname, and API token" do
5 user = User.new(
6 email: "test@example.com",
7 password: "password",
8 nickname: "test",
9 api_token: "token"
10 )
11
12 expect(user).to be_valid
13 end
14
15 it "requires a nickname"
16 it "requires a unique nickname"
17 it "requires an email"
18 it "requires a unique email"
19 it "requires a password"
20 it "requires an API token"
21 it "sets a new user's API token"
22 end
Este sencillo ejemplo usa un matcher de RSpec llamado be_valid para verificar que nuestro modelo sabe cómo debe lucir para ser válido. Preparamos un objeto (en este caso, una instancia nueva pero sin guardar de User llamada user) y luego lo pasamos a expect para compararlo con el matcher.
Ahora, si ejecutamos bin/rspec desde la línea de comandos nuevamente, vemos un ejemplo exitoso:
User
is valid with an email, password, nickname, and API token
requires a nickname (PENDING: Not yet implemented)
requires a unique nickname (PENDING: Not yet implemented)
requires an email (PENDING: Not yet implemented)
requires a unique email (PENDING: Not yet implemented)
requires a password (PENDING: Not yet implemented)
requires an API token (PENDING: Not yet implemented)
sets a new user's API token (PENDING: Not yet implemented)
Pending: (Failures listed here are expected and do not affect your suite's status)
1) User requires a nickname
# Not yet implemented
# ./spec/models/user_spec.rb:15
2) User requires a unique nickname
# Not yet implemented
# ./spec/models/user_spec.rb:16
3) User requires an email
# Not yet implemented
# ./spec/models/user_spec.rb:17
4) User requires a unique email
# Not yet implemented
# ./spec/models/user_spec.rb:18
5) User requires a password
# Not yet implemented
# ./spec/models/user_spec.rb:19
6) User requires an API token
# Not yet implemented
# ./spec/models/user_spec.rb:20
7) User sets a new user's API token
# Not yet implemented
# ./spec/models/user_spec.rb:21
Finished in 0.02298 seconds (files took 0.56166 seconds to load)
8 examples, 0 failures, 7 pending
¡Felicitaciones, has escrito tu primera prueba!
Los matchers son componentes clave en RSpec, así que tomemos un momento para analizar qué implica be_valid. Podríamos haber escrito la prueba como
expect(user.valid?).to eq true
o
expect(user.valid?).to be true
En estas variaciones, eq y be también son matchers. Los matchers ayudan a que las pruebas se lean como lenguaje natural, en lugar del estilo assert que quizás hayas visto en otros frameworks. be_valid tiene algo más en juego: antes de comparar el lado izquierdo del matcher con el derecho, también llama al método valid? sobre el usuario que se está probando. Por eso no necesitamos llamar explícitamente a user.valid? en la prueba original.
RSpec incluye muchos matchers integrados; exploraremos algunos de ellos a lo largo del libro. ¡Pero por ahora, escribamos algunas pruebas más!
Probando validaciones
Las validaciones son excelentes para familiarizarse con las pruebas automatizadas. Estas pruebas generalmente se pueden escribir en tan solo unas pocas líneas de código. Completemos nuestra spec de validación de nickname:
1 it "requires a nickname" do
2 user = User.new(nickname: nil)
3
4 expect(user).to be_invalid
5 end
Esta vez, esperamos que un nuevo usuario con el atributo nickname explícitamente establecido en nil no sea válido. En este caso, el matcher be_invalid llama a user.invalid? antes de comparar los valores esperado y real.
Eso está bien, pero la prueba no le dice al lector por qué. Arreglemos eso:
1 it "requires a nickname" do
2 user = User.new(nickname: nil)
3
4 expect(user).to be_invalid
5 expect(user.errors[:nickname]).to include("can't be blank")
6 end
Ahora, la prueba aclara la razón por la que el usuario no es válido, asegurando que el mensaje de error coincida con lo que esperaríamos de esta validación. Lo comprobamos usando el matcher include de RSpec, que verifica si un valor está contenido dentro de un valor enumerable (en este caso, errors). Y cuando ejecutemos RSpec nuevamente, deberíamos tener dos specs que pasen.
Hay un pequeño problema en nuestro enfoque hasta ahora. Tenemos un par de pruebas que pasan, pero nunca las vimos fallar. Esto puede ser una señal de advertencia, especialmente al comenzar. Necesitamos asegurarnos de que el código de prueba haga lo que se pretende, lo que también se conoce como ejercitar el código bajo prueba.
Hay un par de cosas que podemos hacer para demostrar que no estamos obteniendo falsos positivos. Primero, invirtamos esa expectativa cambiando to por to_not:
1 it "requires a nickname" do
2 user = User.new(nickname: nil)
3
4 expect(user).to be_invalid
5 expect(user.errors[:nickname]).to_not include("can't be blank")
6 end
Y efectivamente, RSpec reporta un fallo:
Failures:
1) User requires a nickname
Failure/Error: expect(user.errors[:nickname]).to_not include("can't be blank")
expected ["can't be blank"] not to include "can't be blank"
# ./spec/models/user_spec.rb:19:in `block (2 levels) in <top (required)>'
Finished in 0.02385 seconds (files took 0.55168 seconds to load)
8 examples, 1 failure, 6 pending
Failed examples:
rspec ./spec/models/user_spec.rb:15 # User requires a nickname
También podemos modificar el código de la aplicación para ver cómo afecta a la prueba. Deshaz el cambio que acabamos de hacer en la prueba (cambia to_not de vuelta a to), luego abre el modelo User y comenta temporalmente la validación de nickname:
1 require "open-uri"
2
3 class User < ApplicationRecord
4 include Clearance::User
5
6 has_many :recipes, dependent: :destroy
7 has_many :favorites, dependent: :destroy
8 has_many :favorite_recipes, through: :favorites, source: :recipe
9 has_many :comments, dependent: :destroy
10 has_one_attached :avatar
11
12 # validates :nickname, presence: true, uniqueness: true
13 validates :api_token, presence: true, uniqueness: true
14
15 before_validation :set_api_token, on: :create
16 before_create :set_avatar
17
18 # remainder of file omitted ...
Ejecuta los specs de nuevo. Esta vez, deberías ver nuevamente un fallo. Le dijimos a RSpec que un usuario sin nickname debería ser inválido, pero el código de nuestra aplicación no contemplaba eso.
Estas son formas sencillas de verificar que tus tests funcionan como se espera, especialmente a medida que avanzas desde probar validaciones simples hasta lógica más compleja, y estás probando código que ya ha sido escrito. Si no ves ningún cambio en la salida de los tests, es muy probable que el test no esté interactuando realmente con el código, o que el código se comporte de manera distinta a lo que esperas.
Ahora podemos usar el mismo enfoque para probar la validación de :email.
1 it "requires an email" do
2 user = User.new(email: nil)
3
4 expect(user).to be_invalid
5 expect(user.errors[:email]).to include("can't be blank")
6 end
Puede que estés pensando que estos tests son relativamente sin sentido–¿qué tan difícil puede ser asegurarse de que las validaciones estén incluidas en un modelo? La verdad es que pueden ser más fáciles de omitir de lo que imaginas. Más importante aún, si piensas en qué validaciones debería tener tu modelo mientras escribes los tests (idealmente, y con el tiempo, siguiendo un estilo de desarrollo guiado por pruebas (Test-Driven Development)), es más probable que recuerdes incluirlas.
Ampliemos lo que hemos aprendido hasta ahora para escribir un test un poco más complicado–esta vez, para verificar la validación de unicidad en el atributo nickname:
1 it "requires a unique nickname" do
2 User.create(
3 nickname: "test",
4 email: "test1@example.com",
5 password: "password"
6 )
7
8 user = User.new(
9 nickname: "test"
10 )
11
12 expect(user).to be_invalid
13 expect(user.errors[:nickname]).to include("has already been taken")
14 end
Fíjate en una diferencia sutil aquí: en este caso, primero persistimos un usuario (llamando a create en User en lugar de new) para construir nuestros datos de prueba, y luego instanciamos un segundo usuario como sujeto de la prueba en sí. Esto, por supuesto, requiere que el primer usuario persistido sea válido (con nickname, email y password) y tenga la misma dirección de email asignada. En el capítulo 4, veremos utilidades para agilizar este proceso. Mientras tanto, ejecuta bin/rspec para ver la salida del nuevo test.
Ahora probemos una validación más compleja. Para hacerlo, dejaremos de lado los tests del modelo User y nos centraremos en el modelo Recipe.
Supongamos que queremos asegurarnos de que los usuarios no puedan darle a dos de sus recetas el mismo nombre–el nombre debe ser único dentro del ámbito de ese usuario. En otras palabras, yo no puedo tener dos recetas llamadas Vegetable Stir Fry, pero tú y yo podríamos tener cada uno nuestra propia receta llamada Vegetable Stir Fry. ¿Cómo podrías probar eso?
Como hicimos con los usuarios, empezaremos creando un nuevo archivo spec para el modelo Recipe:
$ bin/rails g rspec:model recipe
A continuación, añade dos ejemplos al nuevo archivo. Probaremos que un solo usuario no puede tener dos recetas con el mismo nombre, pero dos usuarios distintos sí pueden tener cada uno una receta con el mismo nombre.
1 require 'rails_helper'
2
3 RSpec.describe Recipe, type: :model do
4 it "does not allow duplicate recipe names per user" do
5 user = User.create(
6 nickname: "test-user",
7 email: "test-user@example.com",
8 password: "password"
9 )
10
11 category = Category.create(name: "Test Category")
12
13 user.recipes.create(
14 name: "Test Recipe",
15 category: category
16 )
17
18 second_recipe = user.recipes.build(
19 name: "Test Recipe",
20 )
21
22 expect(second_recipe).to_not be_valid
23 expect(second_recipe.errors[:name]).to include("has already been taken")
24 end
25
26 it "allows two users to share a project name" do
27 user = User.create(
28 nickname: "test-user",
29 email: "test-user@example.com",
30 password: "password"
31 )
32
33 other_user = User.create(
34 nickname: "another-test-user",
35 email: "another-test-user@example.com",
36 password: "password"
37 )
38
39 category = Category.create(name: "Test Category")
40
41 user.recipes.create(
42 name: "Test Recipe",
43 category: category
44 )
45
46 second_recipe = other_user.recipes.build(
47 name: "Test Recipe",
48 category: category
49 )
50
51 expect(second_recipe).to be_valid
52 end
53 end
Esta vez, dado que los modelos User y Recipe están acoplados mediante una relación de Active Record, al igual que los modelos Recipe y Category, necesitamos proporcionar un poco más de información. En el caso del primer ejemplo, tenemos un usuario al que se le asignan ambas recetas. En el segundo, el mismo nombre de receta se asigna a dos recetas únicas que pertenecen a usuarios únicos. Ten en cuenta que, en ambos ejemplos, tenemos que usar create con los usuarios, es decir, persistirlos en la base de datos, para poder asignarlos a las recetas que estamos probando. Y aunque no forma parte de lo que estamos probando aquí, para cumplir con el requisito de la aplicación de que cada receta pertenezca a una categoría, también necesitamos crear una categoría en cada uno de los tests.
Dado que el modelo Recipe tiene la siguiente validación:
validates :name, presence: true, uniqueness: { scope: :user_id }
Estos nuevos specs pasarán sin problema. No olvides verificar tu trabajo: intenta comentar temporalmente la validación, o cambia los tests para que esperen algo diferente. ¿Fallan ahora?
Por supuesto, las validaciones pueden ser más complejas que simplemente requerir un scope específico. Las tuyas podrían involucrar una expresión regular compleja o un validador personalizado. Adquiere el hábito de probar estas validaciones, no solo los casos exitosos en los que todo es válido, sino también las condiciones de error. Por ejemplo, en los ejemplos que hemos creado hasta ahora, probamos qué sucede cuando un objeto se inicializa con valores nil. Si tienes una validación para asegurarte de que un atributo debe ser un número, intenta enviarle una cadena de texto. Si tu validación requiere que una cadena tenga entre cuatro y ocho caracteres, prueba enviándole tres caracteres y luego nueve.
Probando métodos de instancia
Retomemos ahora las pruebas del modelo User. Tenemos los comienzos de una funcionalidad para tratar a los usuarios nuevos de manera diferente a los usuarios que llevan un tiempo en el sitio. Quizás algún día podríamos limitar la cantidad de recetas o comentarios que un usuario nuevo puede publicar. Por ahora, simplemente mostramos una insignia al atribuir una receta a un usuario nuevo.
Para manejar esto, tenemos este método en la clase User:
def new_to_site?
created_at > 1.month.ago
end
Podemos usar las mismas técnicas básicas que utilizamos en nuestros ejemplos de validación para crear un ejemplo exitoso de esta funcionalidad:
1 it "indicates a new user" do
2 user = User.new(created_at: Time.now)
3
4 expect(user.new_to_site?).to be true
5 end
Genial, pero ¿qué hay de un usuario que lleva un tiempo aquí? También deberíamos probarlo:
1 it "indicates an established user" do
2 user = User.new(created_at: 1.month.ago)
3
4 expect(user.new_to_site?).to be false
5 end
No está mal, pero pulamos un poco estos tests con algo de magia de matchers. ¿Recuerdas que anteriormente en este capítulo usamos be_valid y be_invalid como matchers para probar la validez de un objeto User? ¡También podemos usar be_ con nuestros propios métodos que devuelven booleanos!
1 it "indicates a new user" do
2 user = User.new(created_at: Time.now)
3
4 expect(user).to be_new_to_site
5 end
6
7 it "indicates an established user" do
8 user = User.new(created_at: 1.month.ago)
9
10 expect(user).to_not be_new_to_site
11 end
Personalmente, me encanta esta funcionalidad de RSpec. Creo que hace que los tests se lean más como documentación y menos como código. Si tú o tu equipo no están de acuerdo, no hay nada de malo en las versiones anteriores de estos tests.
De cualquier manera, estamos estableciendo un patrón para las pruebas: crear datos de prueba y luego indicarle a RSpec cómo esperamos que se comporten. Sigamos adelante.
Probando métodos de clase y scopes
Nuestros usuarios pueden buscar títulos de recetas por un término proporcionado. A modo de demostración, actualmente está implementado como un scope en el modelo Recipe:
1 scope :by_word_in_name, ->(query) {
2 where("name LIKE ?", "%#{query}%") if query.present?
3 }
Agreguemos otra prueba a recipe_spec para cubrir esto:
1 it "finds recipes that contain the search term in their name" do
2 user = User.create(
3 nickname: "test-user",
4 email: "test-user@example.com",
5 password: "password"
6 )
7
8 category = Category.create(name: "Test Category")
9
10 first_recipe = user.recipes.create(
11 name: "Pepperoni Pizza",
12 category: category
13 )
14
15 second_recipe = user.recipes.create(
16 name: "Cheese Pizza",
17 category: category
18 )
19
20 results = Recipe.by_word_in_name("pepperoni")
21
22 expect(results).to include(first_recipe)
23 expect(results).to_not include(second_recipe)
24 end
El scope by_word_in_name debería devolver una colección de recetas que coincidan con el término de búsqueda, y esa colección solo debería incluir esas recetas, no las que no contienen el término.
Esta prueba nos da otras cosas con las que experimentar: ¿Qué pasa si invertimos las variaciones to y to_not en las pruebas? ¿O si añadimos más recetas que contengan el término de búsqueda?
Probando todos los casos
Hemos probado el camino feliz —un usuario busca un término para el cual podemos devolver resultados— pero ¿qué pasa cuando la búsqueda no devuelve ningún resultado? Sería mejor que también lo probáramos. La siguiente spec debería bastar:
1 it "returns an empty collection when no recipes matching the search term are found" do
2 user = User.create(
3 nickname: "test-user",
4 email: "test-user@example.com",
5 password: "password"
6 )
7
8 category = Category.create(name: "Test Category")
9
10 first_recipe = user.recipes.create(
11 name: "Pepperoni Pizza",
12 category: category
13 )
14
15 second_recipe = user.recipes.create(
16 name: "Cheese Pizza",
17 category: category
18 )
19
20 results = Recipe.by_word_in_name("veggie")
21
22 expect(results).to be_empty
23 end
Esta spec verifica el valor devuelto por Recipe.by_word_in_name("veggie"). Como la colección resultante está vacía, ¡la spec pasa! No solo estamos probando los resultados ideales, sino también las búsquedas sin resultados.
Más sobre los matchers
Ya hemos visto cuatro matchers en acción: be_valid, eq, include y be_empty. Primero usamos be_valid, que proviene de la gema rspec-rails para probar la validez de un modelo de Rails. eq e include provienen de rspec-expectations, que se instala junto con rspec-rails cuando configuramos nuestra app para usar RSpec en el capítulo anterior.
Una lista completa de los matchers predeterminados de RSpec se puede encontrar en el README del repositorio rspec-expectations en GitHub. A lo largo de este libro veremos varios de ellos. En el capítulo 9, exploraremos cómo crear nuestros propios matchers personalizados.
Resumen
Este capítulo se centró en las pruebas de modelos, pero hemos cubierto muchas otras técnicas importantes que querrás usar en otros tipos de specs a medida que avances:
- Usa expectativas activas y explícitas: Usa verbos para explicar cuáles deberían ser los resultados de un ejemplo. Intenta verificar solo un resultado por ejemplo. (Hablaremos de las excepciones a esto en un capítulo posterior.)
- Prueba tanto lo que esperas que suceda como lo que esperas que no suceda: Piensa en ambos caminos al escribir ejemplos y escribe las pruebas en consecuencia.
- Prueba los casos límite: Si tienes una validación que requiere que una contraseña tenga entre cuatro y diez caracteres, no te conformes con probar solo una contraseña de ocho caracteres. Un buen conjunto de pruebas verificaría en cuatro y diez caracteres, así como en tres y once. (Por supuesto, también podrías aprovechar la oportunidad para preguntarte por qué permitirías contraseñas tan cortas o no permitirías contraseñas más largas. Las pruebas también son una buena oportunidad para reflexionar sobre los requisitos y el código de una aplicación.)
Con una sólida colección de specs de modelos incorporada en tu app, estás en buen camino hacia un código más confiable. ¡Excelente trabajo!
Ejercicios
Si estás siguiendo el libro con tu propio código Rails sin pruebas, echa un vistazo a sus modelos ahora. ¿Qué atributos se corresponden con sus tablas en la base de datos? ¿Cómo se validan los datos? ¿Qué otra lógica de negocio contienen?
Todos estos son excelentes candidatos para tu primera cobertura de pruebas. Empieza generando una spec de modelo para un modelo dado, luego esboza los escenarios que quieres probar. Piensa en cómo construir solo los datos de prueba necesarios para cada escenario y en cómo demostrarían que tu código se comporta como deseas. Luego escribe cada spec y ejecútala. Te recomiendo hacerlo una prueba a la vez. No te preocupes si te estás repitiendo de prueba en prueba. En el próximo capítulo exploraremos las opciones de deduplicación.
Mientras escribes y ejecutas tus nuevas especificaciones, ¿notas algo inesperado en el código de tu aplicación? Por ejemplo, ¿alguna funcionalidad que no funciona del todo como creías, o código que se beneficiaría de una refactorización? ¡Felicidades! ¡Ya estás viendo los beneficios del diseño de software guiado por pruebas!