Skip to main content
Version: 10.3

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:

  1. 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>
  2. If your project is a part of the workfusion-meta pipeline, add a dependency to worker-task-test into the components.yaml file of the workfusion-meta project:

    your-project:
    dependencies:
    - worker-task-test
  3. Import worker-task-test-bom in 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>
note

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:

  1. Add the @JnwJunitConfig annotation at the class level:

    @JnwJunitConfig
    class TestClassName {

    // tests methods

    }
  2. 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 classes property in the @JnwJunitConfig annotation:

    @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 basePackages property in the @JnwJunitConfig annotation:

    @JnwJunitConfig(basePackages = {"user.configuration.pacage", "user.beans.package"})
    class TestClassName {

    // tests methods

    }
    info

    You do not need to add the com.workfusion.spa.core, com.workfusion.spa.jnative.worker, and com.workfusion.odf2.jnw packages to basePackages as they are added automatically.

    caution

    The 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.

  1. To change or add properties in the application.yml configuration file, use the properties property in the @JnwJunitConfig annotation:

    @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 create TaskInput<TaskInputWithContext> and execute a task. The approach does not have the ability to quickly change inputs and cannot be reused. SingleTaskLauncher allows 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 of TaskRunner.

note

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:

  1. Initialize TaskRunner from the processor class without configuration:

    @Test
    void yourTest(JnwTaskFactory taskFactory) {
    TaskRunner taskProcessorRunner = taskFactory.createTaskRunner(TaskProcessor.class);
    }
  2. Initialize TaskRunner with TaskInstance, where TaskInstance is a configuration builder that has the same structure as SingleTaskLauncher but 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);
    }
  3. Initialize TaskRunner from the processor class and the TaskInstance unary 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 TaskRunner instance with configuration changes; can be done using the modify() method with a TaskInstance unary operator or methods that TaskRunner duplicated from TaskInstance:

    @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 the update() method with a TaskInstance unary 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();
    }
note

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 with Map objects, for example, when you take input from a file or have a method that provides input in the Map format. 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
    }
  • InputRow object. InputRow is an interface that has two implementations:

    • JsonBasedInputRow uses ObjectMapper to cast the value of an object to a JSON string. It also has methods that allow you to add a plain String value to a row.
    • StringBasedInputRow works with plain String values.
    @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
    }
  • Input object. Input data is a container for InputRow objects. If you have one row, you can add only one row to the Input data and use it to execute the task processor. At the same time, if you try to execute the Input data without rows inside, TaskRunner creates 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
    }
  • JsonBasedInputRow unary operator. Sometimes, you need to use custom InputRow with more than one column and no Map object with the input data. In this case, you can use the JsonBasedInputRow unary operator to construct your InputRow inside the runWithInput() 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
}
note

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:

  1. Inject JnwTaskFactory into your test method and call the fromClass(Class<? Extend ITaskProcessor>) method:

    @Test
    void yourTest(JnwTaskFactory taskFactory) {
    //when
    SingleTaskLauncher singleTaskLauncher = taskFactory.fromClass(TaskProcessor.class);
    }
  2. 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);

    }
    note

    The withInputData() methods use ObjectMapper to convert the Object value to String. Thus, if you use the String value in this method, it will have the "value" format instead of value in the input.

  3. 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

    }
  4. 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

    }
tip

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 ChainTaskRunnerBuilder provided by JnwTaskFactory. It allows you to build task chains by TaskRunners, 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 ChainTaskRunner instance. A new ChainTaskRunner instance has a limitation on methods that allow processors to be added to the chain. It can only add or update the existing TaskRunners.

    @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
    }
note

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:

  • IsSchemaBased is set to false. The Input data contains only the columns from the private task as if running a classic input-output task.

  • IsSchemaBased is set to true. The Input data for the current task contains the input data from the previous task combined with the output data, resulting in a global context for each InputRow. The input contains the full InputRow context with all columns.

In its current state, the framework does not support contracts or filter task inputs; instead, it uses the full context.

note

If you use ChainTaskRunner and one of the tasks is schema-based, each task in the chain is treated as a schema-based one.

tip

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:

MethodDescription
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.
  • getResultByInputMarkers(Set<String> markers)
  • getResultByInputMarkers(String... markers)
  • getResultByInputMarker(String marker)
Get results by the InputRow markers (List<TaskResult<TaskOutputData>>).
  • getSingleResultByInputMarkers(Set<String> markers)
  • getSingleResultByInputMarkers(String... markers)
Get a single result by the InputRow markers (TaskResult<TaskOutputData>).
getResultByInputRow(InputRow inputRow)Gets a result by InputRow.
  • getOutputByInputMarkers(Set<String> markers)
  • getOutputByInputMarkers(String... markers)
Get TaskOutput by the InputRow markers.
  • getOutputByInputRow(InputRow inputRow)
  • getOutputByInputRowWithParent(InputRow inputRow)
Get TaskOutput by InputRow.
tip

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:

MethodDescription
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.
  • getInputRows()
  • getInput()
Get input rows as the List or Input objects from the first processor.
  • getResultByInputMarkers(Set<String> markers)
  • getResultByInputMarkers(String... markers)
  • getResultByInputMarker(String marker)
Get results by the InputRow markers (List<TaskResult<TaskOutputData>>).
  • getSingleResultByInputMarkers(Set<String> markers)
  • getSingleResultByInputMarkers(String... markers)
Get a single result by the InputRow markers (TaskResult<TaskOutputData>).
getResultByInputRow(InputRow inputRow)Gets result by InputRow.
  • getOutputByInputMarkers(Set<String> markers)
  • getOutputByInputMarkers(String... markers)
Get ChainTaskOutput by the InputRow markers.
getOutputByInputRow(InputRow inputRow)Gets ChainTaskOutput by InputRow.
  • getTaskRunnerOutputByRunnerInfo(RunnerInfo runnerInfo)
  • getTaskRunnerOutputByProcessorName(String processorName)
  • getTaskRunnerOutputByProcessorClass(Class<? extends ITaskProcessor> processorClass)
  • getTaskRunnerOutputByRunnerUuid(UUID uuid)
  • getTaskRunnerOutputByRunner(TaskRunner taskRunner)
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.

note

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.

warning

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.

note

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"));
}
note

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:

MethodDescription
  • containsOutputRows(TaskOutputRow... value)
  • containsExactlyOutputRows(TaskOutputRow... rows)
  • doesNotContainOutputRows(TaskOutputRow... value)
Contain methods.
extractRowByIndex(int index)Extracts a row by index.
  • allRowsSatisfy(Consumer<TaskOutputRow> requirements)
  • anyRowsSatisfy(Consumer<TaskOutputRow> requirements)
  • noRowsSatisfy(Consumer<TaskOutputRow> requirements)
  • satisfyRowsByExistingColumn(String columnName, Consumer<TaskOutputRow> requirements)
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).
  • extractingColumnByKey(String key)
  • extractingColumnByKey(String key, Class<T> type)
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.
  • hasSize(int expected)
  • hasSizeGreaterThan(int boundary)
  • hasSizeLessThan(int boundary)
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.

MethodDescription
  • containsValue(Object value)
  • containsValues(Object... values)
  • doesNotContainValue(Object value)
Check if a row contains values.
  • contains(Map.Entry<String, Object>... entries)
  • containsEntry(String key, Object value)
  • containsAllEntriesOf(Map<String, Object> other)
  • doesNotContain(Map.Entry<String, Object>... entries)
  • doesNotContainEntry(String key, Object value)
Check if a row contains entries.
  • hasValue(String key, Class<T> clazz)
  • hasValue(String key)
Assert a specific value.
  • hasSize(int expected)
  • hasSizeGreaterThan(int boundary)
  • hasSizeLessThan(int boundary)
Check the number of columns in a row.
note

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:

MethodDescription
  • hasMessage(String message)
  • hasMessageStartingWith(String message)
  • hasMessageEndingWith(String description)
  • hasMessageContaining(String message)
  • hasMessageNotContaining(String content)
Standard message approval methods.
  • isErrorInstanceOf(Class<? extends Throwable> errorType)
  • hasError(Class<? extends Throwable> errorClass, String message)
Check the error type.
  • message()
  • stackTrace()
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.

MethodDescription
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.
isFailedChecks 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);
}
note

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.

warning

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.

note

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 a SecretDto object.

  • updateSecret(String alias, String key, String value): update a SecretDto object.

  • deleteSecret(String alias): delete a SecretDto object.

  • getSecret(String alias): retrieve a SecretDto object from SecretService.

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.

warning

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();
}

}