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:

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.htmlopen in new window).

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:

  1. Java File: This file declares the tested business rules, includes necessary test files, and specifies the parameter test file for parameterized tests.

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

    1

  • Navigate to Templates / JUnit

    2

  • Click the "Browse" icon on the "Environment variable" line

    3

    • 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"

    4

    • Click "Apply" then "OK"

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

MatcherDescription
toHaveName(expected: string): booleanCheck if a node has the expected name.
expected: Expected name for the given node.
toHaveEmptyName(): booleanCheck if a node has an empty name.
toHaveSimpleValue(expected: { attributeId: string, value: string }): booleanCheck 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 }): booleanCheck 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): booleanCheck if a node has an empty value for the specified attribute.
expected: Attribute ID to be checked.
toHaveValues(expected: { attributeId: string, values: string[] }): booleanCheck 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[] }): booleanCheck 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

MatcherDescription
toBeInState(expected: com.stibo.core.domain.state.State | { workflowId: string, stateId: string }): booleanCheck 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): booleanCheck 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 }): booleanCheck if a workflowable node has the expected value for the specified workflow variable.
expected: Object containing workflowId, variableId, and value.

Task Matchers

MatcherDescription
toBeAssignedTo(expected: string): booleanCheck if a task is assigned to the expected user.
expected: Expected user ID.

Workflow Instance Matchers

MatcherDescription
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): booleanCheck if a workflow instance has an empty value for the specified workflow variable.
expected: Workflow variable ID.

Workspace Aware Revisable Node Matchers

MatcherDescription
toBeApproved(): booleanCheck if a workspace-aware revisable node is approved.

Trigger Result Matchers

MatcherDescription
toBeRejected(): booleanCheck if a trigger result is rejected.
toBeRejectedWithMessage(message: string): booleanCheck if a trigger result is rejected with the specified message.

Array Matchers

MatcherDescription
toBeSameArrayAs(expected: any[])Check if the given array is the same as the expected array.
expected: Expected array.

Last Updated: