Faster Ruby Testing: Only Test What Matters02 Aug 2015
Automated testing is important. Fast, exhaustive automated testing is even more important. Tests are responsible for ensuring the code you spend hours creating actually works. A great test suite can be a safeguard against bugs, a directional guide towards extending the code, and an accurate measurement of the codebase’s health. The key to writing good tests is understanding where pieces of responsibility begin and end. Maintaining small concise automated tests can make all the difference.
Speed is King
Aside from the general importance of short feedback cycles, a fast test suite will greatly improve your work-flow and mood. Conversely, a slow test suite can make your development cycle a living nightmare. How many unfortunate software artisans have dealt with something like:
Bleh! What a horrible thing to deal with. What, am I supposed to just wait 5 whole minutes every time I want to test my system? A test suite this slow is simply unacceptable.
So what could be causing this slowness? More than likely, a number of tests in this application are doing too much. To try and replicate and solve this problem, let’s assume we have a Ruby on Rails application we test using RSpec.
Testing Database Models is Slow
We can assume that there exists a very important class called
class MyAwesomeClass < ActiveRecord::Base def assign_and_save! saved_on_the_weekend = weekend? self.save! end def weekend? Time.now.saturday? || Time.now.sunday? end end
If we wanted to test that the
assign_and_save! method works correctly, we might write a test like:
describe '#assign_and_save!' do it 'assigns and saves' do awesome_test = MyAwesomeClass.new expect do awesome_test.assign_and_save! end.to change(MyAwesomeClass, :count).by(1) expect(awesome_test.saved_on_the_weekend).to_not be_nil end end
These tests make sure that the model is saved correctly to the database and a value is assigned to
saved_on_the_weekend. However, the test code is overstepping the boundaries of the method it is testing. The
assign_and_save! method’s job is to simply assign a value to an object and then save it. It does not care about how the actual saving works, that is the job of other validations on the model and
To avoid this, we can assert that the
save! method is called, which will not actually write to our database:
describe '#assign_and_save!' do it 'assigns and saves' do awesome_test = MyAwesomeClass.new expect(awesome_test).to receive(:save!) awesome_test.assign_and_save! expect(awesome_test.saved_on_the_weekend).to_not be_nil end end
Voila! We have made the same assertions about our code and did not do any slow input/output operations.
Now I know what some of the more detail oriented Software Artisans reading this will initially think: “What about validation concerns with the model?” and “If you stub out
save! like that you can’t be sure it worked”. While these are totally valid points, there exists a simple solution for dealing with the uncertainty of the save:
describe '#assign_and_save!' do it 'assigns and saves' do awesome_test = MyAwesomeClass.new expect(awesome_test).to receive(:save!) awesome_test.assign_and_save! expect(awesome_test.saved_on_the_weekend).to_not be_nil end it 'is save-able' do awesome_test = MyAwesomeClass.new allow(awesome_test).to receive(:save!) awesome_test.assign_and_save! expect(awesome_test).to be_valid end end
With the new test assertion, we make sure that the model’s
valid? method returns true, ensuring the model will be saved properly.
valid? method is what
ActiveRecord uses before saving the model to the database. This method returns either
false and writes the data to the database if
true was returned. Now the code is tested and
ActiveRecord’s’ validation errors are considered.
Not Perfect, but Good Enough
However, there is one more thing that this test will not catch: database specific uniqueness constraints. If your application uses a database to enforce uniqueness and not in
ActiveRecord validations, this testing method will fail. For that you will need a more robust integration tests which guard against duplicate data.
All that aside, not actually saving data will be fine for the 90% case which is most codebases.
Only Test What Matters
In this example, removing the testing of
save! resulted in a speed increase. However, the idea of only testing the crucial parts of a method is not only about speed, it is about encapsulation. If a method’s only responsibility is to call helper methods, that is the only thing that should be tested. The test should simply assert that the helper methods are called, not the logic within them. Following this pattern will help keep test code small, concise, readable, and input/output operations to a minimum.