Skip to main content

Version: 22.04

Unit Testing Guide

Journey Platform provides good support developing automated unit tests for Groovy services and functions.

Whenever you are developing services and functions you must build corresponding unit test code to ensure your business logic works correctly. Developing unit test coverage is critical the successful delivery of Journey Platform applications. Unit tests provide the following critical functions in Journey Platform solutions.

  • Verify that business logic executes correctly.
  • Verify that error handling logic functions as expected.
  • Enable the business to continually deliver application improvements into the market without having to go through potentially expensive and time consuming manual testing processes.
  • Enable Journey Platform systems to be upgraded to the latest version, or receive security patches and automatically verify whether there are impacts or regressions.
  • Support modern software development best practices.

To develop your services business logic, you should write unit test code to execute your Groovy script under a variety of test scenarios. Developing your services in this way helps you to be more productive and develop much more robust code. Unit Tests enable you to exercise Groovy services in ways which are really difficult to produce manually.

Journey Platform Unit Testing Features

Journey Platform provides a number of important features to support services and functions unit testing these include:

  • Groovy Service and Function Unit Test templates to get developers started immediately
  • Mocking Libraries
  • JUnit4 Support
  • Ant and Maven based CI build support
  • IntelliJ and Eclipse IDE unit test execution support

JUnit4 Library Support

Support for JUnit4 test annotations enables developers to write service unit tests that can be executed both locally and remotely on a JM server.

  • If you need unit tests that can run locally and on JM servers then subclass AbstractJUnitTest. Note that these unit tests will not support IDE debugger sessions.
  • If you want unit tests that can be run directly in your IDE debugger, then subclass TransactSDKUnitTest for your tests. Note that these unit tests cannot be executed on the JM server (because of dependency injection constraints).

Supported JUnit4 annotations include:

AnnotationDescription
@BeforeClassSpecifies method to be called once when test class in setup
@BeforeSpecifies method to be called before each test method invocation
@TestSpecifies test method to be invoked
@AfterSpecified method to be called after each test method invocation
@AfterClassSpecifies method to be called once when test class is finished
@IgnoreSpecifies test method that will not be executed

Test Coverage

Typically your service unit test code should incorporate many @Test case methods to executing different scenarios to ensure you have adequate test coverage. A multi-test method unit test is provided below.

 public class GetAccountsTest extends TransactSDKUnitTest {
@Test
void testOk() throws Exception {
// Test normal service call
...
}
@Test
void testBadRequest() throws Exception {
// Test HTTP 400 bad request
...
}
@Test
void testServiceUnavailable() throws Exception {
// Test HTTP 503 service unavailable
...
}
}

Mockito Library Support

Journey Platform includes support for the Mockito unit testing mocking framework. This library is included in the Fluent SDK distribution and has been added to the Secure Compiler Whitelist for unit test execution.

note

The Mockito library is not whitelisted for production execution as it provides extensive reflection and interception methods that could compromise the Fluent services security model.

Mocking VO Constructors

The Fluent Value Object (VO) classes provide immutable value objects for core TM entities represented in the database. You can create these mock objects using the VO constructors which take a map of field attributes. This enables you to mock VO classes without having to execute your unit tests in a TM server.

An example mock Txn object is shown below.

import com.avoka.tm.vo.*

// Unit Test Code
Map fields = [
"id": new Long(123),
"formCode": "CCA-FTX",
"userSaved": true
]

Txn tx = new Txn(fields)
...
note

For security reasons, the VO map constructors are not permitted in production code execution.

Mocking HTTP and Service / Function Calls

Journey Platform provides a MockRegister class that supports testing your service code with mock responses for external services calls when executing in a unit test context.

MockRegister supports mocking responses for calls of the following classes.

Test mocking is an important practice in modern software development. It enables the following activities.

  • Test service code in isolation from other components.
  • Develop your service code before other services are available.
  • Test error handling modes which are difficult to achieve when calling real services.
  • Test in environments where external services may not be available; for example, a dev machine or CI server.
  • Build CI tests to execute much faster than when calling external services.

MockRegister registers a mock response object in a thread local store which your Journey Platform solution will return when executing services code in a unit test context. This works as follows.

  1. Unit test code registers a mock response for an external service call.
  2. Unit test code calls a service's invoke method.
  3. The invoked service calls an external service.
  4. Journey Platform finds a matching registered mock request object and returns the associated mock response object.
  5. Service code receives a mock response object and performs its logic.
  6. Unit test code verifies that service code performed correctly for the given execution context.

The following HTTP service example illustrates this process.

Mocking HTTP Calls

In this example, a GetAccounts service makes a HTTP request to retrieve a list of accounts. The HTTP endpoint configuration details are provide by the services SvcConn object. If the HTTP call does not return a 200 OK status code then our service will return a validation error to the form.

 public class GetAccounts {
public Logger logger
FuncResult invoke(FuncParam param) {
SvcConn svcConn = param.svcDef.svcConn
HttpResponse response = new GetRequest(svcConn.endpoint)
.setBasicAuth(svcConn.username, svcConn.password)
.execute()
FormFuncResult result = new FormFuncResult()
if (response.isStatusOK()) {
result.data.accounts = response.getTextContent()
} else {
result.errors.add(new ValidationError("accountsSvcError"))
}
return result
}
}

Now, to test our service's handling of HTTP errors, we will mock a HttpResponse object returning a HTTP 503 Service Unavailable error. This response object is registered using the same GetRequest object that is used in the service call. Transact's mock object matching rules are discussed later.

 public class GetAccountsTest extends TransactSDKUnitTest {
@Test
void testFunction() throws Exception {
FuncParam funcParam = new MockVoBuilder().createFuncParam(TRIGGER_FORM_UPDATE, svcDef, testParams)
// Create a matching GetRequest object
SvcConn svcConn = svcDef.svcConn
HttpRequest matchRequest = new GetRequest(svcConn.endpoint).setBasicAuth(svcConn.username, svcConn.password)
// Create a mock response object
HttpResponse mockResponse = new HttpResponse().setStatus(503)
// Register the request object to return mock response object when a matching HTTP request is made
new MockRegister().when(matchRequest).thenReturn(mockResponse)
// Invoke service
FormFuncResult result = (FormFuncResult) new ServiceInvoker(svcDef)
.setLogger(logger)
.invoke("funcParam", funcParam)
// Test results
logger.info result
assert result.errors.get(0).errorKey == "accountsSvcError"
}
}

The key line in the code above is the MockRegister call which registers the HttpRequest object and associated mock response object.

new MockRegister().when(request).thenReturn(response);

HttpRequest.execute() checks whether there is a registered thread-local HttpRequest object which semantically matches the current HttpRequest object. If there is, the execute method will return the registered mock response object immediately and not perform the actual HTTP request.

HttpRequest Matching

When Journey Platform is looking for a mock response object registered with a request, it tests the registered request object's attributes against the HttpRequest object that is executing currently. The attributes that are matched include:

  • matching - request method (GET, POST, PUT, DELETE)
  • matching - request URI
  • matching - request message data
  • mock request headers is a matching set or is a subset of actual request headers
  • mock request parameters is a matching set or is a subset of actual request parameters

Mocking Service / Function Calls

The other scenario where the MockRegister class is useful is when one service or function is calling another service and we need to mock the response of the second service or function. Service mocking is supported in the ServiceInvoker, FluentFuncInvoker and GroovyServiceInvoker classes.

To visualize this, consider the following execution path.

Trigger          ->  Service A
Service A -> Service Invoker
Service Invoker -> Service B
Service B -> returns response

To facilitate unit testing we use the MockRegister to skip calling Service B and instead return a test result object directly from the ServiceInvoker class.

Test Trigger     ->  Service A
Service A -> Service Invoker
Service Invoker -> return mock result

To illustrate this scenario, we've defined a PrefillFunction class which is calling the shared Groovy Service to look up some data from another system.

 public class PrefillFunction {
public Logger logger
FuncResult invoke(FuncParam param) {
Map params = [:]
params.request = param.request
String profileData = new GroovyServiceInvoker()
.setServiceName("LoadProfile")
.setClientCode(param.svcDef.clientCode)
.invoke(params)
Map profileMap = new GsonBuilder().create().fromJson(profileData, Map.class)
FormFuncResult result = new FormFuncResult()
result.data.profileData = profileMap;
return result
}
}

To mock the Groovy Service call profileData result in our PrefillFunction unit test we register a mock result object with MockRegister. Note the registered service definition matches that used in the PrefillFunction code.

 public class PrefillFunctionTest {
public Logger logger
FuncResult invoke(FuncParam param) {
FuncParam funcParam = new MockVoBuilder()
.createFuncParam(FuncParam.TRIGGER_FORM_OPEN, Txn.FORM_SAVED, svcDef, testParams);
SvcDef matchSvcDef = new SvcDef([
"name": "LoadProfile",
"clientCode": svcDef.clientCode,
])
Map profileData = [
"firstName": "John",
"lastName": "Smith",
"email": "[email protected]"
]
new MockRegister().when(matchSvcDef).thenReturn(profileData)
// Invoke service
FormFuncResult result = (FormFuncResult) new ServiceInvoker(svcDef)
.setLogger(logger)
.invoke("funcParam", funcParam);
// Test results
logger.info result
assert result.data != null
assert result.data.profileData == profileData
}
}

Service / Function Matching

The service or function being called is matched using the registered SvcDef attributes. The registered SvcDef should have the same attributes which are used in the service call. These attributes include:

  • name: the service name (required)
  • version: the service version number (optional)
  • clientCode: the service's organization client code (required)
  • parameters: the service parameters (optional)