If you're like most Rails developers I know (including myself), you're probably used to writing "unit" tests in RSpec that load up the whole Rails framework before each test, which takes a few seconds to do, even if you're only testing one tiny thing.
This is of course annoying because it's hard to get into a flow state when you're always having to wait a few seconds to see the results of your test. What do you do during that few seconds? It's not a long enough time to work on something else, but it's too long to just idly sit there.
No more slow tests
One good way to get rid of the delayed-test-result problem is to separate your domain logic from your persistence logic, and then test your domain logic separately without loading Rails or doing database interation. This is much easier said than done. I hope to shed some light on the how in this post.
An example
What I'm going to do is show you a pretty simple Ruby class along with its tests, and then demonstrate how you might persist this class's objects to the database. My example will be incomplete because, perhaps like you, this way of testing is new to me and the path to doing this is not yet clear. Hopefully I'm metaphorically hacking away a few feet of trail and helping some people get a little further.
The domain model
The example I'll use is an Appointment
class that knows how to calculate its own length based on the services for that appointment. Here's the class:
class Appointment
attr_accessor :start_time, :errors, :services
def initialize
@errors = []
@services = []
end
def length
@services.collect(&:length_in_minutes).inject(0, :+)
end
end
Just ignore @errors
part for now. I'll explain that later. Now here's the spec for theAppointment
class:
The spec
# Notice the absence of "require 'spec_helper'." We're only
# including the file for the class we're wanting to test.
require File.dirname(__FILE__) + "/../../app/models/appointment"
describe Appointment do
before do
@appointment = Appointment.new
@appointment.start_time = "2000-01-01 00:00:00"
end
it "can have services added to it" do
service = Object.new
@appointment.services << service
expect(@appointment.services).to eq([service])
end
describe "#length" do
it "is the sum of all its services' lengths" do
[FIRST_SERVICE_LENGTH = 30, SECOND_SERVICE_LENGTH = 45].each do |length|
@appointment.services << service_stub_with_length(length)
end
expect(@appointment.length).to eq(FIRST_SERVICE_LENGTH + SECOND_SERVICE_LENGTH)
end
it "is 0 when there are no services" do
@appointment.services = []
expect(@appointment.length).to eq(0)
end
end
def service_stub_with_length(length)
service = double()
service.stub(:length_in_minutes) { length }
service
end
end
There's also this other tiny class, Service
:
class Service
attr_accessor :name, :price, :length_in_minutes
end
It of course does almost nothing.
If you run the spec for Appointment
, it takes barely any time at all. For me, if I puttime
around the command, it takes just over a second, and adding more tests doesn't make the whole test run meaningfully slower. You can have 10 or presumably 1000 tests like this and still see the results almost immediately. This allows you to get into and stay in a flow state.
Here's what a view would look like
It's neat that these classes I wrote can be tested quickly without loading Rails, but this whole example is kind of meaningless so far since you can't save anything to the database. Let's do that next.
Here's a form that could be used to save a new appointment. Notice how it's just a form_tag
and not a form_for
:
<%= form_tag(create_appointment_path) %>
<% if @appointment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@appointment.errors.count, "error") %> prohibited this appointment from being saved:</h2>
<ul>
<% @appointment.errors.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= datetime_field_tag :start_time, "", placeholder: "Start Time" %>
</div>
<%= submit_tag "Create appointment" %>
</form>
And here's the controller
class AppointmentsController < ApplicationController
def index
@appointments = Perpetuity[Appointment].all
end
def new
@appointment = Appointment.new
end
def create
# Create a PORO from params
@appointment = Appointment.new
@appointment.start_time = params[:start_time]
# Instantiate a validator for the appointment
# (which itself is incidentally also a PORO)
validator = AppointmentValidator.new(@appointment)
# If @appointment is valid, save it using the
# Perpetuity gem, an implementation of the Data
# Mapper ORM pattern
if validator.validate
Perpetuity[Appointment].insert(@appointment)
redirect_to action: 'index'
else
@appointment.errors = validator.errors
render action: 'new'
end
end
private
def prospective_user_params
params.require(:appointment).permit(:start_time)
end
end
In closing
I don't think the details of how AppointmentValidator
or Perpetuity work are necessarily terribly important to explain here. What I want to demonstrate is that POROs can be plugged into Rails in a way that's not necessarily totally awkward. I'm sure I'll start running into more challenges as I start separating domain layers from persistence layers more in production projects, but I think for now this is a pretty neat proof of concept.