Mock Objects

nzakas edited this page Nov 23, 2011 · 1 revision

Mock objects are used to eliminate test dependencies on other objects. In complex software systems, there's often multiple object that have dependence on one another to do their job. Perhaps part of your code relies on the XMLHttpRequest object to get more information; if you're running the test without a network connection, you can't really be sure if the test is failing because of your error or because the network connection is down. In reality, you just want to be sure that the correct data was passed to the open() and send() methods because you can assume that, after that point, the XMLHttpRequest object works as expected. This is the perfect case for using a mock object.

To create a mock object, use the YUITest.Mock() method to create a new object and then use YUITest.Mock.expect() to define expectations for that object. Expectations define which methods you're expecting to call, what the arguments should be, and what the expected result is. When you believe all of the appropriate methods have been called, you call YUITest.Mock.verify() on the mock object to check that everything happened as it should. For example:

//code being tested
function logToServer(message, xhr){"get", "/log.php?msg=" + encodeURIComponent(message), true);

//test case for testing the above function
var testCase = new YUITest.TestCase({

    name: "logToServer Tests",

    testPassingDataToXhr : function () {
        var mockXhr = YUITest.Mock();

        //I expect the open() method to be called with the given arguments
        YUITest.Mock.expect(mockXhr, {
            method: "open",
            args: ["get", "/log.php?msg=hi", true]                            

        //I expect the send() method to be called with the given arguments
        YUITest.Mock.expect(mockXhr, {
            method: "send",
            args: [null]                            

        //now call the function
        logToServer("hi", mockXhr);

        //verify the expectations were met

In this code, a mock XMLHttpRequest object is created to aid in testing. The mock object defines two expectations: that the open() method will be called with a given set of arguments and that the send() method will be called with a given set of arguments. This is done by using YUITest.Mock.expect() and passing in the mock object as well as some information about the expectation. The method property indicates the method name that will be called and the args property is an array of arguments that should be passed into the method. Each argument is compared against the actual arguments using the identically equal (===) operator, and if any of the arguments doesn't match, an assertion failure is thrown when the method is called (it "fails fast" to allow easier debugging). The call to YUITest.Mock.verify() is the final step in making sure that all expectations have been met. It's at this stage that the mock object checks to see that all methods have been called. If open() was called but send() was not, then an assertion failure is thrown and the test fails. It's very important to call YUITest.Mock.verify() to test all expectations; failing to do so can lead to false passes when the test should actually fail. In order to use mock objects, your code must be able to swap in and out objects that it uses. For example, a hardcoded reference to XMLHttpRequest in your code would prevent you from using a mock object in its place. It's sometimes necessary to refactor code in such a way that referenced objects are passed in rather than hardcoded so that mock objects can be used.

Note that you can use assertions and mock objects together; either will correctly indicate a test failure.

Special Argument Values

There may be times when you don't necessarily care about a specific argument's value. Since you must always specify the correct number of arguments being passed in, you still need to indicate that an argument is expected. There are several special values you can use as placeholders for real values. These values do a minimum amount of data validation:

  • YUITest.Mock.Value.Any - any value is valid regardless of type.
  • YUITest.Mock.Value.String - any string value is valid.
  • YUITest.Mock.Value.Number - any number value is valid.
  • YUITest.Mock.Value.Boolean - any Boolean value is valid.
  • YUITest.Mock.Value.Object - any non-null object value is valid.
  • YUITest.Mock.Value.Function - any function value is valid.

Each of these special values can be used in the args property of an expectation, such as:

YUITest.Mock.expect(mockXhr, {
    method: "open",
    args: [YUITest.Mock.Value.String, "/log.php?msg=hi", YUITest.Mock.Value.Boolean]                            

The expecation here will allow any string value as the first argument and any Boolean value as the last argument. These special values should be used with care as they can let invalid values through if they are too general. The YUITest.Mock.Value.Any special value should be used only if you're absolutely sure that the argument doesn't matter.

Property Expectations

Since it's not possible to create property getters and setters in all JavaScript environments, creating a true cross-browser property expectation isn't feasible. YUI Test mock objects allow you to specify a property name and it's expected value when YUITest.Mock.verify() is called. This isn't a true property expectation but rather an expectation that the property will have a certain value at the end of the test. You can specify a property expectation like this:

//expect that the status property will be set to 404
YUITest.Mock.expect(mockXhr, {
    property: "status",
    value: 404                            

This example indicates that the status property of the mock object should be set to 404 before the test is completed. When YUITest.Mock.verify() is called on mockXhr, it will check the property and throw an assertion failure if it has not been set appropriately.