Skip to content

Syntax for creating JBehave stories

Richard Tomlinson edited this page Mar 22, 2017 · 44 revisions

Structure

Tests are structured in story files, each file having one or more scenarios. Story files should each have a unique file name, ideally describing the context they're testing.

Test steps within a scenario follow the BDD naming approach using simple phrases.

Steps allow setting up of pipeline variables establishing mocks, invoking services, validating mock invocation and verifying pipeline contents. Tests are executed with Junit which runs all of the stories, with a green tick shown for each successful story step and overall green bar if all test scenarios pass

Each sequential step is a phrase based on:

  • Given - This is the set up of the test where there can be one or more steps
  • When - A single step to invoke the service you want to test
  • Then - Multiple steps to verify that the Given and Invoke have the expected returns.

Taking a standard service, string concatenation, this service can be tested:

Scenario: Concatenate two strings
Given pipeline values inString1 = "hello "; inString2 = "world"
When invoke pub.string:concat without idata
Then pipeline has value == "hello world"

The Given is setting up two pipeline values which correspond to the inputs required by the service. The When is invoking a service without passing any additional values to the pipeline. Values are passed into services in the form of IData files. The Then performs a test on the pipeline looking for value to be the concatenated result. Running this test, it should pass with a green bar and green ticks against all the steps.

If the test fails then the unit test will fail, red bar and the step in error highlighted with a cross. It also shows what the problem could be in the Failure Trace. In the example below, the Then has failed when testing the string match:

Failed execution of a story

The word And can be used as a synonym in place of Then to make the scenario more readable:

Scenario:
Given pipeline values inString1 = "hello "; inString2 = "world"
When invoke pub.string:concat without idata
Then pipeline has value == "hello world"
And pipeline has inString1 == "hello "

Functionality in detail

Expressions

Within a Then, expected outcome is described as an expression to test the pipeline that something has happened. The expression is based on the JEXL syntax with full details of how to use it on the Pipeline Expressions - Using Jexl page

Pipeline values

The scenarios rely on the setting up of data into the pipeline then testing that the pipeline has values. To do this there are two options: Direct set up using a Given pipeline values phrase or supplying data from a file.

Setting pipeline values directly within the scenario

A phrase exists that allows one or more simple values such as strings numbers, etc to be set in to the pipeline before the service is called. Using the phrase Given pipeline values followed by one or more name = value assignments sets those into the pipeline. Where there is more than one required, separate each on the same line with a semicolon followed by a space, e.g Given pipeline values inString1 = "hello"; inString2 = " world"

This method of assignment is not suitable for setting long strings of complex XML, lots of values, etc and where that is required, use an IData file instead.

Supplying data using IData files

The internal pipeline format of webMethods is known as IData and is a key-value map hierarchy of the pipeline and its documents. You can see this format if using the pub.flow:savePipeline to capture pipeline contents during a service execution. The format is well defined and can be easily edited with an XML tool. Using savePipeline will give examples and if you want to capture data without modifying any flow, the RESTful pipeline capture functionality in wmaop can do that.

Once you have IData files they can be used to set the pipeline contents when the service under test is invoked with the phrase When invoke xxx with yyy This phrase will load the IData from the yyy specified file and set it in the pipeline before the service xxx is called. An example might be When invoke org.wmaop.test.services:rootSvc with data/lorem.xml This is loading the file lorem.xml from the data directory within the project (See Test project set up and options for more information on how the files are located and loaded) then the rootSvc is invoked.

Using Given to overwrite IData supplied values

When testing multiple conditions with many varied inputs, its sometimes required to alter some input parameters to a service. Creating lots of IData files where one value has changed is tedious and verbose. It is possible to create one IData file and have values overridden by using the Given pipeline values step in conjunction with the When invoke .. with .. step. An example of this is where we have a simple IData content:

<IDataXMLCoder version="1.0">
    <record javaclass="com.wm.data.ISMemDataImpl">
        <value name="lorem">ipsum</value>
        <value name="dolor">sit</value>
    </record>
</IDataXMLCoder>

And we invoke a service but first override lorem to have a different value. The test is looking for lorem being amet and not the ipsum value from the IData file

Given pipeline values lorem = "amet"
When invoke org.wmaop.test.services:rootSvc with data/lorem.xml
Then pipeline has lorem == "amet"

Invoking a service at the start of the test

A story always has a When invoke line which indicates which service to call. There is a choice of whether to call the service and supply some IData to set up the pipeline or just to call the service with an empty pipeline. Using the 'with ...' suffix which indicates a file containg data in the IData format (The same XML format as that when you capture a pipeline). File references are relative from the src/test/resources directory so, for example, specifying data/pubresponse.xml as the name would pull the file from src/test/resources/data/pubresponse.xml

Alternatively the service can be invoked with no inputs. This is useful if simple pipeline data is already set with the Given pipeline values or no pipeline content is required.

Special attention must be made to the service and whether caching is enabled. Any service specified in the When invoke line that has caching enabled will still return cached values and therefore give unexpected values, particularly if you're trying to mock services. You may have to switch off the caching (Not recommended) or alternatively refactor your service to separate the caching from the service behaviour (recommended). Note that cached services elsewhere in the call stack (Ie called from within the 'invoke' service, etc) will always respond correctly to mocking and should need changing.]

Mocks

Injecting data into your service and testing the output form the basis of the tests but an important part is eliminating service dependencies so that the test and the package it is deployed in becomes independent. Mocking out dependant services allows you to control what's returned so that the service under test is exercised without having to set up other systems or complex data flows.

Using Given mock, a service can be replaced with a mock such that it returns some pipeline values instead of calling the real service. There are two alternatives to this approach, always returning data or conditionally returning it.

Always returning IData

When a fixed response is required where the same data is always supplied, the syntax looks like: Given mock org.wmaop.test:aMockedService always returning data/mydata.xml where the service and the IData files are specified.

Returning data based on a condition and multiple returns

In some cases, the data needs to be varied, depending on the pipeline or presented a particular condition applies. The syntax in this case is Given mock org.wmaop.test:aMockedService returning data/option-a.xml when selection == "a" This mock will only fire when selection is set to 'a' otherwise the real service is invoked. This allows for a service to be overridden only when needed

Taking the conditional approach one step further by overloading the mock definition, the data can be varied depending on the pipeline content and by using an always returning mock as the last definition, a default can be provided:

Given mock org.wmaop.test.services:mockedOption returning data/option-a.xml when selection == "a"
Given mock org.wmaop.test.services:mockedOption returning data/option-b.xml when selection == "b"
Given mock org.wmaop.test.services:mockedOption always returning data/option-c.xml
When invoke org.wmaop.test.services:concatOptions without idata
Then pipeline has result == "abc"

Returning more than one file

Sometimes its necessary to return more than one value from a mock, for instance in a loop. You could define multiple Given mock statement checking for some counter but that's not easy, particularly when nothing indicates the position. Fortunately its possible to specify more than one file as a comma separated list:

Given mock org.wmaop.test.services:mockedOption always returning data/option-c.xml,option-a.xml,option-b.xml

In this example when the mock is called it first returns the option-c.xml content from the data directory. On the next call it returns the option-a.xml file, from the implicit data directory used by the first, then the option-b.xml file. If called again it loops back around to serve the option-c.xml file, etc.

If the directory is specified anywhere that becomes the default for that and subsequent files so:

Given mock org.wmaop.test.services:foo always returning data/a.xml,bar/b.xml,c.xml

Would server a.xml from data then both b.xml and c.xml from the bar directory.

Did my mock fire?

Mocks are a form of assertion (See below for more details) and this allows for checking on the number of times the mock has been called. In the Then section of the scenario the syntax looks like: Then mock org.wmaop.test.services:aMock was invoked 2 times Because data is being supplied and the outcome should be known, the invoke check is for the exact number of times its been actioned.

Expressions

Key to a number of statements is the use of expressions to specify the condition to be met before the mock, assertion, etc is actioned. If you haven't already, take a look at defining pipeline expressions using JEXL for more information about the syntax and options available.

Matching documents

In some cases, matching complex data structures with multiple Then pipeline may be overly verbose and some form of direct xml matching is required. While the idea of capturing a document and matching against the pipeline seems like the easiest way to match pipeline contents, it's not the most declarative and can lead to lazy matching - Ie, just dump and compare without thinking about or declaring what's important within the document. To this end, document matching should only be used where there's a lot of data to compare and not as the default means of testing the pipeline.

Documents can be matched with either XML or IData outputs of the document. The following two examples are the same document but in XML format:

<?xml version="1.0" encoding="UTF-8"?>
<locality>
    <pcounty id="30" name="North Yorkshire">
        <parea id="HG">
            <ptown name="Harrogate"/>
            <ptown name="Ripon"/>
            <ptown name="Knaresborough"/>
        </parea>
    </pcounty>
    <pcounty id="46" name="West Yorkshire">
        <parea>
            <ptown name="Bradford"/>
            <ptown name="Shipley"/>
            <ptown name="Skipton"/>
        </parea>
    </pcounty>
</locality>

And IData format as the pipleline document called 'document':

<?xml version="1.0" encoding="UTF-8"?>
<IDataXMLCoder version="1.0">
    <record javaclass="com.wm.data.ISMemDataImpl">
        <record name="document" javaclass="com.wm.data.BasicData">
            <value name="@version">1.0</value>
            <value name="@encoding">UTF-8</value>
            <record name="locality" javaclass="com.wm.data.BasicData">
                <array name="pcounty" type="record" depth="1">
                    <record javaclass="com.wm.data.BasicData">
                        <value name="@id">30</value>
                        <value name="@name">North Yorkshire</value>
                        <record name="parea" javaclass="com.wm.data.BasicData">
                            <value name="@id">HG</value>
                            <array name="ptown" type="record" depth="1">
                                <record javaclass="com.wm.data.BasicData">
                                    <value name="@name">Harrogate</value>
                                </record>
                                <record javaclass="com.wm.data.BasicData">
                                    <value name="@name">Ripon</value>
                                </record>
                                <record javaclass="com.wm.data.BasicData">
                                    <value name="@name">Knaresborough</value>
                                </record>
                            </array>
                        </record>
                    </record>
                    <record javaclass="com.wm.data.BasicData">
                        <value name="@id">46</value>
                        <value name="@name">West Yorkshire</value>
                        <record name="parea" javaclass="com.wm.data.BasicData">
                            <array name="ptown" type="record" depth="1">
                                <record javaclass="com.wm.data.BasicData">
                                    <value name="@name">Bradford</value>
                                </record>
                                <record javaclass="com.wm.data.BasicData">
                                    <value name="@name">Shipley</value>
                                </record>
                                <record javaclass="com.wm.data.BasicData">
                                    <value name="@name">Skipton</value>
                                </record>
                            </array>
                        </record>
                    </record>
                </array>
            </record>
        </record>
    </record>
</IDataXMLCoder>

To make a document match, the following syntax is looking for myDocument in the pipeline and is being matched against the snippet.xml: Then pipeline document myDocument matches data/snippet.xml

The data being offered for match can be either an exact match or partial. For an exact match, the example above where the document and the data are one for one in their elements and attributes.

A partial match takes the data being offered and as long as there's a corresponding element and attribute where specified it assumes a match. In this case you dont have to specify every element, just the ones of interest that are to be validated. So, for the IData shown above we could partially match using:

<locality>
    <pcounty id="30">
        <parea id="HG">
            <ptown name="Ripon"/>
        </parea>
    </pcounty>
    <pcounty id="46">
        <parea>
            <ptown name="Skipton"/>
        </parea>
    </pcounty>
</locality>

Note that key attributes are specified to identify the elements, such as id and that not all child elements are specified, ie only one ptown element per parea. Where more than one child element exists, say two ptown elements, then as long as both exist, in any order, the match is successful.

When using an IData file as the data to match, the same principal applies in that the data can be trimmed to match just key elements (Represented as record elements). It will also perform partial match where the IData is represented as an array of records. In this case, the match is successful if the records match within the array, regardless of order.

Assertions

Firing mocks and testing pipeline values might be enough to satisfy you that the test has worked, but sometimes its useful to know that a particular service has been invoked and how many times (Eg, if the service has iterated some data, did it action the correct number?). Its also useful to know if a service has produced the expected output but it might not be possible to check the eventual pipeline contents due to values being dropped, etc.

Using Assertions allows you to 'peek' at the pipeline and check that something has happend.

Given MyAssertionName assertion before service org.wmaop.test.services:svcB always
When invoke org.wmaop.test.services:rootSvc with data/lorem.xml
Then assertion MyAssertionName was invoked 1 times

In this case, before svcB is actioned, the assertion called MyAssertion is triggered to prove that svcB was called. The Then checks for the assertion name and the number of invocations.

Just like mocks, assertions can have conditions applied to them so that the assertion only registers as invoked if the condition is met.

In the above examples, the invocation point has been before a service. By specifying after instead its possible to verify that a service has returned data or check at the end of a flow. If you specify invoke as the intercept point this will effectively replace the service with the assertion, effectively creating a blank, featureless mock.

Always ensure there is a Then assertion... statement for every Given ... assertion to verify the assertion - Simply creating an assertion without checking whether its fired wont give you a meaningful result!

Note that you only need assertions when checking flow through a service, usually before or after a particular service of interest is invoked. If you want to check that your mocks have fired, use the Then mock syntax as shown above in Did my mock fire?

The assertion (And mock) invoke count lookup uses the serviceName as a prefix rather than an exact match. This allows a combined count for all mocks or assertions within a package or a combined count where they begin with the name. For example, a serviceName of "pub.foo:ba" will give a count for "pub.foo:bar" and "pub.foo:bat". Likewise just using "pub.foo" gives a combined count for all mocks or assertion in that package. Note that the combined count is only for the mocks and assertions declared in the Given for the scenario, not the whole story (ie just that particular test)

Exceptions

Testing of behaviour should include negative paths where exceptional circumstances need handling and exeception handling requires execution. Instead of a service executing, its possible to raise an exception instead thereby simulating a failure. An example of the syntax is Given exception java.lang.RuntimeException thrown calling service foo:bar always The exception class name must be any java class that is Throwable and with a String constructor (Ie one where the exception message is passed in; This is the norm for most exceptions).

Conditional Exceptions

When testing within a loop you may want an exception to be thrown when a pipeline variable has a particular value (Eg, a piece of data from a service or a point within the loop counter). Like the mocking of a service, an exception can have a condition applied: Given exception java.lang.RuntimeException thrown calling service pub.foo:bar when loopCounter == 5

Interrupting execution around services

  • Defining exceptions to be thrown in place of a service or before/after to interrupt flow

Checking for exceptions

When an exception is raised in a test, be it from using a Given exception step or from having a service throw an exception, the exception must have a check applied to catch it and verify its existence. Without a check, the exception is treated as unexpected and the test will fail. To check for an exception use the syntax of:

Then exception com.wm.app.b2b.server.ServiceException was thrown

Its also possible to check the message and match if it contains a value, etc. After a Then exception... statement the pipeline has the message inserted as exceptionMessage which allows for a regular pipeline check such as:

And pipeline has exceptionMessage.contains("Service interface name required")

More examples

A variety test options can be seen in the stories used to test the framework. Browse through https://github.com/wmaop/OrgWmAOPTest/tree/master/src/test/resources/stories for examples of how to use the framework. Its also possible to deploy the OrgWmAOPTest package and run all of the tests if you want to experiment with what's there.

Syntax

Each line in the story is made up from a step statement based on set phrases. The phrases are shown below in the table. Each step must appear on its own line so ignore the word wrapping in the table due to formatting limitations within the wiki space.

  • Parameter place holders are shown as $...$ and should be replaced with actual values
    • $jexVariableExpression$ one or more expressions to set a pipeline value in the form of name = value such as myVar = "hello" Where there is the need to set more than one variable, separated with a semicolon, e.g: a = 1; b = "two"
    • $serviceName$ The fully qualified webMethods namespace of a service, eg: org.wmaop.pub.connectivity:doSomething
    • $idataFile$ A file containing data in the IData format (The same XML format as that when you capture a pipeline) File references to IData are relative from the src/test/resources directory so, for example, specifying data/pubresponse.xml as the name would pull the file from src/test/resources/data/pubresponse.xml
    • $jexlPipelinExpression$ An expression in JEXL format that tests the contents of the pipeline and returns a boolean value of true to indicate the expression has passed its test. See the page detailing JEXL Expressions for more information about the format.
    • $assertionId$ A name used to identify an assertion. This name is used to define and refer to the assertion and will be displayed at test failure. Should be something meaningful to the test, unique, camelcase for easy reading and be alphanumeric with no spaces, such as BeforeSendInvoice or hasExpiredPolicy
    • $interceptPoint$ The point at which the mock or assertion will be triggered. Values can be either: before to be triggered before a service; after to trigger after a service; or invoke to be triggered in-place of a service.
    • $exception$ The fully qualified Java class name of an exception that needs to be raised or tested for, such as java.lang.RuntimeExcpetion
Given Purpose Description
Given pipeline values $jexlVariableExpression$ To set up pipeline variables prior to a test execution Sometimes setting up complete IData structure might seem overkill, such as the simple test above. This step allows the declaration of one or more simple variables to be inserted into the pipeline. It is a JEXL statement expression so JEXL syntax applies. Where more than one variable is defined, each must be separated by a semi-colon.
E.g: Given pipeline values inString1 = "hello "; inString2 = "world"
Given mock $serviceName$ always returning $idataFile$ To create a mock that always returns the same IData content Replace serviceName$ with the name of the service and specify $idataFile$ as the relative source of the IData file
Given mock $serviceName$ returning $idataFile$ when $jexlPipelineExpression$ Returns content only when the pipeline matches the JEXL expression This step allows content to be conditionally returned if the $jexlPipelineExpression$ expression matches the content of the pipeline that is submitted by the invoke. When matching, the contents of the $idataFile$ file are set in the pipeline.
Given $assertionId$ assertion $interceptPoint$ service $serviceName$ always Creates an assertion to unconditionally count the number of invocations For the $interceptPoint$ specify either before or after

E.g: Given EnrichAssertion assertion before service org.wmaop.pub.functinality:doSomething always
Given $assertionId$ assertion $interceptPoint$ service $serviceName$ when $jexlPipelineExpression$ Defines an assertion to occur on a condition This is useful for testing that the pipeline had a specific value before or after a particular service was called. For the $interceptPoint$ specify either before or after
Given exception $exception$ thrown calling service $serviceName$ always Raise an exception in place of a service Always throw an exception whenever the service is called. Can be used to interrupt service flow
Given exception $exception$ thrown calling service $serviceName$ when $jexlPipelineExpression$ Raise and exception on a condition This can be useful to raise an exception in loops or ensuring that the fault condition has been met by interrogating the pipeline. The Then exception step can be used to test this has occurred.

Given exception java.lang.RuntimeException thrown calling service org.wmap.pub.invoice:createInvoice when lineCount = 2
Given exception $exception$ thrown $interceptPoint$ calling service $serviceName$ when $jexlPipelineExpression$ Raise an exception before or after a service when a condition occurs
Given exception $exception$ thrown $interceptPoint$ calling service $serviceName$ always always raise an exception before or after a service
When Purpose Description
When invoke $serviceName$ with $idataFile$ Invoke a service with data Used to invoke a service and pass in data to the pipeline. Normally used to comply with the service inputs and supply data to initiate the test condition
When invoke $serviceName$ without idata Invoke a service without any input A straight invoke without using data from a file. Can be useful if simple pipeline data is already set with the Given pipeline values and no further pipeline content is required
Then Purpose Description
Then assertion $assertionId$ was invoked $invokeCount$ times Verification that an assertion was executed. Used to check that an assertion set up with a Given xxx assertion has been called a number of times. This helps to verify that a loop or iteration through some data for instance has been executed correctly
Then mock $serviceName$ was invoked $invokeCount$ times Verify mock was called Useful to check that a mock was invoked a number of times. Only works with mocks, not normal services. The lookup uses the $serviceName$ as a prefix so its possible to get a combined count for all mocks within a package or services beginning with the name (Ie a $serviceName$ of "pub.foo:ba" will give a count for "pub.foo:bar" and "pub.foo:bat")
Then pipeline has $jexlPipelineExpression$ Verify pipeline contents
Then exception $exception$ was thrown Check exception occurred Verifies that the service exited having thrown the exception specified
Then pipeline document $document$ matches $idataOrXmlFile$ Match XML or IData against a pipeline document Locates the documented named $document$ and verify its match against the supplied $idataOrXmlFile$ data file. The data file can be IData or XML and either full or a snippet of data with the correct element hierarchy requried
Others Purpose Description
Then show pipeline in console See the state of the returned pipeline When debugging failing tests, using this step will display in the console the values returned from the service called in the When. Remove this step once the test passes otherwise it causes unnecessary output during test runs. Ensure you have a log4j properties file, as noted in the creating a test project page, and it is set to output on INFO otherwise the pipeline will not be visible.