Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
During development of our Cloud Foundry Java app we faced some challenges of testing our coding in an automated way.

With this blog post I want to share the possibilities we found for our service.

This is only our way. I Java there are always several ways of doing something. So perhaps other teams found other solutions. Perhaps we can also 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


For all these components above we want to write integrations test which are in our case Spring Boot tests. For some of them we need to mock some components, for others we HTTP endpoints we can call.

In the following sections I will explain what can be done or not to test the components using the SAP Cloud Application Programming Model.

 

Our integration tests run during the maven build process and if the tests fail the project can't be built.


Exits in SAP  Cloud Application Programming Model (CAP)


 

CAP provides the possibility to enhance their generic logic with some custom exits. In these exits you can have logic before there logic, after their logic and also replace their logic.

Testing such before and after exits is not possible within the generic call stack in our setup. The problem here is, that in our setup the generic part only supports SAP HANA database which we do not have during integration tests. So, we can't test the exits embedded in the generic OData stack.

What we are doing in our tests is to call the exits directly and check if they are working as expected.

Here nothing special is to mention regarding other integration tests of "normal" developed classes.

Exits which replace the CAP logic for storing or reading data from the database can be tested within the generic part. Because here the data access logic is implemented in the exit it can be done if the logic which is used to store or read data support other databases.

In our case we do not use JPA or other known Java frameworks to access the data, but we use the CDS4J data access provided by CAP. This data access support not only the SAP HANA database but also the SQLite database which is an in-memory database and can be used during the tests.


How to call CAP exists in Spring Boot tests


 

To call the CAP OData endpoints in our stack we can't use the Spring provided MockMvc. This MockMvc will call endpoint known by Spring without starting a web server. The CAP stack and endpoint are not known by Spring because they are registered in a separate web filter.

So for calling the CAP OData endpoint we have to start the web service which is included in Spring and call the endpoints using this web server.

This can be done very easily within Spring Boot Tests. A new Spring Boot test needs to be created and annotated with the annotations:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

After that the web service included in Spring will be started for the test class and the endpoint can be called.

Here an example of the complete setup:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestHandlerForDataAccessIT {

@LocalServerPort
int port;

private TestRestTemplate testRestTemplate;

@Before
public void setup() {
testRestTemplate = new TestRestTemplate();
baseUrl = "http://localhost:" + port + "/odata/v2/";
}

@Test
public void checkThreadScope() {

String url =
baseUrl + "someTestEndpoint/";

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", jwtDummyScope);

ResponseEntity<String> response = testRestTemplate
.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), String.class);

String body = response.getBody();

// ... do some asserts
}

}

 

An endpoint "/odata/v2/someTestEndpoint/" is called with HTTP-GET to retrieve some data.

After the request is received the body can be evaluated.


Setup Database for Testing CAP exits


 

Calling endpoints is ok, but without retrieving there will not test the complete logic. So, for the CDS4J data access a database needs to be setup to prepare some data or after test check if the data are stored.

We use the SQLite database in our tests.

The database can be added in the project POM to have it available in the tests:
    <dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.28.0</version>
<scope>test</scope>
</dependency>

After the database is added some configuration is needed for this database:
spring:
datasource:
# why this url is described here: https://www.sqlite.org/inmemorydb.html
url: "jdbc:sqlite:file:memdb1?mode=memory&cache=shared"
driver-class-name: org.sqlite.JDBC
initialization-mode: always
# needed if tables still there from other tests to have no error in tests
continue-on-error: true

For the URL defined in the properties different URLs can be mentioned. More information can be found here. Our used URL defines a database which is in memory and can be used in multi thread scenarios which we have, because we start asynchronously threads in our logic.

The continue-on-error property needs to be set because if several different IT classes are executed and every class again tries to create tables an error occurred, that the table is still existing.

The problem with these kinds of setup, where multi thread are supported is, that not for each test a new database is created. So, we need to take care, that we delete created table entries before executing the next test. This will be explained later.

After the properties are set, Spring will automatically start the database in Spring Boot tests.


Including tables in database


 

After the database is setup tables needs to be

Now the database is started but does not have the tables needed to store data or read the data. For creating the tables, a file schema.sql needs to be included in the resources folder /src/test/resources. Spring will exactly search for a file with name schema.sql in the resources folder. The schema.sql needs to have create statements for all the tables and views like this:
CREATE TABLE some_test_Table (
ID NVARCHAR(36) NOT NULL,
modifiedAt TIMESTAMP,
createdAt TIMESTAMP,
createdBy NVARCHAR(255),
modifiedBy NVARCHAR(255),
externalID NVARCHAR(5000),
name NVARCHAR(100),
status_code NVARCHAR(100),
);

Of course we do not want to write all create statements manually we generate the schema.sql file based on the CAP CDS modelling we did for our project.

For generating the schema.sql the following node script needs to be executed:
cds c -2 sql index.cds > ./src/test/resources/schema.sql

This will compile all the artifacts mentioned in the index.cds and creates the schema.sql. We use the CAP provided CDS tool for the generation.

The index.cds includes a reference to all our data definitions. So, with this we can create all table CREATE statements we need.

Now we add the execution of this generation to our POM to execute the generation during build of the project. For achieving this we create at first a file package.json:
{
"name": "integration-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"schema": "cds c -2 sql index.cds > ./src/test/resources/schema.sql"
},
"author": "",
"license": "ISC",
"cds": {
"odata": {
"version": "v2"
}
},
"dependencies": {
"@sap/cds": "^3.16.0"
}
}

Under "scripts" -> "schema" we found the mentioned script for the generation. In the dependencies section the tool for the generation is referenced.

Now we add a plugin in the POM of our maven project to execute the package.json script:
<!-- STEPS TO GENERATE CDS ARTIFACTS -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<executable>npm</executable>
</configuration>
<executions>
<execution>
<id>npm install</id>
<goals>
<goal>exec</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<arguments>
<argument>install</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>npm run schema</id>
<goals>
<goal>exec</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<arguments>
<argument>run</argument>
<argument>schema</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>

There are two executions. The first one installs the needed node modules and the second executes the script with the arguments "run schema".


Adding/Removing data in the tables


 

After all the steps for the setup are done the database tables can be accessed in the tests like they are accessed in the productive coding.

With our current setup the data base is not reset or deleted after an integration test. So, for this the table entries written by a test needs to be deleted after each test method or test class. This can be done like this:
@After
public void teardown() {
deleter.deleteProcedureRuns();
}


Method in deleter:
@Component
public class DataTableDeleter {

@Autowired
private DataStoreProvider provider;

@Transactional
public void deleteProcedureRuns() {
CdsDataStore store = provider.getDataStore();

CqnDelete deletion = Delete.from(ProcedureRuns_.CDS_NAME);
store.execute(deletion);
}
}

To delete the data a new transaction is needed. Because of this we decided to create a new class for the deletion and adding the Spring annotation @Transactional to the method which do the deletion. This will only work if the class is auto wired in the test and in this auto wired instance the public method which includes the annotation is called.

Why only in this case it is working:

Spring will create a proxy if it finds the annotation @Transactional. In this proxy a new transaction is started. If the method is called without Spring (no auto wiring, called from same class with this....) the proxy is not used and so the transaction can't be started.


Writing data


 

For writing data during an integration test using the CDS4J data access a new transaction needs to be created. If data will be written in a test without a transaction the following error occurs:
com.sap.cds.transaction.TransactionRequiredException: There is no currently active transaction

Therefore, in the deleter method above a @Transactional was added. This annotation is only working on public methods. If a transaction is needed in a test without such an annotation the following coding can be used:
private void insertDataUsingTransaction(CdsDataStore dataStore, Map<String, Object> data) {
TransactionTemplate txTemplate = new TransactionTemplate(txMgr);
txTemplate.execute(s -> {
CqnInsert insert = Insert.into("table_name").entry(data);
dataStore.execute(insert);

return null;
});
}


 

Logging


 

To enable more detailed logs for the data access the following lines can be added in the application.yaml of the java project:
logging.level:
com.zaxxer:
hikari:
pool:
HikariPool: DEBUG
org:
springframework:
jdbc:
datasource: DEBUG
com:
sap:
cds:
impl:
JDBCClient: DEBUG

After including these lines, the data access will be logged.


Conclusion


 

In this blog post I wanted to show what is already possible and what not when it comes to testing a Java application using the SAP Cloud Application Programming Model with the mentioned versions.

In other, newer versions it might be possible to test more, but for these versions I showed what is possible.

Because there are only few parts which can't be tested now, a good test coverage is possible. For the parts which can't be tested now some other tests like automatic tests against a deployed app can be used. These tests would then be no Integration Tests but System Tests.

 

Back to overview
3 Comments