Ruby Unit Tests breaking dependencies

One definition of a unit test is that it has to run fast or in the words of Michael Feathers in his book “Working Effectively with Legacy Code”:

A unit test that takes 1/10th of a second to run is a slow unit test.

If all operations happen in memory they tend to work rather quickly.(1)
This changes when we start to use files or network connections in classes that we put under unit tests. IO is costly as every roundtrip to the hard drive uses compared to RAM access forever LINK COMPARISON HDD TO RAM. So lets say we want to have a Ruby class that lists all entries of a directory and puts it into a nicely formatted string ready to print out to the console:


class DirLister
  def list_dir( path )
    raise ArgumentError, "Path #{path} does not exist." unless Dir.exists?( path )

    Dir.entries( path )
  end
end

To avoid the hit in execution time of our tests consider using default parameters for your IO classes in the constructor. So we would have to change our little dummy class to the following:


class DirLister
  @directory #defaults to Rubys Dir, see constructor

  def initialize( directory = Dir )
    @directory = directory
  end

  def list_dir( path )
    raise ArgumentError, "Path #{path} does not exist." unless @directory.exists?( path )

    @directory.entries( path )
  end
end

With these changes our test can now pass in a stub or if needed even a mock object. So how would we do this? Well remember Ruby is dynamic so if it quacks, walks and looks like a duck your all good when it comes to stubbing a object in Ruby. Here is an example of how the original MiniTest Spec could look like:


describe DirLister do
  it "lists all entries of directory" do
    test_path = "/tmp/test_path"
    expected_result = ["one", "two", "three"]

    DirLister.new().list_dir(test_path).must_equal expected_result
  end
end

Now if we wanted to pass in a fake dir object we could use the built in Mock from MiniTest which would result in the following code:

describe DirLister do
  it "lists all entries of directory" do
    test_path = "/tmp/test_path"
    expected_result = ["one", "two", "three"]

    dir_mock = MiniTest::Mock.new

    #MockMethod   MethodName, ReturnValue, Parameter(s)
    dir_mock.expect(:exists?, true, [test_path])
    dir_mock.expect(:entries, expected_result, [test_path])

    DirLister.new(dir_mock).list_dir(test_path).must_equal expected_result
  end
end

This approach of passing in system classes to your class can also be used to replace the Time class to simulate different times etc. as the current time given on the system.

One may argue that using a Mock class in this case is overkill. Another alternative would be to pass in a fake class object that would behave the same way we set up the MiniTest mock class.

(1) There are of course algorithms or big datablocks that tend to take more time. Consider in those cases to only tests aspects of the algorithm and move the more complex and time consuming test to your integration tests.