Skip to content
spadgos edited this page Mar 22, 2012 · 6 revisions

Myrtle Mocking Framework

Method Mocking

Spying

Let's say you have a Validator object, and you want to check that your validate method is being executed after a change in your UI. The first step is to spy on the validate method:

var handle = Myrtle.spy(Validator, 'validate');

Spying on a method modifies it so that metadata about that method is stored with each call to it, without changing the actual functionality. You can access this metadata through the API handle returned by Myrtle.spy.

var handle = Myrtle.spy(Validator, 'validate');
$(myInputElement).val('a changed value').trigger('change');

// let's check that validate was called
handle.callCount(); // 1

// and let's see what it returned
handle.lastReturn(); // true

// and let's check what the parameters passed to it were
handle.lastArgs(); // [myInputElement, "a changed value"]

// what was "this"?
handle.lastThis(); // Validator

// were there any errors thrown?
handle.lastError(); // undefined -- no errors

What's important to remember here is that the validate method is still executed as if nothing ever happened. Myrtle wouldn't be a good spy otherwise... This applies equally to any error thrown during execution, though Myrtle will intercept it to provide you the information about the execution, it will rethrow the same error.

Stubbing

Other times you want to actually stop functions from running. For example, in a test environment, you might want to suppress error messages which you're triggering on purpose, or to stop AJAX functions from executing. In these cases, you can stub out a function.

Myrtle.stub(Notifications, 'displayError');

This replaces the displayError method with a function which does nothing. Care should be taken in these cases that other code isn't expecting a return value from functions which you stub out completely.

You can also replace a function using stub. This is useful if you want to simulate a method, but not actually execute it

  • for example, when an AJAX method is called, you can put together a fake response.
Myrtle.stub(Network, 'send', function (origFn, url, callback) {
    callback({isSuccess : true});
});

You'll see here that the first parameter passed to the stub function is the original method. This is useful in cases where you may want to only stub it out based on some criteria, or to modify arguments or return values of the original method.

Myrtle.stub(
    Notifications, 'displayError', function (origFn, message) {
        if (message !== 'My expected error') {
            origFn(message);
        }
    }
);

Profiling

Myrtle also supports some basic speed profiling. If you want to find out how fast your function is executed, or if it runs slower given some input, you can turn on profiling and find out.

var handle = Myrtle.profile(calc, 'factorial');
calc.factorial(3);
calc.factorial(8);
calc.factorial(12);

handle.getAverageTime(); // a number, like 12 (ms)
handle.getQuickest();    // { args:  [3], ret: 6,         time:  1 }
handle.getSlowest();     // { args: [12], ret: 479001600, time: 20 }

The nice thing about these features is that they can all be combined in any way. You can spy on and stub out a method, or spy and profile. (Stubbing and profiling probably isn't so useful though, since you'd only be measuring the speed of performing your replacement method.)

There are a few ways to combine the options, and it's important to know that each of the Myrtle functions returns the exact same API object per function. To demonstrate:

var handle1 = Myrtle.spy(Obj, 'foo'); // turn on spying
var handle2 = Myrtle.stub(Obj, 'foo'); // turn on stubbing
handle1 === handle2; // true

There's also the Myrtle function itself which can be used:

var handle = Myrtle(Obj, 'foo', {
    spy : true,
    stub : function () {
        // my replacement
    },
    profile : true
});

Of course, the last thing to cover is an important one: tidying up. Chances are that you don't want to stub out your methods for all the tests, and you want to restore them to how they were before at some point. Myrtle makes this super easy.

// if you have a reference to the API object:
handle.release();

// and even if you don't, remember that Myrtle will give you it easily.
Myrtle(Obj, 'foo').release();

// and if you're really lazy, just clean them all up!
Myrtle.releaseAll();

// if you're not sure if you've left some hanging about, just check!
Myrtle.size();

Faking Timers

Sometimes some code will make use of timing functions such as setTimeout and setInterval which makes testing that code much harder:

var obj = {
    doSomethingSoon : function () {
        setTimeout(function () {
            console.log("Woo! Horse party!");
        }, 1000);
    }
};

test("doSomethingSoon starts a horse party", function () {
    var handle = Myrtle.spy(console, 'log');
    obj.doSomethingSoon();

    equal(handle.callCount(), 1); // FAIL. the horse party doesn't start for 1 more second.
});

So, Myrtle provides a function which will fake the internal timer, allowing you to control when timeouts and intervals are actually fired. The above code would be changed to this:

test("doSomethingSoon starts a horse party", function () {
    var handle = Myrtle.spy(console, 'log');

    Myrtle.fakeTimers(); // enable the fake timers

    obj.doSomethingSoon();

    equal(handle.callCount(), 0); // PASS. the horse party hasn't started yet.

    Myrtle.tick(1000);  // advance the clock by 1000 milliseconds.

    equal(handle.callCount(), 1); // PASS. the horse party is in full effect.

    Myrtle.realTimers(); // reenable the real timers, so other code doesn't break.
});

This works equally as well with setInterval:

var obj = {
    foo : function () {}
};
var handle = Myrtle.spy(obj, 'foo');

Myrtle.fakeTimers();

setInterval(obj.foo, 10); // call obj.foo every 10ms

Myrtle.tick(10);

equal(handle.callCount(), 1); // after 10 ms it gets called once

Myrtle.tick(50);

equal(handle.callCount(), 6); // now we're 60ms in, so it has been called 6 times.

And finally, clearTimeout and clearInterval work like you'd expect:

Myrtle.fakeTimers();

var timeoutId = setInterval(obj.foo, 10);

Myrtle.tick(10); // triggers obj.foo() once

clearInterval(timeoutId);

Myrtle.tick(10); // obj.foo() is not called again

Function generator

When you are using stub functions, the replacement function often performs a very simple task: given these hardcoded inputs, return this hardcoded value. Traditionally, you'd have to write something like this:

Myrtle.stub(obj, 'foo', function (orig, val) {
    if (val === 'a') {
        return 1;
    } else if (val === 'b') {
        return 2;
    } else {
        return 3;
    }
});

Since that's a bit tedious and hard to read, Myrtle provides a function builder to simplify the operation:

Myrtle.stub(obj, 'foo', Myrtle.fn()
    .when('a').then(1)
    .when('b').then(2)
    .otherwise(3)
);

It starts with a call to Myrtle.fn() which returns a function object which has been enhanced with further functions to modify its behaviour. Each of these functions returns the function itself, allowing you to chain them together like in the example above.

The some of methods it provides (when, then, otherwise and run) should be used in a specific order to produce the right results:

.when(x).then(r)     // when the arguments to this function are exactly x, return r

.when(x, y).then(r)  // when the arguments are exactly x and y, return r

.when().then(r)      // when no arguments are provided, return r

.when(x).run(fn)     // when the arguments are exactly x, run the function fn and return the result

.otherwise(r)        // if none of the other preconditions are met, return r

.otherwise().run(fn) // if none of the other preconditions are met, run the function fn and return the result

Another way to build a function is to start with an existing one, by passing that to .fn(). This is essentially the same as calling .otherwise().run(fn)

var orig = function (a) {
    return 2 / a;
};
var f = Myrtle.fn(orig).when(0).then('foobar');

f(8); // 4.5
f(0); // 'foobar'

Additionally, Myrtle allows further modification of the behaviour to be prevented, by using .seal()

var f = Myrtle.fn().when(1).then('A');
f.seal();                   // seal the function
f.when(1).then('B');        // this is valid syntax, but it performs no action at all

f(1); // 'A'

// that could also have been written in one line

var f = Myrtle.fn().when(1).then('A').seal().when(1).then('B');

Finally, to remove the additional interfaces provided by Myrtle, you can "export" the function by using .get()

var f = Myrtle.fn().when(1).then('A').get();

f(1); // 'A'

f.otherwise('B');  // Error! f does not have the otherwise method (or when, then, etc..)

.and()

A common use case for using a mocking framework is to spy on or stub out a function, perform some actions and tests, and then clean up afterwards. To save lots of boilerplate try { .. } finally { .. } blocks, Myrtle provides a convenience method named .and(). Usage is simple:

Myrtle.spy(myObj, 'myFunc').and(function () {
  doSomething();
});

This is equivalent to:

var handle = Myrtle.spy(myObj, 'myFunc');
try {
  doSomething();
} finally {
  handle.release();
}

Inside of the function passed to .and(), the context is the handle created by calling Myrtle. So, a practical example might be something like this:

Myrtle(myView, 'render', {
  spy: true,
  stub: true
}).and(function () {
  myModel.set('blah', 'foo');
  assert(this.callCount() === 1);
  // or, if using Tyrtle:
  assert(this).called(1)();
});