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.
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
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_position_tracker. Why doesn’t it just rely on
mars_rover, which after all is the only class it is testing?
MarsRover class itself doesn’t need to require
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.