Bot Task JUnit
Bot Task JUnit is a JUnit 5 API for running and testing bot tasks. It can be used for running bot tasks locally from IDE of choice during development and/or for executing tests in a CI/CD pipeline.
How to add required Bot Task JUnit dependencies to existing project
To start working with Bot Task JUnit add the following dependencies to your project:
bot-task-junit- the library that contains API required for writing JUnit tests for bot tasksbot-execution-engine- the engine responsible for running bot tasks, compatible with the current ODF version
Both libraries are published on repository.workfusion.com. Usually, this repository is already a part of each ODF project. You don't have to configure anything additionally. Just add bot-task-junit and bot-execution-engine as test dependencies to a BCB module (the module which contains bot tasks to be tested):
<dependencies>
<dependency>
<groupId>com.workfusion.odf</groupId>
<artifactId>bot-task-junit</artifactId>
<version>${latest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.workfusion.studio</groupId>
<artifactId>bot-execution-engine</artifactId>
<version>${odf.compatible.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
note
We recommend using the latest 0.0.7 version of bot-task-junit since it contains the most recent features and bug fixes.
At the same time, choose bot-execution-engine based on the ODF version your project is built. The relevant version of bot-execution-engine is specified in the dependency management of the ODF's BOM file.
tip
We propose using AssertJ, that is the library with a rich set of assertions. Basically, you can use any assertions library you are used to, but mind that all the examples bellow are written with the help of AssertJ.
Bot Task JUnit test in a nutshell
The first thing to do is to write a simple test that will help us to launch a bot task. The following listing shows how to do it:
import org.junit.jupiter.api.Test;
import com.workfusion.odf.test.junit.WorkerJUnitConfig;
import com.workfusion.odf.test.launch.BotTaskResult;
import com.workfusion.odf.test.launch.LaunchSettingsFactory;
import com.workfusion.odf.test.launch.WorkerAgent;
import com.workfusion.studio.bot.model.LaunchSettings;
import static org.assertj.core.api.Assertions.assertThat;
@WorkerJUnitConfig
class TransactionSupplierTest {
@Test
void shouldCreateTransaction(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
// given
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/OdfTransactionSupplier.xml")
.build();
// when
BotTaskResult actualResult = agent.run(settings);
// then
assertThat(actualResult.getOutputData().getRecords()).hasSize(1);
}
}
The @WorkerJUnitConfig annotation is mandatory. It tells JUnit to make sure that all related environment (worker, in-memory database, S3 mock server, etc.) is started before test execution. Here, you can see that the WorkerAgent and LaunchSettingsFactory classes are used as test parameters. JUnit will make sure these parameters are populated with valid objects from the @WorkerJUnitConfig context.
WorkerAgent
WorkerAgent is a class responsible for launching bot tasks. Let's take a look at its interface:
interface WorkerAgent {
/**
* Blocks current thread and waits until bot task execution is completed.
*
* @param settings launch settings to be used for creating worker's task
* @return execution result
*/
BotTaskResult run(LaunchSettings settings);
/**
* Blocks current thread and waits specified time until bot task execution is completed.
*
* @param settings launch settings to be used for creating worker's task
* @param timeout the maximum amount of time to wait
* @return execution result
*/
BotTaskResult run(LaunchSettings settings, Duration timeout);
}
WorkerAgent provides two methods to run bot tasks. Both of them are blocking methods, but the second one let you set up a timeout.
If a bot task will fail to complete its execution in a set time, an exception will be thrown. Also, to run a bot task you have to provide the LaunchSettings object.
LaunchSettings
The LaunchSettings class represents all the data that Worker should know about a bot task. All possible settings are stored inside this single object. To make sure you can set up this object correctly, LaunchSettingsFactory provides a fluent builder. Here is how it works:
@WorkerJUnitConfig
class TransactionSupplierTest {
@Test
void shouldCreateTransaction(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/OdfTransactionSupplier.xml")
.inputData(InputData.fromResource("input-data.csv"))
.s3(cfg -> cfg.withEndpointUrl("localhost:3040"))
.ocr(cfg -> cfg.withApiBaseUrl("localhost:3041"))
.secureEntries(cfg -> cfg.withEntry("alias", "key", "value"))
.outputData("output-data.csv")
.build();
}
}
Explore com.workfusion.odf.test.launch.LaunchSettingsBuilder to find more settings. Also, mind that LaunchSettings is a simple
DTO class which means you can set anything you need directly to this class if the builder is not sufficient in your case.
Input data provisioning
Input data is configured using a com.workfusion.odf.test.launch.InputData object. Let's take a look at factory methods of this class:
public class InputData {
/**
* Creates {@link InputData} from the provided CSV file path, relative to `src/test/resources` or current project.
*/
public static InputData fromResource(String inputFilePath);
/**
* Creates {@link InputData} from the provided CSV file path, relative to `src/test/resources` or current project.<p/>
* Start and end indexes specify records range which should be processed from the provided CSV file.
*/
public static InputData fromResource(String inputFilePath, int startIndex, int endIndex);
/**
* Creates temporary single column {@link InputData} from the provided content.
*/
public static InputData of(String header, String... values);
/**
* Creates temporary multi columns {@link InputData} from the provided content.
*/
public static InputData of(List<String> headers, List<String>... rows);
}
Basically, input data can be created from a .CSV file or from a String directly before test execution.
The first approach might be useful when you have a huge input and want to keep it away from a test, or reuse it for multiple tests.
In this case, you simply put .CSV file with prepared input data under src/test/resources and specify its path in a test:
@WorkerJUnitConfig
class InputDataTest {
@Test
void someTest(LaunchSettingsFactory settingsFactory) {
LaunchSettings settings = settingsFactory.builder()
.inputData(InputData.fromResource("input-data-from-resources.csv"))
.build();
}
}
The second approach is useful when you have just a couple of columns or want to pass a runtime value as input data:
@WorkerJUnitConfig
class InputDataTest {
@Test
void someTestWithSingleColumn(LaunchSettingsFactory settingsFactory) {
LaunchSettings settings = settingsFactory.builder()
.inputData(InputData.of("header", "value-1", "value-2", getRuntimeValue()))
.build();
}
@Test
void someTestWithMultipleColumns(LaunchSettingsFactory settingsFactory) {
LaunchSettings settings = settingsFactory.builder()
.inputData(InputData.of(
asList("column_1", "column_2", "column_3"),
asList("value_1", "value_2", "value_3")))
.build();
}
}
Data Stores state provisioning
Data Stores are configured using a com.workfusion.odf.test.datastore.Datastores object which can be injected into any method of test class annotated as @Test, @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll.
You can specify required Data Stores using .CSV files or code-based configuration:
- Call the
Datastores.addFrom()method with path(s) to your .CSV files or directories containing those. Each .CSV file will be used to create a separate table; the file name will be used as a table name, and the file content will be used to populate it. The path can be relative to the project root orsrc/main/resources, orsrc/test/resources. - Call
Datastores.add()to invoke a builder that allows you to define a table structure and content on-the-fly, without any file. - Call
Datastores.add(DatastoreSchema.ODF_TRANSACTIONS)to create an empty ODF transactions Data Store.
@WorkerJUnitConfig
class DatastoresTest {
@Test
void shouldCreateTransaction(Datastores datastores) {
// will add all .csv files from 'datastore1' directory
datastores.addFrom("/datastore1");
// will add 'some_data.csv' file
datastores.addFrom("/datastores/some_data.csv");
// you can specify any number of files and/or directories in any combination
datastores.addFrom("/datastore1", "/datastore2", "/datastores/some_data.csv", "/datastores/some_other_data.csv");
// you can define a table with code, without files
datastores.add(datastoreBuilder -> datastoreBuilder
.name("some_table")
.headers("a", "b", "c")
.row("1", "2", "3")
.row("4", "5", "6"));
// you can define empty ODF transaction datastore
datastores.add(DatastoreSchema.ODF_TRANSACTIONS);
// you can define ODF transaction datastore containing your Transaction objects
datastores.add(datastoreBuilder -> datastoreBuilder
.schema(DatastoreSchema.ODF_TRANSACTIONS)
.row("id1", "runId1", InternalStorageFormats.toJson(new Transaction()))
.row("id2", "runId2", InternalStorageFormats.toJson(new Transaction())));
}
}
Data Stores scopes
Data Stores object that you are using can have one of three scopes that define the lifecycle of configured tables.
- Tables defined in the
@BeforeAllmethods will be kept until all tests are finished. It means that changes made in one test will be visible to others (depending on the order of execution). This is called aCLASSscope. - Tables defined in
@BeforeEachmethods will be dropped and re-created between test methods. It means that all tests will see the data exactly as you defined, without interference between tests. This is anEACH_TESTscope. - Tables defined in the
@Testmethod itself will be dropped right after the method is finished. It means this data will be invisible to other tests. This is aTESTscope.
@WorkerJUnitConfig
class DatastoresTest {
@BeforeAll
public static void configureGlobalData(Datastores datastores) {
datastores.addFrom("global_data.csv");
}
@BeforeEach
public static void configureDataForEachTest(Datastores datastores) {
datastores.addFrom("each_test_data.csv");
}
@Test
void someTest(Datastores datastores) {
datastores.addFrom("data.csv");
// 'global_data' is visible and all changes made will last until all tests in this class are finished
// 'each_test_data' table is visible, but any change to it will be lost between tests
// 'data' table is visible, but will be deleted right after the test
// 'other_data' table from other test does not exist in this moment
}
@Test
void someOtherTest(Datastores datastores) {
datastores.addFrom("other_data.csv");
// 'global_data' is visible and all changes made will last until all tests in this class are finished
// 'each_test_data' table is visible, but any change to it will be lost between tests
// 'other_data' table is visible, but will be deleted right after the test
// 'data' table from other test does not exist in this moment
}
}
note
- Data Store names will be sanitized according to a simple rule: any symbol that is not a latin letter, digit, or underscore, will be replaced with underscore (for example, "my-table-01.csv" will become "my_table_01").
- Unlike in WorkFusion Studio, changes made in Data Stores will not be saved to working copies of .CSV files but will be lost after tests are finished. To access contents of Data Stores, use the
Datastores.read()method which returns aDatastoreTableobject.
Reading data from Data Stores
To read data from a specific Data Store, there is the Datastores.read() method. It returns a com.workfusion.odf.test.datastore.table.DatastoreTable object with data from the Data Store:
@WorkerJUnitConfig
class DatastoresTest {
@Test
void shouldAssertDatastoreRecords(Datastores datastores) {
DatastoreTable datastoreTable = datastores.read("datastore_name");
// assert that datastore is not empty
assertThat(datastoreTable.isEmpty()).isFalse();
// assert that datastore has only 2 rows
assertThat(datastoreTable.getRowCount()).isEqualTo(2);
// assert that datastore has correct headers
assertThat(datastoreTable.getColumns()).containsExactly("column1", "column2");
// assert that some row has specific data
assertThat(datastoreTable.getRow(0)).containsExactly("value1", "value2");
assertThat(datastoreTable.getLastRow()).containsExactly("value3", "value4");
assertThat(datastoreTable.getValues(1)).isEqualTo(ImmutableMap.of(
"column1", "value3",
"column2", "value4"
));
// assert that some column has specific data
assertThat(datastoreTable.getColumn("column1")).containsExactly("value1", "value3");
// assert that cell has specific value
assertThat(datastoreTable.getValue(0, "column1")).isEqualTo("value1");
}
}
Also, you can print DatastoreTable in a user-friendly format:
@WorkerJUnitConfig
class DatastoresTest {
@Test
void shouldPrintDatastoreRecords(Datastores datastores) {
DatastoreTable datastoreTable = datastores.read("datastore_name");
// Print full table
System.out.println(datastoreTable);
// Return 'TablePrinter' object
TablePrinter tablePrinter = datastoreTable.getTablePrinter();
// Print exact rows
System.out.println(tablePrinter.printTable(0, 0));
// Print exact columns
System.out.println(tablePrinter.printTable("column1"));
// Print exact rows and columns
System.out.println(tablePrinter.printTable(0, 0, "column1"));
}
}
OCR mocking
For each test run, there is an underlying in-memory service which is able to mock real OCR behaviour. To interact with this service, inject a com.workfusion.odf.test.ocr.OcrMock object into any method of test class annotated as @Test, @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll. OcrMock helps to respond with any desired text for any desired image.
@WorkerJUnitConfig
class OcrTest {
@Test
void shouldRunOcrTask(OcrMock ocrMock) {
ocrMock.shouldReturn("Lorem ipsum dolor sit amet").forImage("/img/ocr-test-image.png");
}
}
In this example ocrMock is configured to respond with "Lorem ipsum dolor sit amet" when an image at "/img/ocr-test-image.png" is submitted.
This is called an expectation. A test author can define any number of expectations for different images. There is also an option to create default expectation that will result in the same text for any image submitted, unless this image is covered with a more specific expectation.
In the following example, OcrMock will return "Default result" for any submitted image except hypothetical "a.png", for which
"Specific result" will be returned.
@WorkerJUnitConfig
class OcrTest {
@Test
void shouldRunOcrTask(OcrMock ocrMock) {
ocrMock.shouldReturn("Default result").forAnyImage();
ocrMock.shouldReturn("Specific result").forImage("a.png");
}
}
OCR scopes
OcrMock can have one of three scopes, that define lifecycle of expectations:
- Expectations created in the
@BeforeAllmethods will be kept until all tests are finished. - Expectations created in the
@BeforeEachmethods will be deleted and re-created between test methods. It means that OCR tasks created from those expectations will be invisible to other tests. - Expectations created in the
@Testmethod itself will be deleted right after method is finished. It means these expectations and corresponding tasks will be invisible to other tests.
@WorkerJUnitConfig
class OcrTest {
@BeforeEach
void setUpOcr(OcrMock ocrMock) {
ocrMock.shouldReturn("Lorem ipsum dolor sit amet").forImage("/img/ocr-test-image.png");
}
@Test
void shouldRunOcrTask(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
// given
String imagePath = Paths.get(getClass().getResource("/img/ocr-test-image.png").toURI()).toString();
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/bot-task-with-ocr.xml")
.inputData(InputData.of("image_to_ocr", imagePath))
.build();
// when
String actualOcrResult = agent.run(settings).getOutputData().getFirstRecord().get("ocr_result");
// then
assertThat(actualOcrResult).isEqualTo("Lorem ipsum dolor sit amet");
}
}
S3 mocking
For each test run, there is an underlying in-memory S3 server that is ready to handle user requests. To interact with this server, inject a com.workfusion.odf.test.s3.S3MockClient object into any method of test class annotated as @Test, @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll.
@WorkerJUnitConfig
class S3Test {
@BeforeEach
void setUpS3(S3MockClient s3Client) {
s3Client.putObject("test-bucket", "test-file.txt", "test file content");
}
}
Mock client works the same way as a real Amazon S3 client and has most of its methods. For example, a user can create a bucket before test execution and populate it with objects from test resources, or receive a test object as a String for assertion after bot task execution.
Explore the S3MockClient interface to see all methods and their descriptions.
@WorkerJUnitConfig
class S3Test {
@Test
void shouldRunBotTask(WorkerAgent agent, LaunchSettingsFactory settingsFactory, S3MockClient s3Client) {
// given
s3Client.createBucket("test-bucket");
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/bot-task-with-s3-interaction.xml")
.build();
// when
agent.run(settings, Duration.ofSeconds(10));
// then
assertThat(s3Client.getObjectAsString("test-bucket", "created-file.txt")).isNotEmpty();
}
}
S3 scopes
S3MockClient can have one of three scopes, that define the lifecycle of uploaded buckets and objects:
- Buckets and objects created in the
@BeforeAllmethods will be kept until all tests are finished. It means that changes made in one test will be visible to others (depending on the order of execution). - Buckets and objects created in the
@BeforeEachmethods will be deleted and re-created between test methods. It means that all tests will see the data exactly as defined, without interference between tests. - Buckets and objects created in the
@Testmethod itself will be deleted right after the method is finished. It means this data will be invisible to other tests.
Secrets Vault provisioning/mocking
There are two ways of working with Secrets Vault entries in Bot Task JUnit:
- set up a bot task test with mock secret entries
- connect to the external Secrets Vault server (for example,
HashiCorp).
We recommend using mock entries in all possible scenarios, because it usually helps to eliminate dependencies on external services which is a good idea for a test.
Here is an example of how you can mock Secrets Vault entries prior to running a bot task:
@WorkerJUnitConfig
class SecretVaultTest {
@Test
void shouldRunBotTaskWithSecretEntries(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
// given
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/SecretVaultProcessor.xml")
.secureEntries(cfg -> cfg
.withEntry("alias-1", "key-1", "value-1")
.withEntry("alias-2", "key-2", "value-2"))
.build();
// then
assertThatCode(() -> agent.run(settings)).doesNotThrowAnyException();
}
}
There might be circumstances when mocking Secrets Vault entries are not sufficient. For example, you have to connect to external service and therefore have to use real credentials. Exposing credentials inside test code poses formidable security risks. That is why for these scenarios the external Secrets Vault server (for example,, HashiCorp) must be used.
Basically, all you have to do is to provide connection settings to the Secrets Vault server using the @TestPropertySource annotation:
@WorkerJUnitConfig
@TestPropertySource("classpath:secret-vault-settings.properties")
class SecretVaultTest {
@Test
void shouldRunBotTaskWithSecretEntries(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
// given
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/SecretVaultProcessor.xml")
.build();
// then
assertThatCode(() -> agent.run(settings)).doesNotThrowAnyException();
}
}
Here is an example of the secret-vault-settings.properties file (mind to put it inside the src/test/resources folder):
secure.storage.type=VAULT
secure.storage.safe.internal=safe_internal
secure.storage.platformId=platform_id
secure.storage.serverApi=https://localhost:8200
secure.storage.client.certificate=/path/to/certificate
secure.storage.client.keyPass=key_pass
secure.storage.applicationId=application_id
note
In case secret-vault-settings.properties contains any sensitive information, you must not commit it directly to the source code repository. Instead, make sure this file appears only during the test runtime (for example, during the CI/CD build).
Assertions
Output data
The com.workfusion.odf.test.launch.OutputData object helps to manipulate output data (for example, the .CSV data created by the export webharvest plugin).
You can obtain it from BotTaskResult after bot task execution is completed. The following example shows how to assert output data:
@WorkerJUnitConfig
class OutputDataTest {
@Test
void shouldAssertOutputData(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
OutputData outputData = agent.run(settingsFactory.builder()
.botTask("configs/main/OutputDataTask.xml")
.build())
.getOutputData();
// assert that file with output data is created
assertThat(outputData.getFile()).exists();
// assert that file with output data contains exact content
assertThat(outputData.getFile()).hasContent("expected content");
// assert that output data contains only two columns with exact names
assertThat(outputData.getHeaders()).containsExactly("column-1", "column-2");
// assert that output data contains exact values in specific column
assertThat(outputData.getColumnValues("column-1")).containsExactly("value 1", "value 2");
// assert that output data contains exact values in specific column
assertThat(outputData.getColumnValues("column-1", Integer::valueOf)).containsExactly(1, 2, 3);
// assert that output data contains exact value in specific cell
assertThat(outputData.getValue("column-1", 1)).isEqualTo("value 2");
}
}
Datastores
Usually, bot tasks produce data during execution which is stored inside various datastores. It might be a good idea to assert this data at the end of a test to make sure everything works as expected. To do so, you can use the Datastores.read() method. It returns a com.workfusion.odf.test.datastore.table.DatastoreTable object with data from the requested datastore:
@WorkerJUnitConfig
class DatastoresTest {
@Test
void shouldAssertDatastoreRecords(Datastores datastores) {
DatastoreTable datastoreTable = datastores.read("datastore_name");
// assert that datastore is not empty
assertThat(datastoreTable.isEmpty()).isFalse();
// assert that datastore has only 2 rows
assertThat(datastoreTable.getRowCount()).isEqualTo(2);
// assert that datastore has correct headers
assertThat(datastoreTable.getColumns()).containsExactly("column1", "column2");
// assert that some row has specific data
assertThat(datastoreTable.getRow(0)).containsExactly("value1", "value2");
assertThat(datastoreTable.getLastRow()).containsExactly("value3", "value4");
assertThat(datastoreTable.getValues(1)).isEqualTo(ImmutableMap.of(
"column1", "value3",
"column2", "value4"
));
// assert that some column has specific data
assertThat(datastoreTable.getColumn("column1")).containsExactly("value1", "value3");
// assert that cell has specific value
assertThat(datastoreTable.getValue(0, "column1")).isEqualTo("value1");
}
}
ODF transaction
The following example shows how to assert ODF transactions:
@WorkerJUnitConfig
class OdfTransactionTest {
@Test
void shouldAssertOdfTransaction(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
// run bot task which produce ODF transactions
agent.run(settingsFactory.builder().botTask("configs/main/TransactionSupplier.xml").build());
DatastoreTable transactionsTable = datastores.read("_odf_transactions");
List<Transaction> transactions = transactionsTable.getColumnValues("transaction_data", InternalStorageFormats::fromJson);
// assert that only one transaction is created
assertThat(transactions).hasSize(1);
// assert that only one document is created
assertThat(transactions.get(0).getDocs()).hasSize(1);
Document actualDocument = transactions.get(0).getDocs().get(0);
// assert that document has specific data
assertThat(actualDocument.getName()).isEqualTo("expected name");
assertThat(actualDocument.getType()).isEqualTo("expected type");
assertThat(actualDocument.getExtractedFields()).isEmpty();
}
}
RPA tasks
note
If you require to run an RPA task from a JUnit test, IA Cloud Developer is mandatory. Make sure to install it in your local environment. You may check a compatible version from the Compatibility matrix section.
A JUnit test for the RPA task looks completely the same as a regular Bot Task JUnit test. The only thing that changes is that you have to replace the @WorkerJUnitConfig annotation in favor of @IacDeveloperJUnitConfig. The following listing shows an RPA test example:
@IacDeveloperJUnitConfig
class RpaIntegrationTest {
@Test
void shouldRunRpaTask(WorkerAgent agent, LaunchSettingsFactory settingsFactory) {
// given
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/rpa-task.xml")
.build();
// when
BotTaskResult actualResult = agent.run(settings);
// then
assertThat(actualResult.getOutputData().getRecords()).hasSize(1);
}
}
IA Cloud Developer integration
Changing just one annotation might sound like a small transformation, but in reality, it completely changes the test lifecycle.
Instead of setting up an in-memory environment (queues, workers, etc.), @IacDeveloperJUnitConfig does the following:
- searches for IA Cloud Developer installation
- connects to Secrets Vault to obtain required settings and credentials
- establishes a connection with RabbitMQ
- puts a task to the RabbitMQ and waits until the worker from IA Cloud Developer completes the task
Thus, you should install and launch IA Cloud Developer before running an RPA test. For our example, we would need to start the RPA Worker group. It already contains Secrets Vault, RabbitMQ, and RPA Worker itself.

note
In case IA Cloud Developer is not available, the test will fail with ConnectException: Connection refused: connect.
It is important to highlight that the test will try to use all services directly from IA Cloud Developer including File Storage, OCR, and Secret Vault. Mind to put your test files directly to File Storage or save credentials to Secret Vault if required. You should start the related services prior to running the test. The rest of the components located in the OCR group include:
- OCR Worker
- OCR Rest API
- RabbitMQ
- File Storage
- Secrets Vault

Using mocks together with IA Cloud Developer
At some point, you may find that you want to use, for example, real OCR from the IA Cloud Developer installation and, at the same time, mock S3 behavior, instead of using real File Storage. By default, @IacDeveloperJUnitConfig will try to use ALL services from the IA Cloud Developer installation (both OCR and File Storage in our case). You can change this behavior if needed. Here is an example of how to do it.
@IacDeveloperJUnitConfig
class RpaIntegrationTest {
@Test
void shouldRunOcrTaskWithS3MockClient(WorkerAgent agent, LaunchSettingsFactory settingsFactory, S3MockClient s3Client) {
// given
s3Client.createBucket("doc-upload");
LaunchSettings settings = settingsFactory.builder()
.botTask("configs/main/ocr-task.xml")
.s3(cfg -> cfg.withEndpointUrl(s3Client.getServerEndpoint()))
.build();
// when
BotTaskResult actualResult = agent.run(settings);
// then
assertThat(actualResult.getOutputData().getRecords()).hasSize(1);
}
}
AutoML tasks
caution
Bot Task JUnit does not work with AutoML tasks.
Known issues
Error running '%fileName%': Command line is too long. Shorten command line for %fileName% or also for JUnit default configuration
Symptoms
When running a Bot Task JUnit test from Intellij IDEA for the first time, you might see the following error:
Error running '%fileName%': Command line is too long. Shorten command line for %fileName% or also for JUnit default configuration.
Solution
In the Run/Debug Configuration dialog, navigate to the Shorten command line field and choose JAR manifest from the dropdown list.

For more details, refer to the blog post from JetBrains.
Could not initialize class okhttp3.OkHttpClient
Symptoms
If you are trying to run a test and using the ODF version prior to 10.1.0.25, you might see the following error in the console:
java.lang.NoClassDefFoundError: Could not initialize class okhttp3.OkHttpClient
Solution
Exclude com.xing.qa.selenium.grid:selenium-api from the bot-execution-core dependency in your pom.xml:
<dependency>
<groupId>com.workfusion.spa.bot</groupId>
<artifactId>bot-execution-core</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>selenium-api</artifactId>
<groupId>com.xing.qa.selenium.grid</groupId>
</exclusion>
</exclusions>
</dependency>
Exception opening port (port may be in use)
Symptoms
When running a test on Windows OS, you may experience the following exception saying that the port is already in use:
Exception opening port "port_number" (port may be in use), cause: "java.net.BindException: Address already in use: JVM_Bind" [90061-200]
The exception occurs quite rarely. Usually, it means that at the time of the test being configured, the operating system returned a free port, but at the runtime, the port had already been taken by some other process. Unfortunately, bot-task-junit cannot re-configure
itself at this stage and fails with an error.
Solution
Restart the failed test.