Benito Serna Tips and tools for Ruby on Rails developers

5 Reglas para desacoplar tu aplicación de Rails y tener pruebas realmente rápidas

November 11, 2016

En los últimos proyectos que he trabajado, tanto solo como en equipo, poco a poco he ido descubriendo ciertas practicas o reglas que usamos y que quiero compartir porque creo que también te pueden servir.

Estas practicas nos han ayudado a:

Nota: Cuando digo pruebas rápidas me refiero más o menos a esto…

Las reglas que seguimos son…

1. La lógica de tu aplicación o lógica de “negocio” va detrás de métodos o funciones de módulo.

No es necesario hacer mucha “ceremonia” y hablar de “Interactors”, “Service Objects”, “Presenters” o algún otro patrón.

Aunque probablemente usarás algo similar a estas cosas ya dependerá de tu aplicación.

Un ejemplo de las funciones que podría tener un módulo sería algo así:

module Projects
  def self.publish_project
  end
  def self.add_termination_date_form
  end
  def self.add_termination_date
  end
  def self.details_page_for_project
  end
  def self.terminate_funding_form
  end
  def self.terminate_funding
  end
  def self.terminated_funding_page
  end
  def self.revert_funding_termination
  end
end

2. Estas funciones de módulo las llamas dentro de de Rails, es decir de tu controlador/mailer/job/model/etc.

Algo importante aquí es que las funciones sean de primer nivel es decir que no usemos funciones con doble namespace como Projects::TerminateFunding.form.

Tal vez en tu aplicación habrá casos donde ocupes hacer este namespace, pero a mi no me ha tocado.

Un ejemplo podría ser así:

class Admin::ProjectFundingTerminationsController < Admin::BaseController
  def new
    project = find_project
    form = Projects.terminate_funding_form(project)
    render locals: { project: project, form: form }, layout: false
  end

  def create
    termination = Projects.terminate_funding(
      find_project.id,
      params,
      projects_store: Project,
      investment_receipts_creator: DelayedInvestmentReceiptsCreator
    )
    if termination.success?
      render partial: "admin/projects/projects_list", locals: { projects: admin_projects_list }
    else
      render status: :unprocessable_entity
    end
  end
end

3. En estos módulos no puedes tener ninguna dependencia directa de ninguna clase de Rails.

En esta regla, se vale incluir librerías como ActiveModel o ActiveSupport, pero es mejor ser prudente.

En general todas las dependencias son “inyectadas” como parámetros de la función.

El anterior ejemplo muestra una forma de hacerlo, pero aquí muestro un ejemplo más:

class Admin::ProjectFundingTerminationsController < Admin::BaseController
  def destroy
    Projects.revert_funding_termination(
      find_project,
      projects_store: Project,
      receipts_cancelator: ReceiptsCancelator
    )
    redirect_to admin_projects_path
  end

  private

  def find_project
    Project.find_by_slug params[:project_id]
  end
end

4. Todas las pruebas son a través de las funciones de módulo.

Es decir que vamos a probar justo a través de las funciones que vamos a llamar desde los componentes de Rails.

Por lo que no hay pruebas para clases internas o métodos privados. ¡Esto es lo que nos permitirá refactorizar!

En algunos casos, tendrás que ingeniártelas para hacer que las pruebas sean claras o específicas, como en el siguiente ejemplo:

it "(when project is refunded)" do
  portfolio = portfolio_on(Date.new(2018, 2, 4))
  date = next_payment_date_for_project(
    portfolio,
    refunded_project,
    formatter: ->(date) { "My date: #{date}" },
    not_executed_message: "No se ha ejecutado",
    no_payments_left_message: "No hay más pagos",
    project_refunded_message: "No aplica"
  )

  expect(date).to eq "No aplica"
end

def next_payment_date_for_project(portfolio, project, options = {})
  portfolio.
    projects_with_investment.
    detect { |p| p.id == project.id }.
    next_payment_date(options)
end

def portfolio_on(current_date)
  portfolio_with([project_one, project_two, not_executed_project, refunded_project], current_date)
end

def portfolio_with(projects, current_date)
  projects_finder = FakeProjectFinder.new(*projects)
  UserPortfolio.portfolio_for_user(user, current_date, projects_finder)
end

5. No usar active record en las pruebas.

Esto es hace que tus pruebas corran mucho más rápido porque no necesitas cargar Rails y hacer cada operación en la base de datos.

Y también te ayuda a modelar mejor la interfaz entre tu sistema y active record porque podrás trabajar el diseño por medio de las pruebas antes de crear tablas en la base de datos.

Yo tiendo a hacer esto por medio de simples objetos de ruby como en el siguiente ejemplo:

before do
  @user = user_with(first_name: "Maria")
  @project_one = project_with(
    fixed_annual_rate: 11,
    execution_date: Date.new(2015, 10, 8),
    investments: [investment_with(amount: 100_000, user: user)])
end

def project_with(attrs)
  FakeProject.new({ fixed_annual_rate: 11 }.merge(attrs))
end

def investment_with(attrs)
  FakeInvestment.new(attrs)
end

def user_with(attrs)
  FakeUser.new(attrs)
end

class FakeInvestment
  attr_reader :id, :date, :user, :amount, :user_id, :project, :project_id, :investment_type

  def initialize(attrs = {})
    @id = attrs[:id] || SecureRandom.uuid
    @date = attrs[:date]
    @user = attrs[:user]
    @user_id = attrs[:user_id] || user && user.id
    @amount = attrs[:amount]
    @project = attrs[:project]
    @project_id = attrs[:project_id] || project && project.id
    @investment_type = attrs[:investment_type]
  end
end

… ¡Y listo!… ¡Espero que a ti también te sean de utilidad!

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.