Technology Blog Posts by SAP
cancel
Showing results for 
Search instead for 
Did you mean: 
Anjali_Pathak
Product and Topic Expert
Product and Topic Expert
1,920

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:

  • Catches bugs early
  • Improves code quality
  • Simplifies maintenance
  • Boosts developer confidence
  • Speeds up delivery

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:

  • Book Does Not Exist: The test verifies that the system throws an error if an order contains a non-existent book.
  • Insufficient Stock: The test ensures an error is raised when the requested quantity exceeds available stock.
  • Successful Stock Update: The test confirms that stock is properly updated when the order is valid.

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.

Anjali_Pathak_0-1753693277839.png

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.

Anjali_Pathak_1-1753693552451.png

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.

Anjali_Pathak_2-1753693678632.png

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.

1 Comment