- Understand what stubbing is, how to stub in Ruby with Minitest, when to use it
- Understand what mocking is, how to mock in Ruby with Minitest, and when to use it
- What’s the difference between behavior and state testing
- Mock Object
- Test Optimization
- Why do we write tests?
- In Black Thursday, how many of your tests are loading CSV data?
- Have you attempted to adjust those tests/your code to not rely on CSV data? Had any luck?
Mocks are objects that stand in for other objects. The other object might be one that’s not implemented yet, doesn’t yet have the functionality we need, or maybe we just want to work with a simpler situation.
my_mock = mock("paint")
A stub is a fake method added to or overriding an existing method on an object. It can exist as a method on an existing mock, or be created on its own as an object that will return a value that you provide when a specific method that you identify is called.
my_stub = stub(color: "Alizarin Crimson") my_mock = mock("paint") my_mock.stubs(:color).returns("Van Dyke Brown")
Mocks can do more than just stand there. They can also verify tht they have been called:
my_mock = mock("paint") my_mock.expects(:color).returns("Van Dyke Brown")
We’re going to be creating a
Bob class that can store a collection of instances of a
Paint class (in a bit of an homage to Bob Ross).
We want to explore and push on this idea of using mocks and stubs in place of live objects.
To get access to methods that create mocks and stubs, we’ll need to install and require
gem install mocha
Once that’s set, create a
bob_ross directory with
test sub-directories. Create a
bob_test.rb file in your
Testing for Bob
Let’s start by adding a test for Bob. Inside of our
bob_test.rb file, add the following code.
require 'minitest/autorun' require 'minitest/pride' require './lib/bob' class BobTest < Minitest::Test def test_it_exists bob = Bob.new assert_instance_of Bob, bob assert_equal , bob.paints end end
Work with your partner to make that test pass.
Testing that Bob Can Add Paints
Let’s imagine we wanted to test
paints method to see that it returns a collection of
Paint instances. We might write a test like the following.
def test_it_can_have_paint bob = Bob.new paint_1 = Paint.new("Alizarin Crimson") paint_2 = Paint.new("Van Dyke Brown") bob.add_paint(paint_1) bob.add_paint(paint_2) assert_equal [paint_1, paint_2], bob.paints end
Go ahead and write that test. Run it. Don’t create a Paint class at this time!
- What happens?
- What would you need to do to make this test pass?
A First Mock
In this particular example, we could go work to make the Paint class to move this test forward. That wouldn’t be too bad. However, we might have a case where our teammate is already working on that class and we don’t want to duplicate work. We may also want to isolate this test from that particular interaction so that if a test breaks it is easier to identify what exactly has broken.
Instead of moving to build out the paint class, we’re going to use a mock.
Remember, a mock is a simple object that stands in for another object. At the base level, a mock is just a “thing” – a blank canvas that we can use for just about anything. You create a mock like this:
my_thing = mock('name')
Looks weird, right? Read this code carefully and figure out:
- what is
mock, from a Ruby perspective? (like an “object”, “integer”, what?)
- what type thing is
Let’s try to put it to use. Add the following line under your existing require statements in your test:
Replace the existing lines creating instances of Paint within our
test_it_can_have_paint method with the following:
paint_1 = mock('paint') paint_2 = mock('paint') binding.pry
Run your test and pry should pause things. Identify the class of
paint_2 is. Find the documentation for this class on the web to get an idea of what’s possible. Query the
paint_1 object to find out what methods it has. Exit pry.
binding.pry, run the test, and it still fails.
Add the functionality needed to
Bob to make it work!
Now that your tests are passing, notice how the mocks allowed you to build out the
Bob functionality without actually implementing a
Imagine we want to test a
paint_colors method that returns an array of colors as strings.
Also imagine our future
Paint class will be initialized with a parameter of
color that will be returned to us by a
color method on it.
A test for this functionality might look something like this:
def test_it_can_return_colors bob = Bob.new paint_1 = Paint.new("Alizarin Crimson") paint_2 = Paint.new("Van Dyke Brown") bob.add_paint(paint_1) bob.add_paint(paint_2) assert_equal ["Alizarin Crimson", "Van Dyke Brown"], bob.paint_colors end
Run this test to see what happens.
Again, we’re focusing on tests for
Bob, so we shouldn’t get sidetracked by the
Paint class requirements.
Try to implement a mock as we did above. You should be able to start building out the method
paint_colors. What happens?
A First Stub
Here we need something slightly smarter than our original mock. We need some sort of object that can respond to a method call. We’re going to start with a stub.
Replace your test of
paint_colors with the following.
def test_it_can_return_colors bob = Bob.new paint_1 = stub(color: "Alizarin Crimson") paint_2 = stub(color: "Van Dyke Brown") bob.add_paint(paint_1) bob.add_paint(paint_2) assert_equal ["Alizarin Crimson", "Van Dyke Brown"], bob.paint_colors end
Now, run your test. If you haven’t yet made a
paint_colors method that works, go ahead and finish it now.
Doing Terrible Things
That’s great. We can test our Bob class without creating a Paint class, and we can even have imagined methods on Paint. But what if we implemented some code that didn’t actually do what we wanted?
Replace your existin
paint_colors method with the following:
def paint_colors ["Alizarin Crimson", "Van Dyke Brown"] end
Run your test. What happens?
Adding Expectations to Our Mocks
We want to make sure that our implementation of
paint_colors actually uses our Paint object. How can we do this? By adding expectations to our Mock.
When you look at the mocha documentation you’ll find that a mock has methods we can call on it.
.expectsdefines a method that can be called on the mock
.returnsdefines the value that the expected method should return
Putting that together we can do the following within
def test_it_can_return_colors bob = Bob.new paint_1 = mock("paint") paint_1.expects(:color).returns("Alizarin Crimson") paint_2 = mock("paint") paint_2.expects(:color).returns("Van Dyke Brown") require 'pry'; binding.pry bob.add_paint(paint_1) bob.add_paint(paint_2) assert_equal ["Alizarin Crimson", "Van Dyke Brown"], bob.paint_colors end
Once you’re in pry, try calling the
color method on
paint_1. Does it work? Call it again? Does it work?
Run your test again. What happens? A failure? Great!
Read the error. What is happening? Why is this test failing?
Now change your existing method back so that it uses the
color method that we expect to be implemented on Paint.
Run the test again. What happens? A pass? Great!
That’s how mocks work. You create a mock to stand in for other objects and can add some simple capabilities to get you the functionality you need.
A Second Stub
You can also create stubs on existing mock objects.
Let’s create another test for
Bob that checks how much paint he has left with a
Let’s set up our
Paint instances with an
amount method that returns something numeric.
With these in place, let’s leverage our advanced enum knowledge to get this test to pass with a total
paint_amount of 62.
Check for Understanding
With your partners, teach back the difference between stubs and mocks. Check the mocha docs for more details.
The Ultimate CFU
- Can you think of a Black Thursday test you’ve already written that could use mocks and stubs instead?
- When would you use a stub over a mock with expectations and returns?