Unit Testing Guide
Transact 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 Transact Platform applications. Unit tests provide the following critical functions in Transact Platform solutions:
- verifies that business logic executes correctly
- verifies that error handling logic functions as expected
- enables the business to continually deliver application improvements into the market without having to go expensive and through time consuming manual testing processes
- enables Transact Platform systems to be upgrade to the latest versions or receive security patches and automatically verify whether there are impacts or regressions
- supports 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. If you develop your services in this way you will be more productive and can develop much more robust code. Unit Tests enable you to exercise Groovy services in ways which are really difficult to produce manually.
Transact Platform Unit Testing Features
Transact 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 TM server.
- If you need unit tests that can run locally and on TM servers then subclass
AbstractJUnitTest
, but these unit tests will not support IDE debugger sessions. - If you want unit tests that can be run directly in your IDE debugger, your tests will need to subclass
TransactSDKUnitTest
, but these unit tests cannot be executed on the TM server (because of dependency injection constraints).
Supported JUnit4 annotations include:
Annotation | Description |
---|---|
@BeforeClass | Specifies method to be called once when test class in setup |
@Before | Specifies method to be called before each test method invocation |
@Test | Specifies test method to be invoked |
@After | Specified method to be called after each test method invocation |
@AfterClass | Specifies method to be called once when test class is finished |
@Ignore | Specifies 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
Transact 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
Transact 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.
- HTTP
GetRequest
,PostRequest
,PutRequest
,DeleteRequest
, andPatchRequest
GroovyServiceInvoker
FluentFuncInvoker
ServiceInvoker
Test mocking is an important practice in modern software development as it enables you to:
- 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 Transact Platform solution will return when executing services code in a unit test context. This works as follows.
- Unit test code registers mock response for an external service call.
- Unit test code then calls service's invoke method.
- The invoked service now calls an external service.
- Transact Platform finds a matching registered mock request object and returns the associated mock response object.
- Service code receives mock response object and performs its logic.
- Unit test code verifies that service code performed correctly for the given execution context.
To illustrate this see the concrete example HTTP service code below.
Mocking HTTP Calls
In this example we are developing a GetAccounts service which makes a HTTP request to an 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 Transact 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)