Happy !

zeroflag / BabyMock

Project infos

License MIT
Tags unit testing, mock, tdd, mocking
Creation date 2013-04-21
Website

Monticello registration

About BabyMock


You are probably looking for BabyMock2, the current development version of BabyMock

BabyMock is a visual mock object library for Pharo Smalltalk, that supports test-driven development. Like other mock object libraries, BabyMock helps you design and test the interactions between the objects in your programs. BabyMock makes these interactions visible, which may help you identify test smells easier.

Getting started

Installation

BabyMock with visual mode (recommended)

Gofer new
    smalltalkhubUser: 'zeroflag' project: 'BabyMock';
    package: 'ConfigurationOfBabyMockGUI';
    load.

((Smalltalk at: #ConfigurationOfBabyMockGUI) project version: '1.1') load.

This code loads both the core and the visualization package. Copy the script to a workspace and execute it with the do it command.

BabyMock only

Gofer new
    smalltalkhubUser: 'zeroflag' project: 'BabyMock';
    package: 'ConfigurationOfBabyMock';
    load.

((Smalltalk at: #ConfigurationOfBabyMock) project version: '1.1') load.

If you don't need the visualization package you can use this script to load the core package only.

Examples

((Smalltalk at: #ConfigurationOfBabyMock) project version: '1.1') load: #('Examples')

This code loads the example package into your workspace, which contains sample test cases. But there is a complete sample application written with TDD here.

Write a unit test case

The recommended way to use BabyMock is to extend your test from BabyMockTestCase, which is a subclass of the SUnit TestCase. After that you can run your test just like any other SUnit based unit test.

BabyMockTestCase subclass: #PublisherTest
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Demo'

We are going to write a test for a simple publisher/subscriber system. Subscribers can be registered to the publisher in order to receive notifications about events. We want to test the publisher, but to do so we need a mocked subscriber in order to check the outgoing messages coming from the publisher.

PublisherTest>>testNotifiesSubscriberAboutAnEvent
    [...]

Creating mocks

Mocks can be created by sending a mock: message to the context with an arbitrary name string. The context is an instance variable in BabyMockTestCase, and it represents the peer objects (the subscribers) of the object under test.

subscriber := context mock: 'subscriber'.

Testing the interaction

The inter-object protocol between the publisher and the subscriber is fairly simple. When we are publishing an event using the publisher, it will notify all the registered subscribers. These outgoing messages are what we are interested in.

In this testcase we are registering one subscriber mock to the publisher, and we expect the publisher to send the eventReceived: message with the correct argument to the subscriber mock.

| event publisher subscriber |

event := 'test event'.
publisher := Publisher new.
subscriber := context mock: 'subscriber'.
publisher addSubscriber: subscriber.

subscriber should receive: #eventReceived:; with: event.

publisher publish: event.

The "subscriber should receive: #eventReceived:; with: event" is an expectation on the subscriber mock that specify the messages to be sent to the subscriber.

The test will fail:

  • if the expected message is not sent to the mock during the test
  • if the expected message is sent more than once
  • if the argument of the message is different
  • if an other, unexpected message is sent to the mock

Visual mode

To enable the visual mode send a visualize: message to the context with the object under test as a parameter.

context visualize: publisher.

You can put this row in the setUp method to affect all tests, or at the beginning of the test method you want to visualize.

BabyMock intercepts the messages between the test, the object under test and the mocks. After you executed the unit test, it will draw these interactions on a window. Then you can replay or step through the animation.

Arrows with different colors indicate expected, unexpected or missing messages. An allowed or expected message is represented as a green arrow. Unexpected messages are red, and expected but missing messages are gray. A yellow arrow indicates an exception.

Watch this video to get an idea how does it look like.

Usage

Cardinalities

The should receive:, without any modifiers can be used when we expect exactly one message to be received by the mock object.

These two expectations are equivalent:

mock should receive: #message.
mock should receive: #message; exactly: #once

But it is possible to define an exact message count, or an interval.

The valid arguments of the exactly: message are #once, #twice, #threeTimes .. #tenTimes

Expecting messages at least n times, or at most n times:

mock should receive: #message; minimum: #once.
mock should receive: #message; maximum: #twice.
mock should receive: #message; minimum: #once; maximum: #twice.

Not expected messages:

mock shouldnt receive: #message.

This expectation is equivalent of not having it at all. This is used to make tests more explicit and so easier to understand.

Returning values from mocked methods

mock should receive: #theUltimateQuestion; answers: 42.

Allowing versus expecting

The differentiation of allowing and expecting makes the tests easier to understand and maintain. Not to mention that it also could make the coupling between the test and the implementation looser.

"Not recommended:"
phonebook should receive: #findNumber:; with: user; answers: '+3630405060'.

"Recommended:"
phonebook can receive: #findNumber:; with: user; answers: '+3630405060'.

The #findNumber message is not a command, but a query. It is reasonable to expect that the findNumber method doesn't have any side effects, so you can invoke it any number of times you want. Usually it is not needed to test these outgoing queries explicitly. Using the "should" message would make the test unnecessary brittle.

In the second example we are using the "can" message, which allows the object under test to send the #findNumber message any number of times it wants (including never).

Here is a simple rule to follow: allow queries (use can) and expect commands (use should). Of course, this is an oversimplified rule that does not hold all the time, but in can be useful in many cases.

phonebook should receive: #saveNumber:; with: '+36304050'.

The saveNumber message is a command, sending it multiple times would cause bad things happen.

Consecutive messages

When you want to answer different values to the same message, you can do so by cascading the answers: message.

mock can receive: #message; answers: 1; answers: 2; answers: 3.

mock message. "-> prints 1"
mock message. "-> prints 2"
mock message. "-> prints 3"
mock message. "-> prints 3"

After the third message the mock will keep returning the return value of the previous method.

Matching parameter values

calculator 
    can receive: #add:to:; 
    with: 1 and: 2; 
    answers: 3.

calculator 
    can receive: #add:to:; 
    with: 3 and: 4; 
    answers: 7.

calculator add: 1 to: 2. "-> prints 3".
calculator add: 3 to: 4. "-> prints 7"

Blocks as argument matchers

Any block that returns booleans, and has the correct number of arguments can be used as an argument matcher.

calculator 
    can receive: #sqrt:;
    with: [:number | number < 0];
    signals: MathDomainException.

calculator sqrt: -1. "-> will signal MathDomainException"

You can use different blocks for each arguments, or one block which has the same number of arguments as the method has.

calculator 
    can receive: #subtract:from:;
    with: [:a :b | a > b];
    signals: NegativeNotSupportedException.

calculator subtract: 3 from: 1. "-> will signal NegativeNotSupportedException"
calculator 
    can receive: #add:to:;
    with: [:a | a > 10] and: [:b | b < 20];
    answers: 'something'.

Ignoring an irrelevant mock object

mock can receive: anything.

The default answer will be nil.

The anything is an instance variable in BabyMockTestCase.

Using states

States are handy if you want to bring your object under test in a particular state, and check whether an action was take in the correct state, without accessing the object's internals.

state := context states: 'machine state' startsAs: #ready.

vendingMachine 
    should receive: #insertCoin:; with: 1 dollar;
    when: state is: #ready;
    then: state is: #hasCoin.

vendingMachine
    should receive: #press:; with: 2
    when: state is: #hasCoin. 

vendingMachine
    should receive: #press:; with: 5;
    when: state is: #hasCoin.

The user of the vending machine could press the button "2" and "5" in any order, as long as the vending machine is in the #hasCoin state, the test will pass. What is important is that the insertCoin should happen first. States are useful for loosening ordering constraints (one event happens first and others follow it in any order).

Mocking object methods

Methods defined in the Object or ProtoObject class cannot be mocked by default. If you need to mock or stub them for some reason, send the following message to the context.

context excludedObjectMethods: #('methodYouWantToMock').

For example:

context excludedObjectMethods: #('class').

mock can receive: #class; answers: String.
mock class. "-> prints String instead of BmMock"

Don't use this feature unless you have a good reason to do so.

Syntax cheat sheet

mock 
    should/shouldnt/can receive: aSymbol;
    with: arg1 and: [:arg2 | arg2 > 3] and: ..;
    minimum: #once;
    maximum: #twice;
    exactly: #threeTimes;
    answers: returnValue;
    signals: Error;
    when: state is: #state1;
    then: state is: #state2.

Note that not all combinations are valid, or make sense.