Skip to content

Creating Behavioural Driven Scenario mocks from RESTful calls

Richard Tomlinson edited this page Feb 22, 2017 · 18 revisions

Scenarios

Sometimes, simplistic mocks are not sufficient and more complex mock behaviour is required. Creation of a scenario mock allows the definition of conditions on when to trigger the mock and the type of response required. The structure is such that different outputs from the mock can occur based on pipeline values and the sequence of data returned can be varied. This provides equivalent behaviour to that seen when using wm-jbehave unit testing.

Defining the inputs

Scenarios are created with a Behaviour Driven approach to their declaration. Within each exist configuration of the given service which will be intercepted and when the condition occurs then the mock returns data to the pipeline or performs other required actions. Its also possible for default behaviour to happen when there is no condition or no match occurs.

The XML structure is:

<scenario id="... scenario identifier...">
    <scope>
        ....scope...
    </scope>
    <given>
        ...service..
    </given>
    <when id="...when identifier..." condition="... a condition ...">
        <then>
            ...perform some task...  return... etc...
        </then>
    </when>
</scenario>

Notable about these elements:

  • scope defines who can see and is affected by the scenario
  • given indicates the services affected within the scope
  • when is a holder for the actions and optionally contains a condition of when to fire the actions. Note, when is always required to hold the then
  • then is an action, be it a setting of data in to the pipeline, pipeline capture, etc.
  • Each scenario has optionally a scope, always one given and a when, and one or more then actions

The schema for the structure is located in bddSchema.xsd

Structure of the XML scenario

XML structure for defining mocks

Registering and Unregistering Scenarios

  • To register a scenario, call your server with a POST to: /rest/org/wmaop/define/scenario e.g: http://localhost:5555/rest/org/wmaop/define/scenario and the body containing the scenario.
  • To unregister a single scenario, make a DELETE request with /rest/org/wmaop/define/scenario/{scenarioId} e.g. /rest/org/wmaop/define/scenario/mockjmssend

Scope of the interaction

Specifying the <scope> element allows the scenario to be restricted to a user, session or applicable globally.

To limit the scope to the current browser (Or tool) session, specify:

    <scope>
        <session/>
    </scope>

If the session is for any user or interaction use the global scope:

    <scope>
        <global/>
    </scope>

Where the scenario is only applicable to a particular user, specify the user scope with the associated username:

    <scope>
        <user username="Administrator"/>
    </scope>

When no scope is specified, the default is user scope for the current user Ie, the user which was used to authenticate when defining the scenario. Generally you shouldn't need to specify the scope as the test will run, set up and tear down within the user scope very quickly so as to make the scope irrelevant (Ie, you're not going to have the opportunity to interact with the generated mocks).

Specifying the service

In detail, the options for the given service are:

<service intercepted="...intercept point...">serviceName</service>

The intercepted attribute can be one of three values:

  • before - The interception occurs before the service. The service named is not replaced and is called after any of the when/then values execute. This allows for data to be inserted in the pipeline before a service is called (Eg to change the behaviour of the service) or some other action to occur, e.g. capture the pipeline
  • invoke - This will replace the named service with the functionality in the when/then. The service is not called.
  • after - The functionality in the when/return is called after the service. Any pipeline values the service has created are available. This can be used to change the values returned by a service or capture its output.

The serviceName should be the regular format, copy'n'pasted from Designer, e.g. the service wm.tn.delivery:getRegisteredService would be represented as a mock fired in place of the original with the declaration:

    <given>
        <service intercepted="invoke">wm.tn.delivery:getRegisteredService</service>
    </given>

When

One or more when elements can appear in the scenario to determine if the service is intercepted and what the resulting response should be. A then must always be surrounded by a when, even if there is no condition (Ie the when is just a holder and not a conditional to the then)

A when consists of two attributes describing an id and optionally a condition. The id provides descriptive detail, shown in the log when the condition is met and thereby giving easy traceability of what's happened when the scenario was executed. The condition must be a valid JEXL expression. See Pipeline expressions using JEXL for more details on how to describe and use expressions to check the pipeline and documents for values.

Conditional service interception

For a given, a when can be added along with the service to apply a condition to the interception. The service interception only applies when the condition evaluates as true. If the condition is false, the service isn't intercepted and the actual service (Or another interceptor if one has been added for this service) is executed instead.

    <given>
        <service intercepted="invoke">org.wmaop.foo:bar</service>
        <when id="triggerWhenTwoFoos" condition="foo == 2"/>
    </given>

In the example above the intercept occurs when foo == 2 and the subsequent when/then statements are executed. If foo doesn't exist or is another value nothing further is executed.

Conditional returns

After the given, a series of when entries can be declared. They are executed in order and each can have an optional condition attribute.

If no condition is required, i.e. always do something such as returning a fixed value from a mock, then the condition attribute is not required.

Not specifying the condition is also useful as an 'else' clause where a number of when elements describe various test conditions and if all do not succeed then some default behaviour is required.

    <given> ....etc... </given>
    <when id="input 1" condition="input == 1">
        <then>
            <return><![CDATA[...etc...]]></return>
        </then>
    </when>
    <when id="input 2" condition="input == 2">
        <then>
            <return><![CDATA[...etc...]]></return>
        </then>
    </when>
    <when id="default">
        <then>
            <return><![CDATA[...etc...]]></return>
        </then>
    </when>

Executing what you think you are executing...

This example shows that when the interception occurs there are three when statements with the first two based on conditions and the third acting as a default.

If no default is specified, nothing happens - Ie, the intercept occurs but no action is taken so nothing is output or set to the pipeline. This is particularly important when using intercepted="invoke" in the service because it can result in the real service not being executed (Ie, it's been intercepted) but there's no action taking its place. Be wary of this when wanting conditional interception.

The guidelines are:

  • I want to replace a service with a mock - Within the service use intercepted="invoke" and choose your outcome with a then/when
  • I want my mock to return something or use the real service depending on a condition - Add a when to the given with a condition, use intercepted="invoke" with the service and choose your outcome with a then/when
  • I want my mock to always return a value with conditions - Use multiple when after the given, each with a condition apart from the last one which has an id only. The last entry without a condition is the default behaviour. Combine from the above two points if you want conditionally execute the set of when statements or have them always executed.
  • I only have one action to perform always - Specify a single when with no condition and whatever's required with the then (See below)

Then making something happen

The then section of the scenario specifies what should happen. Only one element can exist per then but multiple then elements can exist within a when to allow different things to happen, e.g. capture the pipeline and return some new content.

Inserting data in the pipeline

Using the return element, IData can be inserted into the pipeline (if before/after specified as the intercepted element) or be used to replace the content of the service (If intercepted as 'invoke'). Note that IData must be encapsulated with a CDATA declaration. Any existing values with the same name in the pipeline will be overwritten.

<then>
    <return><![CDATA[<IDataXMLCoder version="1.0">
        <record javaclass="com.wm.data.ISMemDataImpl">
            <value name="first">alpha</value>
            <value name="second">beta</value>
        </record></IDataXMLCoder>]]>
    </return>
</then>

Throwing an exception

In some cases it may be required for a mocked service to throw an exception. Use the throw element and specify the java exception class:

<then>
    <throw>java.lang.Exception</throw>
</then>

Capturing the pipeline

Contents of the pipeline can be saved to file using the pipelineCapture element and specifying a location for the files:

<then>
    <pipelineCapture>directorytarget/pipelinefile.xml</pipelineCapture>
</then>

Each time the capture is executed, a count is appended to the file name so that the example above would be pipelinefile-1.xml then pipelinefile-2.xml, etc The ensures that captures are not overwritten. Note that when the scenario is unregistered, registered again or the AOP framework reset then the count will always commence from 1 and will overwrite the files. Run your scenario, capture the output then move the files to another location to retain them.

Note that unlike file writing from within IS, the pipelineCapture doesnt need any changes to enable writing. By default it will write files into the instance directory, e.g. if we have an instance called 'default' then <pipelineCapture>pipelinefile.xml</pipelineCapture> will write to: yourSoftwareagDirectory/IntegrationServer/instances/default/pipelinefile-1.xml. Directories can be relative to this location or absolute, eg '/tmp/pipelinefile.xml' or '../../../tmp/pipelinefile.xml' etc

Returning random content or sequential content

Where the mock is required to simulate varied behaviour, the then can have a dispatch attribute added to vary the output:

        <then dispatch="random">
            <return><![CDATA[<IDataXMLCoder version="1.0">
                <record javaclass="com.wm.data.ISMemDataImpl">
                    <value name="result">alpha</value>
                </record>
                </IDataXMLCoder>]]>
            </return>
            <return><![CDATA[<IDataXMLCoder version="1.0">
                <record javaclass="com.wm.data.ISMemDataImpl">
                    <value name="result">beta</value>
                </record>
                </IDataXMLCoder>]]>
            </return>
            <return><![CDATA[<IDataXMLCoder version="1.0">
                <record javaclass="com.wm.data.ISMemDataImpl">
                    <value name="result">gamma</value>
                </record>
            </IDataXMLCoder>]]>
            </return>
        </then>

By changing the dispatch to sequential the then content is returned in order, looping around once all have been supplied.

Adding an assertion

If you want to add an assertion around an existing service (Ie, a real service, not a mock) then you can define an assertion which can be later tested with a separate call. In this example we can see that an assertion 'PreBarAssertion' is recorded when the condition foo == 2 is met before calling org.wmaop.foo:bar

    <given>
        <service intercepted="before">org.wmaop.foo:bar</service>
    </given>
    <when id="fire On condition" condition='foo == 2'>
        <then>
            <assert id="PreBarAssertion"/>
        </then>
    </when>

To check for the assertion, perform a call to GET /define/assertion/{adviceId} (In this example, the {adviceId} would be PreBarAssertion). See the assertion calls on the API Page

An example conditional mock

This example shows the ability to have multiple when conditions and a default behaviour if none of the conditions apply. An example, simple service which can be found in OrgWmAOPTest is rootSvc which sequentially calls svcA -> svcB -> svcC

Mock services found in the OrgWmAOPTest package

Output from sub-service is a letter per service and an indicator that svcC has detected svcB has run:

Output when executing rootSvc

We can create a mock of svcB by POSTing the following to /rest/org/wmaop/define/scenario

<scenario id="Test Aspect">
    <given>
        <service intercepted="invoke">org.wmaop.test.services:svcB</service>
    </given>
    <when id="input 1" condition="input == 1">
        <then>
            <return><![CDATA[<IDataXMLCoder version="1.0">
                <record javaclass="com.wm.data.ISMemDataImpl">
                    <value name="apple">alpha</value>
                </record>
            </IDataXMLCoder>]]>
            </return>
        </then>
    </when>
    <when id="input 2" condition="input == 2">
        <then>
            <return><![CDATA[<IDataXMLCoder version="1.0">
            <record javaclass="com.wm.data.ISMemDataImpl">
                <value name="apple">beta</value>
            </record>
        </IDataXMLCoder>]]>
        </return>
        </then>
    </when>
    <when id="default">
        <then>
            <return><![CDATA[<IDataXMLCoder version="1.0">
            <record javaclass="com.wm.data.ISMemDataImpl">
                <value name="apple">gamma</value>
            </record>
        </IDataXMLCoder>]]>
        </return>
        </then>
    </when>
</scenario>

We can then call org.wmaop.test.services:rootSvc and without any input values, the 'default' will fire and the output from the service will be:

Default output when mock doesnt fire

When the input variable is set to 1 we get the first condition executing:

Result when mock executes