DEV Community

loading...
Cover image for RSpec(Pt. 3): Test Doubles

RSpec(Pt. 3): Test Doubles

Ethan Gustafson
`${Frontend Developer}`
・4 min read

Test Doubles in RSpec have been a little difficult to wrap my head around. No doubt you may have encountered many examples online of using test doubles, but you were not able to exaclty land down how or why you would use them in your tests.

Resources

Why would you use them?

Example 1: You have one class written, but it depends on another class which is not yet written. You can create fake objects and methods of the missing class so that it could work with the class that is written. If the written class fails a test, then you know immediately the issue is with that class, since it is independent, not depending on other classes. This is where you can use Mocks and Stubs.

Mocks & Stubs

  • A Mock is defined using the double keyword. It's essentially a fake object. It stands in for the object you want to test.
  • A Stub is defined using the allow keyword. A Stub is a fake method you can use on the double. You use allow in order to define what messages are allowed to be called on the double.

If methods aren't given values, the stub returns a value of nil by default. You need to set what the return value will be of the stub.

it "creates a mock and a stub" do
  dbl = double("greeting") 
  allow(dbl).to receive(:hello)
  expect(dbl.hello).to be_nil
end
Enter fullscreen mode Exit fullscreen mode

Below, I stubbed methods when the double was created. To do that, you provide the double an argument of a hash. The hash keys will be set as the stubbing methods for the double.

class Restaurant

    def initialize(workers)
      @workers = workers
    end

    def workers
      @workers.map{|w| w.name}
    end

end

require_relative '../restaurant.rb'
describe Restaurant do
    it "returns a list of workers" do
      dbl1 = double("manager", name: "Nancy") # The new fake objects from the missing class
      dbl2 = double("host", name: "Chris")
      dbl3 = double("waiter", name: "Michael")

      restaurant = Restaurant.new([dbl1, dbl2, dbl3])

      expect(restaurant.workers).to eq(["Nancy", "Chris", "Michael"])
    end
  end
Enter fullscreen mode Exit fullscreen mode

Running the test:

➜  rspec spec

Restaurant
  returns a list of workers

Finished in 0.00341 seconds (files took 0.13641 seconds to load)
1 example, 0 failures
Enter fullscreen mode Exit fullscreen mode

Partial Test Doubles

A partial test double uses a real object from your application, but you stub its methods.

Example 2: You have a class, but certain methods are not yet written for that class.

class Worker
end

describe Worker do
  it "greets customer" do
    dbl = double("worker")
    allow(dbl).to receive(:greet).and_return("Welcome!")
    expect(dbl.greet).to eq("Welcome")
  end
end
Enter fullscreen mode Exit fullscreen mode

Example 3: You need to make a database call, but it would be faster to just use a double.

describe Worker do
  it "stubs a database call" do
    dbl = double("worker", :name => "Ethan")
    allow(Worker).to receive(:find).and_return(dbl)
    worker = Worker.find
    expect(worker.name).to eq("Ethan")
  end
end
Enter fullscreen mode Exit fullscreen mode

Example 4: When you have a object that is expensive to use, it would probably be better to test using mocks and stubs.

Message Expectations

Ruby uses dot notation to send messages to objects. Those messages are methods. "hello".capitalize - .capitalize is the method, sending a message to the string "hello" which the string will receive and perform the message request. Then the value is returned as "Hello"

RSpec can also use expectations in this same way. The test will expect an object to receive a method, and then you call that method.

it "is a message expectation" do
  dbl = double("Cashier")
  expect(dbl).to receive(:take_order).and_return("Order when you're ready.")
  dbl.take_order
end
Enter fullscreen mode Exit fullscreen mode

Spies

Spies are exactly like doubles. The only difference is that you don't have to stub methods in order to make them available for use. Remember that in order to stub a method, you either provide a hash argument to a double, or you use the allow keyword to stub a method.

it "host gives a greeting" do
  host_dbl = spy("Host")
  expect(host_dbl).to receive(:say_hello).and_return("Welcome!")
  host_dbl.say_hello
  expect(host_dbl).to have_received(:say_hello)
end
Enter fullscreen mode Exit fullscreen mode

So you don't have to expect the double to receive the method and return a value, instead you can just see if the message has been received.

it "host gives a greeting" do
  host_dbl = spy("Host")
  host_dbl.say_hello
  expect(host_dbl).to have_received(:say_hello)
end
Enter fullscreen mode Exit fullscreen mode

Resources

More on Test Doubles: Test Doubles

RSpec Mocks

RSpec Stubs

RSpec Partial Test Doubles

RSpec Spies

Message Expectation Constraints

Message Count Constraints

Discussion (0)