Application testing
1. Overview
Testing in the Northbound API project is structured to balance speed, reliability, and coverage. A combination of unit tests, integration tests, and API-level tests ensures confidence in business logic, service interaction, and external system integration.
Tests are organized by type and placed alongside the code they validate (typically in src/test/java).
Each test type serves a specific purpose:
-
Unit Tests: Fast and isolated. Ideal for verifying business logic and utility methods.
-
Integration Tests: Validate how components interact in a realistic Spring Boot context.
-
API Tests: Verify that both REST and SOAP interfaces function correctly.
-
Multi-Database Tests: Ensure proper interaction with ACS database.
2. Best Practices
-
Follow naming conventions like
*Testor*ITto distinguish between unit and integration tests. -
Use
@MockBeanor Testcontainers to isolate dependencies in integration tests. -
Use
@ActiveProfiles("test")to activate test-specific configurations. -
Prefer readable assertions (
assertThat,assertEquals, etc.) over excessive mocking. -
Avoid external dependencies or shared state between tests unless explicitly required.
-
For dual-interface testing (REST/SOAP), use appropriate client tools for each.
-
Ensure all tests are idempotent and can be run repeatedly.
3. Coverage Goals
-
The project enforces a minimum of 80% code coverage via JaCoCo.
-
All new code should be accompanied by appropriate tests.
-
Focus on testing meaningful business logic, not just line coverage.
-
Coverage goals by category:
-
Line coverage: >80%
-
Branch coverage: >70%
-
Method coverage: >85%
-
Class coverage: >90%
-
jacoco {
toolVersion = "0.8.12"
}
test {
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
reports {
xml.required = false
csv.required = false
html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
}
}
4. Unit Testing
Unit tests focus on testing individual components in isolation, with dependencies mocked or stubbed using Mockito.
4.1. Service Layer Example
@ExtendWith(MockitoExtension.class)
class DeviceInfoServiceTest {
@Mock
private DeviceRepository deviceRepository;
@InjectMocks
private DeviceInfoService deviceInfoService;
@Test
void shouldReturnDeviceWhenSerialNumberExists() {
// Arrange
String serialNumber = "SN12345";
Device device = new Device();
device.setSerialNumber(serialNumber);
when(deviceRepository.findBySerialNumber(serialNumber)).thenReturn(Optional.of(device));
// Act
var result = deviceInfoService.findBySerialNumber(serialNumber);
// Assert
assertNotNull(result);
assertEquals(serialNumber, result.getSerialNumber());
}
@Test
void shouldThrowWhenSerialNumberNotFound() {
// Arrange
when(deviceRepository.findBySerialNumber("NONEXISTENT")).thenReturn(Optional.empty());
// Act & Assert
assertThrows(FtApiException.class, () -> deviceInfoService.findBySerialNumber("NONEXISTENT"));
}
}
4.2. Controller Layer Example
@WebMvcTest(DeviceInfoController.class)
@ExtendWith(SpringExtension.class)
class DeviceInfoControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private DeviceInfoService deviceInfoService;
@Test
void shouldReturnDeviceInfo() throws Exception {
// Arrange
String serialNumber = "SN12345";
DeviceResponse response = new DeviceResponse();
response.setSerialNumber(serialNumber);
when(deviceInfoService.getDeviceInfo(serialNumber)).thenReturn(response);
// Act & Assert
mockMvc.perform(get("/api/Device/{serialNumber}", serialNumber)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.serialNumber").value(serialNumber));
}
}
5. Integration Testing
Integration tests verify interactions between multiple components in a Spring Boot context.
The Northbound API project uses a comprehensive BaseIT class to provide common setup for all integration tests.
5.1. Integration Test Base Class
package com.friendly.northboundapi;
import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.web.client.RestTemplate;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.testcontainers.containers.MySQLContainer;
@SpringBootTest(
classes = NorthboundApiApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@ActiveProfiles("test")
public abstract class BaseIT {
protected static final String SERIAL = "integration_test1";
protected static final WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
protected static final RestTemplate restTemplate = new RestTemplate();
private static final MySQLContainer<?> mysqlContainer = new MySQLContainer<>("mysql:latest");
@LocalServerPort
private int port;
static {
mysqlContainer
.withDatabaseName("ftacs")
.withUsername("root")
.withPassword("ftacs")
.withInitScript("init.sql")
.start();
}
@BeforeAll
public static void setUp() throws Exception {
configureWebServiceTemplate();
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.acs.jdbcUrl", mysqlContainer::getJdbcUrl);
registry.add("spring.datasource.acs.username", mysqlContainer::getUsername);
registry.add("spring.datasource.acs.password", mysqlContainer::getPassword);
}
protected String getFtApiSOAPUrl() {
return getServerUrl() + "/iot-webservice/FTACSWS/ACSWS";
}
protected String getFtApiRESTUrl(String path) {
return getServerUrl() + "/iot-webservice/api/" + path;
}
private @NotNull String getServerUrl() {
return "http://localhost:" + port;
}
private static void configureWebServiceTemplate() throws Exception {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setPackagesToScan("com.friendly.northboundapi");
marshaller.afterPropertiesSet();
webServiceTemplate.setMarshaller(marshaller);
webServiceTemplate.setUnmarshaller(marshaller);
}
}
This base class provides:
-
Spring Boot test configuration with a random server port
-
MySQL database container via TestContainers
-
Configuration for ACS datasource
-
Pre-configured WebServiceTemplate for SOAP testing
-
RestTemplate for REST API testing
-
Helper methods for building test URLs
5.2. Database Integration Testing
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class DeviceInfoIT extends BaseIT {
@Autowired
private DeviceRepository deviceRepository;
@Test
void shouldFindDeviceBySerialNumber() {
// Act
Optional<Device> result = deviceRepository.findBySerialNumber(SERIAL);
// Assert
assertTrue(result.isPresent());
assertEquals(SERIAL, result.get().getSerialNumber());
}
}
7. Running Tests
To execute all tests and generate a coverage report, use:
./gradlew clean test
The coverage report is available at:
build/reports/jacoco/test/html/index.html
8. Continuous Integration
The Northbound API project includes CI pipeline configurations that:
-
Set up the build environment
-
Start test databases using TestContainers
-
Run all unit and integration tests
-
Generate and publish test reports
-
Verify coverage thresholds are met
-
Fail the build if tests don’t pass or coverage is not enough
This ensures consistent quality validation for all code changes.