Have you had started a project trying to do TDD “the right way”, writing “unit tests” for every class and method, trying to hit that famous 100% coverage….
… And then few months later, rewriting many of them just for a little change or refactor!
Did you thought that TDD was just not for you?…
Well, don’t worry… I am not sure why but this is a common stage while learning to use TDD.
The good thing is that this is not the only way to do it!
You can design your code in a little different way… Instead of focusing on your model and each class you build to solve the problem, put your eyes (and your tests) on your API (but not the HTTP API).
Work to build an API that can expose the expected use cases in a simple and clear way… (As a tip try to start with a function).
Then, in the process of TDD after you have been writing tests against that API, and the implemented behavior starts to grow, you will need more classes, modules and methods, and maybe you will be tempted to open a new file and write some tests for those new classes and methods… But don’t do it, yet!
Try to tests as much as you can through the API that you want to expose. This is what will let you refactor your code when you think is needed.
Imagine you are working on a blog application in the function to add a post. Maybe in your first tests (the happy path) you could have something like this…
describe "with all attributes..." do
attr_reader :params
before do
@params = { "title" => "P1", "body" => "Super!" }
end
it "returns success" do
status = add_post(params, store)
expect(status).to be_success
end
it "creates a record" do
expect(store).to receive(:create).with(title: "P1", body: "Super!")
add_post(params, store)
end
end
And to implement the expected behavior, maybe something like…
module Blog
def self.add_post(attrs, store)
store.create({ title: attrs["title"], body: attrs["body"] })
SuccessStatus
end
class SuccessStatus
def self.success?
true
end
end
end
Do you think we need to write test for your SuccessStatus
class?
To add a post the title will be required, and the system should return an error if the title is not present.
To test that behavior, maybe you could write something like this…
describe "without title..." do
it "does not returns success" do
status = add_post({}, store)
expect(status).not_to be_success
end
it "does not creates the record" do
expect(store).not_to receive(:create)
add_post({}, store)
end
it "returns a blank error" do
status = add_post({}, store)
expect(status.form.errors[:title]).to eq ["can't be blank"]
end
end
And a possible implementation could be something like…
module Blog
def self.add_post(attrs, store)
form = Form.new(attrs)
if form.valid?
store.create(form.to_h)
SuccessStatus
else
ErrorStatus.new(form)
end
end
class Form
include ActiveModel::Model
attr_accessor :title, :body
validates_presence_of :title
def to_h
{ title: title, body: body }
end
end
class ErrorStatus
attr_reader :form
def initialize(form)
@form = form
end
def success?
false
end
end
class SuccessStatus
def self.success?
true
end
end
end
Do you think you should write new test cases to test the ErrorStatus
and Form
objects directly?
Let’s think a little…
You have already tested the complete required behavior through the use case API and the tests are fast!…
You can add tests for the ErrorStatus
and Form
objects, but…
If you ask me… I would not write those new tests cases to test them directly.
Here I try 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 posts.