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…
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
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
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
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
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!
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.