As I wrote in the previous article about using end-to-end tests in your TDD cycle… This kind of tests can be very helpful in some situations, and I think that is a good thing to try if it works for you and your current project.
I actually used the end-to-end tests to start my TDD cycle, more ore less like in the GOOS book, for a lot of time and projects… But to be honest I normally don’t put end-to-end tests in my TDD workflow. I normally prefer to write this kind of tests after the implementation, and not for every feature.
The problem with not using end-to-end tests is that that you could end with tests for a lot small objects interacting with each other, that in my opinion normally that’s a good thing, but with no evidence that you have wired them together in the right way =S
So to tackle this problem I almost always try to design “entry” points for my application code, in similar way of what some people call “Application services” or “Ports”…
In this way if in a feature dozens of different kind of objects/components will interact with each other… I try to “pack” them in this kind of commands to the application that I call use case functions. Resulting in 1 to 3 commands (or sometimes a little more) that take place in a bigger feature.
… And this things are the things that I write tests for! So instead of having independent tests for each piece of my “model” I test to 1 to 3 different actions in the application…
This help me to…
In a guest list app, we have a function to add an invitation… That we call in the controller like this…
class InvitationsController < ApplicationController
def new
render locals: { list_id: list_id, invitation_form: Lists.get_invitation_form }
end
def create
status = Lists.add_invitation(list_id, params[:invitation], ListInvitationRecord)
if status.success?
# render list
else
render status: 422, locals: { list_id: list_id, invitation_form: status.form }
end
end
end
And we have a test file like this…
require_relative "../lists_spec"
module Lists
describe "Add invitation" do
def invitation_with(attrs)
{id: rand(100)}.merge(attrs)
end
def store_with(records)
FakeInvitationsStore.new(records)
end
it "has a form with some attributes..." do
form = Lists.get_invitation_form
expect(form.title).to eq nil
expect(form.phone).to eq nil
expect(form.group).to eq nil
expect(form.email).to eq nil
expect(form.guests).to eq nil
end
describe "with good params" do
attr_reader :store, :list_id, :params
before do
@store = store_with([])
@list_id = 1234
@params = {
"title" => "Uno",
"guests" => "Benito, Maripaz",
"group" => "Amigos Benito",
"phone" => "1234-1234",
"email" => "bh@example.com"
}
end
it "creates a record" do
expect(store).to receive(:create).with({
list_id: list_id,
title: "Uno",
group: "Amigos Benito",
guests: "Benito, Maripaz",
phone: "1234-1234",
email: "bh@example.com"
}).and_call_original
Lists.add_invitation(list_id, params, store)
end
it "returns success" do
status = Lists.add_invitation(list_id, params, store)
expect(status).to be_success
end
it "returns the id of the created record" do
allow(store).to receive(:create).and_return(invitation_with(id: "234"))
status = Lists.add_invitation(list_id, params, store)
expect(status.invitation_id).to eq "234"
end
end
describe "without a title" do
attr_reader :store, :list_id, :params
before do
@store = store_with([])
@list_id = 1234
@params = {
"title" => "",
"guests" => "Benito, Maripaz",
"group" => "Amigos Benito",
"phone" => "1234-1234",
"email" => "bh@example.com"
}
end
it "does not create the record" do
expect(store).not_to receive(:create)
Lists.add_invitation(list_id, params, store)
end
it "does not return success" do
status = Lists.add_invitation(list_id, params, store)
expect(status).not_to be_success
end
it "returns a blank error" do
status = Lists.add_invitation(list_id, params, store)
form = status.form
expect(form.errors[:title]).to eq "no puede estar en blanco"
end
it "keeps the sent params" do
status = Lists.add_invitation(list_id, params, store)
form = status.form
expect(form.title).to eq ""
expect(form.guests).to eq "Benito, Maripaz"
expect(form.group).to eq "Amigos Benito"
expect(form.phone).to eq "1234-1234"
expect(form.email).to eq "bh@example.com"
end
end
end
end
And an implementation like this…
module Lists
#....
def self.add_invitation(list_id, params, store)
form = InvitationForm.new(params)
errors = NewInvitationValidator.validate(form)
if errors.empty?
record = store.create(form.to_h.merge(list_id: list_id))
InvitationCreatedResponse.new(record[:id])
else
form.add_errors(errors)
ErrorWithForm.new(form)
end
end
end
As you can see we have several objects talking to each other, but we are not testing them directly…
For me this feels like writing unit tests as feature tests, but you know, naming is hard…
I have found this kind of use case functions very helpful and I hope you can take some ideas and apply something =)
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.