From the Blogosphere
Slow Tests Are the Symptom, Not the Cause
Test slowness is merely the symptom; what you should really address is the cause
Oct. 19, 2013 05:00 PM
If you have a slow test suite and you are asking yourself "how can I make my tests faster?" then you are asking the wrong question. Most chances are that you have bigger problems than just slow tests. The test slowness is merely the symptom; what you should really address is the cause. Once the real cause is addressed you will find that it's easy to write new fast tests and straightforward to refactor existing tests.
It's surprising how quickly a rails app's test suite can become slow. It's important to understand the reason for this slowness early on and address the real cause behind it. In most cases the reason is excessive coupling between the domain objects themselves and coupling between these objects and the framework.
In this refactoring walk-through we will see how small, incremental improvements to the design of a rails app, and specifically, decoupling, naturally lead to faster tests. We will extract service objects, completely remove all rails dependencies in test time and otherwise reduce the amount of coupling in the app.
Our goal is to have a simple, flexible and easy to maintain system in which objects can be replaced with other objects with minimal code changes. We will strive to achieve this goal and observe the effect of it on our tests speed.
Starting with a Fat Controller
Suppose we have a controller that's responsible for handling users signing up for a mailing list:
class MailingListsController < ApplicationController
respond_to :json
def add_user
user = User.find_by!(username: params[:username])
NotifiesUser.run(user, 'blog_list')
user.update_attributes(mailing_list_name: 'blog_list')
respond_with user
end
end
We first find the user (an exception is raised if the user is not found). Then we notify the user she was added to the mailing list via NotifiesUser
(probably asking her to confirm). We update the user record with the name of the mailing list and then hand the user
object to respond_with
, which will render the json representation of the user or the proper error response in case saving of the object failed.
The logic here is pretty straight-forward, but it's still too complicated for a controller and should be extracted out. But where to? The word user
in every line in this method suggests that we should push it into the User
model (that's called Feature Envy). Let's try this:
Extracting Logic to a Fat Model
class MailingListsController < ApplicationController
respond_to :json
def add_user
user = User.add_to_mailing_list(params[:username], 'blog_list')
respond_with user
end
end
class User < ActiveRecord::Base
validates_uniqueness_of :username
def self.add_to_mailing_list(username, mailing_list_name)
user = User.find_by!(username: username)
NotifiesUser.run(user, 'blog_list')
user.update_attributes(mailing_list_name: 'blog_list')
user
end
end
This is better: the User
class is now responsible for creating and updating users. But there is a problem: now User
is handling mailing list additions, as well as user notifications. These are too many responsibilities for one class. Having an active record object handle anything more than CRUD, associations and validations is a (further) violation of the Single Responsibility Principle.
The result is that business logic in active record classes is a pain to unit test. You often need to use factories or to heavily stub out methods of the object under test (don't do that), stub all instances of the class under test (don't do that either) or hit the database in your unit tests (please don't). As a result, testing active record objects can be very slow, sometimes orders of magnitude slower than testing plain ruby objects.
Now, if the code above was the entire User
class and my application was small and simple I might have been happy with leaving User#add_to_mailing_list
as is. But in a bit bigger rails apps that are not groomed often enough, models, controllers and domain logic tend to get tangled (coupled) together and needlessly complicate things (Rich Hickey, the inventor of clojure, calls it incidental complexity). This is when introducing a service objectis helpful:
Extracting a Service Object
class MailingListsController < ApplicationController
respond_to :json
def add_user
user = AddsUserToList.run(params[:username], 'blog_list')
respond_with user
end
end
class AddsUserToList
def self.run(username, mailing_list_name)
user = User.find_by!(username: username)
NotifiesUser.run(user, 'blog_list')
user.update_attributes(mailing_list_name: 'blog_list')
user
end
end
We created a plain ruby object, AddsUserToList
, which contains the business logic from before. In the controller we call this object and not User
directly. This is an improvement, but hard-coding the name of the class of your collaborator is a bad idea since it couples the two together and makes it impossible to replace the class with a different implementation. Not surprisingly, the result of this coupling is that testing becomes harder and tests slower. Testing this service object would require us to somehow stub User#find_by!
to avoid hitting the database, and probably also stub out NotifiesUser#run
in order to avoid sending a real notification out.
Also, referencing the class User
directly means that our unit tests will have to load active record and the entire rails stack, but even worse - the entire app and its dependencies. This load time can be a few seconds for trivial rails apps, but can sometimes be 30 seconds for bigger apps. Unit tests should be fast to run as part of your test suite but also fast to run individually, which means they should not load the rails stack or your application (also seeCorey Haines's talk on the subject).
The most straight forward way to decouple the object from its collaborators is to inject the dependencies of AddsUserToList
:
Injecting Dependencies
class AddsUserToList
def self.run(username, mailing_list_name, finds_user = User, notifies_user = NotifiesUser)
finds_user.find_by!(username: username)
notifies_user.(user, mailing_list_name)
user.update_attributes(mailing_list_name: mailing_list_name)
user
end
end
We can now pass as an argument any class that finds a user and any class that notifies a user, which means that passing different implementations will be easy. It also means that testing will be easier. Since we supplied reasonable defaults we don't need to be explicit about these dependencies if we don't change them, and our controller can stay unchanged.
The fact that we are specifying User
as the default value of finds_user in the parameter list does not mean that this class and all its dependents (ActiveRecord, our app and other gems) will get loaded. Ruby's Deferred Evaluation of the default values means that if these default values are not needed they will not get loaded, so we can run this unit test without loading rails.
Simplifying the Interface
The method AddsUserToList#run
receives 4 arguments. Users of this method need to know the order of the list. Also, it is likely that over time you'd discover you need to add more arguments. When this happens you will need to update all users of the method. A more flexible solution is to use a hash of arguments. This will make the interface more stable and ensure the number of arguments does not grow when we find that we need to add more arguments. It will also make refactoring a little easier, which is important. I often find that for many classes I end up changing from an argument list to a hash of arguments at some point, so why not use it in the first place? But does it mean that we need to give up the advantages of deferred evaluation of the default values? Not at all.
We will use Hash#fetch
, passing a block to it, which will not get evaluated unless the queried key is absent. In our tests, the code in the block to fetch
will never get evaluated, and User
won't get loaded. Also, when specifying the defaults in the argument list it is not possible to evaluate more than one statement, but we can do it using Hash#fetch
.
One more thing: when my classes contain only one public method I don't like calling it run
, do
or perform
since these names don't convey a lot of information. In this case I'd rather call it call
and use ruby's shorthand notation for invoking this method. This also enables me to pass in a proc instead of the class itself if I need it.
class AddsUserToList
def self.run(args)
finds_user = args.fetch(:finds_user) { User }
notifies_user = args.fetch(:notifies_user) { NotifiesUser }
finds_user.find_by!(username: args.fetch(:username))
notifies_user.(user, args.fetch(:mailing_list_name))
user.update_attributes(mailing_list_name: args.fetch(:mailing_list_name))
user
end
end
Using Ruby 2.1's Keyword Arguments Syntax
We can get the same exact functionality by using ruby's 2.1's keyword argument syntax. See how much less verbose this version is:
class AddsUserToList
def self.call(username:, mailing_list_name:, finds_user: User,
notifies_user: NotifiesUser)
user = finds_user.find_by_username!(username)
notifies_user.(user, mailing_list_name)
user.add_to_mailing_list(mailing_list_name)
user
end
end
About Manuel WeissI am the cofounder of Codeship – a hosted Continuous Integration and Deployment platform for web applications.
On the Codeship blog we love to write about Software Testing, Continuos Integration and Deployment. Also check out our weekly screencast series 'Testing Tuesday'!