Skip to content

Mocking

Alex Clay edited this page Jun 12, 2020 · 17 revisions

Overview

OmnisTAP has an integrated mocking tool, designed make creating Mock Objects easy. It supports:

  • Expecting specific parameters
  • Returning specific values
  • Different expectations and returns for multiple calls to a method
  • Expecting a specific number of calls to a method
  • Mocking objects, table classes, and windows
  • Setting field reference return

Example

Consider this example object. $greet returns a greeting message to a person based on their relationship to you.

Sample ogGreeter object

ogGreeter {
	$greet(pcName,pcRelationship) {
		If pcRelationship="Friend"&left(pcName,4)="Maid"
			Quit method $cinst.$_greetFriend(pcName,"Formal")
		Else If pcRelationship="Friend"
			Quit method $cinst.$_greetFriend(pcName,"Normal")
		Else If pcRelationship="Enemy"
			Quit method $cinst.$_greetEnemy(pcName)
		End If
 
		Quit method $cinst.$_nod()
	}
 
	$_greetFriend(pcName,pcDisposition) {...}
	$_greetEnemy(pcName) {...}
	$_nod() {...}
}

A unit test using mocking would look like this:

OmnisTAP for ogGreeter using mocking

Do $cinst.$mock($objects.ogGreeter) Returns lorGreeter

Do $cinst.$when("greeting different people")
Do lorGreeter.$mock("$_greetFriend").$expect("Maid Marian","Formal").$return_char("M'lady").$because("we greet maids formally")
Do lorGreeter.$mock("$_greetFriend").$expect("Robin Hood","Normal").$return_char("Hail").$because("we greet friends informally")
Do lorGreeter.$mock("$_greetEnemy").$expect("Prince John").$return_char("En garde").$because("enemies will be challenged to a duel")
Do lorGreeter.$greet("Maid Marian","Friend") Returns lcGreeting
Do ioTAP.$is_char(lcGreeting,"M'lady","We greet a maid formally")

Do lorGreeter.$greet("Robin Hood","Friend") Returns lcGreeting
Do ioTAP.$is_char(lcGreeting,"Hail","We greet a non-maid normally")

Do lorGreeter.$greet("Prince John","Enemy") Returns lcGreeting
Do ioTAP.$is_char(lcGreeting,"En garde","We greet an enemy as an enemy")

Do lorGreeter.$assert()

Do $cinst.$when("greeting people we don't know")
Do lorGreeter.$reset()
Do lorGreeter.$mock("$_nod").$call(1).$return_char("'Sup.").$because("we don't know them")
Do lorGreeter.$mock("$_nod").$call(2).$return_char("Hey.").$because("we don't know them, but like to vary our greetings")

Do lorGreeter.$greet("A random peasant","NPC") Returns lcGreeting
Do ioTAP.$is_char(lcGreeting,"'Sup.","We greet the first unknown relationship properly")

Do lorGreeter.$greet("A barkeep","NPC") Returns lcGreeting
Do ioTAP.$is_char(lcGreeting,"Hey.","We greet the second unknown relationship properly")

Do lorGreeter.$assert()

Test output looks like this:

TAP output for the sample tests

1..13
# When greeting different people
ok 1 We greet a maid formally
ok 2 We greet a non-maid normally
ok 3 We greet an enemy as an enemy
ok 4 We call $_greetFriend twice because we greet friends informally
ok 5 pcName for call 1 to $_greetFriend is correct
ok 6 pcDisposition for call 1 to $_greetFriend is correct
ok 7 pcName for call 2 to $_greetFriend is correct
ok 8 pcDisposition for call 2 to $_greetFriend is correct
ok 9 We call $_greetEnemy once because enemies will be challenged to a duel
ok 10 pcName for call 1 to $_greetEnemy is correct
# When greeting people we don't know
ok 11 We greet the first unknown relationship properly
ok 12 We greet the second unknown relationship properly
ok 13 We call $_nod twice because we don't know them, but like to vary our greetings

Creating a mock object

Use $cinst.$mock() on a subclass of ogTAPSuper to create an instantiate a mock object.

$cinst.$mock([item reference to a class to mock]) Returns [instance of the mocked class]

If mocking an object, you will receive an object reference back from $mock(). If mocking a window, report, menu, toolbar, etc. you will receive an item reference to the window. In both instances, $construct() and $destruct() will be automatically overridden.

Mocking Table Classes

When mocking a table class, use a slightly different syntax.

$cinst.$mock([item reference to a table class to mock],[field reference to a row or list to hold the mock])

This syntax is necessary to avoid copying a mocked instance to a new instance. OmnisTAP can instantiate the mocked class directly into the passed variable.

The mocked instance will be automatically destroyed at the end of the test.

Example

Do $cinst.$mock($tables.tgGreeter,lrGreeter)

Getting a mocked object instead of an object reference

By default mocking an object will give you an object reference to an instance of the mocked object. If, however, you need a plain object you can get one by calling $new([object]) on the object ref.

Do $cinst.$mock($objects.ogGreeter).$new(loGreeter)

Use this object exactly as you would the object instance. This is particularly handy for mocking utility objects.

Note that you need to pass the object variable by reference to $new(). This allows the mocker to get a persistent reference to the object since we can avoid copying it to a returned variable.

Example

Do $cinst.$mock($objects.ogTextUtils).$new() Returns uoText

Mocking a task variable

When needing to mock out a task variable such as toDatabaseServers, these are global variables shared amongst classes. So even if you need to access it from a mocked object/item reference, it would still need to be done in the current instance. Storing a variable automatically replaces it with a mock. You can also store primitives like booleans, numbers, or strings.

Example

Do $cinst.$store(toDatabaseServers) 
Do toDatabaseServers.$mock("Your method").$expect/return([Your variables])

Mocking a method

The mocked object has a $mock() method that mock a specific method.

Do [mocked instance].$mock("[method name]")

Note: Omit the parentheses when mocking a method name. Normally the custom is to always add parentheses to a method, but the mocker needs the name of the method, not an invocation of it.

Example

Do lorGreeter.$mock("$_greetFriend")

If you attempt mock a method that does not exist, you will produce a test failure. Check the test message for details on what method and class you tried to mock that OmnisTAP couldn't find.

Mocking a method sets an expectation that it will not be called. You can customize the mocked method's behavior by chaining calls to $mock().

Mocking a method on a window object

When mocking a window, you can mock a method that resides on an object. This can be a local method like $click(), or if the object is a sub window/smart field, it can be a method on the class represented by the smart field.

Do [mocked instance].$mock("$objs.[objectname].[method name]")

Example

Do lirWindows.$mock("$objs.efDate.$redraw").$callcount(1).$because("we need to redraw the date after updating it")

It may be necessary to add the method dynamically before you can mock it. This is common with sub-windows that you are dynamically calculating their $classname in the $construct. If the method requires parameters this is a shortcut to have those be the same as the method you are needing to mock.

; First dynamically add the method to your mocked class
Do iirLoanPayment_1LoanPayment.$objs.ppTop.$objs.ppChurch.$objs.swChurch.$methods.$add("$setFilter") Returns lirMockMethod
; Next copy the method to be like the original method you are mocking
Calculate lirMockMethod as $windows.we_Church.$methods.//$setFilter//

If you are assigning the $classname in the construct you will need to dynamically add any methods you wish to mock for your test. This technique will allow you to keep your test a unit test if your sub window/smart field would normally execute SQL when instantiated.

Do iirChurch.$objs.swChurch.$methods.$add("$getAddressRow")

Do iirChurch.$mock("$objs.swChurch.$getAddressRow").$return_list(lrChurch)

 

Expecting parameters

A mocked method can expect parameters to be passed into it.

Do [mocked instance].$mock("[method name]").$expect([param 1],[param 2],...,[param N])

You must call $expect() immediately after calling $mock() on the method you want to expect. $mock() will setup the $expect() method to receive the same parameters as the method being mocked.

This expectation will be verified by calling $assert() after testing the method that calls the mocked method. See below for information on asserting the mock method.

You can call $expect() multiple times for methods that will be invoked more than once during a test. By default the mock will expect calls in the order that you call $expect(), although this can be overridden using $call(). See below for more information.

Example

Do lorGreeter.$mock("$_greetFriend").$expect("Maid Marian","Formal")

Returning data

You can set the return from a mocked method. Due to Omnis' lack of a declared return type, you must use a return method specific to the type of data you want to return.

Do [mocked instance].$mock("[method name]").$return[_optional type]()

Return Types

  • $return([boolean])
  • $return_binary([binary])
  • $return_boolean([boolean])
  • $return_char([character])
  • $return_datetime([date/time])
  • $return_itemref([item reference])
  • $return_list([list])
  • $return_number([number or integer, any precision])
  • $return_objectref([object reference])
  • $return_row([row])

Like $expect(), you must call $return() immediately after calling $mock() on a method. This lets the mock object know for which method you want the return.

You can chain a $return() to an $expect() to return a specific value when a specific set of parameters is passed in. Or, you can simple call $return() directly on the mocked method to ignore parameters and just set the return value. Subsequent calls to $return() will override the return value for the current call, or you can use $call() to specific a call number for the $return.

Example

Do lorGreeter.$mock("$_nod").$return_char("'Sup.")

Field references

When using $expect() on a method that takes field references, pass a variable in the test code for the reference. If this variable is populated, OmnisTAP will assert the value passed to this reference will match the value passed to $expect().

You can set the return value for a field reference. Use $expect() on the mocked method and pass in the value for the field reference you'd like the mock to return.

Do [mocked instance].$mock("[method name]").$expect(lcFieldReference1Value,[parameter 1 expectation],llFieldReference2Value,...,l[Type]FieldReference[N]Value)

You can mix field references and parameters as the method dictates.

You can specify the mock should return a different value by accessing it through the $parameters() method.

Do [mocked instance].$mock("[method name]").$parameter(pnFieldReferenceParameterNumber).$return[_type]([return value])

You can use any of the standard $return() variants for a field reference.

Do llCandidateLines.$add()
Do llCandidateLines.$add()
Do llCandidateLines.$add()
  
Calculate llMatchedLines as llCandidateLines
Calculate llMatchedLines.2.$selected as kTrue
Calculate llMatchedLines.3.$selected as kTrue
  
Do iorMock.$mock("$_selectMatchingLines").$expect(llCandidateLines).$return_number(2)
Do iorMock.$mock("$_selectMatchingLines").$parameter(1).$return_list(llMatchedLines)
... run tests

Call count

Each time you set an expectation or return for a mocked method, you add an expectation for how many times that method will be called. If, however, you only care about this call count and don't want to expect parameters or set a return value, you can use $callcount().

Do [mocked instance].$mock("[method name]").$callcount([number of expected calls])

Using $callcount(0) is an explicit way to say you don't expect a method to be called.

Example

Do lorGreeter.$mock("$goodbye").$callcount(0)

Specific calls

For $expect() and $return() you can specify the call to the method that you want to mock by using $call().

Do [mocked instance].$mock("[method name]").$call([method call number]).[$expect()|$return()]

Example

Do lorGreeter.$mock("$_nod").$call(2).$return_char("Hey.")

Copying calls

You can copy an expected call and its return to another call. This provides a convenient way to mock repeated calls to a method, or to copy all expectations but vary the return on one call.

Do [mocked instance].$mock("[method name]").$call([destination method call number]).$copy([source method call number])

Example

Do lorGreeter.$mock("$_nod").$expect("person").$return_char("Hey.")
Do lorGreeter.$mock("$_nod").$call(2).$copy(1).$return_char("Sup.")

Twice and times

OmnisTAP provides two convenience methods for copying mocks. If you expect a mocked method to be called twice, add $twice() to the end of the mock.

Example

Do lorGreeter.$mock("$_nod").$expect("person").$return_char("Hey.").$twice()

This is equivalent to writing:

Do lorGreeter.$mock("$_nod").$expect("person").$return_char("Hey.")
Do lorGreeter.$mock("$_nod").$call(2).$copy(1)

If you need to copy a mock more than twice, use the $times([calls]) method. Pass the total number of times the mock should be expected:

Example

Do lorGreeter.$mock("$_nod").$expect("friend").$return_char("Hi!").$times(3)

This is equivalent to writing:

Do lorGreeter.$mock("$_nod").$expect("friend").$return_char("Hi!").$times(3)
Do lorGreeter.$mock("$_nod").$call(2).$copy(1)
Do lorGreeter.$mock("$_nod").$call(3).$copy(1)

Both $twice() and $times([calls]) will copy the current call's expectation and return value. So this code:

Do lorGreeter.$mock("$_nod").$expect("person").$return_char("Hey.")
Do lorGreeter.$mock("$_nod").$call(2).$copy(1).$return_char("Sup.").$twice()

Is the same as this code:

Do lorGreeter.$mock("$_nod").$expect("person").$return_char("Hey.")
Do lorGreeter.$mock("$_nod").$call(2).$copy(1).$return_char("Sup.")
Do lorGreeter.$mock("$_nod").$call(3).$copy(2)

Always returning the same value

By default, OmnisTAP assumes every call to a mocked method will be expected in by your test code. This ensures the method is called no fewer and no more times than expected, thereby asserting correct behavior.

However, you may not care how many times a method is called, but instead need to mock the method to always return fixture data. This is especially valuable when testing looping or branching code that calls another method to get the application's state.

In this example, OmnisTAP provides the $always() feature to return the same data no matter how many times the code is called.

Do [mocked instance].$mock("[method name"]).$return[_datatype]([return value]).$always()

When $always() is used, asserting mocks does not evaluate expected calls to that method.

Example

Do $cinst.$when("Read-only")
Do iorMock.$mock("$isReadOnly").$return(kTrue).$always()
#Add tests for read-only conditions

Explaining why you expect calls

Mocking makes it clear what you expect the code to do. But you can also note why it should have that behavior using $because().

Do [mocked instance].$mock("[method name"]).[$expect()|$callcount()].$because("reason")

Example

Do lorGreeter.$mock("$_nod").$callcount(1).$because("we nod to people we don't know")

Keep the first word of the reason lowercase to let the test messages be more readable.

Adding context

While not technically part of the mocking utility, OmnisTAP lets you group related tests with a $when() call.

Do $cinst.$when("context or scenario for the following tests")

Example

Do $cinst.$when("it's any given Sunday")
Do lorTV.$mock("$lineup").$expect("football").$because("it's the American way")
Do ioTAP.$is_char(lorTV.$getSchedule(),"next up: football","We display football next on the schedule")
Do $cinst.$assertMocks()

Like $because(), use lowercase to start the context for $when().

Asserting the mock was called properly

OmnisTAP will track parameters that are passed to a mocked method and the number of times the method was called. After calling your test method, you can assert the expected behavior of the mocked methods using $assert().

Do [mocked instance].$assert()

If you have multiple mocked instances that you want to assert all at once, the TAP superclass offers a convenience method to do just that.

Do $cinst.$assertMocks()

Calling $assert() will test expected parameters and call counts, then output the tests inline with your other assertions.

After asserting the mock was used properly, the mock will reset its expectations allowing you to return expected parameters and calls in subsequent tests.

Example

Do lorGreeter.$mock("$_isFriendly").$return(kFalse)
Do lorGreeter.$mock("$_greetFriendly).$callcount(0)
 
Do lorGreeter.$greet("Villain")
Do lorGreeter.$assert()
 
Do lorGreeter.$mock("$_isFriendly").$return(kTrue)
Do lorGreeter.$mock("$_greetFriendly).$callcount(1)

Do lorGreeter.$greet("Ally")
Do lorGreeter.$assert()

Resetting the mock

Subsequent calls to the mocked object will return the same mocked values, and the same expectations will be asserted. You can reset a single method or an entire mock using $reset().

Do [mocked instance].$reset()
Do [mocked instance].$mock("[method name]").$reset()

Example

Do $cinst.$when("it's any given Sunday")
Do lorTV.$mock("$lineup").$expect("football").$because("it's the American way")
Do ioTAP.$is_char(lorTV.$getSchedule("Sunday"),"next up: football","We display football next on the schedule")
Do $cinst.$assertMocks()
 
Do $cinst.$when("the tv is off")
Do lorTV.$reset()
Do ioTAP.$isclear(lorTV.$getSchedule("offline"),"The schedule is clear")
Do $cinst.$assertMocks()

Calling private methods

While not technically mocking, OmnisTAP provides a convenience function to create a public forwarding method for calling a private method using $insertForwardingMethod on a test class:

Do $cinst.$insertForwardingMethod([instance with the private method],[private method name])

This produces a new public method with the same name and parameters as the private method.

Example

Do $objects.oTree.$newref() Returns lorTreeRef
Do $cinst.$insertForwardingMethod(lorTreeRef,"isTree")

Do lorTreeRef.$isTree("spruce", "blue") Return lbActualValue
; Do some tests on the lbActualValue