Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ouch, I just mocked my toe

Ouch, I just mocked my toe

Mocks, stubs, doubles, fakes, dummies, spies… What are they, and when should you use them?

should I use them?

Context

OO is all about message passing. An object sends messages to its
collaborators, whose methods respond to these messages by doing things and/or
returning values. The important parts of the design are how objects interact
by sending messages to each other, not the data they hold internally.

When test-driving a design from the outside-in, we start with a high-level
integration test against the external behaviour of the system, then work our
way in, discovering the required collaborating roles as we go ("programming
by wishful thinking"). But how do we test which messages the class we're
writing sends to its collaborators, when they don't yet exist?

Undurprisingly, with mocks, stubs ...

Terminology (from xUnit Test Patterns)

Test double - generic term for anything used in tests instead of real object

Dummy - no behaviour (eg when we have to pass in a parameter we aren't
going to use)

Fake - a working, but not production, implementation (eg in-memory
database)

Stub - provides canned responses to messages, and may store call information

Mock - expects specific messages, and fails tests if it doesn't receive
them (or receives others)

Spy - like a stub, but can be interrogated about the messages it received

Classification of objects and messages

Values and objects (terminology from GOOS)

Values are pieces of data with no behaviour, eg name, location or address.
Should be treated as immutable.

Objects have identity, state and behaviour.

Nouns and verbs

Just to add to the confusion, "stub" and "mock" are both nouns and verbs.
And, especially in a dynamic language like Ruby, you can mock a call to a
stub or stub a call to a mock. In fact, in rSpec you get an instance of the
same class whether you ask for a stub or a mock. You can also ask for a
"double", which may or may not help reduce the confusion.

Also - and again especially in dynamic languages - you can stub or mock
methods on real instances of a class.

Queries and commands

A query is a request for some object or piece of data, which should have no
side-effects.

A command is a request for the receiver to take some action. It may or may
not also return something.

The "tell don't ask" principle of OO design says that we should prefer to
tell an object to do something (command), rather than ask it for some data
(query) and perform an operation ourselves on the thing that's returned.
This helps maintain encapsulation. Obviously this isn't universal -
sometimes we do need to ask as well.

Usage

Mocks sometimes get a bad name, when people overuse them and find they make
tests too brittle and closely-tied to the implementation.

Queries

Calling methods to get data, not because you want to do something.

Antipattern: mock to check a query method is called (worse if you don't
check return!)

Test the behaviour that depends on the returned value. Stub the query to
return appropriate values.

If arguments to query are important, stub the method to specifically
respond to the correct arguments.

Commands

Calling methods because you want collaborators to do something.

Replace collaborator with mock object, and assert that the expected
messages are received.

Think of the collaborator you're mocking as a role (or interface), not an
instance of a specific class. The class under test shouldn't need to know
exactly what kind of object it's sending messages to, just that it's
something that can respond to those messages.

Values

Antipattern: replace value objects with stubs

Just use real objects. They don't have any behaviour to stub.

Your own methods

Don't stub or mock calls to your class's own methods (public, private or
superclass). That way lies madness.

Just test the class using its external interface. Private methods are an
implementation detail that should be able to be refactored without breaking
any tests.

Other people's code

Mock objects are often introduced as a means of testing interactions with
third-party code. This is unfortunate as they were initially introduced as a
design technique, and you can't use mocking to drive the design of code you
don't control.

In general it's better to wrap external APIs with a thin layer which can be
integration-tested with the real API. This gives you an interface that you
can control, and you can drive its design by mocking calls from the code that
needs to use it.

Summary

Stub queries; mock commands

Don't stub value objects

Recognise when to stop mocking and test behaviour by asserting state

Don't stub or mock internal calls

Don't mock classes you don't own

Listen to the tests! Mocks are there to help drive the design - if you're
having to stub lots of calls, think about how you could refactor to reduce
coupling.

Kerry Buckley

May 24, 2013
Tweet

More Decks by Kerry Buckley

Other Decks in Technology

Transcript

  1. Ouch, I just mocked my toe! Mocks, stubs, doubles, fakes,

    dummies, spies… What are they, and when should you use them? Kerry Buckley, 24 May 2013
  2. class Blog attr_writer :logger def post title, content # do

    some bloggy stuff logger.info "Created new post: #{title}" if logger end end describe Blog do it "logs new posts" do FileUtils.rm_f "test.log" logger = Logger.new "test.log" blog = Blog.new blog.logger = logger blog.post "Hello world", "some content here" File.read("test.log").should =~ /INFO: Created new post: Hello world/ end end
  3. describe Blog do it "logs new posts" do logger =

    mock blog = Blog.new blog.logger = logger logger.should_receive(:info). with "Created new post: Hello world" blog.post "Hello world", "some content here" end end
  4. class Post def comments Comment.for_post_id id end end describe "Post#comments"

    do it "looks up comments for its ID" do post = Post.new Comment.should_receive(:for_post_id).with post.id post.comments end end
  5. describe "Post#comments" do it "returns all comments for its ID"

    do comment = stub post = Post.new Comment.stub(:for_post_id).with(post.id). and_return [comment] post.comments.should == [comment] end end
  6. class Player attr_reader :position def initialize(position, lives) @position, @lives =

    position, lives end def move(dx, dy) @position.x += dx @position.y += dy end end describe "moving" do it "adjusts position" do point = stub x: 4, y: 6 point.should_receive(:x=).with 5 point.should_receive(:y=).with 10 Player.new(point, 3).move 1, 4 end end
  7. describe "moving" do it "adjusts position" do point = Point.new

    4, 6 player = Player.new point, 3 player.move 1, 4 Player.position.should == Point.new(5, 10) end end
  8. class Comment def initialize name, timestamp, text @name, @timestamp, @text

    = name, timestamp, text end def intro "On #{format_date}, #{name} wrote:" end private def format_date @timestamp.strftime "%d %B %Y" end end describe "Comment#intro" do it "includes the date and name" do comment = Comment.new "Fred", Time.now, "" comment.stub(:format_date).and_return "1 January 2013" comment.intro.should == "On 1 January 2013, Fred wrote:" end end
  9. describe "Comment#intro" do it "includes the date and name" do

    comment = Comment.new "Fred", Time.new(2013, 1, 2, 3, 4, 5), "" comment.intro.should == "On 2 January 2013, Fred wrote:" end end
  10. class Logger def initialize filename @filename = filename end def

    info message File.open @filename, "a" do |f| f.puts "INFO: #{message}" end end end describe "Logger#info" do it "logs the message as INFO" do filename = "test.log" file = mock File.stub(:open).with(filename, "a").and_yield file file.should_receive(:puts).with "INFO: hello" logger = Logger.new filename logger.info "hello" end end
  11. describe "Logger#info" do it "logs the message as INFO" do

    filename = "test.log" FileUtils.rm_f filename logger = Logger.new filename logger.info "hello" File.read(filename).should == "INFO: hello\n" end end
  12. Stub queries; mock commands Don’t stub or mock value objects

    Don’t test private methods Don’t mock what you don’t own Know when to stop Don’t forget integration tests Listen to the tests!
  13. ?