Skip to content

Instantly share code, notes, and snippets.

@bansalakhil
Created November 20, 2012 11:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bansalakhil/4117298 to your computer and use it in GitHub Desktop.
Save bansalakhil/4117298 to your computer and use it in GitHub Desktop.
Writing Faster Rails Tests
Hey there! And welcome to my little mailing list about writing faster Rails tests.
My goal is to send you only focused, actionable info that will help you speed up your test suites right away. Let's get started!
The first thing we'll cover is the leading cause of test slowness in Rails apps: bloated fixtures.
Here's an example of a test that will run far slower than it needs to (using factory_girl (a great tool, despite its involvement in this test's slowness)):
describe "#full_name" do
it "returns the order placer's concatenated name" do
order = FactoryGirl.create(:order, first_name: 'Ben', last_name: 'Orenstein')
order.full_name.should == 'Ben Orenstein'
end
end
The stultifyingly slow culprit here is our call to FactoryGirl.create.
Many people blithely include a line like that, without realizing that it performs a boatload of work.
First, factory_girl instantiates and saves every association required to build a valid Order. It's easy for important domain objects to have 4-6 associated objects. Each of these may have their own dependencies that must be instantiated too (in this example, Order needs a Merchant, which needs a MerchantAdmin, which needs a User, and so forth.)
Also, consider that every one of these dependent objects must be validated and have callbacks run. Some of these callbacks will generate additional objects which must be saved, such as an audit record. Some will spend time rendering confirmation emails that are never delivered.
In short, it's extremely easy for an innocuous little line like this to create dozens of objects, persist them all to a lumbering database, and spend tons of time executing code paths that have NOTHING to do with what you're testing.
This isn't a made up example, by the way. On a real e-commerce app I've worked on, FactoryGirl.create(:order) takes 7.3 seconds.
Seven seconds.
When it's cooking along, my processor can perform around two billion operations per second.
I want to test a method that concatenates two strings.
Does seven seconds sound right to you? It shouldn't.
What should our test look like instead? How about this (I've changed only the line that creates our Order):
describe "#full_name" do
it "returns the order placer's concatenated name" do
order = Order.new(first_name: 'Ben', last_name: 'Orenstein')
order.full_name.should == 'Ben Orenstein'
end
end
Wanna know how long this test takes? Two ten-thousandths of a second.
This test's execution time is 35,000 times faster than the first one. That's the kind of win worth working for, and the kind that will be the focus of this mailing list.
As a bonus, using such a minimal fixture means this test is isolated from the rest of the system. It's now extremely unlikely to break when an unrelated part of the system changes. Not so in our first example.
In general, you should strive to only build the most minimal fixture possible. Anything more is slow, brittle, and may confuse the person reading your test as to what's actually needed.
Follow that guideline, and you'll be well on your way to faster tests.
Let's look at one more example of building only the minimum required, but with a slightly different approach.
Here's a simple class that needs to be tested:
class UserNotifier
def initialize(user)
@user = user
end
def notify
send_email_to(user.email)
end
end
To test the notify method, we're going to need to construct an instance of the UserNotifier with a User object.
Here's one way to set up that test:
user = FactoryGirl.create(:user)
notifier = UserNotifier.new(user)
Having just discussed the previous example, you're likely to spot the unnecessary creation and persisting of a User to the database. Since this is a lot of needless work, perhaps you'd change the setup to look like the following:
user = User.new(email: 'foo@bar.com')
notifier = UserNotifier.new(user)
Make no mistake, this code is much better. It's going to be vastly more performant and the test isolation has been improved. We can, however, do even better.
If you look at the implementation of UserNotifier, you'll notice we don't really need a bona fide User object. As long as we pass in something that responds to a method called email, our code will be quite happy.
This is exactly the use-case that stubs were designed for. Let's try that:
user = stub('user', email: 'foo@bar.com')
notifier = UserNotifier.new(user)
Now we're talking.
First, creating a stub like this is around 200 times faster than instantiating a real ActiveRecord object. If that kind of thing doesn't make you happy, you're probably on the wrong mailing list.
Second, we've now achieved a maximum amount of isolation in our test. This test will fail if UserNotifier is broken, but for no other reason. Changing User around won't bother it in the slightest.
(I'm sure some of you are concerned about that last statement. Doesn't this sort of mocking mean we could have passing tests even if we removed the email method from User? It sure does. And that's why isolated tests like these must be paired with integration tests. More on that in a future email.)
One other thing: notice that I reached for a stub in this example but not in the first one. That's because the first example was testing a method directly on the object under test. I don't like to stub parts of the system that's under test (specifically, I won't stub ANY methods on Order if I'm testing how an Order should behave). But when UserNotifier is what's being tested, and I'm just satisfying its dependency, a stub is a great choice.
We've covered a lot, so let's wrap up with some recommendations for you to try this week:
1. Keep your eye out for FactoryGirl.create, and all other forms of creating too much fixture for the job. Strive for minimalism.
2. Keep your other eye peeled for needless persisting. Don't save things unless you're specifically testing your app's interaction with your database (testing finders, for example.) Prefer in-memory objects.
3. Look for places where you can swap in ultra-light stubs to satisfy dependencies.
This article has gotten us started, but there's tons more I want to cover. Here are some of the things you can expect to read about in future emails:
* Tools for reducing Rails' boot time
* Running tests from within your editor
* Running tests in parallel on many cores
* A review of CI-as-a-service offerings
* Speeding up notoriously-slow integration tests
* Why fast tests tend to lead to good design
Finally, if you found this email useful, I'd appreciate it if you'd consider doing me a favor (or two, if you're really feelin' the love right now):
1. If you have a friend or coworker who would like to learn this stuff too, please point them toward my signup page: goo.gl/FCeIg
2. Hit 'Reply', and tell me what what you thought of this email. Maybe mention what you'd most like for me to cover next. Replies go straight to my personal inbox and I promise to read every response.
More soon!
-Ben
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment