-
Notifications
You must be signed in to change notification settings - Fork 28
Approach to Mocks
Home -> Developer Guide -> Implementation Notes ->
As of January 2021, support for mocks has not been implemented in Cobol Check. Aiming for release 0.2.0 for basic mock support. We intend to redesign mock support based on lessons learned from the proof of concept project, cobol-unit-test.
In the proof of concept project, some mocks represented resources and others represented behavior, such as reading a file or calling a subprogram.
In Cobol Check, a mock represents (or will represent) an external resource and the user can specify one or more behaviors for that resource. By "external" we mean external to the test case at hand. Usually this will also be external to the program under test (e.g., a batch file), but it may be external to the compilation unit (e.g., a statically-linked subprogram) or to the paragraph under test (e.g., another paragraph in the same program).
So a Mock definition could be structured something like this:
Mock [resource-type] [resource-identifier]
On [action]
[optional desired behavior - Cobol statements]
End-Mock
An example could be:
Mock Dataset "MyDatasetName"
On READ RIDFLD GTEQ
Move "Fake record key" to WS-KEY-AREA
Move "Fake record 1 data" to WS-RECORD-AREA
On READNEXT
Move ENDFILE to EIBRESP
End-Mock
The proof-of-concept considered the Mock to represent EXEC CICS READ DATASET; the propossed design change is that Cobol Check will consider the Mock to represent the dataset, not the read operation as such. Operations such as read can be specified as behaviors of the mock.
The user should be able to specify only the minimum attributes necessary to identify the mocked resource. In the example above, the program under test might specify several more parameters on the EXEC CICS READ DATASET statement than the ones mentioned in the Mock definition. We want Cobol Check to match just the parameters mentioned in the Mock definition with the code in the program under test.
The example above would match EXEC READ DATASET "MyDatasetName" that specifies GTEQ for any value given in the RIDFLD parameter. If the user wanted to be more specific, they could include more parameters in the Mock definition:
Mock Dataset "MyDatasetName"
On READ RIDFLD 'AB1' GTEQ KEYLENGTH 3
Move "Fake record key" to WS-KEY-AREA
Move "Fake record 1 data" to WS-RECORD-AREA
On READNEXT
Move ENDFILE to EIBRESP
End-Mock
In this example, we have added a RIDFLD value and the KEYLENGTH parameter, with a value. The Mock behavior for ON READ will be triggered only when the program under test calls EXEC CICS READ DATASET "MyDatasetName" RIDFLD(WS-KEY-AREA) GTEQ KEYLENGTH(WS-KEYLENGTH), and WS-KEY-AREA contains 'AB1', and WS-KEYKENGTH contains 3.
The previous example implies that Cobol Check will look at the values of WS-KEY-AREA and WS-KEYLENTH when matching a Mock definition with the EXEC CICS statements in the program under test. The proof-of-concept was only able to deal with literal values.
An early adopter of the proof-of-concept, Alexander Kotschenreuther, has suggested improving the scoping of mocks in Cobol Check. The proof-of-concept pays no attention to this; once a Mock has been defined, it continues to exist until the end of the test run.
It makes sense for a Mock to "live" for the duration of a single TestCase. Therefore, we want Cobol Check to delete Mocks that are defined in a TestCase at the end of that TestCase.
In addition, we plan to add Before-Each and Before-All functionality to Cobol Check. This is relevant to the scoping question. For mocks defined in a Before-Each block, the scope is a single TestCase; it is refreshed or recreated for each TestCase. For mocks defined in a Before-All block, the scope is all TestCases that are defined in the TestSuite (not the single test suite file, but the concatenated test suite files for the particular run).
For example:
TestSuite "something"
Before-All
Mock File "File1"
. . .
End-Mock
End-Before
Before-Each
Mock File "File2"
. . .
End-Mock
End-Before
* Mock File "File1" applies
* Mock File "File2" applies
TestCase "first test case"
Move "something" to WS-SOMEWHERE
Perform Paragraph-100
Expect WS-THING to be "something"
* Mock File "File1" applies
* Mock File "File2" applies
* Mock Subprogram "blah" applies
TestCase "second test case"
Mock Subprogram "blah"
. . .
End-Mock
Move "something else" to WS-SOMEWHERE
Perform Paragraph-200
Expect WS-OTHER-THING to be "something else"
All references to external resources will be stubbed. This is not left as an option for the user. We explicitly and deliberately want to discourage external dependencies in unit test cases, and we want the tests to be runnable off-platform, outside the usual mainframe execution environment, where many such external dependencies will not be available and possibly cannot be simulated. Mock behavior can be defined for the stubbed resources to support the needs of test cases.
We expect this approach to be easier for users and less error-prone than the previous approach.
Here are some examples of how users might code Mocks in Cobol Check (subject to change as development continues).
Hypothetical scenario: A batch update of taxpayer information for a government agency that processes tax returns. We want to check that the program populates the correct "error code" values (as defined for that application) when the inbound sequential update file is not found, and another case when an input record doesn't have a postal code for the taxpayer's street address.
When Cobol Check reaches the point in the program where it opens the file (somewhere inside paragraph 1100-open-files), instead of actually trying to open the file the generated test program will set the value "35" in the file-status item defined for Taxpayer-Update-File. The token "file-not-found" is syntactic sugar for the Cobol file status code 35, which indicates the file was not found.
When Cobol Check reaches the point in the program where it reads the file (somewhere inside paragraph 1500-validate-in-rec), instead of actually reading the file the generated test program will move predefined test values into various fields in the input record area, including moving spaces to the postal code field, which we think will trigger the desired error-handling behavior.
TestCase "It handles file-not-found"
mock Taxpayer-Update-File
on open file-status is file-not-found
end-mock
perform 1100-open-files
expect w-error-code to be "TUFNOTFND"
TestCase "It handles missing postal code"
mock Taxpayer-Update-File
on read
move "testID123" to tp-in-taxpayer-id
move "55 main st." to tp-in-taxpayer-addr1
move "bakersfield, ca" to tp-in-taxpayer-addr2
move spaces to tp-in-taxpayer-postcode
end-mock
perform 1500-validate-in-rec
expect ws-error-code to be "TUFNOPOST"
Hypothetical scenario: A CICS application functions as a TCP/IP server. When a message arrives on the input port, CICS populates an item in a CICS Temp Storage Queue. A CICS task (the COBOL program we test in this example) performs a blocking read on the queue, and CICS dispatches the task whenever an item becomes available to read from the queue. The program processes items from the queue in a loop until the queue is once again empty, and the task is suspended.
For each request item, the program looks at the first 4 bytes to determine what sort of request it is, and starts another CICS transaction to process the request and send the response to the client. We want to write unit test cases to check whether the program chooses the correct CICS transaction to initiate based on particular values it finds in the first 4 bytes of the request.
Maybe the program has CICS commands like these:
. . .
EXEC CICS READQ TS
QUEUE("TCPREQUEST")
SET(ADDRESS OF REQUEST-DATA)
LENGTH(256)
NEXT
. . .
EXEC CICS START
TRANSID(TRANS-TO-START)
FROM(REQUEST-DATA)
LENGTH(LENGTH OF REQUEST-DATA)
REQID("TCPWORKER")
. . .
We might mock the Temp Storage Queue as follows. This assumes the READQ TS command is somewhere inside paragraph GET-NEXT-REQUEST, and we only need to check that the correct value has been set in Data Division item TRANS-TO-START. These test cases don't touch the EXEC CICS START command, so there's no need to mock it here.
TestCase "It chooses transaction RTB4 to handle message type "X209"
Mock QUEUE "TCPREQUEST"
on READQ
move "X209" to REQUEST-DATA(1:4)
End-Mock
perform GET-NEXT-REQUEST
Expect TRANS-TO-START to be "RTB4"
TestCase "It chooses transaction RTB5 to handle message type "X211"
Mock QUEUE "TCPREQUEST"
on READQ
move "X211" to REQUEST-DATA(1:4)
End-Mock
perform GET-NEXT-REQUEST
Expect TRANS-TO-START to be "RTB5"
Some programs contain multiple references to an external resource in the same paragraph. Consider the following hypothetical code snippet:
PROCESS-THE-FILE.
EXEC CICS STARTBR
FILE(WS-FILENAME)
RIDFLD(WS-PRIMARY-KEY)
KEYLENGTH(LENGTH OF WS-PRIMARY-KEY)
NOSUSPEND
GTEQ
END-EXEC
PERFORM WITH TEST BEFORE
VARYING TABLE-IX
FROM 1 BY 1
UNTIL TABLE-IX > TABLE-MAX
EXEC CICS READNEXT
FILE(WS-FILENEXT)
RIDFLD(WS-PRIMARY-KEY)
KEYLENGTH(LENGTH OF WS-PRIMARY-KEY)
INTO(WS-RECORD-AREA)
NOSUSPEND
END-EXEC
IF EIBRESP = ZERO
PERFORM PROCESS-RECORD
ELSE
PERFORM BAD-RECORD
END-IF
END-PERFORM
EXEC CICS ENDBR
FILE(WS-FILENAME)
NOSUSPEND
END-EXEC
.
Let's say we want to check the program's behavior in the event the STARTBR and READNEXT commands work, but there is an I/O error on the ENDBR command. We need to mock three behaviors on the file named in WS-FILENAME as well as the paragraphs PROCESS-RECORD and BAD-RECORD.
TestCase "It handles IOERR gracefully"
Move 1 to TABLE-MAX
Move "PLANKTON" to WS-FILENAME
Mock FILE WS-FILENAME
On STARTBR
EIBRESP is ZERO
On READNEXT
EIBRESP is ZERO
On ENDBR
EIBRESP is IOERR
End-Mock
Mock Paragraph PROCESS-RECORD
End-Mock
Mock Paragraph BAD-RECORD
End-Mock
Perform PROCESS-THE-FILE
Verify BAD-RECORD happened once
Expect WS-ERROR-MESSAGE to be "Unexpected error reading file PLANKTON"