-
Notifications
You must be signed in to change notification settings - Fork 29
Mocks and stubs: mocks
Mocks are expected fakes. Your code will invoke the method, as if it is the real method; but the difference is that Rooibos will track the invoction of mocks, and if the method was not invoked in the manner you expected (i.e. with the expected parameters and the expected number of invocations) then a unit test failure will result.
We create mocks by using the methods:
- expectCalled - creates a stub for an invocation; and fails the test if it was called with the wrong params, or the wrong amount of times
- expectNotCalled - creates a stub for an invocation; and fails the test if it was called
expectCalled is used as follows:
function expectCalled(invocation, returnValue)
-
invocation is a method invocation, or function pointer on a stubbable object
- it can be a real invocation, e.g.
m.expectCalled(screen.loader.loadData(item1, item2))
in which case it will expect thatloadData
is called onscreen.loader
with those exact args
- it can be a real invocation, e.g.
-
note you can use callfunc @. operator too, when you expect a function to be called on an node:
m.expectCalled(screen.loader@.loadData(item1, item2))
-
it can be a function pointer, e.g.
m.expectCalled(screen.loader.loadData)
in which caseloadData
is called onscreen.loader
; but not care what aregs were used -
return value is the value returned - so
m.expectCalled(screen.loader.loadData(item1, item2), "data")
, will expect that function to be called, and when it is invoked, return the value"data"
Note: expectCalled and expectNotCalled are bsc-plugin magic. you can observes from the transpiled code that the bsc-plugin for rooibos is automatically transpiling these invocations to internal methods, which ultimate call the legacy expectOnce and expectNone methods.
We favor the new functions, in every case, except for the very rare exceptions, described below, which you are unlikely to face.
As a convenience, Rooibos will automatically assert any mocks for you when your test finishes executing before all other asserts.
If you need to assert them manually, you can do this:
m.assertMocks()
When a mock fails, Rooibos will report to you what caused the failure. The possible reasons are:
- The method was invoked by your code, with the wrong parameters
- The method was invoked too many times by your code
- The method was never invoked by your code; but it should've been
IMPORTANT: this mechansim does not yet support the new expectCalled
function pointer semantics - if you want to do this you have to use the old syntax.
This will be updated in due course.
If you wish you, you can store a reference to a mock or a stub, and later check the invocation values.
All mock and stub methods return a reference to the Fake that it wired into your code. e.g.
executeMock = m.expectOnce(detailsVM,"ExecuteNetRequest", invalid, returnJson)
detailsVM.LoadDetails()
? exceuteMock.invokedArgs
? excecuteMock.invocations
You can save yourself a lot of time, and really think about and kick the tyres of your code, by defining the arguments you expect a function to be invoked with. This is done by passing in an array of the expected invocation arguments via the expectedArgs param. You may also really not care about the args, in which case you can set that value to invalid
and the call to m.assertMocks()
will skip checking the invoked args.
- Up to 15 arguments are supported on mocked methods
- You can have up to 24 mocks.
You may wish to call the same method various times, and return different values, or check different arguments were invoked
In this case, we use overloaded expectCalled
calls, as per the following example:
m.expectCalled(obj.mockMethod(arg1), result1)
m.expectCalled(obj.mockMethod(arg2), result2)
m.expectCalled(obj.mockMethod(arg3), result3)
This will now set the framework to expect 3 calls to mockMethod, the first with value of arg1, returning result1; the second with value of arg2, returning result2; and the last with value of arg3, returning result3
If you specify the invoked args, then by default, rooibos will check that the invoked args match the arguments you specify. You can expect any value for an arg, or use the special m.invalidValue
to indicate you expect the argument to be invalid. This is the default value.
So for example, if one had the following mock
m.expectCalled(myObj.myMethod("a", "b"), true)
and myMethod
was invoked with ["a", "b", "c"]
, this would be a mock failure, because the 3rd argument was expected to be invalid, by default.
In that case, the following mock definition would satisfy the assertion:
m.expectCalled(myObj.myMethod("a", "b", "c"), true)
If you only care about some arguments, then you can use m.ignoreValue
to instruct rooibos to not assert equality for the arguments you've ignored.
In the above example, the assertion will be satisfied with a mock confiugred thusly:
m.expectCalled(myObj.myMethod("a", "b", m.ignoreValue), true)
This will pass when myMethod
is invoked with args: ["a", "b", "c"]
, as would the following mock definition:
m.expectCalled(myObj.myMethod(m.ignoreValue, "b", "c"), true)
In addition to the other basic matchers described above, rooibos provides a set of pre-defined any matchers, and a mechansim for you to provide custom matchers for any mock argument.
Rooibos has matchers that will pass, if the values are of the specified anyXXXMatcher type. For convenience, the matchers are stored on m.anyXXXMatcher
on your test suite. The following are available.
- anyBoolMatcher
- anyNumberMatcher
- anyStringMatcher
- anyArrayMatcher
- anyAAMatcher
- anyNodeMatcher
Simply specify the matcher in your mock definition, as follows:
m.expectCalled(myObj.myMethod(m.anyStringMatcher, m.anyBoolMatcher), true)
In this case, the mock be satisfied if it was called with 2 params, the first one a string, the second a bool, both of any value.
It is simple to use a custom matcher to assert your mock arguments. Simply:
- implement a function that takes one argument, and returns true or false
- ensure it's in scope
- return your function (either by pointer, or inline), in an aa as follow: `{"matcher": yourMatcherFunction}
For example, using a function pointer (.brs):
m.expectCalled(m.myClass.doWork({"matcher": Rooibos_Matcher_anyArray}), returnValue)
For example, using a function pointer (bs):
m.expectCalled(m.myClass.doWork({"matcher": Rooibos.Matcher.anyArray}), returnValue)
And inline:
m.expectCalled(m.myClass.doWork({ "matcher": function(value)
return value = true
end function }), returnValue)
IMPORTANT: this mechansim does not yet support the new expectCalled
function pointer semantics - if you want to do this you have to use the old syntax.
This will be updated in due course.
Simply set your return value to a non-invalid value, to specify a return value.
You can also specify multiple return values, if you expect your mock to be executed multiple times, and would like to use different values. In that case, return a pobo, with the special key : multiResult
. The value will be returned for each invocation - if the mock is invoked more than the number of return values, the last one is reutrned. e.g.
m.expect(obj, "mockMethod", 5, invalid, {"multiResult": ["one", 2, invalid, "last"]}, true)
m.assertEqual(obj.mockMethod(), "one")
m.assertEqual(obj.mockMethod(), 2)
m.assertEqual(obj.mockMethod(), invalid)
m.assertEqual(obj.mockMethod(), "last")
m.assertEqual(obj.mockMethod(), "last")
rooibos allows you to mock functions that do not exist, so you can easily create any {}
dictionary and start prentending it's your code.
videoService = {}
m.expectCalled(videoService.getVideos(), someJson)
Note, you can also opt to disable the error at the whole test suite level; by setting m.allowNonExistingMethods = true
in your test suite code.
- mocks DO NOT work with globally scoped methods (i.e. subs and functions which are not assigned to an associative array).
- E.g. if you have a method, which is not accessed via
m.SomeMethod
, orsomeObject.SomeMethod
, then you cannot mock it. - This is a long term limitation, with no plans to remedy it. Class based development is way more fun anyhow, give it a go :)
- E.g. if you have a method, which is not accessed via