Monday, April 22, 2013

mmockito: How stubbing works

The previous post discussed some MATLAB-imposed limitations on the syntax we can offer. In the end, we've opted for a mock.when.aFunc(args).thenReturn(res) syntax. In the meanwhile, stubbing support has slowly matured and is essentially complete now, so it might be a good idea to showcase some of the functionality supported. While the syntax and the features provided were mostly inspired by (Java) mockito and python-mockito, we've diverged when we felt it made more sense for MATLAB code.

Basic usage


>> m = Mock();
>> m.when.aFunc(1,2).thenReturn(42);
>> m.aFunc(1,2)
ans =
    42
>> m.when.aFunc(1,'str').thenThrow(MException('a:b:c', 'exception!'));
>> m.aFunc(1,'str')
Error using Mock/subsref (line 57)
exception!


In the simplest use case, we can define an arbitrary function, provide the exact arguments it should take and the result it should give. "thenThrow" is offered as a convenience method to make code easier to read, the error would also be thrown if we just pass a MException object to "thenReturn". Also available is "thenPass", which is short for "thenReturn(true)".

Matchers


Because defining the exact arguments can be tedious, we've implemented Matchers. Matchers allow us to accept any argument satisfying a certain condition, instead of an exact value. Combining matchers and exact values is of course, also possible. For example, the Any matcher, if constructed with a class (either as a string or a meta.class object) only accept arguments of that class. If called with no arguments, the Any() matcher will accept anything at all.

>> m.when.a(Any('char')).thenReturn('ok');
>> m.when.a(Any(?double)).thenReturn('bad');
>> m.a('asdf')
ans =
ok
>> m.a(15)
ans =
bad
>> m.when.b(NumberBetween(1,5)).thenReturn(true);
>> m.b(3)
ans =
     1


It is very easy to define a custom matcher of arbitrary complexity, a couple of simple examples are included as a demonstration (and there might be a future blog post on this topic). A special case is the "ArgThat" matcher, which takes a "Constraint" in it's constructor (from the matlab.unittest.Constraints package). Since Constraints from the new unittest module serve basically the same purpose, they can be reused as Matchers. Our initial solution had us using Constraints directly, but they have a somewhat unfortunate naming scheme which doesn't "read" well. Nevertheless, with the "ArgThat" matcher users can reuse existing and custom Constraints, if needed.

>> import matlab.unittest.constraints.*;
>> m.when.a(ArgThat(HasNaN)).thenReturn('a Nan');
>> m.when.a(ArgThat(IsFinite)).thenReturn('no NaN');
>> m.a([5 6 14])
ans =
no NaN
>> m.a([1 2 NaN])
ans =
a Nan


Consecutive calls


Sometimes, we want to only stub a certain amount of calls. To do this, we can just chain the then* statements in any order. A "times" keyword is also offered for ease of use. Usually, the last stubbed return value will remain valid, but if we end the chain with the "times" keyword, it also will only apply the given number of times.

>> m.when.stub(1).thenReturn(1).times(2).thenReturn(2).thenReturn(3);
>> m.stub(1)
ans =
     1

>> m.stub(1)
ans =
     1

>> m.stub(1)
ans =
     2
>> m.stub(1)
ans =
     3
>> m.stub(1)
ans =
     3


It is important to note that the first stubbing is the one that is valid, and if it is setup for infinite stubbing it will not be possible to override it (short of creating a new mock). While in interactive use this might be a slight inconvenience, we get the added flexibility when making tests. For example, using Matchers, we can stub more complex behavior sanely.

>> m.when.stub(5).thenReturn('ok');
>> m.when.stub(Any(?double)).thenReturn('not implemented');
>> m.when.stub(Any()).thenReturn('bad!');
>> m.stub(5)
ans =
ok
>> m.stub(6)
ans =
not implemented
>> m.stub('asdf')
ans =
bad!


Strict vs. tolerant mocks; mocking real objects


By default, mocks are 'strict' -- methods which are not explicitly mocked will throw an error, the same as when calling a non-existent method. However, we can also create 'tolerant' mocks, which instead just return an empty list (MATLAB's equivalent of None or null in other languages).

It is also possible to pass a real object to the constructor of a Mock. In that case, if we are not stubbing a given method, we will pass it on to the object used in the constructor. Stubbed methods are always preferred, even if they shadow an existing method. Note: this feature isn't strictly complete as of right now and might change, but probably won't.


I hope that the presented features are compelling enough to interested some of you in using mmockito. If there's something more you'd want to see, don't hesitate to let me know. And, if you're interested in testing this code, I would be very glad for the feedback - please contact me! Coming up next, verification!

3 comments:

  1. I've been looking around and I don't see a publicly available version of your code. Am I missing something or is there any other way for me to get my hands on it?

    I'd love to have a mocking library like this. I do matlab development and I've been pushing for more tdd-style development methods on a few of my projects and it's miserable without mocks.

    ReplyDelete
    Replies
    1. Hi, thanks for your interest! There isn't a public version available currently, I wanted to publish it "the right way" and, of course, that meant it gave precedence to other projects. I still intend to make it open-source, most likely using Github. I'll try to contact you directly once I've done so, and there will of course be an announcement on the blog as well.

      Delete