Okay, let me clarify that title first. I, as most of you, have two sets of tests for my Rails application: rspec and cucumber. rspec heavily focusses on testing models and business logic while cucumber focusses on testing the entire application stack and user interaction.
The problem is that as your app grows, your test set grows - and so does the time it takes to run those tests.
This post is inspired by Corey Haines’ talk at Arrrrcamp (Oct 2011) and my own experience with writing fast specs.
Red - Green - Refactor#
If you do TDD/BDD you most likely follow the Red-Green-Refactor pattern:
- Write one test, and see it fail (red)
- Write the most minimal implementation to satisfy that test, and see it pass (green)
- Refactor your code to look/perform better (refactor)
- Repeat
But what if your test suite takes >30 seconds to run. You write a test, then wait 30 seconds to see the test fail. You then write the simple implementation, wait 30 seconds. Oops, you made a typo - fix it, wait 30 seconds. Now it passes. Refactor, again wait 30 seconds.
I think this scenario is very familiar for many rails developers.
Take a closer look#
So, let’s take a closer look at a real-world example.
This is a Post
model, it belongs_to
and author and it can give you a summary of an article by returning the text above a ‘~’ marker.
# app/models/post.rb
class Post < ActiveRecord::Base
DELIMITER = "~\n"
belongs_to :author
def summary
summary = if body =~ /#{DELIMITER}/i
body.split(/#{DELIMITER}/i).first.strip
else
body
end
end
end
This is a spec that’s defined for the Post
model:
# spec/models/post_spec.rb
require 'spec/helper'
describe Post do
context "summary" do
it "should return the summary" do
post = FactoryGirl.build(:post, :body => "Summary\n~\nNo summary.")
post.summary.should == "Summary"
end
end
end
Running this spec would take at least 10 seconds. Running time rspec spec/models/post_spec.rb
woudl output something like this:
Finished in 0.05523 seconds
real 0m10.387s
This means that running the actual spec took 0.05s, but running the entire command took 10 seconds. What is slowing us down?
Dependencies#
As you see in the spec above I use FactoryGirl.build
instead of FactoryGirl.create
to prevent interacting with the database. I do this because hitting the database slows down your test.
I removed the database dependency in order for my test to run faster.
What other dependencies are there that could be removed in order to test the summary functionality?
Any idea?
Yes!
Rails.
Rails is a dependency to your app#
You have a Rails-app. But Rails is not your app, it’s a dependency, just like the pg
and haml
gems are dependencies.
Loading the entire Rails stack takes quite some time. And just like with the database dependency we must ask ourselves: do we really need Rails to perform this test?
There are a lot of scenarios where the answer to that question is NO.
Specs without Rails#
This may seem a bit weird at first, but let’s take another look at the Post
model:
# app/models/post.rb
class Post < ActiveRecord::Base
DELIMITER = "~\n"
belongs_to :author
def summary
summary = if body =~ /#{DELIMITER}/i
body.split(/#{DELIMITER}/i).first.strip
else
body
end
end
end
The summary
method does not interact with the Post
model at all, except that it access the body
attribute. But the body
attribute is just a String
.
So, if we wanted to test the summary
method and remove all Rails dependencies, we’d have to remove ActiveRecord.
Consider this:
# app/logic/myapp/summary.rb
module MyApp
class Summary
def self.for(text, delimiter)
summary = if text =~ /#{delimiter}/i
text.split(/#{delimiter}/i).first.strip
else
text
end
end
end
end
I think you can quickly see that this method does exactly the same as Post#summary
. But it does not have any dependency to ActiveRecord.
Could we rewrite our test for this new class?
# fast_spec/myapp/summary_spec.rb
require 'myapp/summary'
describe MyApp::Summary do
it "should return the summary" do
MyApp::Summary.for("Summary\n~\nNo summary.", "~\n").should == "Summary"
end
end
That looks good! Note that this spec does not include spec_helper
. spec_helper
is responsible for loading up your test environment, which normally includes all your app dependencies, including Rails.
Your new Post model#
The Post
model should also be updated, of course to utilise this new class.
# app/models/post.rb
class Post < ActiveRecord::Base
DELIMITER = "~\n"
belongs_to :author
def summary
MyApp::Summary.for(body, DELIMITER)
end
end
The spec for Post
should also be changed. Since we already have tested that MyApp::Summary#for
returns the right summary for a given text and delimiter, all we have left to do is make sure that Post#summary
calls it correctly.
# spec/models/post_spec.rb
require 'spec/helper'
describe Post do
context "summary" do
it "should return the summary" do
post = FactoryGirl.build(:post)
MyApp::Summary.should_receive(:for).with(post.body, Post::DELIMITER)
post.summary
end
end
end
Running fast specs#
The files above are located in fast_spec
and app/logic
. I do this because I want to separate my fast_specs so I can run them independtly from my normal specs.
Running fast specs works like this:
rspec -I app/logic fast_spec
Try it out with time rspec -I app/logic fast_spec
:
Finished in 0.03223 seconds
real 0m0.421s
That’s your same spec, down from about 10 seconds to 0.5 second.
The big picture#
By now you have seen that you can extract business logic into a seperate class. You’ve also seen that you can write tests that don’t depend on Rails and run amazingly fast.
Of course you ask, how does this relate to normal RSpec and Cucumber tests?
My opinion is that your fast_specs test business logic that does not relate to anything Rails specific, like making calculations, processing text, et cetera.
When you make changes to your business logic, you can test is very quickly. This shortens your TDD cycle and allows you to focus more on the task at hand (instead of waiting for your tests).
RSpec tests are there to test integration with your dependencies like Rails. This is the place where test scopes and mailers.
Cucumber still has its place to test user interaction with your app. Can a user still click the ‘Order’ button and get the proper response from our app.
RSpec and Cucumber will now be ran less often. Maybe only before you commit code - or maybe have your CI run them for you?
Added benefit - Design#
An added bonus of TDD is that your tests dictate the design of your app. By using fast_spec you force your business logic into separate classes. This makes them even more re-usable than normal Rails models.
Your ActiveRecord models now become cleaner and are more a configuration of Rails than anything else.
By doing this your app will become way more maintainable, easier to test, faster to test and your code will be more re-usable. What’s not to like?
Where to go from here?#
As Corey Haines put it:
Try to extract one single method from your app into a fast_spec. Just one. When you have that one spec running so fast it’s easy to add more.
This would also be my advise to you. Try to extract one piece of functionality from a model into a fast_spec. It may be difficult at first, but try it and see the results.
Then, when you have that one example working, add more.