Here at PrimerHammer, we use TDD (Test-Driven Development) as our primary and most trusted method of building code that is sustainable, well designed and easy to read. TDD is what allows us to safely refactor our code without the fear of changing its behavior or “breaking things”. If you are not sure what TDD is or how it works, this post is for you.
TDD is a software development process that relies on the repetition of a very short development cycle: first, the developer writes an (initially failing) automated test case that defines a desired improvement or new function; then, the developer produces the minimum amount of code to pass that test; and finally, the developer refactors the new code to acceptable standards. Some call this cycle a RED-GREEN-REFACTOR.
- RED: This is when a failing spec is created (it’s red).
- GREEN: This is when a minimum amount of code is written to pass that test. The code doesn’t have to be perfect, as long as the spec passes (and becomes green).
- REFACTOR: This is when the code is made to be more readable, and duplications are searched for and removed.
An Example of how TDD works.
From Red to Green
class User < ActiveRecord::Base
end
This is the initial code. The user model has name and company_name attributes, but we want to have a helper method that prints a formatted user name and company_name.
The first thing to do is to write a test in RSpec.
require 'spec_helper'
describe 'UserDecorator' do
let(:user) { User.new name: 'Tony', company_name: 'Stark Industries' }
it 'displays name and company name' do
decorator = UserDecorator.new user
expect(decorator.name_and_company).to eq 'Tony (Stark Industries)'
end
end
What we are trying to do here is to create an instance of UserDecorator class that has a method that formats the name and company. We could add the method to User instead, however, what we want is to keep the formatting of the user data separated, in the UserDecorator class. This spec will initially fail because the UserDecorator class does not yet exist.
Failures:
1) UserDecorator displays name and company name
Failure/Error: decorator = UserDecorator.new user
NameError:
uninitialized constant UserDecorator
# ./spec/decorators/user_decorator_spec.rb:7:in `block (2 levels) in &amp;lt;top (required)&amp;gt;'
1 example, 1 failure
In order to fix this spec (and make it pass) we need to create the UserDecorator class, by doing the following:
class UserDecorator
end
Now, we need to run the test again, by doing the following:
Failures:
1) UserDecorator displays name and company name
Failure/Error: decorator = UserDecorator.new user
ArgumentError:
wrong number of arguments (1 for 0)
# ./spec/decorators/user_decorator_spec.rb:7:in `initialize'
# ./spec/decorators/user_decorator_spec.rb:7:in `new'
# ./spec/decorators/user_decorator_spec.rb:7:in `block (2 levels) in &amp;lt;top (required)&amp;gt;'
1 example, 1 failure
Ouch! The test failed because the initialize method of UserDecorator didn’t accept the arguments, so what we need to do is redefine the method, by doing the following:
class UserDecorator
def initialize(user)
end
end
Now, we will need to run the test again:
Failures:
1) UserDecorator displays name and company name
Failure/Error: expect(decorator.name_and_company).to eq 'Tony (Stark Industries)'
NoMethodError:
undefined method `name_and_company' for #&amp;lt;UserDecorator:0x007f28e2736650&amp;gt;
# ./spec/decorators/user_decorator_spec.rb:8:in `block (2 levels) in &amp;lt;top (required)&amp;gt;'
1 example, 1 failure
All of these steps (fixing the implementation, running the specs again, and so on) can be done repeatedly, but for the sake of reaching the final point in less time, I’m going to take a shortcut and present you with the final implementation needed to make the test green:
class UserDecorator
def initialize(user)
@user = user
end
def name_and_company
@user.name + " (" + @user.company_name + ")"
end
end
Here are the test results, after the final implementation:
1 example, 0 failures
Finally, we have a green test.
Refactor
All right, since we have a passing test now we can continue with the refactoring of that ugly code into something much more readable, by doing the following:
class UserDecorator
def initialize(user)
@user = user
end
def name_and_company
"#{@user.name} (#{@user.company_name})"
end
end
Since we made changes to the UserDecorator class, all we need to do now is to simply run the test to make sure we checked whether the code’s behavior is as expected.
1 example, 0 failures
Finally, we have completed our task. With the help of TDD we have incrementally built a helper to print our user data. However, what you might be asking yourself now is why go through all this trouble?
Benefits of TDD
- TDD helps us detect misunderstandings/ambiguities in the requirements. By using TDD we can save more time by avoiding the implementation of classes that are not really needed.
- TDD Improves the maintainability and changeability of code base.
- By using TDD we can also simplify the implementation and make it more readable (small classes for instance)
- TDD Tests are usually easy to setup.
Drawbacks
- It takes more time – remember the RED-GREEN-REFACTOR? The specs are usually run many times before the implementation of the target class is completed.
- TDD Increases the cost of test maintenance as the project grows (due to the refactoring of slow specs etc.).
- The dogmatism related to TDD about how to test and what tools to use can be distracting, overwhelming, and it can lead to unproductive fears and insecurity about whether one is “doing it right”.
- One can overemphasize the testing and forget what is more important – the product itself.
Tip: When you are dealing with data formatting, you can try out Draper – a library focused on decorators.
This post was inspired by Erich’s lightning talk in our office. Thanks Erich.