Testing SAP Commerce code presents unique challenges. The platform's deep dependency injection, tight coupling to the type system, and reliance on database-backed models mean that standard unit testing practices from vanilla Spring Boot don't translate directly. Many teams end up with either no tests or tests that boot the entire platform for every assertion — both approaches fail at scale.
This article covers practical testing strategies that work for real SAP Commerce projects: unit tests with proper mocking, integration tests against the running platform, Solr search testing, ImpEx validation, and end-to-end testing of OCC APIs.
Unit Testing
Unit tests are the foundation. They test individual classes in isolation by mocking all dependencies.
Testing a Populator
Populators are the most unit-testable code in SAP Commerce. They transform model objects into data objects.
public class LoyaltyPointsPopulator implements Populator<CustomerModel, CustomerData> {
private LoyaltyService loyaltyService;
@Override
public void populate(CustomerModel source, CustomerData target) {
if (source == null || target == null) {
throw new ConversionException("Source or target is null");
}
LoyaltyAccount account = loyaltyService.getAccountForCustomer(source);
if (account != null) {
target.setLoyaltyPoints(account.getPoints());
target.setLoyaltyTier(account.getTier().name());
target.setPointsToNextTier(calculatePointsToNextTier(account));
}
}
private int calculatePointsToNextTier(LoyaltyAccount account) {
Map<String, Integer> thresholds = Map.of(
"BRONZE", 1000, "SILVER", 5000, "GOLD", 20000, "PLATINUM", Integer.MAX_VALUE
);
int nextThreshold = thresholds.getOrDefault(account.getTier().name(), Integer.MAX_VALUE);
return Math.max(0, nextThreshold - account.getPoints());
}
// Setter for injection
public void setLoyaltyService(LoyaltyService loyaltyService) {
this.loyaltyService = loyaltyService;
}
}
Test:
@UnitTest
@RunWith(MockitoJUnitRunner.class)
public class LoyaltyPointsPopulatorTest {
@InjectMocks
private LoyaltyPointsPopulator populator;
@Mock
private LoyaltyService loyaltyService;
@Mock
private CustomerModel customerModel;
private CustomerData customerData;
@Before
public void setUp() {
customerData = new CustomerData();
}
@Test
public void shouldPopulateLoyaltyPoints() {
// Given
LoyaltyAccount account = new LoyaltyAccount();
account.setPoints(3500);
account.setTier(LoyaltyTier.SILVER);
when(loyaltyService.getAccountForCustomer(customerModel)).thenReturn(account);
// When
populator.populate(customerModel, customerData);
// Then
assertEquals(3500, customerData.getLoyaltyPoints());
assertEquals("SILVER", customerData.getLoyaltyTier());
assertEquals(1500, customerData.getPointsToNextTier()); // 5000 - 3500
}
@Test
public void shouldHandleNullLoyaltyAccount() {
when(loyaltyService.getAccountForCustomer(customerModel)).thenReturn(null);
populator.populate(customerModel, customerData);
assertEquals(0, customerData.getLoyaltyPoints());
assertNull(customerData.getLoyaltyTier());
}
@Test(expected = ConversionException.class)
public void shouldThrowOnNullSource() {
populator.populate(null, customerData);
}
@Test(expected = ConversionException.class)
public void shouldThrowOnNullTarget() {
populator.populate(customerModel, null);
}
@Test
public void shouldCalculatePointsToNextTierForBronze() {
LoyaltyAccount account = new LoyaltyAccount();
account.setPoints(250);
account.setTier(LoyaltyTier.BRONZE);
when(loyaltyService.getAccountForCustomer(customerModel)).thenReturn(account);
populator.populate(customerModel, customerData);
assertEquals(750, customerData.getPointsToNextTier()); // 1000 - 250
}
}
Testing a Validator
public class OrderQuantityValidator implements Validator {
private static final int MAX_QUANTITY = 999;
private StockService stockService;
@Override
public boolean supports(Class<?> clazz) {
return AddToCartParams.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
AddToCartParams params = (AddToCartParams) target;
if (params.getQuantity() <= 0) {
errors.rejectValue("quantity", "cart.quantity.invalid",
"Quantity must be greater than zero");
}
if (params.getQuantity() > MAX_QUANTITY) {
errors.rejectValue("quantity", "cart.quantity.exceeded",
new Object[]{MAX_QUANTITY}, "Maximum quantity is {0}");
}
// Check stock availability
Long available = stockService.getAvailableStock(params.getProductCode());
if (available != null && params.getQuantity() > available) {
errors.rejectValue("quantity", "cart.quantity.nostock",
new Object[]{available}, "Only {0} items available");
}
}
}
Test:
@UnitTest
@RunWith(MockitoJUnitRunner.class)
public class OrderQuantityValidatorTest {
@InjectMocks
private OrderQuantityValidator validator;
@Mock
private StockService stockService;
private AddToCartParams params;
private BeanPropertyBindingResult errors;
@Before
public void setUp() {
params = new AddToCartParams();
params.setProductCode("PROD-001");
params.setQuantity(1);
errors = new BeanPropertyBindingResult(params, "params");
}
@Test
public void shouldAcceptValidQuantity() {
when(stockService.getAvailableStock("PROD-001")).thenReturn(100L);
validator.validate(params, errors);
assertFalse(errors.hasErrors());
}
@Test
public void shouldRejectZeroQuantity() {
params.setQuantity(0);
validator.validate(params, errors);
assertTrue(errors.hasFieldErrors("quantity"));
assertEquals("cart.quantity.invalid",
errors.getFieldError("quantity").getCode());
}
@Test
public void shouldRejectNegativeQuantity() {
params.setQuantity(-5);
validator.validate(params, errors);
assertTrue(errors.hasFieldErrors("quantity"));
}
@Test
public void shouldRejectQuantityExceedingMax() {
params.setQuantity(1000);
when(stockService.getAvailableStock("PROD-001")).thenReturn(5000L);
validator.validate(params, errors);
assertTrue(errors.hasFieldErrors("quantity"));
assertEquals("cart.quantity.exceeded",
errors.getFieldError("quantity").getCode());
}
@Test
public void shouldRejectQuantityExceedingStock() {
params.setQuantity(10);
when(stockService.getAvailableStock("PROD-001")).thenReturn(5L);
validator.validate(params, errors);
assertTrue(errors.hasFieldErrors("quantity"));
assertEquals("cart.quantity.nostock",
errors.getFieldError("quantity").getCode());
}
@Test
public void shouldAllowWhenStockIsNull() {
// Null stock means stock tracking is disabled
params.setQuantity(50);
when(stockService.getAvailableStock("PROD-001")).thenReturn(null);
validator.validate(params, errors);
assertFalse(errors.hasErrors());
}
}
Testing FlexibleSearch Queries
FlexibleSearch queries need integration tests because they depend on the type system and database schema.
@IntegrationTest
public class ProductQueryServiceIntegrationTest extends ServicelayerTransactionalTest {
@Resource
private ProductQueryService productQueryService;
@Resource
private CatalogVersionService catalogVersionService;
@Before
public void setUp() throws Exception {
importCsv("/test/testdata/product-query-test-data.impex", "utf-8");
}
@Test
public void shouldFindProductsByPriceRange() {
CatalogVersionModel cv = catalogVersionService.getCatalogVersion("testCatalog", "Online");
List<ProductModel> results = productQueryService.findByPriceRange(
cv, BigDecimal.valueOf(50), BigDecimal.valueOf(150));
assertEquals(2, results.size());
assertTrue(results.stream()
.allMatch(p -> p.getCode().startsWith("TEST-PROD")));
}
@Test
public void shouldReturnEmptyForNonMatchingRange() {
CatalogVersionModel cv = catalogVersionService.getCatalogVersion("testCatalog", "Online");
List<ProductModel> results = productQueryService.findByPriceRange(
cv, BigDecimal.valueOf(10000), BigDecimal.valueOf(20000));
assertTrue(results.isEmpty());
}
@Test
public void shouldFindRecentlyModifiedProducts() {
CatalogVersionModel cv = catalogVersionService.getCatalogVersion("testCatalog", "Online");
// Modify a product
ProductModel product = productService.getProductForCode(cv, "TEST-PROD-001");
product.setDescription("Updated description");
modelService.save(product);
// Query for recently modified
List<ProductModel> results = productQueryService.findModifiedSince(
cv, Date.from(Instant.now().minus(1, ChronoUnit.HOURS)));
assertFalse(results.isEmpty());
assertTrue(results.stream()
.anyMatch(p -> "TEST-PROD-001".equals(p.getCode())));
}
}
Testing ImpEx Scripts
ImpEx scripts are code — they should be tested too.
@IntegrationTest
public class ProductImportImpExTest extends ServicelayerTransactionalTest {
@Resource
private ProductService productService;
@Resource
private CatalogVersionService catalogVersionService;
@Resource
private ImportService importService;
@Test
public void shouldImportProductsSuccessfully() {
// Given
ImportConfig config = new ImportConfig();
config.setScript(
new StreamBasedImpExResource(
getClass().getResourceAsStream("/import/products.impex"),
"utf-8"
)
);
config.setSynchronous(true);
config.setFailOnError(true);
// When
ImportResult result = importService.importData(config);
// Then
assertTrue("Import should succeed", result.isSuccessful());
assertFalse("Should have no unresolved lines", result.hasUnresolvedLines());
// Verify imported data
CatalogVersionModel cv = catalogVersionService
.getCatalogVersion("myProductCatalog", "Staged");
ProductModel product = productService.getProductForCode(cv, "CAM-001");
assertNotNull("Product should exist", product);
assertEquals("Professional DSLR", product.getName());
}
@Test
public void shouldHandleDuplicateProductCodes() {
// Import same file twice — INSERT_UPDATE should handle it
ImportConfig config = new ImportConfig();
config.setScript(new StreamBasedImpExResource(
getClass().getResourceAsStream("/import/products.impex"), "utf-8"));
config.setSynchronous(true);
// First import
ImportResult result1 = importService.importData(config);
assertTrue(result1.isSuccessful());
// Second import (should update, not fail)
config.setScript(new StreamBasedImpExResource(
getClass().getResourceAsStream("/import/products.impex"), "utf-8"));
ImportResult result2 = importService.importData(config);
assertTrue("Re-import should succeed with INSERT_UPDATE", result2.isSuccessful());
}
}
CI/CD Integration
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'ant clean all'
}
}
stage('Unit Tests') {
steps {
sh 'ant unittests -Dtestclasses.packages=com.mycompany.*'
}
post {
always {
junit '**/testresults/junit/*.xml'
}
}
}
stage('Integration Tests') {
steps {
sh 'ant integrationtests -Dtestclasses.packages=com.mycompany.*'
}
post {
always {
junit '**/testresults/junit/*.xml'
}
}
}
stage('API Tests') {
steps {
sh 'ant integrationtests -Dtestclasses.packages=com.mycompany.*.api.*'
}
}
}
post {
always {
publishHTML(target: [
reportDir: 'log/junit',
reportFiles: 'index.html',
reportName: 'Test Report'
])
}
}
}
Summary
A well-tested SAP Commerce project needs tests at every level:
- Unit tests for business logic, populators, validators — fast, no platform required
- Integration tests for services, FlexibleSearch, and ImpEx — running against the platform with transactional rollback
- API tests for OCC endpoints — verifying the full HTTP request/response cycle
- ImpEx tests for data import scripts — ensuring imports are idempotent and produce correct data
- Automated in CI/CD — unit tests on every commit, integration tests on PRs, full suites before deployment
The teams that invest in testing spend less time debugging production issues and more time building features. The platform's complexity makes testing harder, but it also makes testing more valuable — there are simply more places where things can go wrong.
Top comments (0)