Skip to Content
Technical Articles
Author's profile photo Andre Adam

Integration Tests in a Cloud Foundry Java App using SAP Cloud SDK for OData calls

During development of our Cloud Foundry Java app we had the requirement to call an OData service in SAP S/4HANA. We had to call this in a generic way and did not know during development which OData will be called during runtime.

We decided to use the SAP Cloud SDK to get support for these OData calls.

Now after everything was implemented, we wondered how to setup integration tests for such calls.

In this blog post I will explain how we decided to test such calls. Other teams might come up with other solutions. Perhaps we can discuss other options here.

Our Service Setup

 

  • Java 8 application
  • Build tool: Maven
  • Spring Boot 2.x.x
  • Java security 2.x.x
    • Dependency: com.sap.cloud.security.xsuaa:spring-xsuaa
  • Cloud Application Programming Model (CAP) 1.x.x using OData version 2
    • Dependency: com.sap.cloud.servicesdk.prov:odatav2-spring-boot-starter
  • Cloud Application Programming CDS data store
  • SAP Cloud SDK 2.x.x
    • Dependency: com.sap.cloud.s4hana.cloudplatform
  • SAP Enterprise Messaging

If working with the SAP Cloud SDK for calling an OData in a SAP S/4HANA system two things need to be mocked:

  • Destination service
  • OData call

Mock for Destination Service

 

In our project we use the destination service to retrieve the sap-client from the destination the customer must define. In the tests we also want to retrieve the sap-client from the destination to check if the client is correctly used in the OData call.

To mock the destination service, the SAP Cloud SDK provides a mock util. To use it the following dependency needs to be added in the pom:

<dependency>
    <groupId>com.sap.cloud.s4hana</groupId>
    <artifactId>testutil</artifactId>
    <version>2.20.2</version>
    <scope>test</scope>
</dependency>

 

In addition to the added dependency, two files need to be added in the resources folder:

credentials.yaml

credentials:

  - alias: "DESTINATION_ALIAS"
    username: "username"
    password: "password"

systems.yaml

erp:
  systems:
    - alias: "DESTINATION_ALIAS"
      uri: "http://localhost:1111/"
      sapClient: 123
      locale: CH

In the systems.yaml the fields of the mocked destination can be provided like sap-client or locale.

After the dependency and the property files are added the following coding can be used to mock the destination:

private static final String DESTINATION = "UNIT_TEST_DESTINATION";
private static final String DESTINATION_ALIAS = "DESTINATION_ALIAS";

private static final MockUtil mockUtil = new MockUtil();

@BeforeClass
public static void classSetup(){
    mockUtil.mockErpDestination(DESTINATION, DESTINATION_ALIAS);
}

 

Now if you call the destination service in your productive coding the mentioned parameters are returned:

erpConfig = new ErpConfigContext(destination);
String client = erpConfig.getSapClient();

 

Mock for Odata Call

 

Unfortunately, the SAP Cloud SDK does not provide such an easy method to mock the OData call. So, for mocking this call we use a web server which is defined in the test. Here we use WireMock.

The following dependency is needed in the pom:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8-standalone</artifactId>
    <version>2.24.1</version>
    <scope>test</scope>
</dependency>

In the coding the server is started like this:

private static final int LOCAL_PORT = 1111;

private static WireMockServer wireMockServer;

@BeforeClass
public static void classSetup() throws IOException {
    configureFor(LOCAL_PORT);
    wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().port(LOCAL_PORT));
    wireMockServer.start();
}

@AfterClass
public static void classTeardown() {
    wireMockServer.stop();
}

@Before
public void setup() {
    wireMockServer.resetAll();
    createStubForMockServer();
}

private void createStubForMockServer() {
    stubFor(get(urlEqualTo("/odata/Entity?$format=json")).withHeader("sap-client", equalTo("123"))
            .withHeader("sap-language", equalTo("en")).willReturn(
                    aResponse().withStatus(200).withHeader("Content-Type", "application/json")
                            .withBody("some content")));
}

In the BeforeClass method we configure and start the web server. In the AfterClass method we stop the web server.

Before each test we reset the server and create a stub for the mock server. This could also be done in each test by creating different stubs for each test.

In the stub we check the URL and two header parameters and if the request matches these conditions the defined response is returned. So, in tests we can now check if the response we expect from the return of the stub match.

In this example we would call the backend system in our productive coding with the destination UNIT_TEST_DESTINATION. In the settings in systems.yaml we have defined that the URL for this destination is http://localhost:1111/.

Because we call our productive coding with the relative path /odata/Entity?$format=json the complete URL which is called is:

http://localhost:1111/odata/Entity?$format=json

The wiremock server runs on localhost with port 1111 because we start the server with this port:

configureFor(LOCAL_PORT);
wireMockServer = new WireMockServer(WireMockConfiguration
                                       .wireMockConfig()
                                       .port(LOCAL_PORT));
    

So, if the stub returns the correct body, we can be sure, that the destination is used (because auf URL and port) and also the given URL is used.

 

Complete Test Example

 

The following example test the OData calls. Because our productive coding would not be visible in the tests the test does all the steps our productive coding would do in method callBackendSystem.

The test is written for the above-mentioned configuration.

package com.sap.testapp;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
import com.sap.cloud.sdk.testutil.MockUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CloudSdkExampleIT {

    private static final String DESTINATION = "UNIT_TEST_DESTINATION";
    private static final String DESTINATION_ALIAS = "DESTINATION_ALIAS";
    private static final int LOCAL_PORT = 1111;

    private static final String ODATA_PATH = "/odata/S4Entity?$format=json";
    private static final String RESPONSE_CONTENT = "response content for testing";

    private static WireMockServer wireMockServer;
    private static final MockUtil mockUtil = new MockUtil();

    @BeforeClass
    public static void classSetup() {
        //for mocking destination
        mockUtil.mockErpDestination(DESTINATION, DESTINATION_ALIAS);

        //for mocking OData calls
        configureFor(LOCAL_PORT);
        wireMockServer = new WireMockServer(WireMockConfiguration
                                              .wireMockConfig()
                                              .port(LOCAL_PORT));
        wireMockServer.start();
    }

    @AfterClass
    public static void classTeardown() {
        wireMockServer.stop();
    }

    @Before
    public void setup() {
        wireMockServer.resetAll();
        createStubForMockServer();
    }

    @Test
    public void callODataWorking() throws IOException, URISyntaxException {
        HttpResponse response = callBackendSystem(DESTINATION, ODATA_PATH);

        InputStream responseBodyStream = response.getEntity().getContent();
        String responseBody = IOUtils.toString(responseBodyStream, 
                                               StandardCharsets.UTF_8.name());

        assertThat(response
                   .getStatusLine()
                   .getStatusCode())
          .isEqualTo(HttpStatus.OK.value());
        
        assertThat(responseBody).isEqualTo(RESPONSE_CONTENT);
    }

    @Test
    public void callWrongUrlNotWorking() throws IOException, URISyntaxException {
        HttpResponse response = callBackendSystem(DESTINATION, "some/other/path");

        assertThat(response
                   .getStatusLine()
                   .getStatusCode())
          .isEqualTo(HttpStatus.NOT_FOUND.value());
    }


    private void createStubForMockServer() {
        stubFor(get(urlEqualTo(ODATA_PATH))
                 .withHeader("sap-client", equalTo("123"))
                 .withHeader("sap-language", equalTo("ch"))
                 .willReturn( aResponse()
                              .withStatus(HttpStatus.OK.value())
                              .withHeader("Content-Type", "application/json")
                              .withBody(RESPONSE_CONTENT)));
    }

    private HttpResponse callBackendSystem(String destinationString, String odataPath) throws URISyntaxException, IOException {
        HttpClient client = HttpClientAccessor.getHttpClient(destinationString);

        Destination destination = DestinationAccessor.getDestination(destinationString);
        Map<String, String> destinationProperties = destination.getPropertiesByName();

        URIBuilder builder = new URIBuilder(odataPath);
        HttpGet getRequest =  new HttpGet(builder.build().toString());

        destinationProperties.forEach(getRequest::setHeader);
        return client.execute(getRequest);
    }
}

 

Conclusion

 

In this blog post I showed one way to test productive coding using the mock tool of the SAP Cloud SDK and the tool WireMock.

The destinations and OData endpoints where mocked and the productive coding can be tested without any change for testing. Some settings where needed but with this, testing is no problem.

This is only one way to test the logic, the way we decided to got in our team. I’m pretty sure that there are also other ways.

 

Back to overview

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.