Why you don't get mocks

Gregory Moeck
LA Ruby Users Group, 2010-01-13

Creative Commons License
Special thanks to Kerry Buckley for the presentation layout

Viewing presentation

http://gmoeck.github.com/presentations/laruby-why-you-dont-get-mocks

Who am I?

Who am I?

Gregory Moeck

@gregmoeck on Twitter
http://gmoeck.github.com
gmoeck on github

Presentation Format

Why Mocks?

Why Mocks?

People have called mock objects the following:

Why Mocks?

Objections

1. Testing using Mock Objects tests implementation and duplicates code

        describe ShowController, "index" do

          describe "when a TV show has no public videos" do

            it "should not show that TV show" do
              Show.should_receive(:all).
                with(:select => "id, name, public_videos_count",
                     :conditions => "shows.public_videos_count > 0").
                and_return([])

              get 'index'
              response.body.should_not match(/#{@show.name}/)
            end

          end

        end
      

Why Mocks?

Objections

1. Testing using Mock Objects tests implementation and duplicates code

        class ShowController
          def index
            @shows = Show.all(
                         :select => "id, name, public_videos_count",
                         :conditions => "shows.public_videos_count > 0")
          end
        end
      

Why Mocks?

Objections

2. Mock Objects are brittle and do a bad job testing

        module Codebreaker
          describe Game do
            describe "#start" do
              it "sends a welcome message" do
                output = double('output')
                game = Game.new(output)
                output.should_receive(:puts).with('Welcome to Codebreaker!')
                game.start
              end
            end
          end
        end
      

Why Mocks?

Objections

2. Mock Objects are brittle and do a bad job testing

        module Codebreaker
          class Game
            def initialize(output)
              @output = output
            end
            def start
              @output.puts("Welcome to Codebreaker!")
            end
          end
        end
      

Why Mocks?

Objections

2. Mock Objects are brittle and do a bad job testing

Refactor to:

        module Codebreaker
          class Game
            def initialize(output)
              @output = output
            end
            def start
              @output.write("Welcome to Codebreaker!") #changed to write
            end
          end
        end
      

The test fails, even though the output is the SAME!

Why Mocks?

QED

Mocks are stupid!!!

Why Mocks?

Or maybe you just don't understand mocks?

Why Mocks?

Mocking is not primarily about testing

Why Mocks?

Mocking is primarily about object oriented design.

Why mocks?

Mocking is primarily about emergent oo design.

Why mocks?

Mocking is primarily about emergent OO design.

Guided by Tests

OO Design Principles

OO Design Principles

Single Responsibility Principle

OO Design Principles

Single Responsibility Principle

A class should have only one reason to change.

OO Design Principles

Single Responsibility Principle

          class Modem
            ...
            def dial(phone_number) end
            def hangup end
            def send(data) end
            def receive end
          end
        

What's wrong with this?

OO Design Principles

Single Responsibility Principle

          class Modem
            ...
            def dial(phone_number)
              @connection.dial(phone_number)
            end
            def hangup
              @connection.hangup
            end
            def send(data)
              @data_channel.send(data)
            end
            def receive
              @data_chanel.receive
            end
          end
        

OO Design Principles

Single Responsibility Principle

          describe Modem do
            describe '#dial' do
              it "delegates to it's connection" do
                connection = double('connection)
                modem = Modem.new(:connection => connection)
                phone_number = '1234567890'
                connection.should_receive(:phone_number).with(phone_number)
                modem.dial(phone_number)
              end
            end
          end
        

OO Design Principles

Law of Demeter

OO Design Principles

Law of Demeter

Only talk to your immediate friends.

OO Design Principles

Law of Demeter

A class should never chain messages past a single layer

OO Design Principles

Law of Demeter

          class Account
            ...
            def childrens_names
              @owner.children.map{|child| child.name}.join(',')
            end
          end
        

Whats wrong with this?

OO Design Principles

Law of Demeter

          class Account
            ...
            def childrens_names
              @owner.childrens_names
            end
          end
        

OO Design Principles

Law of Demeter

          describe Account do
            describe '#childrens_names' do
              it 'delegates to the account owner to get childrens names' do
                owner = double('owner')
                account = Account.new(:owner => owner)
                owner.should_receive(:childrens_names)
                account.childrens_names
              end
            end
          end
        

Mocking Rules

Mocking Rules

1. Only Mock Types You Own

Mocking Rules

1. Only Mock Types You Own

Mocking is about discovering your domain

Mocking Rules

1. Only Mock Types You Own

  1. Create an interface to the external api
  2. Specify what your doing in domain terms
  3. Stub the external api within the interface

Mocking Rules

2. Tell, Don't Ask

Mocking Rules

2. Tell, Don't Ask

Tell collaborators what you need from them

Mocking Rules

2. Tell, Don't Ask

Push decisions about implementation outside your responsibility

Mocking Rules

2. Tell, Don't Ask

Express those decisions in domain terms

Mocking Rules

2. Tell, Don't Ask

When needing to mock within a class, the tests are telling you to extract another class

Mocking Rules

3. Mock Roles, NOT Objects

Mocking Rules

3. Mock Roles, NOT Objects

A mock should not represent an object but a role that the object will play

Mocking Rules

3. Mock Roles, NOT Objects

You should be able to switch the implementation of the role, so long as the interface doens't change

Opening Examples

Objections

1. Testing using Mock Objects tests implementation and duplicates code

        describe ShowController, "index" do
          describe "when a TV show has no public videos" do
            it "should not show that TV show" do
              Show.should_receive(:all).
                with(:select => "id, name, public_videos_count",
                     :conditions => "shows.public_videos_count > 0").
                and_return([])

              get 'index'
              response.body.should_not match(/#{@show.name}/)
            end
          end
        end
      

What is wrong here?

Opening Examples

Objections

1. Testing using Mock Objects tests implementation and duplicates code

        describe ShowController, "index" do
          it "delegate to the shows collection to find which have public videos" do
            Show.should_receive(:with_public_videos)

            get 'index'
          end
        end
      

And implement "with_public_videos" in your model layer

Opening Examples

Objections

2. Mock Objects are brittle and do a bad job testing

        module Codebreaker
          describe Game do
            describe "#start" do
              it "sends a welcome message" do
                output = double('output')
                game = Game.new(output)
                output.should_receive(:puts).with('Welcome to Codebreaker!')
                game.start
              end
            end
          end
        end
      

What is wrong here?

Opening Examples

Objections

2. Mock Objects are brittle and do a bad job testing

        module Codebreaker
          describe Game do
            describe "#start" do
              it "delegates to it's output to send a welcome message" do
                output_interface = double('output_interface')
                game = Game.new(output_interface)
                output_interface.should_receive(:output).with('Welcome to Codebreaker!')
                game.start
              end
            end
          end
        end
      

Learning more

Growing Object Oriented Software Guided By Tests

Growing Object Oriented Software Guided By Tests

Learning more

xUnit Test Patterns: Refactoring Test Code

xUnit Test Patterns: Refactoring Test Code

Learning more

Blogs

Thank you!

Any questions?

@gregmoeck on twitter

http://gmoeck.github.com