Testing

1. Overview

The project uses JUnit 5, Mockito and AssertJ. Test classes mirror the main package structure under src/test/java.

Tests

1133 across 106 test classes

Failures

0

Instruction coverage

46.5%

Branch coverage

43.4%

Coverage gate

43% instructions / 40% branches (blocks the build)

A rendered dashboard of the same figures — including the defects these tests uncovered — lives at docs/test-coverage-report.html.

1.1. What is covered

Area Tests Focus

util

291

The pipeline from a raw device reading to the graph: monitoring intervals and gap arithmetic (CpeDataProcessor), ingestion and the adaptive batch (QoeDataProcessor), opaque/CBOR decoding for Single device (SpecificCpeDataProcessor), KPI formulas and labels (KpiProcessUtil, GraphDataHelper).

orm

286

Not the entities — the hand-written logic around them: row mappers, row callbacks, the ClickHouse *CustomImpl repositories (SQL text and bound parameters), and the Criteria specifications (which filters apply and which are skipped).

service

229

Alarm history, trends and severity counts (KpiThresholdHistoryService), batch CPE lookups, KPI and view queries.

controllers

212

Reports and CSV exports, groups and templates, KPIs, thresholds, user views.

alarms

45

The threshold engine: what raises an alarm, what merely updates it, what switches it off, and which of those the operator is notified about.

cache, rest, other

70

Hazelcast-backed caches, the REST API, converters, DataTables paging.

2. Writing tests

2.1. Call the class directly, do not raise a Spring context

Controllers and services are tested by instantiating them with mocked collaborators and calling their methods. There is no @SpringBootTest in the current suite: it is slower, and it hides which collaborator a behaviour actually depends on.

@ExtendWith(MockitoExtension.class)
class KpiServiceTest {
    @Mock private KpiRepository kpiRepository;
    @InjectMocks private KpiService kpiService;
}

2.2. Hazelcast must be mocked before the caches that use it

This will cost you an hour if you miss it.

IspCache and CpeParameterNameCache (both from the ft-cache library) build their Hazelcast maps in a static initializer. Mocking them directly fails with Cannot instrument class …​ because it or one of its supertypes could not be initialized: the class fails to initialize, and Mockito then refuses to instrument it at all.

Stub Hazelcast first, before that class is ever loaded. The order matters:

hazelcastCacheFactoryMock = mockStatic(HazelcastCacheFactory.class);
hazelcastCacheFactoryMock.when(HazelcastCacheFactory::getHazelcastInstance)
        .thenReturn(mock(HazelcastInstance.class));
hazelcastCacheFactoryMock.when(() -> HazelcastCacheFactory.createCache(anyString()))
        .thenReturn(mock(IMap.class));

ispCacheMock = mockStatic(IspCache.class);
ispCacheMock.when(IspCache::isIspEnabled).thenReturn(false);

Close every MockedStatic in @AfterEach. Spring beans fetched statically through FTGetBeanStaticFactoryForOrm.getBean(SomeClass.class) are stubbed the same way.

2.3. Other traps

  • Never call when(…​) on a mock inside another when(…​) argument — build the inner mock into a local variable first. This bites when stubbing a ResultSet that returns a java.sql.Array.

  • Use fixed timestamps (private static final long T0 = 1_700_000_000_000L;), never System.currentTimeMillis().

  • Mockito comes from the Spring Boot BOM — do not pin its version, and do not add mockito-inline (obsolete since Mockito 5; the inline mock-maker that mockStatic needs is the default now).

3. Running tests

# Tests plus the coverage gate (this is what CI runs)
./gradlew clean check

# Tests only
./gradlew test

# A single test class, or a single method
./gradlew test --tests "KpiCacheTest"
./gradlew test --tests "KpiCacheTest.methodName"

4. Code coverage

JaCoCo. The HTML report lands in build/jacocoHtml/index.html; the XML that CI and SonarQube read is in build/reports/jacoco/test/.

./gradlew jacocoTestReport

4.1. What is measured

Only JPA entities are excluded — getters, setters and generated equals, which cannot be meaningfully tested. Everything else under orm/ is measured: the row mappers, row callbacks, JDBC repositories and Criteria specifications carry roughly 900 branches of real logic between them.

The exclusion used to be all of /orm/. That hid about 9 000 instructions of live code — including KpiDataForGraphRowCallback, where a real bug had silently emptied the KPI graphs. Do not widen the exclusion back to whole packages.

4.2. The gate is a ratchet, not a target

jacocoTestCoverageVerification fails the build below 43% of instructions or 40% of branches, against current figures of 46.5% and 43.4%.

The margin is deliberate. Set flush against the current numbers, a single new class without tests turns the build red for whoever happened to add it — and a gate that fires on ordinary work gets switched off within a month, after which it protects nothing. With this margin the build fails only when coverage drops meaningfully: tests deleted, or a sizeable untested chunk merged.

Raise the minimums as coverage grows. The 80% goal is reached by moving them up, not by setting them there today.

5. Known gaps

These are honest gaps rather than oversights — closing them needs a different kind of test than the ones above.

  • No integration tests against a live database. Every SQL statement is verified only as "which query string was assembled". A typo in a column name would still ship.

  • No E2E tests. Browser flows are manual QA.

  • Schedulers (util/scheduler), configuration (config), notifications and the JMS consumers sit near 0%: they are wiring, and unit tests buy little there.

← Back | Main Page