Tests
Companion for Delivery tests
To use Companion for Delivery unit test, you need to configure your Maven Project like it is explained in section: Devops -> Maven configuration -> Companion for Delivery tests
With Companion for Delivery, a unit test is divided into 2 files:
- Java file
- Contains annotation specific to Companion for Delivery used to configure your unit test
.spec.js
file- Based on Jasmine 2.3 framework (check documentation for more information at: http://jasmine.github.io/2.3/introduction.html).
- Contains the JavaScript code executed in your STEP environment
Companion for Delivery have two executions mode :
- Companion Tests executor : Tests default execution mode. All modifications made within tests are persisted on Step instance, you may have to clean them at the end of your tests
- Step Javascript test API executor : Execution mode with
@Rollback
annotation. All modifications made within tests are cancelled on Step instance, but you must have configuration management Step component installed on your instance
Before we start
In the folder src/test
:
- Create a new folder named
resources
Best practices
Feel free to create a full and clear package hierarchy
step-integration/
├─── src/
│ ├─── main/step/
│ │ └─── com/step/companion/
│ │ └─── Dummy.companion.xml
│ └─── test/
│ ├─── java/
│ │ └─── integrationtests/
│ │ └─── com/step/companion/
│ │ └─── DummyTest.java
│ └─── resources/
│ └─── integrationtests/
│ └─── com/step/companion/
│ └─── DummyTest.spec.js
└─── pom.xml
Java file
Create a new java class in the src/test/java
folder.
See below example for a test named DummyTest
in the package integrationtests.com.step.companion
.
package integrationtests.com.step.companion;
import fr.cantor.c4d.sunit.annotations.JSTestFile;
import fr.cantor.c4d.sunit.runners.CompanionJUnitRunner;
import org.junit.Ignore;
import org.junit.runner.RunWith;
@RunWith(CompanionJUnitRunner.class)
@JSTestFile(js = "/integrationtests/com/step/companion/DummyTest.spec.js")
public class DummyTest
{
}
The file named DummyTest.spec.js with a relative path from src/test/resources
(we will create this file in the next chapter) is the JavaScript implementation of the test.
This example shows the minimal requirements for a simple test. Companion for Delivery allows to add some customization to this java class using annotations.
Include Business Rules
In order to test a business rule (Action, Condition or Function), you need to include the target .companion.xml
file into your unit test.
You can do this by using the following annotations:
@BusinessRules({
@BusinessRule(id="BA0040", xml = "/com/step/companion/Dummy.companion.xml", aliases="executeDummyActionOrCondition")
})
TIP
Path to .companion.xml
file is relative from src/main/step
.
In order to test a local business rule, specify the path to workflow .companion.xml
file.
Multiple Javascript plugins and Error Messages
If your business rule contains multiple Javascript modules, you should define multiple aliases : aliases= {"executeDummyActionOrCondition_AppliesIf", "executeDummyActionOrCondition"}
.
Business rules plugins are taken in the order they appear in the .companion.xml
file referenced.
In order to test a business condition that uses error message template, you can test its equality with the variable key
or message
. Other variable added to the message can be tested in the same way.
This business rule :
// testMsg is configured like this
// <LocalizableMessageTemplate Key="testMsg" Message="This is a test {myVariable}"></LocalizableMessageTemplate>
var error = new testMsg();
error.myVariable = "Variable";
return error;
Can be tested like this :
it('should work', function () {
var res = TestBC();
expect(res.key).toBe("testMsg");
expect(res.message).toBe("This is a test {myVariable}");
expect(res.myVariable).toBe("Variable");
});
If your business rule is an action or a condition you will now be able to call it by using the alias in the .spec.js
file as
it("Should call dummy business rule ", function () {
executeDummyActionOrCondition(node,manager);
});
Otherwise, if your business rule is a function you will now be able to call it by using the alias. Unlike business action or condition, you need to separate global bindings from function bindings in the .spec.js
file as
it("Should call dummy business function", function () {
const functionWithGlobalBindings = instantiateDummyBusinessFunction(manager, logger); // instantiate function from global binds
functionWithGlobalBindings(node); //call business function that take a node as input
//you can also use with this syntax directly
instantiateDummyBusinessFunction(manager, logger)(node);
});
Library dependencies
You can add dependencies to a STEP Library using the following annotations by two different way:
- The library is provided by Step Instance : only alias and id parameters are needed
@BusinessLibraries({
@BusinessLibrary(alias = "myBusinessLibraryAlias", id = "myBusinessLibraryId")
})
- The library is provided by Companion project (means embedded into the test) : alias, id and xml parameters are needed
@BusinessLibraries({
@BusinessLibrary(alias = "myBusinessLibraryAlias", id = "myBusinessLibraryId", xml="/com/step/companion/myBusinessLibrary.companion.xml")
})
TIP
Path to .companion.xml
file is relative from src/main/step
.
In order to test a business library of your project, specify the path to workflow .companion.xml
file.
WARNING
Do not use same library with different aliases or different libraries with the same alias.
Include files
You can include one or more files using the following annotations:
@Includes({
@Include(js = "/integrationtests/TestSpecHelpers.js")
})
TIP
Path to .js
file is relative from src/test/resources
.
Rollback
In order to rollback changes made by your tests, you can use the following annotation :
@Rollback
WARNING
The rollback is made at the end of test execution (Java file), not between each it
The annotation has some restrictions :
- It can only be used with Step environment where the configuration management Step component is installed
- You cannot add dependencies to libraries provided by Step. All business rules dependencies must be provided by the companion project and their dependencies as well.
.spec.js
file
After creating the Java file, you have to create a new .spec.js
file in the resources folder.
This file describes your unit test using the Framework Jasmine 2.3 (check documentation for more information at: http://jasmine.github.io/2.3/introduction.html).
Nothing specific to Companion for Delivery here.
Parameterized tests
Introduced in version 5.4.0, parameterized tests facilitate the execution of tests with extensive datasets.
When conducting tests within Step using the Step JavaScript test API or the legacy Companion test executor, significant modifications to a test file may result in a transaction that is too large to handle efficiently. This can lead to excessively long test execution times or tests that never conclude within Step.
To address these issues, we have implemented the parameterized feature.
A parameterized test is typically divided into three files:
Java File: This file declares the tested business rules, includes necessary test files, and specifies the parameter test file for parameterized tests.
Test Execution File: In this file, you execute the tests and handle the testing and Step logic. It includes:
- jsParameter: This specifies the JavaScript file used to handle parameterized tests.
- jsParameterArgs: This denotes the names of the arguments in the JavaScript test file.
Parameter Test File: Here, you specify the different test cases as data and pass them to the test logic for execution.
For exemple we want to test if a value is even or odd.
The provided example demonstrates the recommended approach for handling parameterized tests
Java File :
package fr.cantor.notebooks;
import fr.cantor.c4d.sunit.annotations.IncludeCompanionTestHelpers;
import fr.cantor.c4d.sunit.annotations.JSTestFile;
import fr.cantor.c4d.sunit.annotations.Rollback;
import fr.cantor.c4d.sunit.runners.CompanionJUnitRunner;
import org.junit.runner.RunWith;
@RunWith(CompanionJUnitRunner.class)
@JSTestFile(js = "/fr/cantor/notebooks/PairBatchedExempleTest.spec.js", jsParameter = "/fr/cantor/notebooks/PairBatchedExempleTest.testGenerator.js", jsParameterArgs = {"testCases"})
@IncludeCompanionTestHelpers
@Rollback
public class PairBatchedExempleTest {
}
This file declares
/fr/cantor/notebooks/PairBatchedExempleTest.spec.js
as the testing file,/fr/cantor/notebooks/PairBatchedExempleTest.testGenerator.js
as the test data provider, and jsParameterArgs specifies the data binding in the test file.
Test Data Provider JavaScript File PairBatchedExempleTest.testGenerator.js
:
var data = [
{value: 1, isOdd: true},
{value: 2, isOdd: false},
{value: 3, isOdd: true},
{value: 4, isOdd: false},
{value: 5, isOdd: true},
{value: 6, isOdd: false},
{value: 7, isOdd: true},
{value: 8, isOdd: false},
{value: 9, isOdd: true},
{value: 10, isOdd: false},
{value: 11, isOdd: true},
{value: 12, isOdd: false},
{value: 13, isOdd: true},
{value: 14, isOdd: false},
{value: 15, isOdd: true},
{value: 16, isOdd: false},
{value: 17, isOdd: true},
{value: 18, isOdd: false},
{value: 19, isOdd: true},
{value: 20, isOdd: false},
];
const batchSize = 10;
for(var i = 0; i < data.length;) {
var nextI = i + batchSize;
nextI = nextI < data.length ? nextI : data.length;
PairBatchedExempleTest("Test case for " + i + " to " + nextI, data.slice(i, nextI));
i = nextI;
}
In the provided file PairBatchedExempleTest.testGenerator.js
, the test data is generated and passed to the test logic through serialization. `PairBatchedExempleTest refers to the name of the Java test file and is used as a function in this JavaScript file. The function takes multiple arguments:
- The first argument is always the name of the test.
- The subsequent arguments correspond to the bindings provided in
jsParameterArgs
in the Java file.
Test Logic JavaScript File PairBatchedExempleTest.spec.js
:
describe("Event/odd test", function (){
beforeAll(function () {
jasmine.addMatchers(companionTestMatchers)
});
testCases.forEach(function (testCase) {
if(testCase.isOdd) {
it(testCase.value + " to be Odd", function () {
expect(testCase.value % 2).toBe(1);
});
} else {
it(testCase.value + "to be Even", function () {
expect(testCase.value % 2).toBe(0);
});
}
})
} )
In the file PairBatchedExempleTest.spec.js
, the test logic is implemented, wherein test cases are iterated through and assertions are made based on whether the value is even or odd. The values provided from PairBatchedExempleTest.testGenerator.js
are passed through the testCases
variable for processing within the test logic.
Use cases
Workflow tests
For this example, let's start with a simple 2 states workflow (initial and final) : SimpleWorkflow
.
The Java test file: SimpleWorkflowTest.java
package integrationtests.com.step.companion.workflows;
import fr.cantor.c4d.sunit.annotations.JSTestFile;
import fr.cantor.c4d.sunit.runners.CompanionJUnitRunner;
import org.junit.runner.RunWith;
@RunWith(CompanionJUnitRunner.class)
@JSTestFile(js = "/integrationtests/com/step/companion/workflows/SimpleWorkflowTest.spec.js")
public class SimpleWorkflowTest
{
}
The .spec.js
file: SimpleWorkflowTest.spec.js
Here we can test the two simple steps of the workflow, each step is inside a describe bloc, in which we run simple tests:
function createNode() {
return manager
.getProductHome()
.getProductByID("PRODUCT_TEST_ROOT_ID")
.createProduct(null, "PRODUCT_TYPE");
}
function deleteNode(node) {
node.getWorkflowInstances().toArray().forEach(function (instance) {
instance.delete("");
});
node.delete();
}
function expectToBeInState(node, state) {
var isInState = node.getWorkflowInstanceByID(state.getWorkflow().getID()).getTask(state) != null;
expect(isInState).toBeTruthy();
}
describe('SimpleWorkflow Test', function () {
var workflow;
var initialState;
var finalState;
beforeAll(function () {
workflow = manager.getWorkflowHome().getWorkflowByID("SimpleWorkflow");
initialState = workflow.getStateByID("InitialState");
finalState = workflow.getStateByID("FinalState");
});
describe("InitialState", function () {
var node;
var task;
beforeEach(function () {
node = createNode();
var instance = node.startWorkflowByID("SimpleWorkflow", "");
task = instance.getTaskByID(initialState.getID());
});
afterEach(function () {
deleteNode(node);
});
it("should be the initial state", function () {
expectToBeInState(node, initialState);
});
it("should go to FinalState on continue", function () {
var result = task.triggerByID("continue", "");
expect(result.isRejectedByScript()).toBeFalsy();
expectToBeInState(node, finalState);
});
});
describe("FinalState", function () {
var node;
var task;
beforeEach(function () {
node = createNode();
var instance = node.startWorkflowByID("SimpleWorkflow", "");
instance.getTaskByID(initialState.getID()).triggerByID("continue", "");
task = instance.getTaskByID(finalState.getID());
});
afterEach(function () {
deleteNode(node);
});
it("should be in FinalState", function () {
expectToBeInState(node, finalState);
});
});
});
TIP
Use the startWorkflowByID
function to initiate a product in a workflow then the triggerByID
function to move the product to the next state. This is useful to check that all paths in a workflow are correct.
Debugging
To help debugging tests, c4d provide a special binding debugLogger. This binding allows the test logs to be displayed in the c4d console.
Inside DummyTest.spec.js with Dummy businessAction as executeDummyActionOrCondition.
it("Should call dummy business rule ", function () {
debugLogger.info("inside my unit test")
executeDummyActionOrCondition(node, manager, debugLogger);
});
And Dummy businessAction
logger.info("inside my business rule");
Display the following logs inside the console
août 18, 2022 3:10:39 PM fr.cantor.c4d.sunit.jasmine.JasmineJavaReporter$Log log
INFOS: inside my unit test
août 18, 2022 3:10:39 PM fr.cantor.c4d.sunit.jasmine.JasmineJavaReporter$Log log
INFOS: inside my business rule
Running unit tests
Command line
To run unit test, you can run Maven command : mvn verify
.
You can also run only one class of unit test, by adding -Dit.test
parameter to your maven command.
mvn -Dit.test=DummyTest verify
IntelliJ
Running test classes in IntelliJ requires some additional configuration.
Indeed, when tests are run using a Maven command such as mvn verify
, the test classes are executed with the configuration coming from the Maven properties that are defined in your pom.xml or your settings.xml file.
But when the test classes are run from IntelliJ, the test runner is not aware of the Maven configuration. It has to be manually added to the IntelliJ JUnit configuration.
To avoid having to configure it for each test class, you can edit the default JUnit configuration template for your project. The steps are the following :
Click on Run / Edit Configurations
Navigate to Templates / JUnit
Click the "Browse" icon on the "Environment variable" line
- Add the following environment variables
- step.url : The URL of the STEP environment where your tests should be executed
- step.username : The username to connect to the STEP environment
- step.password : The password to connect to the STEP environment
- step.context : The context in which to execute the test BR
- step.workspace : The workspace in which to execute the test BR. Usually "Main"
- Click "Apply" then "OK"
- Add the following environment variables
Companion Tests Helpers
Getting started
Companion tests helpers is an extension to provide Jasmine custom matchers to facilitate business rules testing.
To use it :
- You have to include the following dependency in your project dependencies :
<dependency>
<groupId>fr.cantor.companion.ext</groupId>
<artifactId>companion-test-helpers</artifactId>
<version>1.1.0</version>
</dependency>
- In your Java test file, add the following annotation :
@IncludeCompanionTestHelpers
- In your spec.js file add custom matchers with companionTestMatchers binding to Jasmine matchers. It has to be added inside a Jasmine beforeAll
describe("tests", function () {
beforeAll(function () {
jasmine.addMatchers(companionTestMatchers);
})
it("should work", function (){
//check if node name is "my node"
expect(node).toHaveName("my node");
});
});
Companion Custom Matchers
This documentation provides an overview of the custom matchers available with Companion Custom Matchers. These matchers enhance testing capabilities by allowing you to perform specialized checks on various types of objects from Step API.
- Node Matchers
- Workflowable Node Matchers
- Task Matchers
- Workflow Instance Matchers
- Workspace Aware Revisable Node Matchers
- Trigger Result Matchers
- Array Matchers
Node Matchers
Matcher | Description |
---|---|
toHaveName(expected: string): boolean | Check if a node has the expected name.expected : Expected name for the given node. |
toHaveEmptyName(): boolean | Check if a node has an empty name. |
toHaveSimpleValue(expected: { attributeId: string, value: string }): boolean | Check if a node has a simple value for the specified attribute.expected : Object containing attributeId and value properties. |
toHaveLOVValueId(expected: { attributeId: string, lovId: string, id?: string }): boolean | Check if a node has a LOV (List of Values) value for the specified attribute.expected : Object containing attributeId , lovId , and optionally id properties. |
toHaveEmptyValue(expected: string): boolean | Check if a node has an empty value for the specified attribute.expected : Attribute ID to be checked. |
toHaveValues(expected: { attributeId: string, values: string[] }): boolean | Check if a node has the expected values for a multi-values attribute.expected : Object containing attributeId and values array. |
toHaveLOVValuesId(expected: { attributeId: string, valuesId: string[] }): boolean | Check if a node has LOV (List of Values) values for a LOV multi-values attribute.expected : Object containing attributeId and valuesId array. |
toHaveClassifications(expected: { classificationsIds: string[], linkTypeId?: string }) | Check if a product or entity has the expected classifications.expected : Object containing classificationsIds array and optional linkTypeId for products. |
toHaveChildren(expected: string[]) | Check if a product, entity, or classification has the expected children.expected : Array of expected children IDs. |
toHaveReference(expected: { typeId: string, target: com.stibo.core.domain.ReferenceTarget }) | Check if a reference source has a reference for the expected reference type ID and target.expected : Object containing typeId and target properties. |
Workflowable Node Matchers
Matcher | Description |
---|---|
toBeInState(expected: com.stibo.core.domain.state.State | { workflowId: string, stateId: string }): boolean | Check if a workflowable node is in the expected state of a workflow.expected : A State object or an object containing workflowId and stateId . |
toBeInWorkflow(expected: com.stibo.core.domain.state.Workflow | string): boolean | Check if a workflowable node is in the expected workflow.expected : A Workflow object or a workflow ID as a string. |
toHaveWorkflowValue(expected: { workflowId: string, variableId: string, value: string }): boolean | Check if a workflowable node has the expected value for the specified workflow variable.expected : Object containing workflowId , variableId , and value . |
Task Matchers
Matcher | Description |
---|---|
toBeAssignedTo(expected: string): boolean | Check if a task is assigned to the expected user.expected : Expected user ID. |
Workflow Instance Matchers
Matcher | Description |
---|---|
toHaveSimpleVariable(expected: { variableId: string, value: string }) | Check if a workflow instance has the expected value for the specified workflow variable.expected : Object containing variableId and value . |
toHaveEmptyVariable(expected: string): boolean | Check if a workflow instance has an empty value for the specified workflow variable.expected : Workflow variable ID. |
Workspace Aware Revisable Node Matchers
Matcher | Description |
---|---|
toBeApproved(): boolean | Check if a workspace-aware revisable node is approved. |
Trigger Result Matchers
Matcher | Description |
---|---|
toBeRejected(): boolean | Check if a trigger result is rejected. |
toBeRejectedWithMessage(message: string): boolean | Check if a trigger result is rejected with the specified message. |
Array Matchers
Matcher | Description |
---|---|
toBeSameArrayAs(expected: any[]) | Check if the given array is the same as the expected array.expected : Expected array. |