Best Practices for Loading Code

I’ve made it a whole year without thinking about best practices for loading code. I’ll blame that on the wonderful magic of Rails, for taking care of details like this for me behind the scenes.

I was working on a very small Ruby program and found myself wondering if this is a code smell:

# in spec/mars_rover_spec.rb

require 'spec_helper'
require_relative '../lib/mars_rover'
require_relative '../lib/rover_coordinates'
require_relative '../lib/rover_position_tracker'

describe MarsRover do
  ...
end

To make a long story short, the answer is YES. In this example, our spec files not only know about their dependencies, they even know where those dependencies physically reside on disk in relation to themselves.

Let’s refactor.

I bet most if not all our spec files are going to be requiring ‘spec_helper’. Let’s delete that line and put that information into configuration:

  # in .rspec
  --require spec_helper

All of our source code lives in lib/. Let’s abstract that knowledge into a small file that we can require on application startup:

  # in environment.rb
  $LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
  # in spec/spec_helper
  require_relative '../environment'
  # in Rakefile
  require_relative './environment'

Cool. Now our spec looks like this:

# in spec/mars_rover_spec.rb

require_relative 'mars_rover'
require_relative 'rover_coordinates'
require_relative 'rover_position_tracker'

describe MarsRover do
  ...
end

mars_rover, rover_coordinates, and rover_position_tracker are now required logically, just like an external gem, instead of relatively or absolutely.

The last thing we need to consider is why our spec relies on rover_coordinates and rover_position_tracker. Why doesn’t it just rely on mars_rover, which after all is the only class it is testing?

Even the MarsRover class itself doesn’t need to require rover_coordinates and rover_position_tracker, since those dependencies are injected on initialization:

class MarsRover
  def initialize(rover_coordinates, rover_position_tracker)
    @rover_coordinates = rover_coordinates
    @rover_position_tracker = rover_position_tracker
  end
end

The answer is that while those dependencies are injected into the MarsRover class, in the spec, we need to instantiate them as setup, like this:

# in spec/mars_rover_spec.rb

require_relative 'mars_rover'
require_relative 'rover_coordinates'
require_relative 'rover_position_tracker'

it "Has coordinates and a tracker" do
  coordinates = RoverCoordinates.new()
  tracker = RoverPositionTracker.new()
  rover = MarsRover.new(coordinates, tracker)
  # ... test stuff about the rover
end

I tend to call it a day once we’ve gotten this far. I’ve never done it myself, but I believe we could avoid requiring all this extra code by using rspec doubles:

# in spec/mars_rover_spec.rb

require_relative 'mars_rover'

it "Has coordinates and a tracker" do
  coordinates = instance_double("RoverCoordinates", to_s: "1, 4")
  tracker = instance_double("RoverTracker", rover_positions: [[1, 3], [4, 99]])
  rover = MarsRover.new(coordinates, tracker)
  # ... test stuff about the rover
end

Source: Ruby Tapas Episode # 242, by Avdi Grimm. If you don’t know about Ruby Tapas yet, you should check it out! Any errors in this blog post are probably mine, not Avdi’s.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s