SuperPumpup (dot com)
General awesomeness may be found here.

28 January 2013

My First Sub-millisecond Test

This weekend I had the opportunity to watch Corey Haines's excellent talk: Fast Rails Tests from the 2011 GoGaRuCo. Maybe I'm a bit late to the party, but better late than never, right? I decided that yes. I'll do it. I'll accept your challenge, Corey, and write that first fast test.

And of course, you're reading about it now because I'm going to document it.

I'm currently working on a project to manage operations of construction companies, and the application is nearing a deliverable state and currently our test output looks like this: 472 green dots of glory It's not bad. Honestly, it's the best test suite I've built, and what most of what doesn't get covered by these "unit" tests gets covered by integration tests (we're using Rspec/Cabybara/Selenium to run them rather than cucumber).

But why not push it?

Pick a bit of logic to test -

Not a TON of the "Domain-specific" logic has been built out of this application yet, as the work to date has for the most part been getting the data entry part smooth and efficient. There are a lot of associated models, and we've already battled some with FactoryGirl building associated models that are flaky passing validaitons, and so feeling a bit of pain there. I don't want this to necessarily be extracting out an ActiveRecord-dependent method, as I want it to be more simple, so after looking through a bunch of models settled on:

class Timesheet < ActiveRecord::Base
...

  private
  def calculate_total_hours
    (task_items.inject(0) {|sum, task_item| sum + ((task_item.end_time-task_item.start_time)/3600 || 0)}).round(3)
  end
end

This method is just a calculation method, doesn't do anything exotic, is already private(!!!), so should be a great candidate for extraction.

Test?

I guess a bit of a conundrum here is - do I do 'move method' before or after building my fast test suite? I'm getting impatient, so I'll get a failing test going first. Then as the move goes, I'll have my existing "railsy" unit tests make sure things go well.

Where do I put it? I like the pattern (borrowed from @brynary on the codeclimate blog) of "If it is logic that you think you would use in your side project, a social networking site for pet turtles, MyTurtleFaceSpace, then you should put it in lib, otherwise put it in the app directory." I can't see using it in MyTurtleFaceSpace, so I'll put it in app/modules/. Which means the tests will go in spec_no_rails/modules.

Awesomely, I don't have to set up a spec_helper.rb or anything (I hate my spec_helper file), since this is so "naked". I'll drive out the test like so:

require 'timesheet_extensions/calculates_total_hours'

class DummyTimesheet
  include TimesheetExtensions::CalculatesTotalHours
end

describe "Calculating Total Hours" do
  it "returns the total timesheet hours worked of the timesheet" do
    timesheet = DummyTimesheet.new
    task_items = [stub(start_time:Time.parse("7AM"), end_time:Time.parse("8AM")),
                  stub(start_time:Time.parse("9AM"), end_time:Time.parse("10AM"))]
    timesheet.stub(:task_items) { task_items }
    timesheet.calculate_total_hours.should == 2.0
  end
end

Run the test!

And I have my first error. Uninitialized constant. Not an interesting one, so I'll build the file we're supposed to be testing. But once I do that I see this:

Error

Ok, now I'm being bitten by my less-than-complete understanding of ruby's $LOAD_PATH. It looks like when I run puts $LOAD_PATH in the rails console I get all my application path, in addition to my gems, but in a naked irb, I just get my gems.

What to do? Well to keep everything self-contained, I can just add something to modify the load_path to the modules directory to the top of my spec file.

$:.unshift(File.expand_path('../../../app/modules', __FILE__))

It's a bit klugey, but right now I'm just trying to get the minimal thing to get to green (or really, red).

Red Dot!

Now I finally got rspec to run and it gives me my first real failure - a "No method parse for time." That's no good, since I'm not really trying to test anything about the Time class, I was just wanting to get some set times. Let's see. Should I include more of the time library? Or should I just stub out the times with dummy times?

For now I'll stub it out with dummy times. Now it looks like:

describe "Calculating Total Hours" do
  it "returns the total timesheet hours worked of the timesheet" do
    timesheet = DummyTimesheet.new
    task_items = [stub(start_time: 1359291600, end_time: 1359295200),
                  stub(start_time: 1359295200, end_time: 1359298800)]
    timesheet.stub(:task_items) { task_items }
    timesheet.calculate_total_hours.should == 2.0
  end
end

Green!

Yaay!  Green dot in less than a ms!

Super gratifying, but I skipped a step. Better go back and make sure the code breaks if a change a value:

module TimesheetExtensions
  module CalculatesTotalHours
    def calculate_total_hours
      (task_items.inject(100) {|sum, task_item| sum + ((task_item.end_time-task_item.start_time)/3600 || 0)}).round(3)
    end
  end
end

That fails appropriately, and restoring it gets me back my super-fast, super-awesome first ever fast test! Go me!

Use this method

Ok, so now going back to the application class that I pulled this method from, let's remove the method from the class, include the module, and re-run my tests.

Guard to the rescue!

Sweet! Guard tells me within a few seconds of making the actual change that I haven't broken any of the 10 tests in spec/models/timesheet_spec.rb. That took 1.1 second to determine in RSpec time, plus another 15 seconds or so to load rails (I don't have spork up at the moment), so 160 ms per test. Compare that with my new test that took less than a millisecond! Hot.

He's lonely but SO FAST

In real terms, it looks like this test took about 100x faster to run than the full suite of 472 tests. That's fine, I guess, but what happens if we run this test 472 times?

$:.unshift(File.expand_path('../../../app/modules', __FILE__))

require 'timesheet_extensions/calculates_total_hours'

class DummyTimesheet
  include TimesheetExtensions::CalculatesTotalHours
end

describe "Calculating Total Hours" do
  472.times {
    it "returns the total timesheet hours worked of the timesheet" do
      timesheet = DummyTimesheet.new
      task_items = [stub(start_time: 1359291600, end_time: 1359295200),
                    stub(start_time: 1359295200, end_time: 1359298800)]
      timesheet.stub(:task_items) { task_items }
      timesheet.calculate_total_hours.should == 2.0
    end
  }
end

472 Fast Tests

Wall time of 0.633s. Which is about 50x faster. Not bad at all, especially given that I was starting from a pretty fast foundation. And the best part is that not only have I made the tests faster - which is nice in and of itself, I've taken a non-ActiveRecord method out of my ActiveRecord class. And that's a pure win.