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 both ACS and IOTW databases.

2. Best Practices

  • Follow naming conventions like *Test or *IT to distinguish between unit and integration tests.

  • Use @MockBean or 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 Coverage Configuration
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 both ACS and IOTW datasources

  • 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());
    }
}

6. API Testing

6.1. Dual-Interface Testing

The Northbound API provides both SOAP and REST interfaces. Integration tests verify both interfaces using appropriate clients.

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:

  1. Set up the build environment

  2. Start test databases using TestContainers

  3. Run all unit and integration tests

  4. Generate and publish test reports

  5. Verify coverage thresholds are met

  6. Fail the build if tests don’t pass or coverage is not enough

This ensures consistent quality validation for all code changes.