Test JNW-based tasks
The worker-task-test library is a task-testing framework based on the Java Native Worker (JNW) approach. The library provides functions to configure and run tasks with different types of input data (for example, empty input or with single and multiple rows) or execute a chain of tasks and contains a convenient assertion mechanism.
Integrate worker-task-test into project
To integrate worker-task-test into an existing or new project, perform the following steps, accounting for the the JNW compatibility matrix:
Add a version of worker-task-test to the
<properties>section of a Maven project:<properties>
<wf.worker-task-test.version>1.0.16</wf.worker-task-test.version>
<!--
1.0.16 is an earliest version which supports features described in this article.
You may need to look for recent version.
-->
</properties>If your project is a part of the
workfusion-metapipeline, add a dependency to worker-task-test into thecomponents.yamlfile of theworkfusion-metaproject:your-project:
dependencies:
- worker-task-testImport
worker-task-test-bomin the<dependencyManagement>section of the Maven project. Also, add any required module of worker-task-test to the<dependencies>section of the Maven project:<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-bom</artifactId>
<version>${wf.worker-task-test.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
As of version 1.0.14, groupId and the library package were changed to com.workfusion.jnw.test.
Prepare test class and modify execution context
To prepare a test class and modify the execution context, do as follows:
Add the
@JnwJunitConfigannotation at the class level:@JnwJunitConfig
class TestClassName {
// tests methods
}Add the needed classes and packages:
If you have configuration classes with beans you want to override or use in your tests, add them to the
classesproperty in the@JnwJunitConfigannotation:@JnwJunitConfig(classes = {FirstConfigurationClass.class, SecondConfigurationClass.class})
class TestClassName {
// tests methods
}If you have separate packages containing configurations and beans you want to override or use in your tests, add them to the
basePackagesproperty in the@JnwJunitConfigannotation:@JnwJunitConfig(basePackages = {"user.configuration.pacage", "user.beans.package"})
class TestClassName {
// tests methods
}infoYou do not need to add the com.workfusion.spa.core, com.workfusion.spa.jnative.worker, and com.workfusion.odf2.jnw packages to
basePackagesas they are added automatically.cautionThe framework automatically adds the test class package to
ComponentScan. The last part of the package is removed if it contains the word "task". If your configuration exists in the package, it is added to the test context. If you do not want to automatically add any configuration, move it to another package.
To change or add properties in the
application.ymlconfiguration file, use thepropertiesproperty in the@JnwJunitConfigannotation:@JnwJunitConfig(properties = {"it.test.value=testValue"})
class TestClassName {
// tests methods
}
By default, you do not need to create application.yml as it is created automatically in the target folder. If you have your application.yml file, put it in the resources folder. If the file has a specific name, use this construct to change the configuration file for the current test class:
@JnwJunitConfig()
@TestPropertySource(properties = {"spring.config.location = classpath:test-configuration.yml"})
class TestClassName {
// tests methods
}
Default application.yml file
spring:
main:
web-application-type: none
allow-bean-definition-overriding: true
application:
name: odf2-jnw-regression
profiles:
active: ct-worker,execution-worker
cloud:
zookeeper:
enabled: false
config:
enabled: false
discovery:
enabled: false
register: false
service-registry:
auto-registration:
enabled: false
management:
logging:
enabled: false
secure:
storage:
applicationId: worker-application
safe:
customer:
default: local_WFApp
internal: local_WFInternal
type: local
serverApi: ${java.io.tmpdir}/sms_local
platformId: WFApplication
client:
certificate:
keyPass:
bep:
worker:
ct:
rabbitmq:
billing:
exchange: billing-test-exchange
routing-key: test-billing
hazelcast:
group:
name: test-hz
# enable mock beans for OOTB JNW modules for test context
jnw-module-service-mock-enabled: true
wf:
application:
host: localhost
url: localhost
bot:
plugin:
internal:
host: localhost
url: /localhost
ocr:
completion_polling:
interval:
seconds: 5
timeout:
seconds: 60
jwt:
cache:
token_reuse_period:
minutes: 5
ds:
datasource:
url: jdbc:h2:mem:testdb;MODE=MSSQLServer
s3:
endpoint:
url: localhost
access-key: accessKey
secret-key: secretKey
Run and configure processor
To execute a processor, choose one of the approaches to follow:
TaskRunner: an abstraction around the processor class and task configuration that allows you to reuse it with different inputs and not copy the entire configuration if you only need to change one parameter. The approach makes your codebase look clearer and cleaner.SingleTaskLauncher: an old approach that allows you to createTaskInput<TaskInputWithContext>and execute a task. The approach does not have the ability to quickly change inputs and cannot be reused.SingleTaskLauncherallows you to create tasks with configuration and inputs in one box and produce a single result that can be easier to assert than the result ofTaskRunner.
The TaskRunner approach is preferable for use in tests since the API has more flexible functionality for TaskRunners directly. However, if you want a single instance of TaskResult and do not need to test many cases, using SingleTaskLauncher makes sense.
TaskRunner approach
To run and configure a task with the TaskRunner approach, see the sections below.
TaskRunner initialization and configuration
To initialize a TaskRunner, you have several options JnwTaskFactory provides:
Initialize
TaskRunnerfrom the processor class without configuration:@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = taskFactory.createTaskRunner(TaskProcessor.class);
}Initialize
TaskRunnerwithTaskInstance, whereTaskInstanceis a configuration builder that has the same structure asSingleTaskLauncherbut is unable to add inputs:@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskInstance taskInstance = new TaskInstance(TaskProcessor.class)
.withVariationId(2L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", true))
.withTaskConfiguration(builder -> builder.add("taskConfigValue", 20))
.isSchemaBased(true);
TaskRunner taskProcessorRunner = taskFactory.createTaskRunner(taskInstance);
}Initialize
TaskRunnerfrom the processor class and theTaskInstanceunary operator.@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = taskFactory.createTaskRunner(TaskProcessor.class, taskInstance -> taskInstance
.withVariationId(2L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", true))
.withTaskConfiguration(builder -> builder.add("taskConfigValue", 20))
.isSchemaBased(true));
}
Once TaskRunner is initialized, you can modify or update the TaskRunner configurations.
Modify creates a new
TaskRunnerinstance with configuration changes; can be done using themodify()method with aTaskInstanceunary operator or methods thatTaskRunnerduplicated fromTaskInstance:@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = taskFactory.createTaskRunner(TaskProcessor.class, taskInstance -> taskInstance
.withVariationId(2L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", true))
.withTaskConfiguration(builder -> builder.add("taskConfigValue", 20))
.isSchemaBased(true));
TaskRunner newTaskProcessorRunner = taskProcessorRunner
.withVariationId(3L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", false));
TaskRunner newTaskProcessorRunnerByMethod = newTaskProcessorRunner.modify(taskInstance -> taskInstance
.withVariationId(4L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", false))
.isSchemaBased(false));
Assertions.assertThat(taskProcessorRunner.getInstanceConfiguration().getConfigurationId()).isEqualTo(2L);
Assertions.assertThat(taskProcessorRunner.getInstanceConfiguration().getConfigurationJsonData()).isEqualTo("{\"variationConfigValue\":true}");
Assertions.assertThat(taskProcessorRunner.isSchemaBased()).isTrue();
Assertions.assertThat(newTaskProcessorRunner.getInstanceConfiguration().getConfigurationId()).isEqualTo(3L);
Assertions.assertThat(newTaskProcessorRunner.getInstanceConfiguration().getConfigurationJsonData()).isEqualTo("{\"variationConfigValue\":false}");
Assertions.assertThat(newTaskProcessorRunner.isSchemaBased()).isTrue();
Assertions.assertThat(newTaskProcessorRunnerByMethod.getInstanceConfiguration().getConfigurationId()).isEqualTo(4L);
Assertions.assertThat(newTaskProcessorRunnerByMethod.isSchemaBased()).isFalse();
}Update does not create a new instance but replaces the configuration in the actual
TaskRunner; can only be done using theupdate()method with aTaskInstanceunary operator:@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = taskFactory.createTaskRunner(TaskProcessor.class, taskInstance -> taskInstance
.withVariationId(2L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", true))
.withTaskConfiguration(builder -> builder.add("taskConfigValue", 20))
.isSchemaBased(true));
taskProcessorRunner.update(taskInstance -> taskInstance
.withVariationId(3L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", false)));
Assertions.assertThat(taskProcessorRunner.getInstanceConfiguration().getConfigurationId()).isEqualTo(3L);
Assertions.assertThat(taskProcessorRunner.getInstanceConfiguration().getConfigurationJsonData()).isEqualTo("{\"variationConfigValue\":false}");
Assertions.assertThat(taskProcessorRunner.isSchemaBased()).isTrue();
}
If you perform modify or update actions, keep in mind that you are working with an existing configuration, not a blank one.
TaskRunner execution with single input row
TaskRunner contains many runWIthInput() methods with different signatures. They all return a TaskOutput object that contains all the execution information, specifically InputRows and TaskResults, as the result of executing InputRow.
If you need to execute TaskRunner with only one input row, you have several options supported by TaskRunner:
Key-value pair. The main idea of the option is to make your code cleaner and easier to read when your input data contains only one column.
@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner initialization
TaskOutput result = taskProcessorRunner.runWithInput("columnName", new CustomObject());
//assert result variable
}Map<String, Object>. Sometimes, in tests, you work withMapobjects, for example, when you take input from a file or have a method that provides input in theMapformat. In this case, the signature helps you with the task processor execution.@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner initialization
TaskOutput resultFromImmutableMap = taskProcessorRunner.runWithInput(ImmutableMap.of("firstColumn", new CustomObject(), "secondColumn", true));
TaskOutput resultFromMethod = taskProcessorRunner.runWithInput(getInputDataFromFile("fileName.csv"));
//assert result variables
}
private Map<String, Object> getInputDataFromFile(String fileName) {
//realization of method
}InputRowobject.InputRowis an interface that has two implementations:JsonBasedInputRowusesObjectMapperto cast the value of an object to a JSON string. It also has methods that allow you to add a plainStringvalue to a row.StringBasedInputRowworks with plainStringvalues.
@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner initialization
InputRow jsonBasedInputRow = new JsonBasedInputRow()
.withInputData("firstColumn", new CustomObject())
.withInputData("secondColumn", "json based String")
.withStringInputData("thirdColumn", "plain String");
InputRow stringBasedInputRow = new StringBasedInputRow()
.withInputData("firstColumn", "{\"variationConfigValue\":false}")
.withInputData("secondColumn", "plain String");
TaskOutput resultFromJsonBasedInputRow = taskProcessorRunner.runWithInput(jsonBasedInputRow);
TaskOutput resultFromStringBasedInputRow = taskProcessorRunner.runWithInput(stringBasedInputRow);
//assert result variables
}Inputobject.Inputdata is a container forInputRowobjects. If you have one row, you can add only one row to theInputdata and use it to execute the task processor. At the same time, if you try to execute theInputdata without rows inside,TaskRunnercreates an empty row and executes it.@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner initialization
InputRow jsonBasedInputRow = new JsonBasedInputRow()
.withInputData("firstColumn", new CustomObject())
.withInputData("secondColumn", "json based String")
.withStringInputData("thirdColumn", "plain String");
InputRow stringBasedInputRow = new StringBasedInputRow()
.withInputData("firstColumn", "{\"variationConfigValue\":false}")
.withInputData("secondColumn", "plain String");
Input inputWithJsonBasedRow = new Input(jsonBasedInputRow);
Input inputWithStringBasedRow = new Input().addRow(stringBasedInputRow)
Input emptyInput = new Input();
TaskOutput resultFromJsonBasedInputRow = taskProcessorRunner.runWithInput(inputWithJsonBasedRow);
TaskOutput resultFromStringBasedInputRow = taskProcessorRunner.runWithInput(inputWithStringBasedRow);
TaskOutput resultFromEmptyRow = taskProcessorRunner.runWithInput(emptyInput);
//assert result variables
}JsonBasedInputRowunary operator. Sometimes, you need to use customInputRowwith more than one column and noMapobject with the input data. In this case, you can use theJsonBasedInputRowunary operator to construct yourInputRowinside therunWithInput()method.@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner initialization
TaskOutput result= taskProcessorRunner.runWithInput(inputRow -> inputRow.withInputData("firstColumn", new CustomObject())
.withInputData("secondColumn", "json based String")
.withStringInputData("thirdColumn", "plain String"));
//assert result variables
}
TaskRunner execution with multiple input row
To execute TaskRunner with more than one InputRow, use an Input object. Input is a container for InputRow objects and can be executed by TaskRunner.
For example, executing multiple input rows looks like this:
@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner initialization
InputRow jsonBasedInputRow = new JsonBasedInputRow()
.withInputData("firstColumn", new CustomObject())
.withInputData("secondColumn", "json based String")
.withStringInputData("thirdColumn", "plain String");
InputRow stringBasedInputRow = new StringBasedInputRow()
.withInputData("firstColumn", "{\"variationConfigValue\":false}")
.withInputData("secondColumn", "plain String");
Input input = new Input()
.addRow(stringBasedInputRow)
.addRow(jsonBasedInputRow);
TaskOutput result = taskProcessorRunner.runWithInput(input);
//assert result variables
}
In case of multiple rows, you can set markers for them. The markers are also used to identify results of executing these rows in Output. To add the markers to InputRow, use the constructor and the addMarker(String marker) or addAllMarkers(String... markers) methods:
@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner initialization
InputRow jsonBasedInputRow = new JsonBasedInputRow()
.withInputData("firstColumn", new CustomObject())
.withInputData("secondColumn", "json based String")
.withStringInputData("thirdColumn", "plain String")
.addAllMarkers("successful", "object");
InputRow stringBasedInputRow = new StringBasedInputRow("failed", "string")
.withInputData("firstColumn", "{\"variationConfigValue\":false}")
.withInputData("secondColumn", "plain String");
Input input = new Input()
.addRow(stringBasedInputRow)
.addRow(jsonBasedInputRow);
TaskOutput result = taskProcessorRunner.runWithInput(input);
TaskOutput successfulResult = result.getResultByInputMarkers("successful");
TaskResult<TaskOutputData> failedResut = result.getSingleResultByInputMarkers("failed");
//assert result variables
}
InputRows can have the same markers; for example, two lines have a "successful" marker, and one has a "failed" marker. In the Output, you can filter the results by one or more markers.
SingleTaskLauncher approach
To run and configure a task with the SingleTaskLauncher builder, perform the following steps:
Inject
JnwTaskFactoryinto your test method and call thefromClass(Class<? Extend ITaskProcessor>)method:@Test
void yourTest(JnwTaskFactory taskFactory) {
//when
SingleTaskLauncher singleTaskLauncher = taskFactory.fromClass(TaskProcessor.class);
}Add input. If you want to run the task without input, do not add anything.
@Test
void yourTest(JnwTaskFactory taskFactory) {
//given
final SomeObject object = new SomeObject();
final Map<String, Object> objectMap = ImmutableMap.of() // init map with Object values
final Map<String, String> stringMap = ImmutableMap.of() // init map with String values
//when
SingleTaskLauncher singleTaskLauncher = taskFactory.fromClass(TaskProcessor.class)
.withInputData("objectKey", object)
.withInputData(objectMap)
.withStringInputData("stringKey", "stringValue")
.withStringInputData(stringMap);
}noteThe
withInputData()methods useObjectMapperto convert theObjectvalue toString. Thus, if you use theStringvalue in this method, it will have the"value"format instead ofvaluein the input.Update the JNW task that has access to task configuration and variation:
@Test
void yourTest(JnwTaskFactory taskFactory) {
//given
final SomeObject object = new SomeObject();
final Map<String, Object> objectMap = ImmutableMap.of() // init map with Object values
final Map<String, String> stringMap = ImmutableMap.of() // init map with String values
//when
SingleTaskLauncher singleTaskLauncher = taskFactory.fromClass(TaskProcessor.class)
.withInputData("objectKey", object)
.withInputData(objectMap)
.withStringInputData("stringKey", "stringValue")
.withStringInputData(stringMap)
.withVariationId(2L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", true))
.withTaskConfiguration(builder -> builder.add("taskConfigValue", 20))
.isSchemaBased(true); // By default is false
}Build or build and then run your task:
@Test
void yourTest(JnwTaskFactory taskFactory) {
//given
final SomeObject object = new SomeObject();
final Map<String, Object> objectMap = ImmutableMap.of() // init map with Object values
final Map<String, String> stringMap = ImmutableMap.of() // init map with String values
//when
SingleTaskLauncher singleTaskLauncher = taskFactory.fromClass(TaskProcessor.class)
.withInputData("objectKey", object)
.withInputData(objectMap)
.withStringInputData("stringKey", "stringValue")
.withStringInputData(stringMap)
.withVariationId(2L)
.withVariationJsonConfig(builder -> builder.add("variationConfigValue", true))
.withTaskConfiguration(builder -> builder.add("taskConfigValue", 20))
.isSchemaBased(true); // By default is false
TaskInput<TaskInputWithContext> taskInputWithContext = singleTaskLauncher.build();
TaskResult<TaskOutputData> result = singleTaskLauncher.buildAndRun();
//then
// Assert result
}
To run Taskinput<TaskInputWithContext>, you can use JnwTaskFactory and get the same result as in result variable: taskFactory.run(taskInputWithContext)
Run and configure chain of task processors
To create a chain of TaskRunners or task processors, use one of the two options:
Get
ChainTaskRunnerBuilderprovided byJnwTaskFactory. It allows you to build task chains byTaskRunners, a task processor class, or a class with the configuration.@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner taskProcessorRunner = ... // TaskRunner for TaskProcessor.class initialization
ChainTaskRunner chain = taskFactory.getChainTaskRunnerBuilder()
.addTaskRunner(taskProcessorRunner)
.addTaskRunner(taskProcessorRunner, taskInstance -> taskInstance.withTaskConfiguration("{task configuration}")) //in this case, a new TaskRunner instance will be created with the updated configuration
.addProcessor(NewTaskProcessor.class)
.addProcessor(NewTaskProcessor.class, taskInstance -> taskInstance.withTaskConfiguration("{task configuration}")) //in this case, a new TaskRunner instance
.build();
ChainTaskOutput result = chain.runWithInput("singleColumn", new CustomObject());
//assert result variables
}Create a
ChainTaskRunnerinstance. A newChainTaskRunnerinstance has a limitation on methods that allow processors to be added to the chain. It can only add or update the existingTaskRunners.@Test
void yourTest(JnwTaskFactory taskFactory) {
TaskRunner firstTaskProcessorRunner = ... // TaskRunner for FirstTaskProcessor.class initialization
TaskRunner secondTaskProcessorRunner = ... // TaskRunner for SecondTaskProcessor.class initialization
ChainTaskRunner chain = new ChainTaskRunner()
.addTaskRunner(firstTaskProcessorRunner)
.addTaskRunner(secondTaskProcessorRunner)
.addTaskRunner(firstTaskProcessorRunner, taskInstance -> taskInstance.withTaskConfiguration("{task configuration}"));
ChainTaskOutput result = chain.runWithInput("singleColumn", new CustomObject());
//assert result variables
}
ChainTaskOutput contains all the information about each step and each InputRow. It is a custom implementation of the Output interface and has additional methods to get results for a specific TaskRunner or processor.
If you are uncomfortable with this way of executing chaining, you can run TaskRunner using the runWithOutput() method. In this case, the result of the method is of the ChainTaskOutput type and contains information about the current execution and the previous one. At the same time, you can create an Input object from Output and use it to execute TaskRunner. Then, the output is of the TaskOutput type and does not contain information about previous executions. The getAllTaskResults() method returns the same values in both cases, while the getInputRows() method does not since, in the first case, the input rows from the first processor execution are returned.
static Output result;
@Test
@Order(1)
void yourFirstTest(JnwTaskFactory taskFactory) {
TaskRunner firstTaskProcessorRunner = ... // TaskRunner for FirstTaskProcessor.class initialization
Input input = ... //init Input
result = chain.runWithInput(input); //TaskOutput instance
//assert result variables
}
@Test
@Order(2)
void yourSecondTest(JnwTaskFactory taskFactory) {
TaskRunner secondTaskProcessorRunner = ... // TaskRunner for SecondTaskProcessor.class initialization
TaskOutput taskOutputResult = secondTaskProcessorRunner.runWithInput(new Input(result)); //TaskOutput instance
result = secondTaskProcessorRunner.runWithOutput(result); //ChainTaskOutput instance
assertThat(result.getAllTaskResults()).isEqualTo(taskOutputResult.getAllTaskResults());
//assert result variables
}
Chain execution with schema-based tasks
You can set the schemaBased parameter to true or false in each task configuration. The setting affects how the previous task's Output is converted into Input for the current task:
IsSchemaBasedis set tofalse. TheInputdata contains only the columns from the private task as if running a classic input-output task.IsSchemaBasedis set totrue. TheInputdata for the current task contains the input data from the previous task combined with the output data, resulting in a global context for eachInputRow. The input contains the fullInputRowcontext with all columns.
In its current state, the framework does not support contracts or filter task inputs; instead, it uses the full context.
If you use ChainTaskRunner and one of the tasks is schema-based, each task in the chain is treated as a schema-based one.
If you run a chain of TaskRunners as separate tests and want to use a contextual approach (unless your task has isSchemaBased set to true), set the true flag in the runWithOutput method.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@JnwJunitConfig
class JsonBasedEmailToInvoiceTest {
private static Output stepData;
private static TaskRunner emailProducerTaskRunner;
private static TaskRunner emailToInvoiceTaskRunner;
@BeforeAll
static void initTaskRunners(JnwTaskFactory taskFactory) {
emailProducerTaskRunner = taskFactory.createTaskRunner(EmailProducerTaskProcessor.class);
emailToInvoiceTaskRunner = taskFactory.createTaskRunner(EmailToInvoiceTaskProcessor.class);
}
@Test
@Order(1)
@DisplayName("should produce Email object")
void shouldProduceEmailObject() {
// when
stepData = emailProducerTaskRunner.runWithInput(Collections.emptyMap());
// then
assertThat(stepData).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsOnlyKeys(EmailApiColumn.EMAIL);
}
@Test
@Order(2)
@DisplayName("should convert Email to Invoice")
void shouldConvertEmailToInvoice() {
// when
stepData = emailToInvoiceTaskRunner.runWithOutput(stepData, true); // The second parameter is the isSchemaBased flag. Use it when the TaskRunner is not schema-based but you want to execute it in the context space.
// then
assertThat(stepData).isSuccessful().hasSingleRowThat()
.hasSize(2)
.containsOnlyKeys(EmailApiColumn.EMAIL, EmailApiColumn.INVOICE);
}
}
Work with Output
Output objects provide a lot of information about the input data and execution results, allowing you to use them to create simple chaining logic or conveniently assert results depending on your case. The Output interface has two implementations, both of which can return input rows, results, errors and filter results by InputRow markers:
The main difference between TaskOutput and ChainTaskOutput is that TaskOutput contains information about the execution of a single task only. At the same time, ChainTaskOutput encloses a collection of TaskOutputs and can filter it by the TaskRunner info.
TaskOutput methods
TaskOutput provides the following methods:
| Method | Description |
|---|---|
getAllTaskResults() | Gets all TaskResult objects. Each TaskResult corresponds to one InputRow. |
getResultRows() | Gets result rows or accumulates rows from successful results. |
getSuccessfulTaskOutputData() | Gets only successful results. |
getTaskErrors() | Gets only failed results. |
getInputRows(), getInput() | Get input rows as List or as Input object. |
| Get results by the InputRow markers (List<TaskResult<TaskOutputData>>). |
| Get a single result by the InputRow markers (TaskResult<TaskOutputData>). |
getResultByInputRow(InputRow inputRow) | Gets a result by InputRow. |
| Get TaskOutput by the InputRow markers. |
| Get TaskOutput by InputRow. |
Use the getOutputByInputMarkers(Set<String> markers) and getOutputByInputMarkers(String... markers) methods if you have more than one InputRow with the same markers.
ChainTaskOutput methods
ChainTaskOutput provides the following methods:
| Method | Description |
|---|---|
getAllTaskResults() | Gets the last step TaskResult objects. Each TaskResult corresponds to one InputRow. |
getResultRows() | Gets result rows or accumulates rows from successful results in the final processor. |
getSuccessfulTaskOutputData() | Gets only successful final results. |
getTaskErrors() | Gets only failed final results. |
| Get input rows as the List or Input objects from the first processor. |
| Get results by the InputRow markers (List<TaskResult<TaskOutputData>>). |
| Get a single result by the InputRow markers (TaskResult<TaskOutputData>). |
getResultByInputRow(InputRow inputRow) | Gets result by InputRow. |
| Get ChainTaskOutput by the InputRow markers. |
getOutputByInputRow(InputRow inputRow) | Gets ChainTaskOutput by InputRow. |
| Get TaskOutput by the TaskRunner info (TaskRunner, UUID, processor class, processor name). |
getRunnersChain() | Gets the TaskRunners chain. |
For usage examples, refer to View code examples.
When you filter ChainTaskOutput by InputRow, a new ChainTaskOutput instance is returned that contains only your InputRow execution result and its children in the chain.
Apply assertion mechanism
The assertion mechanism of TaskResult<TaskOutputData>, TaskOutputData, TaskOutputRow is provided by the com.workfusion.jnw.test:worker-task-test-assertions dependency that you can use without worker-task-test-core. This means you can use it in projects where you do not have the option to use worker-task-test-core or in specific JUnit tests.
Do not add com.workfusion.jnw.test:worker-task-test-assertions to your project dependencies if use worker-task-test-core since the dependency is added as transitive.
If you use worker-task-test-core, the assertion class is com.workfusion.jnw.test.assertion.TaskResultAssert. If you use only the assertion module, the assertion class is com.workfusion.jnw.test.assertion.ResultAssert.
The difference between the classes is that TaskResultAssert supports the Output interface and Collection<TaskResult<TaskOutputData>> as a parameter for the assertThat() method, while ResultAssert does not.
In the following sections, TaskResultAssetClass is used.
TaskResult assertion
To assert a TaskReuslt object, use the TaskResultAssert.assertThat(TaskResult<TaskOutputData> data) static method. You get a ResultAssert object that allows you to check whether the result is successful or failed and, depending on it, returns you SuccessfulTaskResultAssert or FailedTaskResultAssert. At the same time, ResultAssert has methods to assert the TaskResult metadata.
@Test
void yourTest(JnwTaskFactory taskFactory) {
InputRow successfulInput = ... //init InputRow
InputRow failedInput = ... //init InputRow
TaskResult<TaskOutputData> successfulResult = taskFactory.fromClass(TaskProcessor.class)
.withInputRow(successfulInput)
.buildAndRun();
TaskResult<TaskOutputData> failedResult = taskFactory.fromClass(TaskProcessor.class)
.withInputRow(failedInput)
.buildAndRun();
//assert
TaskResultAssert.assertThat(successfulResult).isSuccessful(); // expects the outcome to be successful
TaskResultAssert.assertThat(failedResult).isFailed(); // expects the outcome to be failed
TaskResultAssert.assertThat(successfulResult)
.isMetadataEqualTo(ImmutableMap.of("tetKey", "testValue"));
}
If you expect a successful result but your TaskResult has a FAILED status, the framework displays a message and error stack trace in the console to assist with troubleshooting.
TaskOutputData assertion
After verifying your result status, the assertion mechanism allows you to proceed to the TaskOutputData assertion. You can also use the same TaskResultAssert.assertThat() construct as for TaskResult to assert it.
Various methods in the class are used to check the size of rows, if a row satisfies some requirements, if it contains columns and then retrieve columns by a column name, and so on. See the methods you can use to make your assertions cleaner below:
| Method | Description |
|---|---|
| Contain methods. |
extractRowByIndex(int index) | Extracts a row by index. |
| Satisfy methods. |
hasSingleRowThat() | Returns TaskOutputRowAssert for a single column in the data. |
containsColumns(String... columnNames) | Checks columns. |
extractingRowsByColumnName(String key) | Extracts rows by column (returns the implementation of AbstractListAssert for TaskOutputRow). |
| Extract a column by a column name as ListAssert<String> or ListAssert<T>, where T is the class you want to cast all the values in the column to. |
| Check the number of rows in the data. |
See a code example:
@Test
void yourTest(JnwTaskFactory taskFactory) {
InputRow successfulInput = ... //init InputRow
TaskOutputData data = ... //init TaskOutputData
TaskResult<TaskOutputData> successfulResult = taskFactory.fromClass(TaskProcessor.class)
.withInputRow(successfulInput)
.buildAndRun();
//assert
TaskResultAssert.assertThat(successfulResult).isSuccessful()
.hasSize(2)
.containsColumns("result")
.allRowsSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result").startsWith("first row data from"));
}
For usage examples, refer to View code examples.
TaskOutputRow assertion
To assert TaskOutputRow, use the TaskOutputRowAssert class. To access it, you can use some methods from SuccessfulTaskResultAssert or the TaskResultAssert.assertThat() construction. Since TaskOutputRow is a map where the key is the column name and the value is the value of the column value, TaskOutputRowAssert has methods from standard MapAssert.
| Method | Description |
|---|---|
| Check if a row contains values. |
| Check if a row contains entries. |
| Assert a specific value. |
| Check the number of columns in a row. |
If the row you assert is a TaskOutputRow, all values are converted to String, but if your row is JsonTaskOutputRow, all row values are cast to and compared to the object type in the assertion.
See a code example:
@Test
void yourTest(JnwTaskFactory taskFactory) {
InputRow successfulInput = ... //init InputRow
JsonTaskOutputRow data = ... //init JsonTaskOutputRow
TaskResult<TaskOutputData> successfulResult = taskFactory.fromClass(TaskProcessor.class)
.withInputRow(successfulInput)
.buildAndRun();
//assert
TaskResultAssert.assertThat(successfulResult).isSuccessful().hasSinglehasSingleRowThat()
.hasSize(1).containsEntry("result", "first row data from row");
TaskResultAssert.assertThat(data).hasSize(2).containsEntry("result", new CustomObject()); // the value from the row will be converted to a CustomObject type and will be compared with the new CustomObject().
}
For usage examples, refer to View code examples.
TaskError assertion
To assert the TaskError objects, use the FailedTaskResultAssert class. To access it, you can use the class constructor or the TaskResultAssert.assertThat() method. This type of assertion allows you to check the error type or message:
| Method | Description |
|---|---|
| Standard message approval methods. |
| Check the error type. |
| Assert messages and stack traces as StringAssert. |
See a code example:
@Test
void yourTest(JnwTaskFactory taskFactory) {
InputRow failedInput = ... //init InputRow
TaskError error = ... //init TaskError
TaskResult<TaskOutputData> failedResult = taskFactory.fromClass(TaskProcessor.class)
.withInputRow(failedInput)
.buildAndRun();
//assert
TaskResultAssert.assertThat(failedResult).isFailed()
.isErrorInstanceOf(ReadInputException.class)
.hasMessageStartingWith("Input with")
.hasMessageEndingWith("name can not be read.");
TaskResultAssert.assertThat(error)
.isErrorInstanceOf(IllegalArgumentException.class);
}
For usage examples, refer to View code examples.
Output object assertion
The Output object assertion is only available when you use the Runner abstraction. It can enable you to work with one or more output results. To assert an Output object, use the OutputAssert class provided by a constructor or the TaskResultAssert.assertThat() method. The methods provided by the class are similar to TaskResult assertions.
| Method | Description |
|---|---|
isSuccessful() | Checks whether all TaskResult outputs are successful and then provides a ListRowsAssert object that extends AbstractListAssert for the TaskOutptuRow objects. ListRowsAssert has the hasSingleRowThat() method that provides TaskOutputRowAssert if the list contains only one row and also has methods to extract rows and columns by a column name as in the TaskOutputData assertion. |
isPartiallySuccessful() | Use the method if your output object contains successful and failed TaskResult objects. The result of the assertion is ListRowsAssert that contains only successful result rows from Output. |
isFailed | Checks whether all TaskResult outputs are failed and then provides a ListTaskErrorAssert object that extends AbstractListAssert for the TaskError objects. ListTaskErrorAssert has the hasSingleTaskErrorThat() method that provides FailedTaskResultAssert if the list contains only one error. |
isPartiallySuccessful() | Use the method if your output object contains successful and failed TaskResult objects. The result of the assertion is ListTaskErrorAssert that contains only TaskError from Output. |
asListOfTaskResults() | Returns ListTaskResultAssert that extends AbstractListAssert for the TaskResult<TaskOutputData> data. Use the method to create custom filters for your results or when building some specific assertion logic. |
See a code example:
@Test
void yourTest(JnwTaskFactory taskFactory) {
InputRow input = ... //init InputRow
TaskRunner taskRunenr = ... //initTaskRunner
Ouput output = ... //init Output
TaskOutput result = taskRunenr.runWithInput(input);
//assert
TaskResultAssert.assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(2) // this size check belongs to the TaskOutputRow assertion and checks the number of columns in the row
.containsEntry("testColumn", "test value")
TaskResultAssert.assertThat(output).isPartiallySuccessful()
.allRowsSatisfy(taskOutputRow -> TaskResultAssert.assertThat(taskOutputRow) // this check will be applied to every successful result row
.containsEntry("intField", 2))
.extractingColumnByKey("customObject", CustomObject.class)
.hasSize(2); // this size check belongs to the Litst of CustomObject assertion and checks the number of objects in the list
}
For usage examples, refer to View code examples.
Use additional modules
The worker-task-test library has several modules that you might need in some test cases, for example, for database support, S3 support, or when working with the secret storage. The modules are not required to run task processors.
The library of working test tasks provides the following modules:
ORMLite support module
The module enables you to mock ORMLite repositories in your tests. This means that you can create a repository, populate it with data, and check the state of the repository once the task processor runs.
The case is similar to the ODF 2 Data Store testing procedure. For more details, refer to Bot Task JUnit | ODF 2: ORM and Entity classes.
Setup
To get access to the feature, add the worker-task-test-orm-lite-module dependency to your project with the test scope and provide the module version in worker-task-test-bom:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-bom</artifactId>
<version>${wf.worker-task-test.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-orm-lite-module</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Usage
To get started with ORMLite repositories, add the OrmLiteSupport parameter to your test method signature:
private static OrmLiteRepository<User> userRepository;
@BeforeAll
static void prepareDocument(OrmLiteSupport ormLiteSupport) {
ormLiteSupport.createTables(User.class, CustomRepository.class);
userRepository = ormLiteSupport.getRepository(User.class);
userRepository.create(new User(1, "firstName"))
}
@Test
void shouldGetDocumentByUuidWithRepositoryFromCode(OrmLiteSupport ormLiteSupport) {
//given
ormLiteSupport.createTables(Document.class);
OrmLiteRepository<Document> documentRepository = ormLiteSupport.getRepository(Document.class);
documentRepository.create(newDocument);
documentRepository.create(document);
//when
TaskOutput result = ormLiteTaskRunner.runWithInput(new StringBasedInputRow(Document.UUID_COLUMN, document.getUuid().toString()));
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(2)
.containsEntry(Document.DOCUMENT_NAME_COLUMN, document.getDocumentName())
.containsEntry(Document.DOCUMENT_CONTENT_COLUMN, document.getDocumentContent());
// Data Store test
Assertions.assertThat(documentRepository.count()).isEqualTo(2);
Assertions.assertThat(documentRepository.findAll()).containsOnly(document, newDocument);
}
Depending on the test scope where you create the table, the life cycle of the table depends. For example, if you create a table in the BeforeAll scope, the table is available in all tests, and if you create a table within a test, it is available in this test only.
OrmLiteSupport has the following methods used to perform the actions with tables:
createTables(Class<?>... entities): create a table.clearTables(Class<?>... entities),clearAllCreatedTables(): clear a table.dropTables(Class<?>... entities),dropAllCreatedTables(): drop a table.getRepository(Class<T> entity): get a table.
dropAllCreatedTables() and clearAllCreatedTables() only work in the current scope. If you call the methods inside a test method, they clean up or delete only the tables created in this specific test and do not affect the tables in the Before or After scope.
The worker-task-test-orm-lite-module requires the DataSource and ConnectionSource (or DatabaseType) beans. By default, the module only contains the ConnectionSource bean that modifies all table names using the versioned template provided by DigitalWorkerInfo. The DataSource bean is not provided, and you must define it yourself or use a bean that provides the jnw-toolkit-datastores module.
If you do not want to set default ConnectionSource or DatabaseType, you can add worker-task-test.connection-source.provide=false or worker-task-test.database-type.provide=false.
If you use more than one AI Agent in one test class and want to manage repositories for each AI Agent, use MultiDigitalWorkerOrmLiteSupport instead of the OrmLiteSupport option in test methods. The utility allows you to manipulate repositories as simple OrmLiteSupport. At the same time, you can add a DigitalWorkerInfo object to the method and create a repository based on the data provided by the AI Agent information. Methods that do not require a DigitalWorkerInfo object as a parameter use the default DigitalWorkerInfo component or the one provided in the test context.
S3 module
The module provides the mechanism to mock a real Amazon S3 service and use it in your tests.
The case is similar to the ODF 2 S3 mock mechanism. For more details, refer to Bot Task JUnit | S3 mocking.
Setup
To access the feature, add the worker-task-test-s3-module dependency to your project with the test scope and provide the module version in worker-task-test-bom:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-bom</artifactId>
<version>${wf.worker-task-test.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-s3-module</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Usage
For each test run, there is an underlying in-memory S3 server ready to handle user requests. To interact with the server, inject the com.workfusion.jnw.test.s3.S3MockClient object into any test class method annotated as @Test, @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll:
@BeforeAll
static void prepareDocument(S3MockClient s3Client) {
s3Client.createBucket("test-jnw-bucket");
}
@Test
void shouldGetDocumentByUuidWithRepositoryFromCode(S3MockClient s3Client) {
// given
s3Client.createBucket("jnw-bucket");
s3Client.putObject("jnw-bucket", "test_content.txt", "test_content");
// when
TaskResult<TaskOutputData> result = taskFactory.fromClass(ReadContentFromS3TaskProcessor.class)
.withStringInputData(TaskVariable.TRANSACTION_ID.toString(), UUID.randomUuid())
.withVariationJsonConfig(builder -> builder.add("['odf2.regression.jnw.output-bucket-alias']", "jnw-bucket"))
.buildAndRun();
// then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsEntry("content", "test_content");
Assertions.assertThat(s3Client.getObjectAsString("jnw-bucket", "test_content.txt")).isEqualTo("test_content");
}
Explore the S3MockClient interface to see all methods and their descriptions.
Secret module
The module provides the mechanism for working with secret storage in your tests. The main idea of the service is to provide a convenient API for creating, updating, and clearing SecretDto in your tests.
Setup
To access the feature, add the worker-task-test-secret-module dependency to your project with the test scope and provide the module version in worker-task-test-bom:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-bom</artifactId>
<version>${wf.worker-task-test.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.workfusion.jnw.test</groupId>
<artifactId>worker-task-test-secret-module</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Usage
For each test run, there is an underlying in-memory secret store service. To interact with the server, inject the JnwSecretService object into any test class method annotated as @Test, @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll:
private final SecretDto beforeEachSecret = new SecretDto("beforeAlias", "beforeKey", "beforeValue", new Date());
@BeforeAll
static void init(JnwTaskFactory taskFactory) {
taskRunner = taskFactory.createTaskRunner(SecretStoreTestProcessor.class);
}
@BeforeEach
void beforeEach(JnwSecretService secretService) {
secretService.createSecret(beforeEachSecret.getAlias(), beforeEachSecret.getKey(), beforeEachSecret.getValue());
}
@Test
@DisplayName("should read secret dto from secret service")
void shouldReadSecretDtoFromSecretService(JnwSecretService secretService) {
//given
final String secretAlias = "alias";
final String secretKey = "key";
final String secretValue = "value";
secretService.createSecret(secretAlias, secretKey, secretValue);
//when
TaskOutput result = taskRunner.runWithInput(new StringBasedInputRow("secretAlias", secretAlias));
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(2)
.containsEntry("key", secretKey)
.containsEntry("value", secretValue);
}
JnwSecretService has the following methods used to perform the actions with SecretDto objects:
createSecret(String alias, String key, String value): create aSecretDtoobject.updateSecret(String alias, String key, String value): update aSecretDtoobject.deleteSecret(String alias): delete aSecretDtoobject.getSecret(String alias): retrieve aSecretDtoobject fromSecretService.
The scope mechanism of the module works the same as in the previous modules. A secret created in the test area is only available in the current test, but a secret created in the area before or after is available in all tests.
Do not override the global secret in the test scope as it will be deleted once the test is complete.
Apply best practices
Global TaskRunner
The TaskRunner abstraction allows you to create a global TaskRunner object for use in tests, for example, with different inputs or small configuration changes. Using the approach, you can make the code more readable and eliminate code duplication.
See the code without global TaskRunner
@JnwJunitConfig
class BasicProcessorNoGlobalTest {
private static final String configurationPropertyPath = "test.value.['for.test']";
private static final String configurationPropertyValue = "value from task configuration";
private static final String configurationPropertyResultName = "result";
@Test
@DisplayName("should run basic processor")
void shouldRunBasicProcessor(JnwTaskFactory taskFactory) {
//when
TaskResult<TaskOutputData> result = taskFactory.fromClass(BasicTestProcessor.class)
.withTaskConfiguration(builder -> builder.add(configurationPropertyPath, configurationPropertyValue))
.isSchemaBased(true)
.withStringInputData("configurationPropertyResultName", configurationPropertyResultName)
.withStringInputData("configurationPropertyPath", configurationPropertyPath)
.buildAndRun();
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsEntry(configurationPropertyResultName, configurationPropertyValue);
}
@Test
@DisplayName("should run basic processor with new configuration")
void shouldRunBasicProcessorWithNewConfiguration(JnwTaskFactory taskFactory) {
//given
final String newConfigurationPropertyValue = "value from new task configuration";
//when
TaskResult<TaskOutputData> result = taskFactory.fromClass(BasicTestProcessor.class)
.withTaskConfiguration(builder -> builder.add(configurationPropertyPath, newConfigurationPropertyValue))
.isSchemaBased(true)
.withStringInputData("configurationPropertyResultName", configurationPropertyResultName)
.withStringInputData("configurationPropertyPath", configurationPropertyPath)
.buildAndRun();
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsEntry(configurationPropertyResultName, newConfigurationPropertyValue);
}
@Test
@DisplayName("should run basic processor without input")
void shouldRunBasicProcessorWithoutInput(JnwTaskFactory taskFactory) {
//when
TaskResult<TaskOutputData> result = taskFactory.fromClass(BasicTestProcessor.class)
.withTaskConfiguration(builder -> builder.add(configurationPropertyPath, configurationPropertyValue))
.isSchemaBased(true)
.buildAndRun();
//then
assertThat(result).isFailed().isErrorInstanceOf(ReadInputException.class)
.hasMessageStartingWith("Input with")
.hasMessageEndingWith("name cannot be read.");
}
See the code with global TaskRunner
@JnwJunitConfig
class BasicProcessorTest {
private static TaskRunner taskRunner;
private static InputRow input;
private static final String configurationPropertyPath = "test.value.['for.test']";
private static final String configurationPropertyValue = "value from task configuration";
private static final String configurationPropertyResultName = "result";
@BeforeAll
static void initTaskRunner(JnwTaskFactory taskFactory) {
taskRunner = taskFactory.createTaskRunner(BasicTestProcessor.class, taskInstance -> taskInstance
.withTaskConfiguration(builder -> builder.add(configurationPropertyPath, configurationPropertyValue))
.isSchemaBased(true));
input = new StringBasedInputRow()
.withInputData("configurationPropertyResultName", configurationPropertyResultName)
.withInputData("configurationPropertyPath", configurationPropertyPath);
}
@Test
@DisplayName("should run basic processor")
void shouldRunBasicProcessor() {
//when
final TaskOutput result = taskRunner.runWithInput(input);
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsEntry(configurationPropertyResultName, configurationPropertyValue);
}
@Test
@DisplayName("should run basic processor with new configuration")
void shouldRunBasicProcessorWithNewConfiguration() {
//given
final String newConfigurationPropertyValue = "value from new task configuration";
//when
final TaskOutput result = taskRunner
.withTaskConfiguration(builder -> builder.add(configurationPropertyPath, newConfigurationPropertyValue))
.runWithInput(input);
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsEntry(configurationPropertyResultName, newConfigurationPropertyValue);
}
@Test
@DisplayName("should run basic processor without input")
void shouldRunBasicProcessorWithoutInput() {
//when
final TaskOutput result = taskRunner.runWithInput(Collections.emptyMap());
//then
assertThat(result).isFailed().hasSingleTaskErrorThat().isErrorInstanceOf(ReadInputException.class)
.hasMessageStartingWith("Input with")
.hasMessageEndingWith("name cannot be read.");
}
}
As you can see, when you use a global TaskRunner, there is no duplicate information in your test, and the test bulk consists of assertions. The approach is optional, but you can use it if your test class contains tests for only one task processor.
Chain execution with ChainTaskRunner
There are two different ways to execute the task chain:
- With separate tests
- With a single test with
ChainTaskRunner
Check the code example
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@JnwJunitConfig
class JsonBasedEmailToInvoiceTest {
private static Output stepData;
private static TaskRunner emailProducerTaskRunner;
private static TaskRunner emailToInvoiceTaskRunner;
private static TaskRunner invoiceProcessorTaskRunner;
@BeforeAll
static void initTaskRunners(JnwTaskFactory taskFactory) {
emailProducerTaskRunner = taskFactory.createTaskRunner(EmailProducerTaskProcessor.class);
emailToInvoiceTaskRunner = taskFactory.createTaskRunner(EmailToInvoiceTaskProcessor.class);
invoiceProcessorTaskRunner = taskFactory.createTaskRunner(InvoiceProcessorTaskProcessor.class);
}
@Test
@DisplayName("should run process as chain of tasks")
void shouldRunProcessAsChainOfTasks(JnwTaskFactory taskFactory) {
ChainTaskRunner chainTaskRunner = taskFactory.getChainTaskRunnerBuilder()
.addProcessor(EmailProducerTaskProcessor.class)
.addProcessor(EmailToInvoiceTaskProcessor.class)
.addProcessor(InvoiceProcessorTaskProcessor.class)
.build();
final ChainTaskOutput output = chainTaskRunner.runWithInput(Collections.emptyMap());
//First step assertions
assertThat(output.getTaskRunnerOutputByProcessorClass(EmailProducerTaskProcessor.class)).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsOnlyKeys(EmailApiColumn.EMAIL);
//Second step assertions
assertThat(output.getTaskRunnerOutputByProcessorClass(EmailToInvoiceTaskProcessor.class)).isSuccessful().hasSingleRowThat()
.hasSize(2)
.containsOnlyKeys(EmailApiColumn.EMAIL, EmailApiColumn.INVOICE);
//Last step assertions
assertThat(output).isSuccessful().hasSingleRowThat()
.hasSize(2)
.containsOnlyKeys(EmailApiColumn.EMAIL, EmailApiColumn.INVOICE)
.hasValue(EmailApiColumn.INVOICE).satisfies(invoiceJson -> {
final String invoiceStatus = JsonPath.parse(invoiceJson).read("$.status", String.class);
Assertions.assertThat(invoiceStatus).isEqualTo("PROCESSED");
});
}
@Test
@Order(1)
@DisplayName("should produce Email object")
void shouldProduceEmailObject() {
// when
stepData = emailProducerTaskRunner.runWithInput(Collections.emptyMap());
// then
assertThat(stepData).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsOnlyKeys(EmailApiColumn.EMAIL);
}
@Test
@Order(2)
@DisplayName("should convert Email to Invoice")
void shouldConvertEmailToInvoice() {
// when
stepData = emailToInvoiceTaskRunner.runWithOutput(stepData);
// then
assertThat(stepData).isSuccessful().hasSingleRowThat()
.hasSize(2)
.containsOnlyKeys(EmailApiColumn.EMAIL, EmailApiColumn.INVOICE);
}
@Test
@Order(3)
@DisplayName("should process Invoice")
void shouldProcessInvoice() {
// when
stepData = invoiceProcessorTaskRunner.runWithOutput(stepData);
// then
assertThat(stepData).isSuccessful().hasSingleRowThat()
.hasSize(2)
.containsOnlyKeys(EmailApiColumn.EMAIL, EmailApiColumn.INVOICE)
.hasValue(EmailApiColumn.INVOICE).satisfies(invoiceJson -> {
String invoiceStatus = JsonPath.parse(invoiceJson).read("$.status", String.class);
Assertions.assertThat(invoiceStatus).isEqualTo("PROCESSED");
});
}
}
When you use ChainTaskRunner, you need to filter the output by a processor class or name to get the result. If you prefer the other way of chain execution, the result will still be the same. It depends only on your choice how you complete the chain of tasks.
View code examples
ChainProcessorTest.class
package com.workfusion.jnw.task;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import com.workfusion.jnw.test.junit.JnwJunitConfig;
import com.workfusion.jnw.test.launch.JnwTaskFactory;
import com.workfusion.jnw.test.launch.input.Input;
import com.workfusion.jnw.test.launch.input.JsonBasedInputRow;
import com.workfusion.jnw.test.launch.output.ChainTaskOutput;
import com.workfusion.jnw.test.launch.output.Output;
import com.workfusion.jnw.test.launch.runner.ChainTaskRunner;
import com.workfusion.jnw.test.launch.runner.TaskRunner;
import static com.workfusion.jnw.test.assertion.TaskResultAssert.assertThat;
@JnwJunitConfig
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ChainProcessorTest {
private static TaskRunner firstTaskRunner;
private static TaskRunner secondTaskRunner;
private static Output globalOutput;
@BeforeAll
static void initTaskRunner(JnwTaskFactory taskFactory) {
firstTaskRunner = taskFactory.createTaskRunner(FirstChainProcessor.class);
secondTaskRunner = taskFactory.createTaskRunner(SecondChainProcessor.class);
}
@Test
@Order(1)
@DisplayName("should run only one runner")
void shouldRunOnlyOneRunner() {
//when
globalOutput = firstTaskRunner.runWithInput("result", "single test");
//then
assertThat(globalOutput).isSuccessful()
.hasSize(2)
.allSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result"));
}
@Test
@Order(2)
@DisplayName("should run second runner with output from previous test")
void shouldRunSecondRunnerWithOutputFromPreviousTest() {
//given
final ChainTaskRunner chainTaskRunner = new ChainTaskRunner().addTaskRunner(secondTaskRunner);
//when
globalOutput = chainTaskRunner.runWithOutput(globalOutput);
//then
assertThat(globalOutput).hasTaskResultSize(2).isSuccessful()
.hasSize(4)
.allSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result"));
}
@Test
@Order(3)
@DisplayName("should throw exception if chainTaskRunner is empty")
void shouldThrowExceptionIfChainTaskRunnerIsEmpty() {
//given
final ChainTaskRunner chainTaskRunner = new ChainTaskRunner();
//then
Assertions.assertThatThrownBy(() -> chainTaskRunner.runWithOutput(globalOutput)).isInstanceOf(IllegalStateException.class)
.hasMessage("ChainTaskRunner does not contain any TaskRunners, add one or more and try running it again.");
}
@Test
@DisplayName("should run chain")
void shouldRunChain() {
//given
final JsonBasedInputRow secondInputRow = new JsonBasedInputRow("secondRow")
.withStringInputData("result", "second row data from");
final Input input = new Input()
.addRow(inputRow -> inputRow.addMarker("firstRow").withStringInputData("result", "first row data from"))
.addRow(secondInputRow);
final ChainTaskRunner chainTaskRunner = new ChainTaskRunner()
.addTaskRunner(firstTaskRunner)
.addTaskRunner(secondTaskRunner);
//when
final ChainTaskOutput output = chainTaskRunner.runWithInput(input);
//then
//assert by input row marker
assertThat(output.getOutputByInputMarkers("firstRow")).hasTaskResultSize(2).isSuccessful()
.allSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result").startsWith("first row data from"));
assertThat(output.getOutputByInputMarkers("secondRow")).hasTaskResultSize(2).isSuccessful()
.allSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result").startsWith("second row data from"));
//assert by inputRow
assertThat(output.getOutputByInputRow(input.getSingleRowByMarkers("firstRow"))).isSuccessful()
.hasSize(4)
.allSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result").startsWith("first row data from"));
assertThat(output.getResultByInputRow(secondInputRow))
.hasSize(2)
.allSatisfy(result -> assertThat(result).isSuccessful().allRowsSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result").startsWith("second row data from")));
//assert by inputRow marker and taskRunner
assertThat(output.getOutputByInputRow(input.getSingleRowByMarkers("firstRow")).getTaskRunnerOutputByRunner(firstTaskRunner)).isSuccessful()
.hasSize(2)
.allSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result").startsWith("first row data from"));
assertThat(output.getTaskRunnerOutputByRunner(firstTaskRunner).getOutputByInputMarkers("firstRow")).hasSingleTaskResultThat().isSuccessful()
.allRowsSatisfy(taskOutputRow -> assertThat(taskOutputRow).hasValue("result").startsWith("first row data from"));
//input filter + taskRunner filter == taskRunner filter + input filter
assertThat(output.getOutputByInputRow(secondInputRow).getTaskRunnerOutputByRunnerUuid(secondTaskRunner.getRunnerUuid())).isEqualTo(output.getTaskRunnerOutputByProcessorClass(SecondChainProcessor.class)
.getOutputByInputRowWithParent(secondInputRow));
assertThat(output.getOutputByInputRow(input.getSingleRowByMarkers("firstRow")).getTaskRunnerOutputByRunner(firstTaskRunner)).isEqualTo(output.getTaskRunnerOutputByRunner(firstTaskRunner)
.getOutputByInputMarkers("firstRow"));
}
}
StoreContentToS3TaskProcessorTest.class
package com.workfusion.jnw.test.task;
import java.io.IOException;
import java.io.InputStream;
import com.amazonaws.util.IOUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.workfusion.jnw.test.assertion.TaskResultAssert;
import com.workfusion.jnw.test.junit.JnwJunitConfig;
import com.workfusion.jnw.test.launch.JnwTaskFactory;
import com.workfusion.jnw.test.s3.S3MockClient;
import com.workfusion.jnw.test.core.orm.OrmLiteRepository;
import com.workfusion.jnw.test.core.webharvest.TaskVariable;
import com.workfusion.jnw.test.jnw.jnw.test.model.Content;
import com.workfusion.jnw.test.junit.OrmSupport;
import com.workfusion.jnw.test.transaction.model.Transaction;
import com.workfusion.spa.core.execution.api.task.TaskResult;
import com.workfusion.spa.jnative.worker.core.api.TaskOutputData;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
@JnwJunitConfig
class StoreContentToS3TaskProcessorTest {
@BeforeEach
void setUp(OrmSupport ormSupport) {
ormSupport.createTables(Transaction.class, Content.class);
}
@Test
@DisplayName("should store content to S3")
void shouldStoreContentToS3(JnwTaskFactory taskFactory, OrmSupport ormSupport, S3MockClient s3Client) throws IOException {
// given
s3Client.createBucket("jnw-bucket");
Transaction transaction = ormSupport.getTransactionRepository().startNewTransaction("NEW");
createContentEntity(transaction, "test_content", ormSupport);
// when
TaskResult<TaskOutputData> result = taskFactory.fromClass(StoreContentToS3TaskProcessor.class)
.withStringInputData(TaskVariable.TRANSACTION_ID.toString(), transaction.getUuid().toString())
.withVariationJsonConfig(builder -> builder.add("['odf2.regression.jnw.output-bucket-alias']", "jnw-bucket"))
.buildAndRun();
// then
TaskResultAssert.assertThat(result).isSuccessful().hasSingleRowThat()
.containsEntry(TaskVariable.TRANSACTION_ID.toString(), transaction.getUuid().toString());
// then assert S3 object has been created
InputStream objectContent = s3Client.getObjectContent("jnw-bucket", "test_content.txt");
String actualContent = new String(IOUtils.toByteArray(objectContent), UTF_8);
assertThat(actualContent).isEqualTo("test_content");
}
private Content createContentEntity(Transaction transaction, String text, OrmSupport ormSupport) {
OrmLiteRepository<Content> repository = ormSupport.getRepository(Content.class);
Content content = new Content(transaction, text);
return repository.create(content);
}
}
SecretStoreTestProcessorTest.class
package com.workfusion.jnw.test.tst.demo.task;
import java.util.Date;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.workfusion.jnw.test.junit.JnwJunitConfig;
import com.workfusion.jnw.test.launch.JnwTaskFactory;
import com.workfusion.jnw.test.secret.JnwSecretService;
import com.workfusion.spa.core.execution.api.task.TaskResult;
import com.workfusion.spa.jnative.worker.core.api.TaskOutputData;
import com.workfusion.spa.jnative.worker.core.api.TaskOutputRow;
import com.workfusion.spa.jnative.worker.module.secret.entity.SecretDto;
import static com.workfusion.jnw.test.assertion.TaskResultAssert.assertThat;
@JnwJunitConfig
class SecretStoreTestProcessorTest {
private final SecretDto beforeEachSecret = new SecretDto("beforeAlias", "beforeKey", "beforeValue", new Date());
@BeforeEach
void beforeEach(JnwSecretService secretService) {
secretService.createSecret(beforeEachSecret.getAlias(), beforeEachSecret.getKey(), beforeEachSecret.getValue());
}
@Test
@DisplayName("should read secret dto from secret service")
void shouldReadSecretDtoFromSecretService(JnwTaskFactory taskFactory, JnwSecretService secretService) {
//given
final String secretAlias = "alias";
final String secretKey = "key";
final String secretValue = "value";
secretService.createSecret(secretAlias, secretKey, secretValue);
//when
TaskResult<TaskOutputData> result = taskFactory.fromClass(SecretStoreProcessor.class).withStringInputData("secretAlias", secretAlias).buildAndRun();
//then
assertThat(result).isSuccessful().containsExactlyOutputRows(castMapToTaskOutputRow(ImmutableMap.of(
"key", secretKey,
"value", secretValue
)));
}
@Test
@DisplayName("should return secret dto from each scope")
void shouldReturnSecretDtoFromEachScope(JnwTaskFactory taskFactory) {
//when
TaskResult<TaskOutputData> result = taskFactory.fromClass(SecretStoreProcessor.class).withStringInputData("secretAlias", beforeEachSecret.getAlias()).buildAndRun();
//then
assertThat(result).isSuccessful().containsExactlyOutputRows(castMapToTaskOutputRow(ImmutableMap.of(
"key", beforeEachSecret.getKey(),
"value", beforeEachSecret.getValue()
)));
}
private TaskOutputRow castMapToTaskOutputRow(Map<String, String> map) {
TaskOutputRow result = new TaskOutputRow();
map.forEach(result::put);
return result;
}
}
DocumentProcessorTest.class
package com.workfusion.jnw.test.tst.demo.task;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.workfusion.jnw.test.junit.JnwJunitConfig;
import com.workfusion.jnw.test.launch.JnwTaskFactory;
import com.workfusion.jnw.test.launch.output.Output;
import com.workfusion.jnw.test.launch.runner.TaskRunner;
import com.workfusion.jnw.test.ormlitesupport.ormlite.OrmLiteRepository;
import com.workfusion.jnw.test.ormlitesupport.ormlite.OrmLiteSupport;
import com.workfusion.jnw.test.tst.demo.models.Document;
import static com.workfusion.jnw.test.assertion.TaskResultAssert.assertThat;
@JnwJunitConfig
class DocumentProcessorTest {
private static TaskRunner documentProcessorRunner;
private OrmLiteRepository<Document> documentRepository;
@BeforeAll
static void initTaskRunner(JnwTaskFactory taskFactory) {
documentProcessorRunner = taskFactory.createTaskRunner(DocumentProcessor.class);
}
@BeforeEach
void initDatastore(OrmLiteSupport ormLiteSupport) {
ormLiteSupport.createTables(Document.class);
documentRepository = ormLiteSupport.getRepository(Document.class);
}
@Test
@DisplayName("should add new document to repository")
void shouldAddNewDocumentToRepository(JnwTaskFactory taskFactory) {
//given
final Document document = new Document("test_name", "value");
//when
Output result = documentProcessorRunner.runWithInput("document", document);
//then
Assertions.assertThat(documentRepository.count()).isEqualTo(1);
assertThat(result).isSuccessful().hasSingleRowThat().containsEntry("document_uuid", document.getUuid());
}
@Test
@DisplayName("should throw exception if count of documents in repository is done")
void shouldThrowExceptionIfCountOfDocumentsInRepositoryIsDone(JnwTaskFactory taskFactory) {
final Document document = new Document("test_name", "value");
//when
Output result = documentProcessorRunner
.withTaskConfiguration(builder -> builder.add("datastore.document.['max.size']", 0))
.runWithInput("document", document);
//then
assertThat(result).isFailed().hasSingleTaskErrorThat().hasError(IllegalStateException.class, "Can not add new document to repository");
}
@Test
@DisplayName("should add new document to repository with size limit")
void shouldAddNewDocumentToRepositoryWithSizeLimit(JnwTaskFactory taskFactory) {
final Document firstDocument = new Document("first_document", "first_value");
final Document document = new Document("test_name", "value");
documentRepository.create(firstDocument);
//when
Output result = documentProcessorRunner
.withTaskConfiguration(builder -> builder.add("datastore.document.['max.size']", 2))
.runWithInput("document", document);
//then
Assertions.assertThat(documentRepository.count()).isEqualTo(2);
Assertions.assertThat(documentRepository.findAll()).containsOnly(firstDocument, document);
assertThat(result).isSuccessful().hasSingleRowThat().containsEntry("document_uuid", document.getUuid());
}
}
BasicProcessorTest.class
package com.workfusion.jnw.task;
import java.util.Collections;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.workfusion.jnw.exception.ReadInputException;
import com.workfusion.jnw.test.junit.JnwJunitConfig;
import com.workfusion.jnw.test.launch.JnwTaskFactory;
import com.workfusion.jnw.test.launch.input.Input;
import com.workfusion.jnw.test.launch.input.InputRow;
import com.workfusion.jnw.test.launch.input.StringBasedInputRow;
import com.workfusion.jnw.test.launch.output.TaskOutput;
import com.workfusion.jnw.test.launch.runner.TaskRunner;
import static com.workfusion.jnw.test.assertion.TaskResultAssert.assertThat;
@JnwJunitConfig
class BasicProcessorTest {
private static final String configurationPropertyPath = "test.value.['for.test']";
private static final String configurationPropertyResultName = "result";
private static final String configurationPropertyValue = "value from task configuration";
private static InputRow input;
private static TaskRunner taskRunner;
@BeforeAll
static void initTaskRunner(JnwTaskFactory taskFactory) {
taskRunner = taskFactory.createTaskRunner(BasicTestProcessor.class,
taskInstance -> taskInstance.withTaskConfiguration(builder -> builder.add(configurationPropertyPath, configurationPropertyValue)));
input = new StringBasedInputRow().withInputData("configurationPropertyResultName", configurationPropertyResultName)
.withInputData("configurationPropertyPath", configurationPropertyPath)
.addMarker("successful");
}
@Test
@DisplayName("should run basic processor")
void shouldRunBasicProcessor() {
//when
final TaskOutput result = taskRunner.runWithInput(input);
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsEntry(configurationPropertyResultName, configurationPropertyValue);
}
@Test
@DisplayName("should run basic processor with new configuration")
void shouldRunBasicProcessorWithNewConfiguration() {
//given
final String newConfigurationPropertyValue = "value from new task configuration";
//when
final TaskOutput result = taskRunner
.withTaskConfiguration(builder -> builder.add(configurationPropertyPath, newConfigurationPropertyValue))
.runWithInput(input);
//then
assertThat(result).isSuccessful().hasSingleRowThat()
.hasSize(1)
.containsEntry(configurationPropertyResultName, newConfigurationPropertyValue);
}
@Test
@DisplayName("should run basic processor without input")
void shouldRunBasicProcessorWithoutInput() {
//when
final TaskOutput result = taskRunner.runWithInput(Collections.emptyMap());
//then
assertThat(result).isFailed().hasSingleTaskErrorThat().isErrorInstanceOf(ReadInputException.class)
.hasMessageStartingWith("Input with")
.hasMessageEndingWith("name can not be read.");
}
@Test
@DisplayName("should use markers to split multiple input")
void shouldUseMarkersToSplitMultipleInput() {
//given
Input multiInput = new Input()
.addRow(input)
.addEmptyRow("failed");
//when
TaskOutput taskOutput = taskRunner.runWithInput(multiInput);
//then
assertThat(taskOutput.getOutputByInputMarkers("successful")).isSuccessful();
assertThat(taskOutput.getOutputByInputMarkers("failed")).isFailed();
}
}