Blog

17
January 2014

Gavin Pickin

Unit Testing 06 - Mocking External Dependencies Part 2

Dependency Injection, Unit Testing

Welcome back to the 6th post in our Unit Testing for Dummies Series, where I walk you through building a small ColdFusion (cfml) application using Test Driven Development, running our tests with TestBox, Ortus Solutions and ColdBox's testing platform. 

Last time we created our WebsiteDAO.cfc and a simple method getWebsite() that would return a query containing the Website Details. In our method, we returned only an empty query at this time, since we are still fleshing out our App, as we explore through our Test Driven Development. Today, we're going to use Mocks to isolate our CUT component under test, which is our Website.cfc (website service). 

Step 1 - Mock our WebsiteDAO.cfc

Lets look at out testGetWebsiteSuccess() test method.

Inside blog06/Step1/test/unit/Website.cfc

function testGetWebsiteSuccess() {

    $assert.typeOf( "query", website.getWebsite(4), "Query Expected - Not Received" );
    $assert.assert( Website.getWebsite(4).recordcount >= 1, "Query with 1 or more Records expected");
}

 

Our setup() function currently creates a new Website object, and sets the DAO each and every time.

function setup() {

    variables.Website = new blog06.Step1.model.Website().init();
    variables.Website.setDAO( variables.WebsiteDAO );
}

 

We pull the WebsiteDAO from the beforeTests() method. This creates it one time, and then is used in each and every setup() call before every test.

function beforeTests() {

    variables.mockbox = new testbox.system.testing.Mockbox();
    variables.WebsiteDAO = new blog06.Step1.model.WebsiteDAO().init();
}

 

We really want our DAO to be reset each and every test too, especially if we're going to be mocking everything in it, so lets move some code around.

Take out the DAO instantiation in beforeTests()

    variables.WebsiteDAO = new blog06.Step1.model.WebsiteDAO().init();

 

Instead of moving that into the setup() method, lets just create a new empty mock object, and we'll mock everything in the DAO. We are adding a argument called ClearMethods, which removes all the methods in the object, so everything has to be mocked. This ensures you don't forget to mock one, and use the actual original methods. 

function setup() {

    variables.Website = new blog06.Step1.model.Website().init();
    variables.WebsiteDAO = variables.mockbox.createMock(className='blog06.Step1.model.WebsiteDAO', clearMethods=true);
    variables.Website.setDAO( variables.WebsiteDAO );
}

 

Note, you could just use createStub() if you want a completely empty Mock object… but if you do that, in your errors, the component name will be testbox.system.testing.mockutils.Stub instead of blog06.Step1.model.WebsiteDAO. If you have multiple cfcs you are stubbing, it might get confusing all being of the same component type, but its powerful. In fact, I have an idea about stubs, and I might have to write an article about this another day.

So lets run our test, and we get an error… 

component [blog06.Step1.model.WebsiteDAO] has no function with name [getWebsite]

This means we successfully mocked our DAO. 

Step 2- Mock our getWebsite() method in our Mocked WebsiteDAO

We want to mock the getWebsite() method. We have 2 assertions so far if you look in our testGetWebsiteSuccess() method.

Inside blog06/Step2/test/unit/Website.cfc

function testGetWebsiteSuccess() {

    $assert.typeOf( "query", website.getWebsite(4), "Query Expected - Not Received" );
    $assert.assert( Website.getWebsite(4).recordcount >= 1, "Query with 1 or more Records expected");
}

 

To mock a method, we use the $() method, and pass in the method name, and we can pass in a result right there too.

variables.WebsiteDAO.$(method="getWebsite",returns=queryNew(""));

 

Now, since we're going to call this 2 times, we can just have it return the same result every time using this… so for now, this will suffice. 

Lets use Query Sim to simulate a query, and return that.

function testGetWebsiteSuccess() {

    var mockQuery = mockBox.querySim("id, name
        1|www.gpickin.com
        2|www.gavinpickin.com");

    variables.WebsiteDAO.$(method="getWebsite",returns=mockQuery);

    $assert.typeOf( "query", website.getWebsite(4), "Query Expected - Not Received" );
    $assert.assert( Website.getWebsite(4).recordcount >= 1, "Query with 1 or more Records expected");
}

 

Query Sim is a cool little query shorthand tool, set the column names, line breaks for new records, and pipes for separators.
This makes a query, with 2 rows, and returns it every time, so both assertions pass.

Step 3 - Create testGetWebsiteNotFound() test and mock the results

Lets create a new method, called testGetWebsiteNotFound() and we'll use a slight variation on the assertions, and on how we mock the methods. First, lets look at the assertions.

function testGetWebsiteNotFound() {

    $assert.isEqual( 1, website.getWebsite(2).recordcount);
    $assert.isEqual( 0, Website.getWebsite(4).recordcount);
}

 

In this case, we're asserting that the return value matches what we expect. We're looking at our mocked method getWebsite() and expecting when we pass the number 2 in, we'll get 1 record. When we pass in the number 4 in, we'll get 0 records.

So, lets mock it with like our previous test.

var mockQuery = mockBox.querySim("id, name
    1|www.gpickin.com
    2|www.gavinpickin.com");
variables.WebsiteDAO.$(method="getWebsite",returns=mockQuery);

 

This will return 2, both times. Lets run it and see.

As you can see, it states Expected [1] but received [2]

So, how can we make it successfully pass both assertions?

We can set our mock to pass different results… which is really cool… and its easy to do too. Mockbox manages the count, and just rotates through your list of results if you use the $results() method.

So we can do this.

function testGetWebsiteNotFound() {

    var mockQuery1 = mockBox.querySim("id, name");
    var mockQuery2 = mockBox.querySim("id, name
        1|www.gpickin.com");

    variables.WebsiteDAO.$(method="getWebsite").$results(mockQuery1,mockQuery2);

    $assert.isEqual( 1, website.getWebsite(2).recordcount);
    $assert.isEqual( 0, Website.getWebsite(4).recordcount);
}

 

Now the test fails because query1 has 0 records, and query2 has 1 record count, but we have them backwards.
Lets change the order of the returning results.

function testGetWebsiteNotFound() {

    var mockQuery1 = mockBox.querySim("id, name");
    var mockQuery2 = mockBox.querySim("id, name
        1|www.gpickin.com");

    variables.WebsiteDAO.$(method="getWebsite").$results(mockQuery2,mockQuery);

    $assert.isEqual( 1, website.getWebsite(2).recordcount);
    $assert.isEqual( 0, Website.getWebsite(4).recordcount);
}

 

Now the test passes.

The issue with this is, you have to remember the order, so wouldn't it be nice if you could return different results, based on the input? Well, of course you can.

Lets update our tests to look like this.

function testGetWebsiteNotFound() {

    var mockQuery1 = mockBox.querySim("id, name");
    var mockQuery2 = mockBox.querySim("id, name
        1|www.gpickin.com");

    //variables.WebsiteDAO.$(method="getWebsite").$results(mockQuery2,mockQuery1);
    variables.WebsiteDAO.$(method="getWebsite").$args(2).$results(mockQuery2);
    variables.WebsiteDAO.$(method="getWebsite").$args(4).$results(mockQuery1);

    $assert.isEqual( 1, website.getWebsite(2).recordcount);
    $assert.isEqual( 0, Website.getWebsite(4).recordcount);
}

 

Now, the order can be anything it wants to be, when these arguments are passed in, it returns the given results.Note, if you're not catching that argument, it will give you an error.

So we've changed our method a few times, which shows you there are many ways to skin a cat, you just need to decide whats best for you, and your project.

Thanks again for following along,

Gavin

Blog Search