Episode #017: Pay it Forward

Upgrade to download episode video.

Episode Script

Let's say we're writing a library to wrap the UNIX at command, which schedules jobs to be run at a later time. We already have a lower-level method for executing arbitrary commands, called execute. It takes a command, an optional list of flags, and an optional string to write to the command's STDIN. It executes the command, and returns a data structure that combines an ExitStatus object and the output of the command.

CommandResult = Struct.new(:status, :output)

class Shell
  def execute(command, flags=[], input=nil)  
    result = CommandResult.new
    IO.popen([command, *flags], 'w+', err: [:child, :out]) do |io|
      io.write(input) if input
      io.close_write
      result.output = io.read
    end
    result.status = $?
    result
  end
end

We decide to drive these methods test-first using RSpec. First, we write a test for the at method. It should execute the at command, passing it the given time specifier and command arguments. This is straightforward enough to test: we mock out the .execute method to expect those arguments.

describe "#at" do
  it "executes `at` with the given time and command" do
    Shell.should_receive(:execute).
      with("at", ["now + 3 minutes"], "espeak 'tea is ready!'")
    at("now + 3 minutes", "espeak 'tea is ready!'")
  end
end

This test is easy enough to satisfy.

def at(timespec, what)
  Shell.execute("at", [timespec], what)
end

This is a command method: it causes some change to happen in the world around it.

Next we specify the behavior of the atq method, which should return a list of the currently scheduled jobs. This time, we stub the .execute method to return a fake command result. We take the fact that the #atq method returns part of that fake result as sufficient proof that execute was called correctly.

describe "#atq" do
  it "executes returns the output of `atq`" do
    result = double(output: "THE OUTPUT")
    Shell.stub(execute: result)
    atq.should eq("THE OUTPUT")
  end
end

This is even easier to make pass. We execute the atq command and return the resulting output.

def atq
  Shell.execute('atq').output
end

This is an example of a query method: it's job is to return some information.

When the at command is successful, it normally outputs a line of confirmation text showing exactly when the command is scheduled to be run, as well as the job's numeric ID. We realize we'd like to make the scheduled job's ID available to callers of #at. So we add a new example to our spec.

In this example, we stub the .execute method to return some faked-up command results, and verify that the #at method extracts the ID and returns it. We already have a test to verify that #at calls .execute with the right arguments, so we won't repeat any of that here.

describe "#at" do  
  it "executes `at` with the given time and command" do
    Shell.should_receive(:execute).
      with("at", ["now + 3 minutes"], "espeak 'tea is ready!'")
    at("now + 3 minutes", "espeak 'tea is ready!'")
  end

  it "returns the job ID of the scheduled job" do
    result = double(output: "job 42 at Sun Oct 14 20:15:00 2012")
    Shell.stub(execute: result)
    at("some time", "some job").should eq(42)
  end
end

We then modify #at to extract and return the job ID using a regular expression match.

def at(timespec, what)
  result = Shell.execute("at", [timespec], what)
  result.output.match(/\Ajob (\d+)/)[1].to_i
end

But now we have a problem:

Failures:

  1) #at executes `at` with the given time and command
     Failure/Error: Unable to find matching line from backtrace
     NoMethodError:
       undefined method `output' for nil:NilClass
     # at_spec_3.rb:20:in `at'
     # at_spec_3.rb:27:in `block (2 levels) in <main>'

Our original test for #at is now failing. It's failing because the method now uses the return value of .execute. And since the original test is only interested in what the #at method invokes, not what it returns, it doesn't bother to set up a realistic return value for the .execute mock.

To make this pass, we have to update the old test, adding a return value to the should_receive:

describe "#at" do  
  it "executes `at` with the given time and command" do
    Shell.should_receive(:execute).
      with("at", ["now + 3 minutes"], "espeak 'tea is ready!'").
      and_return(double(output: "job 42 at Sun Oct 14 20:15:00 2012"))
    at("now + 3 minutes", "espeak 'tea is ready!'")
  end

  it "returns the job ID of the scheduled job" do
    result = double(output: "job 42 at Sun Oct 14 20:15:00 2012")
    Shell.stub(execute: result)
    at("some time", "some job").should eq(42)
  end
end

Let's take a step back. We just added some extra context to our first test of #at which is completely irrelevant to that test. We did this just to keep the tests passing. From now on, we're going to have to keep doing this every time we make a new assertion about what #at does. And if #at ever starts using more of the return value from .execute, we may have to add even more unrelated boilerplate to our mocks, just to keep the tests passing.

This is the kind of thing that causes “mockist” tests to be accused of being brittle, and rightly so. However, before we throw away our mocks, let's take a look at what this might be telling us about our design.

Earlier, I mentioned that the #at method was a command method, and the #atq method was a query method. This is no longer true. The #at method is now both a command and a query. This violates the principle of command-query separation, which states that keeping commands and queries strictly separated in a program makes that program simpler and more comprehensible. Another way I think about this principle is that when sent a message, you can “pay it back” or “pay it forward”, but never both.

Let's change #at to “pay it forward”. Instead of returning the job ID, we'll make it optionally yield the job ID.

def at(timespec, what)
  result = Shell.execute("at", [timespec], what)
  if block_given?
    yield result.output.match(/\Ajob (\d+)/)[1].to_i
  end
end

Now when we call #at and we care about the job ID, we pass a block to it:

at("now + 25 minutes", "espeak 'Pomodoro!'") do |id|
  puts "The job ID is #{id}"
end

Finally, we update the spec again. In the first example, we remove the unneeded return value from the mocked .execute. Then we change the second example to test for a yielded value instead of a return.

describe "#at" do  
  it "executes `at` with the given time and command" do
    Shell.should_receive(:execute).
      with("at", ["now + 3 minutes"], "espeak 'tea is ready!'")
    at("now + 3 minutes", "espeak 'tea is ready!'")
  end

  it "yields the job ID of the scheduled job" do
    result = double(output: "job 42 at Sun Oct 14 20:15:00 2012")
    Shell.stub(execute: result)
    job_id = nil
    at("some time", "some job") do |id| job_id = id end
    job_id.should eq(42)
  end
end

By converting a return value to a rudimentary callback, we've once again made the #at method a pure command. We push values into it, and values are in turn pushed forward, into the .execute method, and optionally into a callback block.

In future episodes we'll return regularly to the idea of command-query separation, and take closer looks at why methods in which information moves in only one direction—either forwards to other collaborators, or backwards to the caller—can make a design simpler and more composable. For now, I just hope you have a better sense for when your tests are telling you that commands and queries are being mixed together.

Happy hacking!