Test Value Mapping in CPI Groovy Scripts

/

,

/

Views: 503

Introduction

One of the things that Vadim Klimov and I covered in our Developing Groovy Scripts for SAP Cloud Platform Integration E-Bite is how to test CPI Groovy scripts in a local development environment.

To achieve this, we introduce a mock implementation for com.sap.gateway.ip.core.customdev.util.Message since the one provided by SAP in the Maven Central Repository is only sufficient for local development but not for local runtime execution. This provides a basic framework allowing the developer to test Groovy scripts locally prior to deploying it into a tenant.

The idea of mock implementation can further be extended to other classes that are not publicly available or suitable for local testing.

In this post, I will share an approach of how to test value mapping in CPI Groovy scripts using a mock implementation of com.sap.it.api.mapping.ValueMappingApi.


Setting the Stage

To begin, let’s set the stage by extending the XML transformation script used as the example in the E-Bite. In that example, the Groovy script transform a payload from a source XML format to a different target XML format.

We now include a new requirement such that the DocumentType field in the target XML format is converted using a value mapping lookup.

The following source code shows the complete logic for the script, with the new additions highlighted.

import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.ITApiFactory
import com.sap.it.api.mapping.ValueMappingApi
import groovy.xml.MarkupBuilder

import java.text.SimpleDateFormat

def Message processData(Message message) {
    Reader reader = message.getBody(Reader)
    def Order = new XmlSlurper().parse(reader)
    Writer writer = new StringWriter()
    def builder = new MarkupBuilder(writer)

    def sourceDocType = message.getProperty('DocType')
    ValueMappingApi api = ITApiFactory.getService(ValueMappingApi, null)

    def items = Order.Item.findAll { it.Valid.text() == 'true' }
    builder.PurchaseOrder {
        'Header' {
            'ID' Order.Header.OrderNumber
            'DocumentDate' new SimpleDateFormat('yyyy-MM-dd').format(new SimpleDateFormat('yyyyMMdd').parse(Order.Header.Date.text()))
            if (!items.size())
                'DocumentType' api.getMappedValue('S4', 'DocType', sourceDocType, 'ACME', 'DocumentType')
        }

        items.each { item ->
            'Item' {
                'ItemNumber' item.ItemNumber.text().padLeft(3, '0')
                'ProductCode' item.MaterialNumber
                'Quantity' item.Quantity
            }
        }
    }

    message.setBody(writer.toString())
    return message
}

Here Comes the Mock

In order to be able to test the value mapping logic locally, we need the ability to write and read simulated value mapping entries. The following mock implementation of com.sap.it.api.mapping.ValueMappingApi allows us to do just that. Following are the key methods of the class:

  • addEntry – adds a value mapping entry which is stored in the class’s map
  • getMappedValue – retrieves a value mapping entry from the class’s map
package com.sap.it.api.mapping

class ValueMappingApi {

    private Map entries = [:]
    private static ValueMappingApi valueMappingApi

    private ValueMappingApi() {
    }

    static ValueMappingApi getInstance() {
        if (!valueMappingApi) {
            valueMappingApi = new ValueMappingApi()
        }
        return valueMappingApi
    }

    void addEntry(String key, String value) {
        entries.put(key, value)
    }

    void addEntry(String sourceAgency, String sourceIdentifier, String sourceValue, String targetAgency, String targetIdentifier, String value) {
        def key = "${sourceAgency}_${sourceIdentifier}_${targetAgency}_${targetIdentifier}_${sourceValue}"
        def mappedKey = key.toString()
        entries.put(mappedKey, value)
    }

    String getMappedValue(String sourceAgency, String sourceIdentifier, String sourceValue, String targetAgency, String targetIdentifier) {
        def key = "${sourceAgency}_${sourceIdentifier}_${targetAgency}_${targetIdentifier}_${sourceValue}"
        def mappedValue = entries.get(key.toString())
        if (!mappedValue) {
            return null
        } else {
            return mappedValue
        }
    }
}

Simplification using Maven

In the above mentioned E-Bite, we showed the way how a JAR file can be generated from the mock implementation, which then can be included in the classpath for test execution.

An alternative approach using Maven allows us to further simplify local testing. The above mock implementation alongside other mocks for CPI are available from a public GitLab repository (instead of Maven Central). To access this repository, include the following code sections into the POM file.

i) Add the following dependency in the dependencies section of the POM.

<dependency>
    <groupId>com.equaliseit</groupId>
    <artifactId>sap-cpi-mocks</artifactId>
    <version>1.0.2</version>
</dependency>

ii) Add the following GitLab repository URL in the repositories section of the POM.

<repositories>
  <repository>
      <id>gitlab-maven</id>
      <url>https://gitlab.com/api/v4/groups/12926885/-/packages/maven</url>
  </repository>
</repositories>

Preparing the Test Scenario

Now we can make preparation to test the scenario which has been enhanced with value mapping logic.

The following source code shows the updated Spock specification (used to test the Groovy script), with the new additions highlighted as described below.

  • Add required value mapping entry for the test
  • Verify output field is mapped correctly
import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.mapping.ValueMappingApi
import spock.lang.Shared
import spock.lang.Specification

class XMLTransformationSpec extends Specification {
    @Shared
    Script script
    Message msg

    def setupSpec() {
        GroovyShell shell = new GroovyShell()
        script = shell.parse(this.getClass().getResource('/script/XMLTransformation.groovy').toURI())
    }

    def setup() {
        msg = new Message()
    }

    def 'Scenario 2 - Order does not have items'() {
        given: 'the message body and property are initialized'
        msg.setBody(this.getClass().getResource('input2.xml').newInputStream())
        msg.setProperty('DocType', 'HDR')

        // Set up value mapping entries
        ValueMappingApi vmapi = ValueMappingApi.getInstance()
        vmapi.addEntry('S4', 'DocType', 'HDR', 'ACME', 'DocumentType', 'ACME-HDR')

        when: 'we execute the Groovy script'
        script.processData(msg)

        then: 'the output message body is as expected'
        def root = new XmlSlurper().parse(msg.getBody(Reader))
        verifyAll {
            root.Header.ID.text() == 'ORD80002'
            root.Header.DocumentDate.text() == '2020-02-18'
            root.Header.DocumentType.text() == 'ACME-HDR'
            root.Item.size() == 0
        }
    }
}

Bringing It All Together

The following screenshot shows the structure of the project in IntelliJ IDEA.

By executing the Spock specification, we can test the new logic and confirm that it works as required.

Once it works correctly, it can further be included in a CI/CD pipeline, so that the successful execution of the test is a prerequisite before updating and deploying onto a tenant. The following shows the results of the test being executed on GitHub Actions using FlashPipe.

Conclusion

As logic in Groovy scripts become more complex, the ability to test them locally can significantly reduce the turnaround time for development.

The concept of creating and utilising mocks is a powerful way to enable testing of CPI Groovy scripts in a local environment. This overcomes the limitations of public classes provided by SAP that are minimal and not suitable for local runtime execution.

The example shows only a simple logic for value mapping for the sake of illustration, but the concept can be used to craft highly complex scripts that require interaction with value mapping entries.

References

The following links provide the full repository for the source code used in the post.


Comments

Feel free to provide your comment, feedback and/or opinion here.

About the Author