Thursday 13 March 2014

Testbox: using a callback as a mocked method

G'day:
John Whish hit me up the other day, wondering had I looked at a new TestBox (well: at the time it was just for MockBox) feature I'd requested and has been implemented. Rather embarrassedly I had forgotten about it, and had not looked at it. That's a bit disrespectful of Luis and/or Brad (or whoever else put the hard work in to implement it), and for that I apologise. However I have looked at it now.

The feature request is this one, MOCKBOX-8:


Luis, we discussed this briefly @ SotR.

Currently one can specify a expression to use for a $results() value, and the value of that expression is then used as the results for the mocked method.

It would be occasionally handy for some stub code to be run to provide the results when a mocked method is called. This could be effected by being able to pass a callback to $results(), which is then called to provide the results when the mocked method itself is called.

Ideally a mocked method should always be able to be fairly dumb, but sometimes in an imperfect world where the mocked method has other dependencies which cannot be mocked out, an actual usable result is necessary. We've needed to do this about... hmmm... 0.5% of the times in out unit tests I think, so this is quite edge-case-y.

Cheers.
I can't recall what exactly we were needing to do at the time, but basically we were testing one method, and that called a different method which we wanted to mock-out. However we still wanted the mocked method to produce "real" results based on its inputs (for some reason).

MockBox allows to mock a method like this:

someObject.$("methodToMock").$results(0,1,2,3,4,etc);

And each call to methodToMock() will return 0, 1, 2 [etc] for each consecutive call, for as many arguments one cares to pass to the $results() method. Once it's used all the passed-in options it cycles back to the beginning again. This is great, but didn't work for us. What we needed was for the methodToMock() to actually run a stub function when it's called. Hence the enhancement request.

The Ortus guys have implemented this, so here's an example of it in action.

Firstly a MXUnit-compat scenario, running on ColdFusion 9 (so no need for function expressions or "closures" as the TestBox docs tends to refer to them as):

// C.cfc
component {

    public struct function getStructWithUniqueKeys(required numeric iterations){
        var struct = {};
        for (var i=1; i <= iterations; i++){
            struct[getId()] = true;
        }
        return struct;

    }

    private string function getId(){
        // some convoluted way of generating a key which we don't want to test
        return ""; // doesn't matter what it is, we won't be using it
    }

}

Here we have a CFC which has a method we want to test, getStructWithUniqueKeys(). It calls getId() to get a unique ID. For whatever reason we don't want our calls to getStructWithUniqueKeys() to call the real getId() (maybe it requires a DB connection or something?), so we want to mock-out getId(). However we still need it to return unique IDs.

Here's our test CFC:


// TestMxUnitCompat.cfc
component extends="mxunit.framework.TestCase" {

    function beforeTests(){
        variables.testInstance = new C();
        new testbox.system.testing.MockBox().prepareMock(testInstance);
        testInstance.$("getId").$callback(mockedGetId);
    }

    function testGetStructWithUniqueKeys(){
        var iterations    = 10;
        var result        = testInstance.getStructWithUniqueKeys(iterations=iterations);
        assertEquals(iterations, structCount(result), "Incorrect number of struct keys created, implying non-unique results were returned from mock");
    }

    private function mockedGetId(){
        return createUuid();
    }

}

Here I'm using Mockbox to prepare my test object for mocking (which just injects a bunch of MockBox helper methods into it), and then we mock getId() so that instead of calling the real getId() method, we use mockedGetId() as a proxy for it. And mockedGetId() just uses createUuid() to return a unique key.

And, pleasingly, this all works fine:




Note that if I use the actual getId() method, the test actually fails because I'm not returning a unique ID from it. Good stuff. And I'm pleased it runs on CF9 and in MXUnit compat mode.

But let's move forward into 2014, and use some BDD-inspired test syntax:

// TestMockUsingCallback.cfc
component extends="testbox.system.testing.BaseSpec" {

    function beforeAll(){
        variables.testInstance = new C();

        $mockbox.prepareMock(testInstance);
        testInstance.$("getId").$callback(function(){
            return createUuid();
        });

        testInstance.$("getTomorrow").$callback(mockedGetTomorrow);

        testInstance.$("emphasiseString").$callback(function(s){
            return "<em>#s#</em>";
        });
    }

    function run(){
        describe("Test getStructWithUniqueKeys() which has had its call to getId() mocked-out", function(){
            it("returns a correctly-sized struct", function(){
                var iterations = 10;
                var result = testInstance.getStructWithUniqueKeys(iterations);

                expect(structCount(result)).toBe(iterations);
            });
        });

        describe("Test getTomorrow(), which has been mocked with a predefined-function not a function expression", function(){
            it("returns returns tomorrow's date", function(){
                expect(
                    dateCompare(now()+1, testInstance.getTomorrow(), "d")
                ).toBe(0);
            });
        });

        describe("Test mocked emphasiseString(), which takes an argument", function(){
            it("uses the argument", function(){
                var result =  testInstance.emphasiseString("G'day World");
                expect(result).toBe("<em>G'day World</em>");
            });
        });
    }

    private function mockedGetTomorrow(){
        return dateAdd("d", 1, now());
    }

}

Here I'm performing the same test as before of getStructWithUniqueKeys(); so there's not much more to say about that. Note that this time I am not using a defined function as the callback, I am using an inline function expression. I've switched to using ColdFusion 11 for running these tests, which supports function expressions (ColdFusion 9 does not).

One thing I am doing different here - for the sake of expediency - is the other two tests don't test methods of C.cfc, they simply mock fictitious methods in testInstance. This works fine.

The second test is analogous to how we implemented the test in CF9: using a predefined function instead of a function expression as the callback.

The third test pushes the $callback() function further, in that the mocked function takes an argument, and accordingly the callback ought to receive it. I say "ought to" by design. Here's the results:


Damn. note that our third test is erroring. Because the argument doesn't get passed to the callback. I had better talk to Luis about this, as it's kinda important that the arguments do find their way into the callback, in case they're needed.

Still: this is a cool feature to have. It's not the sort of thing one needs all the time, but better to have and not need, than need and not have.

Cheers TestBox dudes.

--
Adam