Cómo SECAR sus pruebas RSpec usando ejemplos compartidos

" Dame seis horas para talar un árbol y pasaré las primeras cuatro afilando el hacha". - Abraham Lincoln

Cuando refactoricé un proyecto hace unas semanas, pasé la mayor parte de mi tiempo escribiendo especificaciones. Después de escribir varios casos de prueba similares para algunas API, comencé a preguntarme si podría deshacerme de muchas de estas duplicaciones.

Así que me dediqué a leer sobre las mejores prácticas para SECAR pruebas (Don't Repeat Yourself). Y así es como llegué a conocer shared examplesy shared contexts.

En mi caso, terminé usando ejemplos compartidos. Y esto es lo que he aprendido hasta ahora al aplicarlos.

Cuando tiene varias especificaciones que describen un comportamiento similar, puede ser mejor extraer ejemplos redundantes shared examplesy usarlos en varias especificaciones.

Suponga que tiene dos modelos Usuario y Publicación , y un usuario puede tener muchas publicaciones. Los usuarios deben poder ver la lista de usuarios y publicaciones. La creación de una acción de índice en los controladores de usuarios y publicaciones servirá para este propósito.

Primero, escriba especificaciones para su acción de índice para el controlador de usuarios. Tendrá la responsabilidad de buscar usuarios y representarlos con el diseño adecuado. Luego, escriba suficiente código para que las pruebas pasen.

# users_controller_spec.rbdescribe "GET #index" do before do 5.times do FactoryGirl.create(:user) end get :index end it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) } it { expect(assigns(:users)).to match(User.all) }end
# users_controller.rbclass UsersController < ApplicationController .... def index @users = User.all end ....end

Normalmente, la acción de índice de cualquier controlador obtiene y agrega datos de pocos recursos según sea necesario. También agrega paginación, búsqueda, clasificación, filtrado y alcance.

Finalmente, todos estos datos se presentan a las vistas a través de HTML, JSON o XML mediante API. Para simplificar mi ejemplo, las acciones de índice de los controladores solo obtendrán datos y luego los mostrarán a través de vistas.

Lo mismo ocurre con la acción de índice en el controlador de publicaciones:

describe "GET #index" do before do 5.times do FactoryGirl.create(:post) end get :index end it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) } it { expect(assigns(:posts)).to match(Post.all) }end
# posts_controller.rbclass PostsController < ApplicationController .... def index @posts = Post.all end ....end

Las pruebas de RSpec escritas tanto para usuarios como para controladores de publicaciones son muy similares. En ambos controladores tenemos:

  • El código de respuesta debe ser 'OK'
  • Ambas acciones de índice deben representar una vista o parcial adecuada, en nuestro caso index
  • Los datos que queremos representar, como publicaciones o usuarios.

SECAMOS las especificaciones para nuestra acción de índice usando shared examples.

Dónde poner tus ejemplos compartidos

Me gusta colocar ejemplos compartidos dentro del directorio specs / support / shared_examples para que todos los shared examplearchivos relacionados se carguen automáticamente.

Puede leer acerca de otras convenciones de uso común para localizar su shared examplesaquí: documentación de ejemplos compartidos

Cómo definir un ejemplo compartido

Su acción de índice debería responder con 200 códigos de éxito (OK) y renderizar su plantilla de índice.

RSpec.shared_examples "index examples" do it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) }end

Además de sus itbloques, y antes y después de sus ganchos, puede agregar letbloques, contexto y describir bloques, que también se pueden definir dentro shared examples.

Personalmente prefiero que los ejemplos compartidos sean simples y concisos, y no agregue contextos y deje bloques. El shared examplesbloque también acepta parámetros, que cubriré a continuación.

Cómo usar ejemplos compartidos

Agregar include_examples "index examples"a sus usuarios y publicaciones las especificaciones del controlador incluye "ejemplos de índice" para sus pruebas.

# users_controller_spec.rbdescribe "GET #index" do before do 5.times do FactoryGirl.create(:user) end get :index end include_examples "index examples" it { expect(assigns(:users)).to match(User.all) }end
# similarly, in posts_controller_spec.rbdescribe "GET #index" do before do 5.times do FactoryGirl.create(:post) end get :index end include_examples "index examples" it { expect(assigns(:posts)).to match(Post.all) }end

También puede usar it_behaves_likeo en it_should_behaves_likelugar de include_examplesen este caso. it_behaves_likey it_should_behaves_likeson en realidad alias, y funcionan de la misma manera, por lo que pueden usarse indistintamente. Pero include_examplesy it_behaves_likeson diferentes.

Como se indica en la documentación oficial:

  • include_examples - incluye ejemplos en el contexto actual
  • it_behaves_likee it_should_behave_likeincluir los ejemplos en un contexto anidado

¿Por qué importa esta distinción?

La documentación de RSpec da una respuesta adecuada:

Cuando incluye ejemplos parametrizados en el contexto actual varias veces, puede anular las definiciones de métodos anteriores y la última declaración gana.

Entonces, cuando se enfrenta a una situación en la que los ejemplos parametrizados contienen métodos que entran en conflicto con otros métodos en el mismo contexto, puede reemplazar include_examplescon it_behaves_likemétodo. Esto creará un contexto anidado y evitará este tipo de situaciones.

Consulte la siguiente línea en las especificaciones del controlador de sus usuarios y las especificaciones del controlador de publicaciones:

it { expect(assigns(:users)).to match(User.all) }it { expect(assigns(:posts)).to match(Post.all) }

Ahora las especificaciones de su controlador se pueden refactorizar aún más pasando parámetros al ejemplo compartido como se muestra a continuación:

# specs/support/shared_examples/index_examples.rb
# here assigned_resource and resource class are parameters passed to index examples block RSpec.shared_examples "index examples" do |assigned_resource, resource_class| it { expect(subject).to respond_with(:ok) } it { expect(subject).to render_template(:index) } it { expect(assigns(assigned_resource)).to match(resource_class.all) }end

Ahora, realice los siguientes cambios en sus usuarios y las especificaciones del controlador de publicaciones:

# users_controller_spec.rbdescribe "GET #index" do before do ... end include_examples "index examples", :users, User.allend
# posts_controller_spec.rbdescribe "GET #index" do before do ... end include_examples "index examples", :posts, Post.allend

Ahora las especificaciones del controlador se ven limpias, menos redundantes y, lo que es más importante, SECAS. Además, estos ejemplos de índices pueden servir como estructuras básicas para diseñar la acción de índices de otros controladores.

Conclusión

By moving common examples into a separate file, you can eliminate duplication and improve the consistency of your controller actions throughout your application. This is very useful in case of designing APIs, as you can use the existing structure of RSpec tests to design tests and create APIs that adhere to your common response structure.

Mostly, when I work with APIs, I use shared examples to provide me with a common structure to design similar APIs.

Feel free to share how you DRY up your specs by using shared examples.