Understanding SAP CAP and SAP CAP Java Framework
SAP Cloud Application Programming (CAP) is a full-stack framework for building enterprise cloud apps with Java or Node.js, using CDS for modeling and offering built-in persistence, APIs, security, and seamless SAP BTP deployment.
The SAP CAP Java Framework, based on Spring Boot, enables Java developers to create CDS models, auto-generate services, and implement business logic, supporting multitenancy, messaging, and flexible database integration.
Target Audience
This article is for developers, partners, and customers building SAP CAP Java applications, focusing on unit testing with Test-Driven Development (TDD) to ensure robust, test-first best practices.
Understanding Unit Testing and Test-Driven Development
Unit Testing involves verifying individual components or functions of a program in isolation to ensure they work correctly. It helps catch bugs early, supports safe code changes, and boosts overall reliability. Automated and fast, unit tests are fundamental to test-driven development and continuous integration.
Test-Driven Development (TDD) is a software development approach where we write tests before writing the actual code. We first create a failing test (Red), then write just enough code to pass it (Green), and finally refactor the code for improvement (Refactor). This cycle ensures better code quality, design, and test coverage from the start.
Benefits:
Getting Started with Test-Driven Development (TDD)
Consider a system with three services:
Books Service: Read-only catalog of books and authors for end users.
Orders Service: Manage orders—view, create, delete.
Admin Service: Full CRUD for products, authors, and categories.
Each service is focused on a single responsibility, making them ideal for TDD.
For detailed guidance on creating the above described SAP CAP Java application, please refer to the tutorial: https://developers.sap.com/mission.cap-java-app.html
In Test-Driven Development (TDD), we start by clearly defining the expected behaviour of a feature through test cases before writing any production code. This means we write tests that initially fail because the functionality doesn’t exist yet.
For example, when adding stock validation and reduction in the Orders Service, we begin by outlining key scenarios as test cases:
The test class below uses JUnit and Mockito to define above mentioned expected behaviour for the "validateBookAndDecreaseStock" method in the Orders Service.
package customer.bookstore.handlers;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.sap.cds.Result;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.persistence.PersistenceService;
import cds.gen.ordersservice.OrderItems;
import cds.gen.sap.capire.bookstore.Books;
class OrdersServiceTest {
@Mock
private PersistenceService db;
@InjectMocks
private OrdersService ordersService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testValidateBookAndDecreaseStock_BookDoesNotExist() {
// Arrange
OrderItems item = OrderItems.create();
item.setBookId("book1");
item.setAmount(2);
Result result = mock(Result.class);
when(db.run(any(CqnSelect.class))).thenReturn(result);
// Act & Assert
ServiceException exception = assertThrows(ServiceException.class, () -> {
ordersService.validateBookAndDecreaseStock(Arrays.asList(item));
});
assertEquals("Book does not exist", exception.getMessage());
}
@Test
void testValidateBookAndDecreaseStock_NotEnoughStock() {
// Arrange
OrderItems item = OrderItems.create();
item.setBookId("book1");
item.setAmount(5);
Books book = Books.create();
book.setStock(3);
Result result = mock(Result.class);
when(db.run(any(CqnSelect.class))).thenReturn(result);
when(result.first(Books.class)).thenReturn(Optional.of(book));
// Act & Assert
ServiceException exception = assertThrows(ServiceException.class, () -> {
ordersService.validateBookAndDecreaseStock(Arrays.asList(item));
});
assertEquals("Not enough books on stock", exception.getMessage());
}
@Test
void testValidateBookAndDecreaseStock_SuccessfulStockUpdate() {
// Arrange
OrderItems item = OrderItems.create();
item.setBookId("book1");
item.setAmount(2);
Books book = Books.create();
book.setStock(5);
Result result = mock(Result.class);
when(db.run(any(CqnSelect.class))).thenReturn(result);
when(result.first(Books.class)).thenReturn(Optional.of(book));
// Act
assertDoesNotThrow(() -> ordersService.validateBookAndDecreaseStock(Arrays.asList(item)));
// Assert
assertEquals(3, book.getStock());
verify(db, times(1)).run(any(CqnUpdate.class));
}
}Initially, the test cases will fail (as shown in the image below) as we have not implemented the functionality yet.
Now, let us make the test cases work by writing the functional logic.
The following method is implemented based on the previously defined test cases, completing the TDD cycle by writing just enough logic to make all tests pass.
package customer.bookstore.handlers;
import java.util.List;
import org.springframework.stereotype.Component;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.persistence.PersistenceService;
import cds.gen.ordersservice.OrderItems;
import cds.gen.ordersservice.OrderItems_;
import cds.gen.ordersservice.OrdersService_;
import cds.gen.sap.capire.bookstore.Books;
import cds.gen.sap.capire.bookstore.Books_;
@Component
@ServiceName(OrdersService_.CDS_NAME)
public class OrdersService implements EventHandler {
private final PersistenceService db;
OrdersService(PersistenceService db) {
this.db = db;
}
@Before(event = CqnService.EVENT_CREATE, entity = OrderItems_.CDS_NAME)
public void validateBookAndDecreaseStock(List<OrderItems> items) {
for (OrderItems item : items) {
String bookId = item.getBookId();
Integer amount = item.getAmount();
// check if the book that should be ordered is existing
CqnSelect sel = Select.from(Books_.class).columns(b -> b.stock()).where(b -> b.ID().eq(bookId));
Books book = db.run(sel).first(Books.class)
.orElseThrow(() -> new ServiceException(ErrorStatuses.NOT_FOUND, "Book does not exist"));
// check if order could be fulfilled
int stock = book.getStock();
if (stock < amount) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Not enough books on stock");
}
// update the book with the new stock, means minus the order amount
book.setStock(stock - amount);
CqnUpdate update = Update.entity(Books_.class).data(book).where(b -> b.ID().eq(bookId));
db.run(update);
}
}
}We can now re-run the test cases to ensure the implementation satisfies all defined scenarios and that all tests pass successfully.
As per the TDD cycle, this is also the stage to refactor the code, improving readability and structure without changing behaviour.
Testing the Code Coverage
Following the implementation of the logic using the TDD methodology, code coverage has been assessed using the JaCoCo plugin, as illustrated in the image below.
Conclusion
This guide helps you in getting started with writing and organizing tests, mocking dependencies, and enhancing code reliability in SAP CAP Java applications. With these core practices in place, you're well-prepared to develop and maintain robust, testable SAP CAP Java solutions.
Disclaimer
The views and opinions expressed in this blog post are those of the author and do not necessarily reflect the official policy or position of SAP or its affiliates. This content is provided “as is” for informational purposes only, based on the author’s personal knowledge and experience. SAP makes no representations or warranties, express or implied, as to the accuracy, completeness, or suitability of the information contained herein. Readers are advised to independently validate any guidance or code samples before use in a production environment.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 36 | |
| 34 | |
| 29 | |
| 28 | |
| 26 | |
| 26 | |
| 25 | |
| 23 | |
| 23 | |
| 22 |