TEST1 - Adding Tests
|
Getting Started
The tutorial step uses the If at any time you want to see the solution to this step, this can be found on the |
Upto now, we’ve been focusing on implementing a number of different capabilities of the IPF Framework and looking at how they can be combined to quickly bootstrap an IPF application. During this time, however, we haven’t yet explored ways to test our application.
In this stage, we’ll introduce Icon’s Test Framework, and show how it can be used to test the application you’ve built. We’ll assume in this tutorial that you have a basic awareness of what the Icon Test Framework is, and an understanding of both BDD and Gherkin syntax.
The Icon Test Framework
Concepts
We’ll start our intro into the Test Framework by summarising some key concepts:
-
Message: An abstraction model for any 'message' that is handled by the framework implementation (request, response, payload etc). A message is typed against a known Java type that represent the contents de-serialised form, also referred to as Document Type.
-
MessageType: A representation of the messages type that can be referred to through the BDD, there should be a one to one mapping between MessageType instance and a Message’s associated Document type.
-
MessageDefinition: A contextual structure that provides functionality for handling messages of the configured type, serving as a point of Inversion of Control with the test-framework. There should be a one-one mapping between the MessageDefinition instance and configured MessageType, and it is common to see both Message and MessageDefinition as arguments to core methods.
-
MessageBucket: A glorified collection that any messages received by the Test Framework (either directly from Consumers, or secondary such as HTTP responses) are appended to. The internal collection is encapsulated and a predicate-based accessor methods are provided in order to "fish" correlated messages from the bucket. A successfully "fished" message is typically removed from the bucket and added to the test’s own Context object.
-
Transporter: An abstraction of a protocol on which a message may be sent to the target system e.g. HTTP, JMS etc
-
Context: A scenario context that holds test information and is accessible from any step, the internal data structure is thread local to facilitate parallelisation and is cleared down between scenarios by JBehave lifecycle hooks.
Don’t worry if these concepts seem somewhat abstract at the moment. They’ll become clearer as we work our way through the rest of this tutorial.
Extensions
In this tutorial, we’re going to use an extension of the Test Framework that has been designed to make the process of writing tests for IPF based applications simpler:
<dependency>
<groupId>com.iconsolutions.ipf.core.test</groupId>
<artifactId>ipf-test-fw-whitebox</artifactId>
<scope>test</scope>
</dependency>
The ipf-test-fw-whitebox extension provides a number of useful things:
-
A set of pre-built steps that utilise:
-
the system events structure of an IPF application to provide rich processing steps that can be used for validation
-
the model operations capability to interrogate the aggregate of any given flow.
-
-
A set of common steps (scenario start / end)
-
A set of transporter utilities to allow easy set up of stubbed HTTP, Kafka and JMS services.
You’ll use these features as we progress through the tutorial.
Project Set Up
Before we can start writing tests, we need to first set up a location in our project where we’ll store them.
This will be a new Maven module which you’ll call ipf-tutorial-application-tests.
If you’re using IntelliJ, you can do this by right-clicking on the root {solution-name} project module you’re working from (e.g. add_http) in the project view and selecting .
You should then be prompted to add a new Maven module:
After clicking "Next", provide the module with the name ipf-tutorial-application-tests
Then press "Finish" to complete the project setup.
Once complete, if you expand the module in the navigator, you can delete the ipf-tutorial-application-tests/src/main directory as we’ll only be adding things to the test folder here.
Add a new directory "resources" under the test directory, and mark this as a test resources root (right-click the folder > Mark Directory As > Test Resources Root). Under this new resources directory, we’ll add one more directory called "stories".
When we’ve completed all these steps, our project structure should look like the below:
A First BDD Test
Now we have a module to store our tests, let’s get on and start writing our first BDD test case.
To do this we need to create a "story" file.
Let’s create a new file called HappyPath.story and add it to the new stories directory.
|
There are some great plugins available within IntelliJ to help support the development of BDD test cases. We recommend using this one: JBehave Support. When installed, it will highlight which steps have already been implemented, and provide click through capability to see the code. |
Let’s now start populating your story file:
Meta:
Narrative:
This test covers the basic tutorial happy path flow.
Scenario: Basic Happy Path
Given the IPF service is healthy
This is the basis of all the stories we’ll write for IPF. The first line of the scenario "Given the IPF service is healthy" is one of the steps we’ll use to check that our IPF application has started successfully and is ready to process messages. This will verify that all the connectors in the application are up and running before we start a test.
|
When running a test, if this step fails, check the logs as it will direct you to which connectors have failed to start up successfully. This is normally down to a configuration error in your test! |
Having confirmed our application is up and running, we need to start thinking about the different steps of the payment journey we’ve constructed as part of the DSL stages of the tutorial. As this is our first attempt at writing a BDD test, we’ll just be covering a simplified example of this payment journey here that demonstrates the core capabilities of the test framework. To explore the full range of functionality provided by the test framework, please refer to the Icon Test Framework guide.
Initiating a Payment
To begin with, let’s consider the first part of the payment journey, which involves the initiation of the payment itself.
In the DSL tutorial stages, the initiation of a payment was performed by sending a request to the (HTTP) initiation controller. The test framework here acts as the initiating party, or "channel", and so we use the When keyword for this BDD step:
When the channel sends a 'initiation request' with values:
| requestId | fraud-happy-request-id |
| value | 25 |
We define the type of request we want to send here — an "initiation request" — and we provide two values to include in this request:
-
a
requestIdcontaining the Stringfraud-happy-request-id -
a
valueof 25.
The value is easy to understand. We are sending in a value < 30 to ensure we follow the correct success path through the flow.
The requestId will be used by our test to signpost the specific scenario we are running. The test framework is capable of running many tests in parallel, and so we need to be able to uniquely identify each test. The requestId will serve that purpose here. Don’t worry, it’ll become more obvious how this works later!
After the controller receives the payment initiation request, all being well, a new Initiation Flow will be started as part of the request handling logic in the controller. As this behaviour sits within the IPF application we’re testing (and not the test framework), we’ll use the Then keyword for this BDD step:
Then a new 'InitiationFlow' flow is started
The last part of the payment initiation process involves sending a response back to the channel after the Initiation Flow has been successfully started. Again, as this behaviour sits within the IPF application, we’ll extend the previous Then step and use the And keyword here:
And the channel receives a 'initiation response'
Putting this together, we now have a set of steps that cover the payment initiation part of our payment journey:
Narrative: This test covers the basic tutorial happy path flow. Scenario: Basic Happy Path Given the IPF service is healthy When the channel sends a 'initiation request' with values: | requestId | fraud-happy-request-id | | value | 25 | Then a new 'InitiationFlow' flow is started And the channel receives a 'initiation response'
We’ll continue to add to this as we progress through the tutorial, but for now, this small example represents a runnable test scenario, so let’s jump into how we can run it!
Test Implementation (Part 1)
There are a number of key things we need to cover to get our tests to run:
-
A "runner" - this is a class that will provide the spring boot test runner that will execute our story file.
-
"Config classes" - these provide the test request/response message definitions and transports that specify how the test framework interacts with the flow in the IPF application. For our current test case, this will just cover the "channel", but as we progress, we’ll also need to include all the external services that the flow interacts with during the payment journey.
-
"Config" file - we will need to supply configuration so the test framework knows how to connect to the tutorial application.
Let’s go through each of these now.
The Runner
The runner actually runs our tests. Its responsibility is to determine all the available story files and execute the scenarios within them.
Before we create this runner, we need to add a few dependencies into our ipf-tutorial-application-tests pom.xml:
<dependencies>
<dependency>
<groupId>com.iconsolutions.ipf.core.test</groupId>
<artifactId>ipf-test-fw-whitebox</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.iconsolutions.ipf.tutorial</groupId>
<artifactId>ipf-tutorial-app</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
As you can see, the first one is the ipf-test-fw-whitebox dependency mentioned earlier. The second dependency we include is the tutorial app module, which enables us spin up the tutorial app from our test runner.
So let’s now set up our runner class as shown below:
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Import({AllTestConfig.class})
public class FeatureTestRunner extends IPFFeatureTestRunner {
}
and add it into a new com.iconsolutions.ipf.tutorial.test package within the application test module.
The runner:
-
extends the
IPFFeatureTestRunnerand imports theAllTestConfigconfiguration class provided by theipf-test-fw-whiteboxdependency.AllTestConfigenables a number of features we’ll use in setting up the running of our test, and we’ll discuss these later. -
uses the
Application.classfrom theipf-tutorial-appmodule as the basis of the spring boot test.
We’ll need a Mongo database to write to as part of the test, and we’re going to use a test container to supply us with the docker implementation for this. We also need to provide a Kafka container here, as the tutorial application connects to Kafka at start up, and we’ll use this when we include the interaction with the Sanctions system in our test later. Taking advantage of the out-of-the-box functionality provided by the test framework, we can provide these containers very easily by first modifying our class definition for the runner:
public class FeatureTestRunner extends IPFFeatureTestRunner KafkaIntegrationTestSupport, MongoIntegrationTestSupport {
}
and then adding the following static block to bring up the Kafka and Mongo containers at the start of the test:
static {
kafkaContainer.withEnv("KAFKA_CREATE_TOPICS", "SANCTIONS_RESPONSE:1:1,SANCTIONS_REQUEST:1:1,PAYMENT_INITIATION_REQUEST:1:1,PAYMENT_INITIATION_RESPONSE:1:1");
SingletonContainerPattern.startAllInParallel(mongoContainer, kafkaContainer);
}
Note that we’ve also provided a set of Kafka topics here, as without them, the health check will fail (think back to the Given the IPF service is healthy step we defined in the test). The sanctions ones will be used when we add the BDD steps to cover the interaction between the tutorial app and the sanctions system later. The payment initiation request/response won’t be used in this test (as we’re initiating a payment via the HTTP controller in our test), but they need to be here owing to the fact that we brought in external Kafka payment initiation send/receive connectors as part of CON1 - Adding Payment Initiation.
That’s everything we need for our runner right now, so lets move onto the configuration class.
The Payment Initiation Configuration Class
As discussed above, we’re going to be initiating payments via the HTTP controller in the tutorial application. In our BDD test scenario, we defined two messages: the "initiation request" and the "initiation response". We will now create message types and definitions for these messages inside an initiation config class.
Let’s create a new package com.iconsolutions.ipf.tutorial.test.config and add a new InitiationConfig class. The first thing we need to do in our initiation config is define an inner enum class, which will contain our message type definitions and implement the test framework provided MessageType interface:
public class InitiationConfig {
enum InitiationTypes implements MessageType {
INITIATION_REQUEST("initiate request"),
INITIATION_RESPONSE("initiation response");
private final String name;
InitiationTypes(String name) {
this.name = name;
}
@Override
public String getName() {
return name();
}
@Override
public Set<String> getAliases() {
return Set.of(name);
}
}
}
The key thing to note in the above definition is that the names (aliases) provided in the constructor of the enum types must match the names provided in your BDD story file.
We now need to create the message definitions for these types. Let’s start with the "initiation request":
@Bean
MessageDefinition<InitiationRequest> initiationRequestMessageDefinition(@Value("${application.base.url}") String baseUrl) {
return new DefaultMessageDefinition.Builder<InitiationRequest>()
.withType(InitiationTypes.INITIATION_REQUEST) (1)
.withDocumentTypeClass(InitiationRequest.class) (2)
.withGenerator(props -> new InitiationRequest()) (3)
.withDestination(baseUrl + "/submit") (4)
.withCorrelatingIdGet(message -> Optional.ofNullable(message.getDocument().getRequestId())) (5)
.withPreSend(message -> ContextUtils.setClientRequestId(message.getDocument().getRequestId())) (6)
.build();
}
Let’s walk through each part of this definition above:
| 1 | Here we’re linking the message definition to the type this definition applies to, in this case, the "initiation request". |
| 2 | This method allows us to define the actual java type of the message definition, which is the InitiationRequest here. |
| 3 | As the test framework is acting as the initiator of the payment, it needs to send a valid initiation request to the tutorial application as part of the test. This generator method tells the test framework that, when it needs to send this initiation request, it will construct it using the implementation passed to it. In this simple case, we just need to provide a new InitiationRequest object to the generator (we’ll create a more comprehensive generator definition later when stubbing the sanctions system). |
| 4 | For the destination, this is the HTTP address that the initiation request will be sent to. Note that we are injecting the url path using the Spring @Value annotation here, so we’ll need to include the value for this in our application.conf file (which we’ll do later in the configuration file section below). |
| 5 | Here we define a function that will tell the test framework how to correlate this request with the subsequent initiation response (that we’ll create shortly). As mentioned above, the test framework is capable of running many tests in parallel, with all messages received by the test framework added to a single "Message Bucket". We therefore need to provide the framework with a way of determining which request/response pairs are linked within each test. The identifier we’re going to use here to achieve this requirement is the requestId field, which is available on both the initiation request and response objects, and contains the same value in each case (as, if you remember, the requestId on the request is passed directly to the response in the controller request handling logic). |
| 6 | In the pre-send method, we can provide any extra implementation logic that we deem useful before the message is sent out. Typically, this will involve setting identifiers in the test context that are then available throughout the test run. In our case, we’re going to pass the requestId from the initiation request to the clientRequestId field in the test context. We’ll be using this for correlation purposes in later BDD steps. |
Now let’s move onto defining the "initiation response":
@Bean
MessageDefinition<InitiationResponse> initiationResponseMessageDefinition() {
return new DefaultMessageDefinition.Builder<InitiationResponse>()
.withType(InitiationTypes.INITIATION_RESPONSE) (1)
.withDocumentTypeClass(InitiationResponse.class) (2)
.withCorrelatingIdGet(message -> Optional.ofNullable(message.getDocument().getRequestId())) (3)
.build();
}
You can see that this definition is much simpler than the "initiation request", which is a result of the fact that the IPF application is generating this object in the test, rather than the test framework, and the role of the test framework here is just to assert that this message was generated in the test.
Again, let’s walk through the message definition here:
| 1 | This time our definition is for the "initiation response" message type. |
| 2 | The java type of the message definition is InitiationResponse here. |
| 3 | As mentioned in the "initiation request" definition, we’re using the requestId on the response to correlate it with the corresponding request. |
That’s our message definitions done. The final step in our initiation configuration setup is to define a transport; i.e. we need to provide the test framework with a mechanism to make a call to the initiation controller. To do this, we’re going to use another test framework utility, the HttpSenderTestTransporter. Let’s create a bean for the transport as follows:
@Bean
public MessageTransport initiationTransport(MessageDefinition<InitiationRequest> initiationRequestMessageDefinition, MessageDefinition<InitiationResponse> initiationResponseMessageDefinition) {
return new HttpSenderTestTransporter.Builder<InitiationRequest, InitiationResponse>()
.withIdentifier("initiation") (1)
.withRequestMessageDefinition(initiationRequestMessageDefinition) (2)
.withResponseMessageDefinition(initiationResponseMessageDefinition) (3)
.build();
}
In this simple definition, we’re constructing a new HttpSenderTestTransporter instance and providing:
| 1 | a unique identifier for the transport (if we have multiple sender transports, each one will need a unique id - you could leave this blank as we’re only creating a single one here, but for tracing any issues, providing a name is always a good idea!). |
| 2 | access to the request message definition we set up earlier. |
| 3 | access to the response message definition we set up earlier. |
The transport will extract all the other information it requires from the provided message definitions!
That’s everything now done for our payment initiation configuration class, so let’s move on.
Configuration file
The next thing we need to do is create a config file in our test resources folder and provide a value for the application.base.url which we defined in the initiation request message definition earlier. By default, the tutorial application listens on port 8080, so let’s create a new application.conf file under src/test/resources and add the following line:
application.base.url="http://localhost:8080"
Executing our Test
We’re now almost ready to execute our test. Before we can do this, we first need to add the junit-vintage-engine dependency to our module. This is required because jBehave utilises JUnit4 rather than JUnit5, and typically, for spring boot projects, tests are written using JUnit5, meaning this is the engine that’s included on the classpath by default:
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</dependency>
Next, as the application sends processing data to the operational data store over HTTP, we’ll need to provide a mock endpoint that will handle these requests in our test. You’ll do this by adding a simple wiremock consumer that will always return a 200 when called by the application.
Add the required dependency to the pom.xml:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
</dependency>
and then create a DummyODSConsumer class with the following definition and add it to the com.iconsolutions.ipf.tutorial.test.config package:
@Configuration
public class DummyODSConsumer {
@Bean
public WireMockServer odsMock() {
WireMockServer wireMockServer = new WireMockServer(
new WireMockConfiguration()
.port(8093)
.needClientAuth(true)
);
wireMockServer.start();
wireMockServer.stubFor(WireMock.post(WireMock.urlEqualTo("/ipf-processing-data"))
.willReturn(WireMock.aResponse()
.withStatus(200)));
return wireMockServer;
}
}
Finally, we add our new initiation config class and the DummyODSConsumer class as imports in our runner
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Import({AllTestConfig.class, InitiationConfig.class, DummyODSConsumer.class})
public class FeatureTestRunner extends IPFFeatureTestRunner implements KafkaIntegrationTestSupport, MongoIntegrationTestSupport {...}
Now we can run our test by right-clicking on our FeatureTestRunner class and clicking run.
After running, the output should look something like:
We have a running test that’s passing! Now let’s go ahead and add the payment execution steps to our test scenario.
Executing a Payment
After the Initiation Flow has been initiated, it immediately starts a new IpftutorialflowV2 flow (Execution flow). As before,
because this behaviour sits within the IPF application, we’ll extend the earlier Then step and use the And keyword again here:
And a new 'IpftutorialflowV2' flow is started
After a number of no-op stages (duplicate check, account validation), the Execution flow sends a request out to the sanctions system. As the test framework is acting as the sanctions system here (i.e. it’s providing a sanctions system stub), the BDD step declares that the sanctions system receives the request. We again use the And keyword here as the sending of the request is done by the tutorial application:
And Sanctions receives a 'sanctions request'
As with the Initiation Request, we are defining the message type of the request the test framework is expecting to receive here, which is a "sanctions request"
The test framework sends a sanctions response back to the tutorial application after receiving the request, and so we use the When keyword for this BDD step definition:
When Sanctions sends a 'sanctions response'
Here we are defining the message type of the response that test framework will have to construct and send back to the tutorial application, which is a "sanctions response"
Under the right conditions (value < 30), the Execution flow then sends a request out to the fraud system. The test framework expects to receive a "fraud request" message type, and, as the tutorial application is sending the request out here, we use the Then keyword again in our step definition:
Then Fraud receives a 'fraud request'
The test framework will construct and send a "fraud response" message type back to the tutorial application after receiving the request, and so we again use the When keyword here:
When Fraud sends a 'fraud response'
After the no-op clear and settle stage, the Execution Flow moves to the terminal Complete state. This behaviour
sits within the IPF application, and so we use the Then keyword in this step definition:
Then the 'IpftutorialflowV2' flow is in state 'Complete'
After the Execution Flow completes, it immediately returns control to the Initiation Flow which then also completes. As before, we extend the previous Then step, and use the And keyword here:
And the 'InitiationFlow' flow is in state 'Complete'
Putting this all together we now have our first full BDD test!
Narrative: This test covers the basic tutorial happy path flow. Scenario: Basic Happy Path Given the IPF service is healthy When the channel sends a 'initiation request' with values: | requestId | fraud-happy-request-id | | value | 25 | Then a new 'InitiationFlow' flow is started And the channel receives a 'initiation response' And a new 'IpftutorialflowV2' flow is started And Sanctions receives a 'sanctions request' When Sanctions sends a 'sanctions response' Then Fraud receives a 'fraud request' When Fraud sends a 'fraud response' Then the 'IpftutorialflowV2' flow is in state 'Complete' And the 'InitiationFlow' flow is in state 'Complete'
Before we can run it, we’ll need to add all the relevant configuration in the same way as we did earlier. So let’s go ahead and do that.
Test Implementation (Part 2)
The Sanctions System Configuration Class
For the sanctions system configuration, the steps are essentially the same as those we followed for the payment initiation config, namely:
-
Create the message types
-
Create the definitions for the request and response
-
Define the transport.
The only difference this time is, rather than using a HttpSenderTransport, we’ll use a KafkaMessageTransport. For this, we’ll need to add another dependency to our pom.xml:
<dependency>
<groupId>com.iconsolutions.ipf.core.test</groupId>
<artifactId>ipf-test-fw-connectors</artifactId>
</dependency>
Now, let’s start by creating a new SanctionsConfig class in the com.iconsolutions.ipf.tutorial.test.config package. As before, we’ll need to first add the message types for the "sanctions request" and "sanctions response":
public class SanctionsConfig {
enum SanctionsTypes implements MessageType {
SANCTIONS_REQUEST("sanctions request"),
SANCTIONS_RESPONSE("sanctions response");
private final String name;
SanctionsTypes(String name) {
this.name = name;
}
@Override
public String getName() {
return name();
}
@Override
public Set<String> getAliases() {
return Set.of(name);
}
}
}
Now let’s think about our "sanctions request" message definition. The IPF application sends out this request and the test framework receives it. As the test framework will consume the request from a Kafka topic, we’ll need to provide a mechanism to convert the serialised string on the topic to the actual SanctionsRequest object.
Let’s see how the request definition looks when we put all this together:
@Bean
MessageDefinition<SanctionsRequest> receiveSanctionsRequest() {
return new DefaultMessageDefinition.Builder<SanctionsRequest>()
.withType(SanctionsTypes.SANCTIONS_REQUEST) (1)
.withDocumentTypeClass(SanctionsRequest.class) (2)
.withSource("SANCTIONS_REQUEST") (3)
.withFromStringMapper(serialisedString -> SerializationHelper.stringToObject(serialisedString, SanctionsRequest.class)) (4)
.withCorrelatingIdGet(message -> Optional.ofNullable(ContextUtils.getClientRequestId())) (5)
.build();
}
| 1 | Here again we define our message type. |
| 2 | And the java object. |
| 3 | The "source" field represents the Kafka topic we’re going to read from. Note that it’s called "source" here as message definitions are protocol independent, and this field could also be used to define a JMS queue as the source of this message. |
| 4 | The fromStringMapper defines how we’re going to convert from the serialized string version of our message to the corresponding java class.
In our case, we’re going to use a pre-defined stringToObject function that is provided by Icon’s SerializationHelper and will perform a simple Jackson mapping of the string. |
| 5 | Again, we use this function to tell the test framework how to correlate this request with the
subsequent sanctions response (that we’ll create below). We’re using the clientRequestId from the test context that we set as part of the payment initiation request message definition earlier, as this identifier is specific to our individual test run and can therefore be used to retrieve the messages that have been generated by our test from the message bucket. |
Next is our response definition. Similar to the payment initiation request, the key here is that we’ll need to provide a generator function implementation to construct the sanctions response that is sent by the test framework to the tutorial application in the test.
@Bean
MessageDefinition<SanctionsResponse> sendSanctionsResponse() {
return new DefaultMessageDefinition.Builder<SanctionsResponse>()
.withType(SanctionsTypes.SANCTIONS_RESPONSE)
.withDocumentTypeClass(SanctionsResponse.class)
.withDestination("SANCTIONS_RESPONSE")
.withGenerator(props -> {
SanctionsResponse sanctionsResponse = new SanctionsResponse(); (1)
sanctionsResponse.setHeader(HeaderUtils.makeHeader("Sanctions", ContextUtils.getClientRequestId())); (2)
sanctionsResponse.getHeader().getTechnical().setOriginalEventId(((SanctionsRequest) PreviousMessages.getLastMessage(SanctionsTypes.SANCTIONS_REQUEST, false).getDocument()).getHeader().getTechnical().getEventId()); (3)
sanctionsResponse.setPayload(new SanctionsResponsePayload()); (4)
return sanctionsResponse;
})
.withCorrelatingIdGet(doc -> Optional.ofNullable(ContextUtils.getClientRequestId()))
.build();
}
| 1 | The first step in constructing the generator implementation involves creating a new SanctionsResponse using the default no-args constructor. |
| 2 | We set the header on the response using the sample systems provided HeaderUtils class, passing the test context clientRequestId as a transactionId. |
| 3 | We then set the originalEventId on the Technical object within the SanctionsResponse Header. We use the "PreviousMessages" capability of the test framework, which allows us to retrieve the previously created sanctions request message, to pass the Header.Technical.EventId from the sanctions request to the originalEventId here. If you remember, in CON2 - Writing your own connector (Kafka), we stored the Header.Technical.EventId from the sanctions request in the correlation store as part of the sanctionsSendConnector definition so that we could correlate the subsequent async response with the request. We then correlated the response as part of the sanctionsReceiveConnector definition using the Header.Technical.OriginalEventId value, which is equal to the Header.Technical.EventId on the sanctions request. So, we’re replicating that behaviour here, and ensuring that the correlation stage in the sanctionsReceiveConnector doesn’t fail in the test! (Time for a coffee after reading all of that I think). |
| 4 | The final step in the generator implementation is to create a new SanctionsResponsePayload using the default no-args constructor and add this to the SanctionsResponse. |
One other thing to mention here is that we use the destination field to define which topic we’re going to produce the response to (in this case, "SANCTIONS_RESPONSE"). Again, as message definitions are protocol independent, if we were using a JMS transport for the test, we’d provide a JMS queue name to this field.
Finally, we again need to consider how the test transport will be implemented. As mentioned above, we’re going to use the provided KafkaTestTransporter:
@Bean
public MessageTransport sanctionsKafkaTransport(MessageDefinition<SanctionsRequest> sanctionsRequestMessageDefinition,
MessageDefinition<SanctionsResponse> sanctionsResponseMessageDefinition,
ClassicActorSystemProvider actorSystem) {
return new KafkaTestTransporter.Builder<SanctionsRequest,SanctionsResponse>()
.withIdentifier("sanctions") (1)
.withPropertiesPath("sanctions") (2)
.withReceiveMessageDefinition(sanctionsRequestMessageDefinition) (3)
.withSendMessageDefinition(sanctionsResponseMessageDefinition) (4)
.withActorSystem(actorSystem) (5)
.build();
}
Let’s take a look at the key parts of this:
| 1 | Again, we provide a unique identifier for the transport |
| 2 | We’re going to retrieve our configuration for the Kafka transport using the configuration under the "sanctions" path in the ipf-tutorial-app module application.conf file |
| 3 | Here we pass our receive message definition, which from the test framework’s perspective, is the request message definition |
| 4 | Here we pass our send message definition, which from the test framework’s perspective, is the response message definition |
| 5 | Finally, we then pass the actor system |
That’s all our sanctions system related setup done, so let’s move onto the fraud system.
The Fraud System Configuration Class
As before, we’ll start by creating a FraudConfig class in the com.iconsolutions.ipf.tutorial.test.config package and then add the fraud message types and definitions as required.
For the "fraud response" message definition, we can use a similar generator implementation to that used for the "sanctions response", but we don’t need to worry about send/receive correlation here as the fraud system communicates synchronously (via HTTP).
.withGenerator(params -> {
var fraudResponse = new OlafResponse();
fraudResponse.setHeader(HeaderUtils.makeHeader("Fraud", ContextUtils.getClientRequestId()));
fraudResponse.setPayload(new FraudFeedbackPayload(new FraudFeedback(new FraudFeedback.PaymentStatusChanged("FraudFeedbackOK", "0"), null)));
return fraudResponse;
})
}
With this generator implementation provided, see if you can now create the message type and definitions in the FraudConfig class yourself. When you’re ready, compare your attempt with the solution provided below:
public class FraudConfig {
@Bean
MessageDefinition<OlafResponse> fraudResponseMessageDefinition() {
return new DefaultMessageDefinition.Builder<OlafResponse>()
.withType(FraudTypes.FRAUD_RESPONSE)
.withCausedByType(FraudTypes.FRAUD_REQUEST)
.withDocumentTypeClass(OlafResponse.class)
.withCorrelatingIdGet(response -> Optional.of(ContextUtils.getClientRequestId()))
.withGenerator(params -> {
var fraudResponse = new OlafResponse();
fraudResponse.setHeader(HeaderUtils.makeHeader("Fraud", ContextUtils.getClientRequestId()));
fraudResponse.setPayload(new FraudFeedbackPayload(new FraudFeedback(new FraudFeedback.PaymentStatusChanged("FraudFeedbackOK", "0"), null)));
return fraudResponse;
})
.build();
}
@Bean
MessageDefinition<OlafRequest> fraudRequestMessageDefinition() {
return new DefaultMessageDefinition.Builder<OlafRequest>()
.withType(FraudTypes.FRAUD_REQUEST)
.withDocumentTypeClass(OlafRequest.class)
.withFromStringMapper(serialisedString -> SerializationHelper.stringToObject(serialisedString, OlafRequest.class))
.withCorrelatingIdGet(fraudRequest -> Optional.of(ContextUtils.getClientRequestId()))
.build();
}
enum FraudTypes implements MessageType {
FRAUD_REQUEST("fraud request"),
FRAUD_RESPONSE("fraud response");
private final String name;
FraudTypes(String name) {
this.name = name;
}
@Override
public String getName() {
return name();
}
@Override
public Set<String> getAliases() {
return Set.of(name);
}
}
}
The final step in setting up the config is, again, to define the relevant transport to handle this request/response. Although we’re going to be using a HTTP message transport again, unlike for the payment initiation case, the test framework is receiving a request from the tutorial application and sending back a response in this scenario. We therefore need to use a "consumer" HTTP transport here (rather than the "sender" one used previously):
@Bean
public MessageTransport fraudTransport(@Value("${fraud.http.client.port}") String port,
MessageDefinition<OlafRequest> fraudRequestMessageDefinition,
MessageDefinition<OlafResponse> fraudResponseMessageDefinition,
ClassicActorSystemProvider actorSystem) {
return new HttpConsumerTestTransporter.Builder()
.withIdentifier("fraud")
.withPort(Integer.parseInt(port)) (1)
.withOperation(new HttpOperation.Builder<>("v1", fraudRequestMessageDefinition, fraudResponseMessageDefinition).withHttpMethod(HttpMethod.POST).build()) (2)
.withActorSystem(actorSystem)
.build();
}
The transport implementation here is very similar to the other test transports we’ve done before. The key differences are:
| 1 | We need to define the port that the transport will be listening on. In this instance, we’re injecting the port using the Spring @Value annotation and referencing the config value defined in the ipf-tutorial-app module application.conf file |
| 2 | We need to configure a HttpOperation for the transport.
To construct the HttpOperation, we use the builder and specify these four parameters:
|
That’s all our fraud system setup now done.
Executing Our Test (Again)
We’re now almost ready to execute our test again. As before, we need to first add our new sanctions and fraud system config files to the runner:
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Import({AllTestConfig.class, FraudConfig.class, SanctionsConfig.class, InitiationConfig.class, DummyODSConsumer.class})
public class FeatureTestRunner extends IPFFeatureTestRunner implements KafkaIntegrationTestSupport, MongoIntegrationTestSupport {...}
And that’s it! Try running the test again by right-clicking on the FeatureTestRunner class and clicking run.
You should see something like this after the test has completed:
Asserting Values
All of the checks up until this point have been about flows being initiated, requests sent and responses received. We can also assert the content of the requests and responses. For example we can enhance the sanctions received request checking its values:
And Sanctions receives a 'sanctions request' with values:
| payload.filteringRequest.amount.currency | US |
You can continue to build out these checks and assertions as per your IPF flow and messages being exchanged.
Running Your Test in Maven
Finally, if we want to run our tests as part of the Maven build, we need to add an extra build plugin to ensure they are executed. To do this, we need to add the Maven failsafe plugin to our pom.xml as follows:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>**/*Runner.java</include>
</includes>
<excludes>
<exclude>**/*Test.java</exclude>
<exclude>**/*Tests.java</exclude>
<exclude>**/*InProgressRunner.java</exclude>
<exclude>**/*RepeatRunner.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
Exercise
As an exercise, try adding steps in the happy path story for the following no-op stages in the payment journey that we didn’t include in our BDD which are:
-
duplicate check passed
-
account validation passed
-
clear and settle passed
Some hints:
-
You’ll need to use the “
a {event name} event is raised” step name format. Think about whether this is aThenorWhenstep type by considering the perspective of the test framework. -
As these stages don’t send/receive any messages, you won’t need to create any additional message configurations here.
When you’re ready, you can compare your solution to the story definition in the add_tests solution.