株式会社クリアコード > フリーソフトウェア開発 > UxU - UnitTest.XUL > How to write testcases with mocks?

How to write testcases with mocks? UxU - UnitTest.XUL

How to create and define mocks?

Mock objects

How to create mock objects

In testcases, a constructor function Mock() is available, and you can create new mock object from the function. You can give a name to the mock as you like, by the first argument of the constructor. The name will be shown in test results and help your debugging, so you should give names for mocks.

var mock = new Mock('my mock');

If you give a constructor or an object to the constructor, then the mock inherits properties and methods of the source object, and it is also named automatically from the source.

// from class
var arrayMock = new Mock(Array);
// from instance
var windowMock = new Mock(window);
// you can give both name and source
var documentMock = new Mock('unique name', document);

Exceptions will be thrown if you access properties (ex. windowMock.title) or methods (ex. windowMock.alert()) inherited from its source, before you define any expectation.

Expectations of methods

Just after you create a new mock, it will throw "unexpected call" exceptions if you call any functions of the mock (except features of the mock itself). To use the mock for a test, you have to define expectations (a set of expected arguments and the return value).

For example, if gBrowser.addTab() and gBrowser.removeAllTabsBut() are expected to be called with this order, then you will define two expectations as:

var mock = new Mock('gBrowser');
mock.expect('addTab', uri, tab); // mock.addTab(uri) => tab
mock.expect('removeAllTabsBut', tab); // mock.removeAllTabsBut(tab) => undefined
gBrowser = mock; // replace gBrowser to the mock

The method expect() receives the name of the imitated method as the first argument, expected arguments as the second (if an array is expected to be given, you can specify it as a 2D-array. You'll specify a blank array for an imitated method which is expected to be called with no argument), and the value to be returned as the third (undefined by default). When the imitated method is called with expected arguments, then it returns the specified value.

mock.expect('open', [uri, name, features], null);
// mock.open(uri, name, features) => null

mock.expect('slice', [[item1, item2], 1], [item2]);
// mock.slice([item1, item2], 1) => [item2]

If you want to imitate a case that a method call throws an error, then use expectThrows() instead of expect(). Then the imitated method will raise the specified exception if it is called with the specified arguments.

mock.expectThrows('loadURI', 'urn:isbn:xxxxx', 'invalid URI');
// mock.loadURI('urn:isbn:xxxxx') => throw new Error('invalid URI')

// You can give a constructor for the exception (and its message).
mock.expectThrows('eval', '{', SyntaxError, 'missing } in compound statement');
// mock.eval('{') => throw new SyntaxError('missing } in compound statement')

Anyway, each expectation matches to each call, including the order. All unexpected calls - calling in wrong order or calling with wrong arguments - will raise exceptions. For cases that the called order and arguments are unmeaning, you should use stubs instead of mocks.

// this is a stub, not a mock.
var gBrowser = {
      addTab : function() {
        return document.createElement('tab');
      }
    };

Expectations of properties

You can define expectations for accesses to properties.

var loc = new Mock('location');

// the "host" property will be accessed just one time,
// then return "www.example.jp".
loc.expectGet('host', 'www.example.jp');
// loc.host => 'www.example.jp'

// a string "http://www.example.com" will be set to the "href" property.
loc.expectSet('href', 'http://www.example.com/');
// loc.href = 'http://www.example.com/' => OK
// loc.href = 'http://www.example.net/' => NG

Moreover, you can define cases which raise exceptions by accesses to properties.

loc.expectGetThrows('host', 'permission denied');
// loc.host => throw new Error('permission denied')

loc.expectSetThrows('href', 'urn:isbn:xxxx', 'invalid uri');
// loc.href = 'urn:isbn:xxxx' => throw new Error('invalid uri')

Same as expectations for method calls, each expectation matches to each access, including the order. All unexpected accesses - accesses in wrong order or settings with wrong values - will raise exceptions. For cases that the accessed order and values are unmeaning, you should use stubs instead of mocks.

// this is a stub, not a mock.
var location = {
      href : 'http://www.exemple.jp/',
      host : 'www.exemple.jp'
    };

Verification of mock objects

You can verify the mock itself by its assert() method anytime. The assertion (verification) will succeed if all of defined expectations were processed correctly. Otherwise, if there are some unprocessed expectations the method raises an exception.

gBrowser = new Mock('gBrowser mock');
gBrowser.expect('addTab', uri, tab);
gBrowser.expect('removeAllTabsBut', tab);

var newTab = openNewTabAndCloseOthers(uri);

gBrowser.assert();

Note, you don't have to call assert() manually. UxU always calls assert() of all mocks created in the test before doing "tearDown", automatically.

Mock functions

You can create a mock which works as a function which is not related to specific object, instead of full-featured mock objects. Mock functions will be useful for callback functions, etc.

How to create mock functions

In testcases, two constructor functions FunctionMock() and MockFunction() are available, and you can create new mock function from them. These is no difference, so you can use one of them as you like. You can give a name to the mock as you like, by the first argument of the constructor. The name will be shown in test results and help your debugging, so you should give names for mocks.

var fmock = new MockFunction('my mock function');

If you give a named function to the constructor, then the mock uses the name of it. Mock function cannot get a name from an anonymous function, so it is strongly recommended to you that specifying names to mock functions manually./p>

var openNewTabWith = new MockFunction(openNewTabWith);
var openUILink = new MockFunction(openUILink);

Expectations of function calls

Just after you create a new mock function, it will throw "unexpected call" exceptions if you call it with any argument. To use it for a test, you have to define expectations.

For example, if openNewTabWith() is expected to be called two times with arguments 'http://www.example.com/' and 'urn:isbn:xxxx', then you will define two expectations as:

var fmock = new MockFunction('openNewTabWith');
fmock.expect('http://www.example.com/', tab); // openNewTabWith('http://www.example.com/') => tab
fmock.expect('urn:isbn:xxxx'); // openNewTabWith('urn:isbn:xxxx') => undefined
openNewTabWith = fmock; // replace to the mock function

The method expect() receives expected arguments as the first argument (if an array is expected to be given, you can specify it as a 2D-array. You'll specify a blank array for an imitated function which is expected to be called with no argument), and the value to be returned as the second (undefined by default). When the imitated function is called with expected arguments, then it returns the specified value.

fmock.expect([uri, name, features], null);
// fmock(uri, name, features) => null

fmock.expect([[item1, item2], 1], [item2]);
// fmock([item1, item2], 1) => [item2]

If you want to imitate a case that a call throws an error, then use expectThrows() instead of expect(). Then the imitated function will raise the specified exception if it is called with the specified arguments.

openMock.expectThrows('urn:isbn:xxxxx', 'invalid URI');
// openMock('urn:isbn:xxxxx') => throw new Error('invalid URI')

// You can give a constructor for the exception (and its message).
evalMock.expectThrows('{', SyntaxError, 'missing } in compound statement');
// evalMock('{') => throw new SyntaxError('missing } in compound statement')

Anyway, each expectation matches to each call, including the order. All unexpected calls - calling in wrong order or calling with wrong arguments - will raise exceptions. For cases that the called order and arguments are unmeaning, you should use stubs instead of mocks.

// this is a stub, not a mock.
var openUILink = function(aURI) {
      return true;
    };

Verification of mock functions

You can verify the mock function itself by its assert() method anytime. The assertion (verification) will succeed if all of defined expectations were processed correctly. Otherwise, if there are some unprocessed expectations the method raises an exception.

openNewTabWith = new MockFunction('openNewTabWith');
openNewTabWith.expect(uri1, document.createElement('tab'));
openNewTabWith.expect(uri2, document.createElement('tab'));

var newTabs = openNewTabs([uri1, uri2]);

openNewTabWith.assert();

Note, you don't have to call assert() manually. UxU always calls assert() of all mocks created in the test before doing "tearDown", automatically.

Overriding methods of existing objects

You can replace (or add) some methods or properties of existing objects, by static methods of Mock.

Mock.expect(gBrowser, 'addTab', uri, document.createElement('tab'));
Mock.expect(gBrowser, 'removeAllTabsBut', tab);
Mock.expectThrows(gBrowser, 'loadURI', ['urn:isbn:xxxx', referrer], 'invalid URI');
Mock.expectGet(gBrowser, 'contentDocument', content.document);
Mock.expectGetThrows(gBrowser, 'docShell', 'permission denied');
Mock.expectSet(gBrowser, 'selectedTab', tab);
Mock.expectSetThrows(gBrowser, 'contentDocument', 'readonly');

Differently from expectations of methods or expectations of properties, static methods receive the object which is the owner of the method (or property) as the first argument.

Method chain of expectations

You can specify details of expectations by method-chain of expectation itself.

var windowMock = new Mock(window);

windowMock.expect('alert', 'OK').andReturn(true)
                                .times(3);

Method-chain is available for methods of mock object: expect(), expectThrows(), expectGet(), expectGetThrows(), expectSet(), expectSetThrows(), and methods of mock functions: expect() and expectThrows().

Object times(in Number aTimes)

Specifies the multiplicity of the last expectation.

This doesn't multiply specs defined after this, so you have to be careful about the order of method-chain. For example, following two cases are different:

// "alert('OK') returns true" will be done three times.
mock.expect('alert', 'OK').andReturn(true)
                          .times(3);

// "alert('OK')" will be called three times, and the last one returns true.
mock.expect('alert', 'OK').times(3)
                          .andReturn(true);
Object bindTo(in Object aContext) alias: boundTo(), andBindTo(), andBoundTo()

Specifies this for the last expectation of the method/function/getter/setter. If the method/function/getter/setter is called with different this, then this throws an exception.

Object andReturn(in Object aReturnValue) alias: andReturns(), thenReturn(), thenReturns()

Specifies a returned value of the last expectation of the method/function/getter/setter. If you call this in a method-chain from expectThrows() or expectGetThrows(), this will be ignroed.

Object andThrow(in Object aException, [in String aMessage]) alias: andThrows(), andRaise(), andRaises(), thenThrow(), thenThrows(), thenRaise(), thenRaises()

Specifies an exception of the last expectation of the method/function/getter/setter. The given object itself will be thrown, but if you specify both a constructor function and a message, result of new aException(aMessage) will be thrown. If you call this in a method-chain from expect(), expectGet() or expectSet(), then the returned value of the expectation will be ignored.

Object andStub(in Function aOperation) alias: then()

Specifies an side-effect of the last expectation of the method/function/getter/setter. Arguments for the imitated method will be given to the specified function as is.

Compatibility to other mock libraries

Built-in mock features of UxU implements APIs compatible to some known libraries for JavaScript. If you are using one of those libraries, you'll be able to use UxU's mock easily.

MockObject.js

Available APIs compatible to MockObject.js are:

var mock = MockCreate(Window);
mock._expect('open', [uri, name, features], null);
mock._expectThrows('moveBy', [20, 20], 'permission denied');
...
mock._verify();

JSMock

Available APIs compatible to JSMock are:

var mockControl = new MockControl();
windowMock = mockControl.createMock(Window);

windowMock.expects().open(uri, name, features).andReturn(null);
windowMock.expects().moveBy(20, 20).andThrow('permission denied');
windowMock.expects().setTimeout(TypeOf.isA(Function), 1000)
             .andStub(function(aFunction, aTimeout) { aFunction(); });

...

mockControl.verify();

createMock(), resetMocks() and verifyMocks() are available even if JSMock.extend() is not called. (In testcases for UxU, JSMock.extend() does nothing.)

var windowMock, documentMock;

function setUp() {
  JSMock.extend(this); // this is not needed

  windowMock = createMock();
  windowMock.addMockMethod('alert');

  documentMock = createMock();
  documentMock.addMockMethod('open');
}

function tearDown() {
  verifyMocks(); // this is not needed
}

function testMyFeature() {
  windowMock.expects().alert('OK');
  documentMock.expects().open();
  ...
}

JsMockito

Available APIs compatible to JsMockito are:

var windowMock = mock(Window);
when(windowMock).open(uri, name, features).thenReturn(null);
when(windowMock).moveBy(20, 20).thenThrow('permission denied');
when(windowMock).setTimeout(anything(), 1000)
             .then(function(aFunction, aTimeout) { aFunction(); });
var addTab = mockFunction();
when(addTab)(uri).thenReturn(document.createElement('tab'));
when(addTab)('urn:isbn:xxxx').thenThrow('invalid URI');
when(addTab).call(gBrowser, uri) // bind "this" to the gBrowser
             .thenReturn(null);

Only pre-definding style mocks (like above) are available in UxU. Verifications using verify() after operations are not supported. Flexible matching based on JsHamcrest is also not available.

Using local HTTP servers as mock

You can use a local HTTP server started in UxU, as a mock. The server object (returned value of utils.setUpHttpServer() and utils.getHttpServer()) has some methods to define expectations. After you specify some expectations, it works just like a mock.

var server = setUpHttpServer(4445, baseURL);

server.expect('/index.html.en', '/index.html'); // 200 OK
// same to server.expect('/index.html.en', 200, '/index.html');

server.expect(/^\/subdir\/.*/, '/index.html'); // regular expression is available
server.expect(/([^\/\.]+)\.jpg/, '/images/$1.jpg'); // like RewriteRule

server.expect('/users', 301, '/accounts'); // redirect

server.expectThrows('/deleted', 404);

gBrowser.loadURI('http://localhost:4445/index.html.en'); // => "/index.html" 200 OK

expect() and expectThrows() receive an expected request (string or regular expression) as the first argument, and rest are details of response on the time. If you specify a file (nsIFile) instead of path string, then the server will return the contents of the file. The schemer and the host of the response URI will be ignored if the response is not a redirect (3XX status).

If the server has any expectation, "500 Internal Server Error" will be returned for any unexpected access. All of internal errors from unexpected accesses are reported after the server is stopped by utils.tearDownHttpServer().

A hash is available to define expectation's response. By the delay property, you can specify a delay for the response. This will help you to test codes working with slow (narrow) network.

server.expect('/index.html.en', { path : '/index.html', delay : 3000 });
// same to server.expect('/index.html.en', { status : 200, path : '/index.html', delay : 3000 });

server.expect('/moved', { status: 301, path : '/index.html', delay : 3000 }); // redirection with delay

Some APIs compatible to other mock libraries are available for HTTP server mocks.

// JSMock style
server.expects()('(.*)\.en').andReturn(302, '$1')

// JsMockito style
when(server)('/deleted').thenThrow(404);