Stubbles - Stubbing and Doubles innit. πŸ§”

Sami Birnbaum

As you start to get more familiar with automated testing, at some point you will likely hear other developers say things like:

“You know you can just stub that method, right?”

“This is a great candidate for a double!”

And at the beginning these things can be confusing to understand. πŸ˜•

So let’s start by defining each term, followed by their practical use, in specific relation to rspec within a rails project.


Definitions πŸ“–

One of the tricky things I have seen when these concepts are defined is that they are often defined within the context of testing.

For me, that just adds a layer of complexity.

It is true that these concepts are often used within the context of testing, but they don’t have to be.

So let’s take a stab or should I say stub πŸ˜† at defining them without talking about tests.

Stubbing

Stubbing is just a term for defining a method on an object.

However, it indicates the short-term nature of that method.

If you stub a method on an object, your intention is that it exists for a short period of time and most likely returns a simple hard-coded value.

You may use a stub to define a method that does not exist yet on an object or to override an existing method to get it to return a value you desire.

For example:

class Feature
  def self.available?(name)
    # all features are disabled by default
    false
  end
end

puts Feature.available?("user_preference")
# => false

# stub: override the method to return a canned response
def Feature.available?(name)
  true
end

puts Feature.available?("user_preference")
# => true

Doubles

A double, is simply an object that is a double of another object.

You may be thinking, “but why would you do that?!”, but lets put that question to one side for now just so we can understand the concept in its purest form.

A double of something is just a copy of that thing.

If I have one chocolate and I double that chocolate I now have two of the same chocolate. We can call the first chocolate the original and the second chocolate a double of the original. yum. 🍫

Great Success!! We made it this far and we haven’t talked about tests!


Testing πŸ§ͺ

Hopefully now that we understand these concepts independent of testing we can talk a little bit about how they can be helpful tools to use when testing.

Stubbing

Sometimes when you are testing a method, the method under test will send a message to another object in order to retrieve some value.

That sounds confusing but look at this example:


class EasyMath
  def sum(a,b)
    if SumFeature.available?
      a + b
    end
  end
end

The available? message is being sent to another class SumFeature in order to return the value true or false.

When we come to test the behaviour of the sum method, it would be really useful if we could just worry about a + b without having to be concerned with what the available? method does.

At the end of the day, the available? method belongs to the SumFeature class and has nothing to do with us over here and we can assume it is tested in the SumFeature class where it is defined.

Well thankfully we have a tool in our arsenal called stubbing that will allow us to stub the available? method so it always returns true.

This is great! We can now focus on testing a + b whilst relying on available? to always return true.

Ok, but what does this look like in a test?

describe "#sum" do
  it "adds two numbers together and returns the total" do
    easy_math = EasyMath.new
    allow(SumFeature).to receive(:available?).and_return(true)

    total = easy_math.sum(2,2)

    expect(total).to eq(4)
  end
end

See that line allow(SumFeature).to receive(:available?).and_return(true)?

The available? method has now been stubbed out to always return true.

No matter how many times you call SumFeature.available? in your application code, it will always return true.

We did a thing! We stubbed a method! And as you can see it had nothing to do with doubles! πŸ’₯

Doubles

Ok if you have made it this far, then stay with me as we are nearly there..

Let’s take the same example we used above with the sum method, but tweak it slightly, to see how we could use a double.


class EasyMath
  def sum(a,b,user)
    if user.likes_math?
      a + b
    end
  end
end

Once again we are sending a message likes_math? somewhere else in order to retrieve a value of true or false.

Similarly we only want to concern ourselves with the behaviour of a + b without having to worry about likes_math?.

Given what we know so far, we might suggest stubbing the likes_math? method in the same way we stubbed available?.

allow(user).to receive(:likes_math?).and_return(true)

However, whilst a class method like available? can be stubbed without the need for a double, an instance method does require a double before it can be stubbed. (There are exceptions to this by using any_instance_of but it is not recommended.

In other words we first need to create a double of the user object before we can stub the likes_math? method on it, to always return true for us.

Now hold on one second!! Why am I talking about stubbing alongside doubles? Didn’t I kind of say I didn’t want to do that?!

Ok hear me out on this one…

Whilst stubbing and doubles are completely separate concepts, a double is only meaningful and useful once you stub methods on it.

This is because when you create a double you are creating a double of that object without any methods on it.

The double is just a plain Ruby Object.

Therefore, in reality, although stubbing is used on its own, when you are using a double you will likely need to use stubbing as well.

Ok so lets see how the code would look for testing our sum method:

describe "#sum" do
  it "adds two numbers together and returns the total" do
    easy_math = EasyMath.new
    user_double = double
    allow(user_double).to receive(:likes_math?).and_return(true)

    total = easy_math.sum(2,2,user_double)

    expect(total).to eq(4)
  end
end

See how we first create a double like this user_double = double.

And on the next line we stub the likes_math? method on to our double like this: allow(user_double).to receive(:likes_math?).and_return(true).

It might be helpful to think about it this way:

A double is a useful tool to help you to be able to easily stub.


And that’s really all there is to it folks!

But before I go, let’s wrap up with some helpful tips.

Take-Away Tips πŸͺΆ

Stubbing

  • Stubbing has nothing to do with doubles.
  • It helps us to force the return value of a method we don’t care about in our test because it is tested elsewhere.
  • A class method can be stubbed without the need for a double.
  • An instance method will need a double (unless you use any_instance_of).

Doubles

  • Doubles are a useful way to create an object that acts as a double of another object.
  • The most basic way to create a double in rspec is to use the double method.
  • Another option is to use instance_double(User). This is the same as double but will verify that any methods you stub exist on the class you are doubling.
  • If you are using FactoryBot you can use build_stubbed(:user) which will create an instance_double but with the methods stubbed out as set in the factory. It also has the added advantage of stubbing out associated objects and created timestamps.

πŸ§”