Skip to content
On this page

Development

Find here information on how to configure applications, different supported databases, spring boot integration, and the CDS Maven Plugin.

Application Configuration

This section describes how to configure applications. CAP Java applications can fully leverage Spring Boot's capabilities for Externalized Configuration. This enables you to define multiple configuration profiles for different scenarios, like local development and cloud deployment.

For a first introduction, have a look at our sample application and the configuration profiles we added there.

Now, that you’re familiar with how to configure your application, start to create your own application configuration. See the full list of CDS properties as a reference.

Service Bindings on SAP BTP, Kyma Runtime

In the SAP BTP, Kyma Runtime, credentials of service bindings are stored in Kubernetes secrets. Using volumes, you can mount secrets into your application's container. These volumes contain a file for each of the secrets properties.

Get the Secret into Your Container

To use a Kubernetes secret with your CAP service, you create a volume from it and mount it to the service's container.

For example:

yaml
spec:
  volumes:
    - name: bookshop-db-secret-vol
      secret:
        secretName: bookshop-db-secret
  containers:
  - name: app-srv
    ...
    volumeMounts:
      - name: bookshop-db-secret-vol
        mountPath: /etc/secrets/sapcp/hana/bookshop-db
        readOnly: true
spec:
  volumes:
    - name: bookshop-db-secret-vol
      secret:
        secretName: bookshop-db-secret
  containers:
  - name: app-srv
    ...
    volumeMounts:
      - name: bookshop-db-secret-vol
        mountPath: /etc/secrets/sapcp/hana/bookshop-db
        readOnly: true

Prepare Your CAP Application

Add the cds-feature-k8s feature in the pom.xml file of your CAP application to consume service credentials:

xml
<dependencies>
	<!-- Features -->
	<dependency>
		<groupId>com.sap.cds</groupId>
		<artifactId>cds-feature-k8s</artifactId>
		<scope>runtime</scope>
	</dependency>
</dependencies>
<dependencies>
	<!-- Features -->
	<dependency>
		<groupId>com.sap.cds</groupId>
		<artifactId>cds-feature-k8s</artifactId>
		<scope>runtime</scope>
	</dependency>
</dependencies>

The feature supports reading multiple credentials from a common base directory and to read credentials from arbitrary directories.

Read Credentials from a Base Directory

The base directory for service credentials is the /etc/secrets/sapcp directory. You can overwrite the default base directory with the cds.environment.k8s.secretsPath property.

Within this base directory, the directory structure for the service credentials is <service-name>/<instance-name>.

Read Credentials from Arbitrary Directories

You can also configure service bindings using the cds.environment.k8s.serviceBindings configuration property.

For example:

yaml
cds:
  environment:
    k8s:
      serviceBindings:
        bookshop-db:
          secretsPath: /etc/secrets/hana
          service: hana
          plan: hdi-shared
          tags:
           - hana
           - db
        bookshop-uaa:
          secretsPath: /etc/somewhere/else/xsuaa
          ...
cds:
  environment:
    k8s:
      serviceBindings:
        bookshop-db:
          secretsPath: /etc/secrets/hana
          service: hana
          plan: hdi-shared
          tags:
           - hana
           - db
        bookshop-uaa:
          secretsPath: /etc/somewhere/else/xsuaa
          ...

The parameters plan and tags are optional.

Spring Boot Integration

This section describes the Spring Boot integration of the CAP Java SDK. Classic Spring isn’t supported. Running your application with Spring Boot framework offers a number of helpful benefits that simplify the development and maintenance of the application to a high extend. Spring not only provides a rich set of libraries and tools for most common challenges in development, you also profit from a huge community, which constantly contributes optimizations, bug fixes and new features.

As Spring Boot not only is widely accepted but also most popular application framework, CAP Java SDK comes with a seamless integration of Spring Boot as described in the following sections.

Spring Dependencies

To make your web application ready for Spring Boot, you need to make sure that the following Spring dependencies are referenced in your pom.xml (group ID org.springframework.boot):

  • spring-boot-starter-web
  • spring-boot-starter-jdbc
  • spring-boot-starter-security (optional)

In addition, for activating the Spring integration of CAP Java SDK, the following runtime dependency is required:

xml
<dependency>
	<groupId>com.sap.cds</groupId>
	<artifactId>cds-framework-spring-boot</artifactId>
	<version>${revision}</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>com.sap.cds</groupId>
	<artifactId>cds-framework-spring-boot</artifactId>
	<version>${revision}</version>
	<scope>runtime</scope>
</dependency>

It might be more convenient to make use of CDS starter bundle cds-starter-spring-boot-odata, which not only comprises the necessary Spring dependencies, but also configures the OData V4 protocol adapter:

xml
<dependency>
	<groupId>com.sap.cds</groupId>
	<artifactId>cds-starter-spring-boot-odata</artifactId>
	<version>${revision}</version>
</dependency>
<dependency>
	<groupId>com.sap.cds</groupId>
	<artifactId>cds-starter-spring-boot-odata</artifactId>
	<version>${revision}</version>
</dependency>

Spring Features

Beside the common Spring features such as dependency injection and a sophisticated test framework, the following features are available in Spring CAP applications in addition:

  • CDS event handlers within custom Spring beans are automatically registered at startup.
  • Full integration into Spring transaction management (@Transactional is supported).
  • A various number of CAP Java SDK interfaces are exposed as Spring beans and are available in Spring application context such as technical services, the CdsModel or the UserInfo in current request scope.
  • Automatic configuration of XSUAA, IAS and mock user authentication by means of Spring security configuration.
  • Integration of cds-property section into Spring properties. See section Externalized Configuration in the Spring Boot documentation for more details.
  • cds actuator exposing monitoring information about CDS runtime and security.
  • DB health check indicator which also applies to tenant-aware DB connections.

TIP

None of the listed features will be available out of the box in case you choose to pack and deploy your web application as plain Java Servlet in a war file.

Spring Beans Exposed by the Runtime

BeanDescriptionExample
CdsRuntimeRuntime instance (singleton)@Autowired
CdsRuntime runtime;
CdsRuntimeConfigurerRuntime configuration instance (singleton)@Autowired
CdsRuntimeConfigurer configurer;
ServiceAll kinds of CDS services, application services and technical services@Autowired
@Qualifier(CatalogService_.CDS_NAME)
private ApplicationService cs;

@Autowired
private PersistenceService ps;
ServiceCatalogThe catalog of all available services@Autowired
ServiceCatalog catalog;
CdsModelThe current model@Autowired
CdsModel model;
UserInfoInformation about the authenticated user@Autowired
UserInfo userInfo;
AuthenticationInfoAuthentication claims@Autowired
AuthenticationInfo authInfo;
ParameterInfoInformation about request parameters@Autowired
ParameterInfo paramInfo;
MessagesInterface to write messages@Autowired
Messages messages;
FeatureTogglesInfoInformation about feature toggles@Autowired
FeatureTogglesInfo ftsInfo;
CdsDataStoreDirect access to default data store@Autowired
CdsDataStore ds;

Minimum Dependency Versions

The CAP Java SDK uses various dependencies that are also used by the applications themselves. If the applications decide to manage versions of these dependencies it is helpful to know the minimum versions of these dependencies that the CAP Java SDK requires. The following table lists these minimum versions for various common dependencies, based on the latest release.

Maintenance Version 1.34.x

DependencyMinimum VersionRecommended Version
Java817
@sap/cds-dk4latest
@sap/cds-compiler2latest
Spring Boot12.72.7
XSUAA2.13latest
SAP Cloud SDK4.10latest

1 Spring Boot 3.0 is supported in 2.x feature version only.

Feature Version 2.x

DependencyMinimum VersionRecommended Version
Java1717
@sap/cds-dk4latest
@sap/cds-compiler2latest
Spring Boot3.03.0
XSUAA3.0latest
SAP Cloud SDK4.13latest
Java Logging3.7latest

Building CAP Java Applications

This section describes various options to create a CAP Java project from scratch, to build your application with Maven, and to modify an existing project with the CDS Maven plugin.

The Maven Archetype

Use the following command line to create a project from scratch with the CDS Maven archetype:

sh
mvn archetype:generate -DarchetypeArtifactId=cds-services-archetype -DarchetypeGroupId=com.sap.cds -DarchetypeVersion=RELEASE
mvn archetype:generate -DarchetypeArtifactId=cds-services-archetype -DarchetypeGroupId=com.sap.cds -DarchetypeVersion=RELEASE
cmd
mvn archetype:generate -DarchetypeArtifactId=cds-services-archetype -DarchetypeGroupId=com.sap.cds -DarchetypeVersion=RELEASE
mvn archetype:generate -DarchetypeArtifactId=cds-services-archetype -DarchetypeGroupId=com.sap.cds -DarchetypeVersion=RELEASE
powershell
mvn archetype:generate `-DarchetypeArtifactId=cds-services-archetype `-DarchetypeGroupId=com.sap.cds `-DarchetypeVersion=RELEASE
mvn archetype:generate `-DarchetypeArtifactId=cds-services-archetype `-DarchetypeGroupId=com.sap.cds `-DarchetypeVersion=RELEASE

It supports the following command line options:

OptionDescription
-DincludeModel=trueAdds a minimalistic sample CDS model to the project
-DincludeIntegrationTest=trueAdds an integration test module to the project
-DodataVersion=[v2|v4]Specify which protocol adapter is activated by default
-DtargetPlatform=cloudfoundryAdds CloudFoundry target platform support to the project
-DinMemoryDatabase=[h2|sqlite]Specify which in-memory database is used for local testing. If not specified, the default value is h2.
-DjdkVersion=[11|17]Specifies the target JDK version. If not specified, the default value is 17.

Maven Build Options

You can build and run your application by means of the following Maven command:

sh
mvn spring-boot:run
mvn spring-boot:run

The following sections describe additional options you can apply during the build.

Project-Specific Configuration in .cdsrc.json

If you can't stick to defaults, you can use the .cdsrc.json to add specific configuration to your project. The configuration is used by the build process using @sap/cds-dk.

Learn more about configuration and cds.env

Using a specific cds-dk version

By default, the build is configured to download a Node.js runtime and the @sap/cds-dk tools and install them locally within the project. The install-cdsdk goal requires a version of @sap/cds-dk which needs to be provided explicitly in the configuration. With this you can ensure that the build is fully reproducible. You can provide this version by adding the following property to the properties section in your pom.xml:

xml
<properties>
    ...
    <cds.install-cdsdk.version>FIXED VERSION</cds.install-cdsdk.version>
</properties>
<properties>
    ...
    <cds.install-cdsdk.version>FIXED VERSION</cds.install-cdsdk.version>
</properties>

TIP

Make sure to regularly update @sap/cds-dk according to our guidance. For multitenant applications, ensure that the @sap/cds-dk version in the sidecar is in sync.

Using a Global cds-dk

By default, the build is configured to download a Node.js runtime and the @sap/cds-dk tools and install them locally within the project. This step makes the build self-contained, but the build also takes more time. You can omit these steps and speed up the Maven build, using the Maven profile cdsdk-global.

Prerequisites:

  • @sap/cds-dk is globally installed.
  • Node.js installation is available in current PATH environment.

If these prerequisites are met, you can use the profile cdsdk-global by executing:

sh
mvn spring-boot:run -P cdsdk-global
mvn spring-boot:run -P cdsdk-global

Refreshing the Local cds-dk

By default, the goal install-cdsdk of the cds-maven-plugin skips the installation of the @sap/cds-dk, if the @sap/cds-dk is already installed. To update the @sap/cds-dk version in your application project do the following:

  1. Specify a newer version of @sap/cds-dk in your pom.xml file.

  2. Execute mvn spring-boot:run with an additional property -Dcds.install-cdsdk.force=true, to force the installation of a @sap/cds-dk in the configured version.

    sh
    mvn spring-boot:run -Dcds.install-cdsdk.force=true
    mvn spring-boot:run -Dcds.install-cdsdk.force=true

Recommendation

This should be done at least with every major update of @sap/cds-dk.

Increased developer efficiency with Spring Boot Devtools

In order to speed up your development turnaround you can add the Spring Boot Devtools dependency to your CAP Java application. Just add this dependency to the pom.xml of your srv module:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

Once this is added, you can use the restart capabilities of the Spring Boot Devtools while developing your application in your favorite Java IDE. Any change triggers an automatic application context reload without the need to manually restart the complete application. Besides being a lot faster than the complete restart this also eliminates manual steps. The application context reload is triggered by any file change on the application's classpath:

  • Java classes (e.g. custom handlers)
  • Anything below src/main/resources
    • Configuration files (e.g. application.yaml)
    • Artifacts generated from CDS (schema.sql, CSN, EDMX)
    • Any other static resource

Spring Boot Devtools and CDS build

The Spring Boot Devtools have no knowledge of any CDS tooling or the CAP Java runtime. Thus, they can't trigger a CDS build in case of changed CDS sources. For more information, please check the Local Development Support section.

TIP

Especially CDS builds result in a lot of changed resources in your project. To have a smooth experience, define a trigger file and use auto-build goal of the CDS Maven plugin started from the command line.

CDS Maven Plugin

CDS Maven plugin provides several goals to perform CDS-related build steps. It can be used in CAP Java projects to perform the following build tasks:

  • Install Node.js in the specified version
  • Install the CDS Development Kit @sap/cds-dk in a specified version
  • Perform arbitrary CDS commands on a CAP Java project
  • Generate Java classes for type-safe access
  • Clean a CAP Java project from artifacts of the previous build

Since CAP Java 1.7.0, that CDS Maven Archetype sets up projects to leverage the CDS Maven plugin to perform the previous mentioned build tasks. On how to modify a project generated with a previous version of the CDS Maven Archetype, see this commit.

See CDS Maven Plugin documentation for more details.

Local Development Support

CDS Watch

In addition to the previously mentioned build tasks, the CDS Maven plugin can also support the local development of your CAP Java application. During development, you often have to perform the same steps to test the changes in the CDS model:

  1. Modify your CDS model.
  2. Build and run your application.
  3. Test your changes.

To automate and accelerate these steps, the cds-maven-plugin offers the goal watch, that can be executed from the command line in the service module folder by using Maven:

sh
cd srv
mvn cds:watch
cd srv
mvn cds:watch

It builds and starts the application and looks for changes in the CDS model. If you make changes to the CDS model, these are recognized and a restart of the application is initiated to make the changes effective.

The watch goal uses the spring-boot-maven-plugin internally to start the application with the goal run (this also includes a CDS build). Therefore, it's required that the application is a Spring Boot application and that you execute the watch goal within your service module folder. When you add the Spring Boot Devtools to your project, the watch goal can take advantage of the reload mechanism described in the linked section. In case your application does not use the Spring Boot Devtools the watch goal performs a complete restart of the Spring Boot application after CDS model changes. As the application context reload is always faster than the complete restart the approach using the Spring Boot Devtools is the preferred approach.

WARNING

The watch goal only works on Windows if the Spring Boot Devtools are enabled.

CDS Auto-Build

If you want to have the comfort of an automated CDS build like with the watch goal but want to control your CAP Java application from within the IDE, you can use the auto-build goal. This goal reacts on any CDS file change and performs a rebuild of your applications's CDS model. However, no CAP Java application is started by the goal. This doesn't depend on Spring Boot Devtools support.

TIP

If the Spring Boot Devtools configuration of your CAP Java application defines a trigger file, the auto-build can detect this and touch the trigger file in case of any file change. The same applies to the watch goal.

Testing CAP Java Applications

This section describes some best practices and recommendations for testing CAP Java applications.

As described in Modular Architecture, a CAP Java application consists of weakly coupled components, which enables you to define your test scope precisely and focus on parts that need a high test coverage.

Typical areas that require testing are the services that dispatch events to event handlers, the event handlers themselves that implement the behaviour of the services, and finally the APIs that the application services define and that are exposed to clients through OData.

TIP

Aside from JUnit, the Spring framework provides much convenience for both unit and integration testing, like dependency injection via autowiring or the usage of MockMvc and mocked users. So whenever possible, it is recommended to utilize it for writing tests.

Best Practices

To illustrate this, the following examples demonstrate some of the recommended ways of testing. All the examples are taken from the CAP Java bookshop sample project in a simplified form, so definitely have a look at this as well.

Let's assume you want to test the following custom event handler:

java
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogServiceHandler implements EventHandler {

    private final PersistenceService db;

    public CatalogServiceHandler(PersistenceService db) {
        this.db = db;
    }

    @On
    public void onSubmitOrder(SubmitOrderContext context) {
        Integer quantity = context.getQuantity();
        String bookId = context.getBook();

        Optional<Books> book = db.run(Select.from(BOOKS).columns(Books_::stock).byId(bookId)).first(Books.class);

        book.orElseThrow(() -> new ServiceException(ErrorStatuses.NOT_FOUND, MessageKeys.BOOK_MISSING)
            .messageTarget(Books_.class, b -> b.ID()));

        int stock = book.map(Books::getStock).get();

        if (stock >= quantity) {
            db.run(Update.entity(BOOKS).byId(bookId).data(Books.STOCK, stock -= quantity));
            SubmitOrderContext.ReturnType result = SubmitOrderContext.ReturnType.create();
            result.setStock(stock);
            context.setResult(result);
        } else {
            throw new ServiceException(ErrorStatuses.CONFLICT, MessageKeys.ORDER_EXCEEDS_STOCK, quantity);
        }
    }

    @After(event = CqnService.EVENT_READ)
    public void discountBooks(Stream<Books> books) {
        books.filter(b -> b.getTitle() != null).forEach(b -> {
            loadStockIfNotSet(b);
            discountBooksWithMoreThan111Stock(b);
        });
    }

    private void discountBooksWithMoreThan111Stock(Books b) {
        if (b.getStock() != null && b.getStock() > 111) {
            b.setTitle(String.format("%s -- 11%% discount", b.getTitle()));
        }
    }

    private void loadStockIfNotSet(Books b) {
        if (b.getId() != null && b.getStock() == null) {
            b.setStock(db.run(Select.from(BOOKS).byId(b.getId()).columns(Books_::stock)).single(Books.class).getStock());
        }
    }
}
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogServiceHandler implements EventHandler {

    private final PersistenceService db;

    public CatalogServiceHandler(PersistenceService db) {
        this.db = db;
    }

    @On
    public void onSubmitOrder(SubmitOrderContext context) {
        Integer quantity = context.getQuantity();
        String bookId = context.getBook();

        Optional<Books> book = db.run(Select.from(BOOKS).columns(Books_::stock).byId(bookId)).first(Books.class);

        book.orElseThrow(() -> new ServiceException(ErrorStatuses.NOT_FOUND, MessageKeys.BOOK_MISSING)
            .messageTarget(Books_.class, b -> b.ID()));

        int stock = book.map(Books::getStock).get();

        if (stock >= quantity) {
            db.run(Update.entity(BOOKS).byId(bookId).data(Books.STOCK, stock -= quantity));
            SubmitOrderContext.ReturnType result = SubmitOrderContext.ReturnType.create();
            result.setStock(stock);
            context.setResult(result);
        } else {
            throw new ServiceException(ErrorStatuses.CONFLICT, MessageKeys.ORDER_EXCEEDS_STOCK, quantity);
        }
    }

    @After(event = CqnService.EVENT_READ)
    public void discountBooks(Stream<Books> books) {
        books.filter(b -> b.getTitle() != null).forEach(b -> {
            loadStockIfNotSet(b);
            discountBooksWithMoreThan111Stock(b);
        });
    }

    private void discountBooksWithMoreThan111Stock(Books b) {
        if (b.getStock() != null && b.getStock() > 111) {
            b.setTitle(String.format("%s -- 11%% discount", b.getTitle()));
        }
    }

    private void loadStockIfNotSet(Books b) {
        if (b.getId() != null && b.getStock() == null) {
            b.setStock(db.run(Select.from(BOOKS).byId(b.getId()).columns(Books_::stock)).single(Books.class).getStock());
        }
    }
}

TIP

You can find a more complete sample of the previous snippet in our CAP Java bookshop sample project.

The CatalogServiceHandler here implements two handler methods -- onSubmitOrder and discountBooks -- that should be covered by tests.

The method onSubmitOrder is registered to the On phase of a SubmitOrder event and basically makes sure to reduce the stock quantity of the ordered book by the order quantity, or, in case the order quantity exceeds the stock, throws a ServiceException.

Whereas discountBooks is registered to the After phase of a read event on the Books entity and applies a discount information to a book's title if the stock quantity is larger than 111.

Event Handler Layer Testing

Out of these two handler methods discountBooks does not actually depend on the PersistenceService.

That allows us to verify its behavior in a unit test by creating a CatalogServiceHandler instance with the help of a PersistenceService mock to invoke the handler method on, as demonstrated below:

TIP

For mocking, you can use Mockito, which is already included with the spring-boot-starter-test "Starter".

java
@ExtendWith(MockitoExtension.class)
public class CatalogServiceHandlerTest {

    @Mock
    private PersistenceService db;

    @Test
    public void discountBooks() {
        Books book1 = Books.create();
        book1.setTitle("Book 1");
        book1.setStock(10);

        Books book2 = Books.create();
        book2.setTitle("Book 2");
        book2.setStock(200);

        CatalogServiceHandler handler = new CatalogServiceHandler(db);
        handler.discountBooks(Stream.of(book1, book2));

        assertEquals("Book 1", book1.getTitle(), "Book 1 was discounted");
        assertEquals("Book 2 -- 11% discount", book2.getTitle(), "Book 2 was not discounted");
    }
}
@ExtendWith(MockitoExtension.class)
public class CatalogServiceHandlerTest {

    @Mock
    private PersistenceService db;

    @Test
    public void discountBooks() {
        Books book1 = Books.create();
        book1.setTitle("Book 1");
        book1.setStock(10);

        Books book2 = Books.create();
        book2.setTitle("Book 2");
        book2.setStock(200);

        CatalogServiceHandler handler = new CatalogServiceHandler(db);
        handler.discountBooks(Stream.of(book1, book2));

        assertEquals("Book 1", book1.getTitle(), "Book 1 was discounted");
        assertEquals("Book 2 -- 11% discount", book2.getTitle(), "Book 2 was not discounted");
    }
}

TIP

You can find a variant of this sample code also in our CAP Java bookshop sample project.

Whenever possible, mocking dependencies and just testing the pure processing logic of an implementation allows you to ignore the integration bits and parts of an event handler, which is a solid first layer of your testing efforts.

Service Layer Testing

Application Services that are backed by an actual service definition within the CdsModel implement an interface, which extends the Service interface and offers a common CQN execution API for CRUD events. This API can be used to run CQN statements directly against the service layer, which can be used for testing, too.

To verify the proper discount application in our example, we can run a Select statement against the CatalogService and assert the result as follows, using a well-known dataset:

java
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class CatalogServiceTest {

    @Autowired
    @Qualifier(CatalogService_.CDS_NAME)
    private CqnService catalogService;

    @Test
    public void discountApplied() {
        Result result = catalogService.run(Select.from(Books_.class).byId("51061ce3-ddde-4d70-a2dc-6314afbcc73e"));

        // book with title "The Raven" and a stock quantity of > 111
        Books book = result.single(Books.class);

        assertEquals("The Raven -- 11% discount", book.getTitle(), "Book was not discounted");
    }
}
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class CatalogServiceTest {

    @Autowired
    @Qualifier(CatalogService_.CDS_NAME)
    private CqnService catalogService;

    @Test
    public void discountApplied() {
        Result result = catalogService.run(Select.from(Books_.class).byId("51061ce3-ddde-4d70-a2dc-6314afbcc73e"));

        // book with title "The Raven" and a stock quantity of > 111
        Books book = result.single(Books.class);

        assertEquals("The Raven -- 11% discount", book.getTitle(), "Book was not discounted");
    }
}

As every service in CAP implements the Service interface with its emit(EventContext) method, another way of testing an event handler is to dispatch an event context via the emit() method to trigger the execution of a specific handler method.

Looking at the onSubmitOrder method from our example above we see that it uses an event context called SubmitOrderContext. Therefore, using an instance of that event context, in order to test the proper stock reduction, we can trigger the method execution and assert the result, as demonstrated:

java
@SpringBootTest
public class CatalogServiceTest {

    @Autowired
    @Qualifier(CatalogService_.CDS_NAME)
    private CqnService catalogService;

    @Test
    public void submitOrder() {
        SubmitOrderContext context = SubmitOrderContext.create();

        // ID of a book known to have a stock quantity of 22
        context.setBook("4a519e61-3c3a-4bd9-ab12-d7e0c5329933");
        context.setQuantity(2);
        catalogService.emit(context);

        assertEquals(22 - context.getQuantity(), context.getResult().getStock());
    }
}
@SpringBootTest
public class CatalogServiceTest {

    @Autowired
    @Qualifier(CatalogService_.CDS_NAME)
    private CqnService catalogService;

    @Test
    public void submitOrder() {
        SubmitOrderContext context = SubmitOrderContext.create();

        // ID of a book known to have a stock quantity of 22
        context.setBook("4a519e61-3c3a-4bd9-ab12-d7e0c5329933");
        context.setQuantity(2);
        catalogService.emit(context);

        assertEquals(22 - context.getQuantity(), context.getResult().getStock());
    }
}

The same way you can verify the ServiceException being thrown in case of the order quantity exceeding the stock value:

java
@SpringBootTest
public class CatalogServiceTest {

    @Autowired
    @Qualifier(CatalogService_.CDS_NAME)
    private CqnService catalogService;

    @Test
    public void submitOrderExceedingStock() {
        SubmitOrderContext context = SubmitOrderContext.create();

        // ID of a book known to have a stock quantity of 22
        context.setBook("4a519e61-3c3a-4bd9-ab12-d7e0c5329933");
        context.setQuantity(30);
        catalogService.emit(context);

        assertThrows(ServiceException.class, () -> catalogService.emit(context), context.getQuantity() + " exceeds stock for book");
    }
}
@SpringBootTest
public class CatalogServiceTest {

    @Autowired
    @Qualifier(CatalogService_.CDS_NAME)
    private CqnService catalogService;

    @Test
    public void submitOrderExceedingStock() {
        SubmitOrderContext context = SubmitOrderContext.create();

        // ID of a book known to have a stock quantity of 22
        context.setBook("4a519e61-3c3a-4bd9-ab12-d7e0c5329933");
        context.setQuantity(30);
        catalogService.emit(context);

        assertThrows(ServiceException.class, () -> catalogService.emit(context), context.getQuantity() + " exceeds stock for book");
    }
}

TIP

For a more extensive version of the previous CatalogServiceTest snippets, have a look at our CAP Java bookshop sample project.

API Integration Testing

Integration tests enable us to verify the behavior of a custom event handler execution doing a roundtrip starting at the protocol adapter layer and going through the whole CAP architecture until it reaches the service and event handler layer and then back again through the protocol adapter.

As the services defined in our CDS model are exposed as OData endpoints, by using MockMvc we can simply invoke a specific OData request and assert the response from the addressed service.

The following demonstrates this by invoking a GET request to the OData endpoint of our Books entity, which triggers the execution of the discountBooks method of the CatalogServiceHandler in our example:

java
@SpringBootTest
@AutoConfigureMockMvc
public class CatalogServiceITest {

    private static final String booksURI = "/api/browse/Books";

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void discountApplied() throws Exception {
        mockMvc.perform(get(booksURI + "?$filter=stock gt 200&top=1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.value[0].title").value(containsString("11% discount")));
    }

    @Test
    public void discountNotApplied() throws Exception {
        mockMvc.perform(get(booksURI + "?$filter=stock lt 100&top=1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.value[0].title").value(not(containsString("11% discount"))));
    }
}
@SpringBootTest
@AutoConfigureMockMvc
public class CatalogServiceITest {

    private static final String booksURI = "/api/browse/Books";

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void discountApplied() throws Exception {
        mockMvc.perform(get(booksURI + "?$filter=stock gt 200&top=1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.value[0].title").value(containsString("11% discount")));
    }

    @Test
    public void discountNotApplied() throws Exception {
        mockMvc.perform(get(booksURI + "?$filter=stock lt 100&top=1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.value[0].title").value(not(containsString("11% discount"))));
    }
}

TIP

Check out the version in our CAP Java bookshop sample project for additional examples of integration testing.