Duplicate Lifecycle Method
ID |
java.test_classes_contain_multiple_lifecycle |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Java |
Tags |
reliability, testing |
Description
Reports a class declaring more than one method for the same lifecycle annotation — @Before, @After, @BeforeClass, @AfterClass in JUnit 4, or @BeforeEach, @AfterEach, @BeforeAll, @AfterAll in JUnit 5.
Rationale
JUnit does not guarantee the relative order in which two methods carrying the same lifecycle annotation run. A test suite can look and pass exactly as intended for a long time, because the current internal ordering happens to work, then start failing after an unrelated change or a JUnit upgrade changes that internal order — with no change to the test itself to point at.
class WarehouseInventoryTest {
@BeforeEach
void resetStock() {
stock = 0;
}
@BeforeEach
void primeSupplierMock() {
// relies on stock already being reset — but that order is not guaranteed
}
}
Remediation
Merge the duplicated lifecycle methods into one. If the original methods needed to run in a specific order, call them in that order from the merged method, so the order is explicit rather than incidental.
class WarehouseInventoryTest {
@BeforeEach
void setUp() {
stock = 0;
primeSupplierMock();
}
}
What is not reported
The check is strictly per class. A lifecycle method declared in a superclass and one declared in a subclass never collide, because JUnit does guarantee that ordering across a class hierarchy: "before" methods run superclass-first, "after" methods run subclass-first. TestNG’s own lifecycle annotations are out of scope, since this defect is specific to JUnit 4 and JUnit 5.
Cucumber’s io.cucumber.java.Before / io.cucumber.java.After hooks are out of scope for the same reason as TestNG’s: they share a simple name with the JUnit 4 annotations but take an explicit order attribute, so two of them in one class state their relative order rather than leaving it unspecified.
class MessagingHook { // not reported: the order is declared, not incidental
@Before(order = 0)
public void waitForListenerStartup() { }
@Before(order = 1)
public void emptyEventStore() { }
}