Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I test the order of method calls in rspec?

I have a class that uses the command pattern to do a bunch of simple transformation steps in order. Data comes in as a data feed (in XML) and then is transformed through multiple steps using single-purpose step classes. So it might look like this (actual class names are different):

raw_data = Downloader.new(feed)
parsed_data = Parser.new(raw_data)
translated_data = Translator.new(parsed_data)
sifted_data = Sifter.new(translated_data)
collate_data = Collator.new(sifted_data)

etc.

I have unit tests for each class, and I have integration tests to verify the full flow, including that each class is called.

But I don't have any way to test the order they are called

I'd like some test so I can know: the Downloader is called first, then the Parser, then the Translator, etc.

This is in Ruby with Rspec 3.

I did find this: http://testpractices.blogspot.com/2008/07/ordered-method-testing-with-rspec.html but this is from 2008 and it's also really ugly. Is there a better way to test method execution order?

Thanks!

like image 875
Dan Sharp Avatar asked Aug 01 '15 13:08

Dan Sharp


People also ask

How do I run a specific test in RSpec?

Running tests by their file or directory names is the most familiar way to run tests with RSpec. RSpec can take a file name or directory name and run the file or the contents of the directory. So you can do: rspec spec/jobs to run the tests found in the jobs directory.

Is RSpec TDD or BDD?

RSpec is a testing tool for Ruby, created for behavior-driven development (BDD). It is the most frequently used testing library for Ruby in production applications. Even though it has a very rich and powerful DSL (domain-specific language), at its core it is a simple tool which you can start using rather quickly.

What is stub in RSpec?

In RSpec, a stub is often called a Method Stub, it's a special type of method that “stands in” for an existing method, or for a method that doesn't even exist yet. Here is the code from the section on RSpec Doubles − class ClassRoom def initialize(students) @students = students End def list_student_names @students.

What is mocking in RSpec?

Mocking is a technique in test-driven development (TDD) that involves using fake dependent objects or methods in order to write a test. There are a couple of reasons why you may decide to use mock objects: As a replacement for objects that don't exist yet.


2 Answers

RSpec Mocks provides ordered since at least RSpec 3.0:

You can use ordered to constrain the order of multiple message expectations. This is not generally recommended because in most situations the order doesn't matter and using ordered would make your spec brittle, but it's occasionally useful. When you use ordered, the example will only pass if the messages are received in the declared order.

Note that RSpec agrees with @spickermann that this is not a recommended practice. However, there are some cases when it is necessary, especially when dealing with legacy code.

Here is RSpec's passing example:

RSpec.describe "Constraining order" do
  it "passes when the messages are received in declared order" do
    collaborator_1 = double("Collaborator 1")
    collaborator_2 = double("Collaborator 2")

    expect(collaborator_1).to receive(:step_1).ordered
    expect(collaborator_2).to receive(:step_2).ordered
    expect(collaborator_1).to receive(:step_3).ordered

    collaborator_1.step_1
    collaborator_2.step_2
    collaborator_1.step_3
  end
end

And failing examples:

RSpec.describe "Constraining order" do
  it "fails when messages are received out of order on one collaborator" do
    collaborator_1 = double("Collaborator 1")

    expect(collaborator_1).to receive(:step_1).ordered
    expect(collaborator_1).to receive(:step_2).ordered

    collaborator_1.step_2
    collaborator_1.step_1
  end

  it "fails when messages are received out of order between collaborators" do
    collaborator_1 = double("Collaborator 1")
    collaborator_2 = double("Collaborator 2")

    expect(collaborator_1).to receive(:step_1).ordered
    expect(collaborator_2).to receive(:step_2).ordered

    collaborator_2.step_2
    collaborator_1.step_1
  end
end
like image 153
amar47shah Avatar answered Sep 28 '22 12:09

amar47shah


I would argue that the order of method calls is not important and should not be tested. Important is the result of a method, not its internals. Testing the order of internal method calls (instead of just the result of the tested method) will make it harder to refactor a method later on.

But if still want to test the order then you might want to test that the methods are called with a mocked result of the methods called before:

let(:raw_data)    { double(:raw_data) }
let(:parsed_data) { double(:parsed_data) }
# ...

before do
  allow(Downloader).to_receive(:new).and_return(raw_data)
  allow(Parser).to_receive(:new).and_return(parsed_data)
  # ...
end

it 'calls method in the right order' do
  foo.bar # the method you want to test

  expect(Downloader).to have_received(:new).with(feed)
  expect(Parser).to have_received(:new).with(raw_data)
  # ...
end
like image 29
spickermann Avatar answered Sep 28 '22 14:09

spickermann