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 |
|---|---|---|
|
291 |
The pipeline from a raw device reading to the graph: monitoring intervals and gap arithmetic ( |
|
286 |
Not the entities — the hand-written logic around them: row mappers, row callbacks, the ClickHouse |
|
229 |
Alarm history, trends and severity counts ( |
|
212 |
Reports and CSV exports, groups and templates, KPIs, thresholds, user views. |
|
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. |
|
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 anotherwhen(…)argument — build the inner mock into a local variable first. This bites when stubbing aResultSetthat returns ajava.sql.Array. -
Use fixed timestamps (
private static final long T0 = 1_700_000_000_000L;), neverSystem.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 thatmockStaticneeds 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.