Testing 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's recommended to use it for writing tests.
Sample Tests
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:
@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 doesn't 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 bundle.
@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:
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class CatalogServiceTest {
@Autowired
@Qualifier(CatalogService_.CDS_NAME)
private CqnService catalogService;
@Test
public void discountApplied() {
CdsResult<Books> 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:
@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());
}
}In the same way you can verify that the ServiceException is being thrown when the order quantity exceeds the stock value:
@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.
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:
@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.
Testing with H2
H2 is the preferred database for CAP Java applications, as it offers a combination of features that make it the best candidate for local development and testing:
Concurrent access and locking
The database supports multiple concurrent connections and implements row-level locking, allowing safe parallel data access without data corruption or race conditions.
Open Source and Java native
As an open-source database written entirely in Java, H2 offers transparency, flexibility, and the benefit of being maintained by an active community. Its Java implementation ensures optimal integration with Java-based applications and platforms.
Administrative tools
H2 includes a built-in web console application, providing a user-friendly interface for database administration, query execution, and data inspection without requiring external tools. CAP Java applications configured with the H2 database expose the administration console under
http://localhost:8080/h2-console(assuming default port8080).
Setup & Configuration
Using the Maven Archetype
When a new CAP Java project is created with the Maven Archetype or with cds init, H2 is automatically configured as in-memory database used for development and testing in the default profile.
Manual Configuration
To use H2, just add a Maven dependency to the H2 JDBC driver:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Next, configure the build to create an initial schema.sql file for H2 using cds deploy --to h2 --dry.
In Spring, H2 is automatically initialized as in-memory database when the driver is present on the classpath.
Learn more about the configuration of H2.
After performing the above mentioned configuration steps, the application.yaml should contain the following lines for default profile:
spring:
config.activate.on-profile: default
sql.init.platform: h2
cds:
data-source:
auto-config.enabled: falseH2 Limitations
When developing a CAP Java application, it's important to understand the limits and constraints of the underlying database. Every database has its own performance characteristics, data type restrictions, indexing behavior, and transaction handling rules.
Read more about known limitations in the H2 section of the Persistence Services guide.
Test local MTXS with H2 not possible
Besides the limitations mentioned above, it is not possible to use H2 database when it comes to testing multitenancy and extensibility (MTXS) scenarios on a local environment.
Hybrid Testing - a way to overcome limitations
Although CAP Java enables running and testing applications with a local H2 database, still there are cases when it is not possible, due to some limitations mentioned previously. In that case, hybrid testing capabilities help you to stay in a local development environment avoiding long turnaround times of cloud deployments. You just selectively connect to services in the cloud.
The section Hybrid Testing describes the steps on how to configure and consume the remote services, including SAP HANA, in a local environment.
H2 and Spring Dev Tools Integration
Most CAP Java projects use Spring Boot. To speed up the edit-compile-verify loop, the Spring Boot DevTools dependency is commonly added to the development classpath. DevTools provide automatic restart and LiveReload integration. For more details check the Spring Dev Tools reference.
The automatic restart and LiveReload provided by DevTools can cause an application restart that results in loss of state held by an in-memory H2 database. To avoid losing data between restarts during development, prefer the H2 file-based mode so the database is persisted on disk and survives DevTools restarts. The simplest application.yaml configuration would look as follows:
spring:
config.activate.on-profile: default
sql.init.platform: h2
url: "jdbc:h2:file:/data/testdb"
cds:
data-source:
auto-config.enabled: falseLearn more about how to configure file-based H2.
Logging SQL to Console
To view the generated SQL statements, which will be run on the H2 database, it is possible to switch to DEBUG log output by adding the following log-levels:
logging:
level:
com.sap.cds.persistence.sql: DEBUGThis is beneficial, when you need to track runtime or a Java Application behavior.