¿Qué son los Concerns y cómo probarlos?

AMV México
6 min readMar 14, 2018

por: Christina Gaitán

Los Concerns son una herramienta que nos permite incluir módulos en las clases para crear mixins, ésta herramienta es ofrecida por la librería de ActiveSupport. Una de las ventajas de utilizar Concerns es que nos da la habilidad de modelar comportamiento compartido entre varias clases, mientras mantenemos el principio de DRY, Don’t Repeat Yourself.

Para demostrar esto, voy a utilizar un ejemplo sencillo, supongamos que estamos desarrollando una aplicación para la venta de Libros, en nuestra aplicación los usuarios tienen la habilidad de dejar comentarios sobre los libros pero también sobre los autores de éstos, utilizando solamente modelos para implementar esta funcionalidad, podríamos crear un código parecido al siguiente:

app/models/libro.rb

class Libro < ApplicationRecord
belongs_to :autor
has_many :comentarios, as: :comentable

def ultimo_comentario
comentarios.last
end

def num_comentarios
comentarios.count
end
end

app/models/autor.rb

class Autor < ApplicationRecord
has_many :libros
has_many :comentarios, as: :comentable

def ultimo_comentario
comentarios.last
end

def num_comentarios
comentarios.count
end
end

app/models/comentario.rb

class Comentario < ApplicationRecord
belongs_to :comentable, polymorphic: true, optional: true
end

En este ejemplo tenemos lógica repetida tanto en el modelo de Libro como en el modelo de Autor, aquí es cuando podemos utilizar los Concerns para extraer esa lógica.

Lo primero que tenemos que hacer es generar un módulo que extienda a ActiveSupport, dentro del directorio app/models/concerns , nuestro módulo en este caso se llamará Comentable

app/models/concerns/comentable.rb

module Comentable
extend ActiveSupport::Concern

end

Ahora, dentro de este nuevo módulo podemos incluir el código que tenemos repetido en el modelo de Libro y en el modelo de Autor.

app/models/concerns/comentable.rb

module Comentable
extend ActiveSupport::Concern

def ultimo_comentario
comentarios.last
end

def num_comentarios
comentarios.count
end
end

Una vez hecho esto, debemos incluir en nuestros modelos el módulo Comentable y eliminar los métodos que acabamos de copiar para que nuestros modelos queden de la siguiente manera:

app/models/libro.rb

class Libro < ApplicationRecord
include Comentable

belongs_to :autor
has_many :comentarios, as: :comentable

end

app/models/autor.rb

class Autor < ApplicationRecord
include Comentable

has_many :libros
has_many :comentarios, as: :comentable

end

Ahora, si ponemos atención, podemos notar que todavía tenemos código repetido en los modelos, en ambos tenemos la relación

has_many :comentarios, as: :comentable

Afortunadamente, esta relación, también la podemos extraer hacia nuestro Concern utilizando el bloque included .

El bloque included es ejecutado dentro del contexto de la clase que incluya éste módulo, por lo que es muy útil para incluir las relaciones de ActiveRecord.

app/models/concerns/comentable.rb

module Comentable
extend ActiveSupport::Concern

included do
has_many :comentarios, as: :comentable
end

def ultimo_comentario
comentarios.last
end

def num_comentarios
comentarios.count
end
end

Finalmente, el código en nuestros modelos quedaría de la siguiente manera:

app/models/libro.rb

class Libro < ApplicationRecord
include Comentable

belongs_to :autor

end

app/models/autor.rb

class Autor < ApplicationRecord
include Comentable

has_many :libros

end

Pero, ¿qué pasaría si tuviéramos algún método de clase en nuestros modelos?, por ejemplo, un método para obtener el Libro o el Autor con más comentarios:

app/models/libro.rb

class Libro < ApplicationRecord
include Comentable

belongs_to :autor

def self.mas_comentado
# regresa el Libro más comentado
end
end

app/models/autor.rb

class Autor < ApplicationRecord
include Comentable

has_many :libros

def self.mas_comentado
# regresa el Autor más comentado
end
end

Esto también lo podríamos extraer en nuestro Concern utilizando el bloque class_methods . El código que se encuentre dentro de este bloque será agregado a la clase misma y nuestro Concern quedaría así:

app/models/concerns/comentable.rb

module Comentable
extend ActiveSupport::Concern

included do
has_many :comentarios, as: :comentable
end

class_methods do
def mas_comentado
# regresa el Libro/Autor mas comentado
end
end

def ultimo_comentario
comentarios.last
end

def num_comentarios
comentarios.count
end
end

En el ejemplo que desarrollamos, los modelos siguen manteniendo la misma funcionalidad, pero gracias al Concern que implementamos el mantenimiento de la aplicación ahora será más fácil porque ya no tenemos código duplicado en los modelos.

¿Cómo probar los Concerns?

Ahora bien, hemos hecho un gran avance evitando la duplicidad de código en nuestros modelos, pero ¿qué podemos hacer con las pruebas?

Suponiendo estamos usando Rspec, FactoryBot y Shoulda Matcher para probar nuestros modelos, el código de nuestras pruebas puede ser parecido al siguiente:

spec/models/autor_spec.rb

require 'rails_helper'

RSpec.describe Autor, type: :model do
it { expect(subject).to have_many(:libros) }
it { expect(subject).to have_many(:comentarios) }

describe '#ultimo_comentario' do
context 'cuando el libro no tiene comentarios' do
it 'regresa nil' do
libro = FactoryBot.create :libro

expect(libro.ultimo_comentario).to eq nil
end
end

context 'cuando el libro tiene comentarios' do
it 'regresa el ultimo comentario' do
libro = FactoryBot.create :libro
comentarios = FactoryBot.create_list :comentario, 3, comentable: libro

expect(libro.ultimo_comentario).to eq comentarios.last
end
end
end

describe '#num_comentarios' do
context 'cuando el libro no tiene comentarios' do
it 'regresa cero' do
libro = FactoryBot.create :libro

expect(libro.num_comentarios).to eq 0
end
end

context 'cuando el libro tiene comentarios' do
it 'regresa el ultimo comentario' do
libro = FactoryBot.create :libro
comentarios = FactoryBot.create_list :comentario, 3, comentable: libro

expect(libro.num_comentarios).to eq 3
end
end
end
end

spec/models/libro_spec.rb

require 'rails_helper'

RSpec.describe Libro, type: :model do
it { expect(subject).to belong_to(:autor) }
it { expect(subject).to have_many(:comentarios) }

describe '#ultimo_comentario' do
context 'cuando el libro no tiene comentarios' do
it 'regresa nil' do
libro = FactoryBot.create :libro

expect(libro.ultimo_comentario).to eq nil
end
end

context 'cuando el libro tiene comentarios' do
it 'regresa el ultimo comentario' do
libro = FactoryBot.create :libro
comentarios = FactoryBot.create_list :comentario, 3, comentable: libro

expect(libro.ultimo_comentario).to eq comentarios.last
end
end
end

describe '#num_comentarios' do
context 'cuando el libro no tiene comentarios' do
it 'regresa cero' do
libro = FactoryBot.create :libro

expect(libro.num_comentarios).to eq 0
end
end

context 'cuando el libro tiene comentarios' do
it 'regresa el ultimo comentario' do
libro = FactoryBot.create :libro
comentarios = FactoryBot.create_list :comentario, 3, comentable: libro

expect(libro.num_comentarios).to eq 3
end
end
end
end

Aunque técnicamente estas pruebas siguen siendo útiles, porque nuestros modelos mantienen la misma funcionalidad, también podemos evitar la duplicidad de código en ellas.

Para esto utilizaremos los Shared example group de Rspec, las pruebas que estén presentes en este grupo serán ejecutadas en el contexto de las pruebas que los incluyan, lo cual es muy útil cuando queremos probar Concerns.

Para incluir los “Shared Example” en nuestras pruebas de modelo debemos de usar el método it_behaves_like(). Y cuando necesitemos hacer referencia a la clase que estamos probando dentro de los “Shared Example” podemos utilizar el método described_class.

Utilizando los “Shared Examples” nuestras pruebas quedarían así:

spec/models/autor_spec.rb

require 'rails_helper'

RSpec.describe Autor, type: :model do
it_behaves_like "comentable"
it { expect(subject).to have_many(:libros) }
end

spec/models/libro_spec.rb

require 'rails_helper'

RSpec.describe Libro, type: :model do
it_behaves_like "comentable"
it { expect(subject).to belong_to(:autor) }
end

spec/concerns/comentable_spec.rb

require 'spec_helper'shared_examples_for "comentable" do
let(:factory_model) { described_class.to_s.underscore.to_sym }

it { expect(subject).to have_many(:comentarios) }

describe '#ultimo_comentario' do
context "cuando el #{described_class} no tiene comentarios" do
it 'regresa nil' do
test_subject = FactoryBot.create factory_model

expect(test_subject.ultimo_comentario).to eq nil
end
end

context "cuando el #{described_class} tiene comentarios" do
it 'regresa el ultimo comentario' do
test_subject = FactoryBot.create factory_model
comentarios = FactoryBot.create_list :comentario, 3, comentable: test_subject

expect(test_subject.ultimo_comentario).to eq comentarios.last
end
end
end

describe '#num_comentarios' do
context "cuando el #{described_class} no tiene comentarios" do
it 'regresa cero' do
test_subject = FactoryBot.create factory_model

expect(test_subject.num_comentarios).to eq 0
end
end

context "cuando el #{described_class} tiene comentarios" do
it 'regresa el ultimo comentario' do
test_subject = FactoryBot.create factory_model
comentarios = FactoryBot.create_list :comentario, 3, comentable: test_subject

expect(test_subject.num_comentarios).to eq 3
end
end
end
end

La principal modificación que sufrieron nuestras pruebas es que ahora en lugar de hacer referencia a un Libro o un Autor hacen referencia a una clase que puede variar dependiendo de en donde sean incluidos los “Shared example”.

Para lograr esa flexibilidad estamos utilizando la variable factory_model, con la cual podemos crear un objeto del modelo que está siendo probado.

test_subject = FactoryBot.create factory_model

Y para facilitar la lectura, también modificamos los contextos para incluir la clase descrita.

context "cuando el #{described_class} tiene comentarios" do

Así es como conseguimos evitar la duplicidad de código en nuestras pruebas y facilitamos el mantenimiento de las mismas.

Y así es como termina este ejemplo en el que cubrimos los conceptos básicos de los Concerns y de “Shared example”, si tienes alguna duda u opinión sobre los Concerns por favor déjala en los comentarios.

--

--

AMV México

AMV (American Marketing Ventures) México es una empresa de tecnología de marketing de rápido crecimiento.