Writing tests is one thing. Writing good tests is another. These best practices come from years of experience across the Java testing community. Following them will save you from a lot of pain down the road.
1. Follow the FIRST Principles
+---------+--------------------------------------------------+
| Principle | Meaning |
+---------+--------------------------------------------------+
| F | Fast - Tests should run quickly |
| I | Independent - Tests should not depend on each |
| | other |
| R | Repeatable - Same result every time |
| S | Self-validating - Pass or fail, no manual check |
| T | Timely - Write tests at the right time |
+---------+--------------------------------------------------+
// BAD: Test depends on another test
@Test
void test1() {
sharedState = "value";
}
@Test
void test2() {
// Depends on test1 setting sharedState!
assertEquals("value", sharedState);
}
// GOOD: Each test is independent
@Test
void test2() {
String state = setupState();
assertEquals("value", state);
}
2. Use Descriptive Names
// BAD
@Test
void test1() { }
@Test
void testAdd() { }
// GOOD
@Test
@DisplayName("should return sum when adding two positive numbers")
void addPositiveNumbers() { }
@Test
@DisplayName("should throw exception when dividing by zero")
void divideByZero() { }
3. One Assertion Per Test
// BAD: Testing too many things
@Test
void testUser() {
assertEquals("John", user.getName());
assertEquals("john@example.com", user.getEmail());
assertTrue(user.isActive());
assertEquals(25, user.getAge());
}
// GOOD: Focused tests
@Test
@DisplayName("should return user name")
void userName() {
assertEquals("John", user.getName());
}
@Test
@DisplayName("should return user email")
void userEmail() {
assertEquals("john@example.com", user.getEmail());
}
4. Arrange-Act-Assert (AAA)
@Test
void shouldCalculateDiscount() {
// Arrange
Customer customer = new Customer("premium");
Order order = new Order(100.00);
// Act
double discount = discountService.calculate(customer, order);
// Assert
assertEquals(20.00, discount);
}
5. Test Edge Cases
class StringHelperTest {
@Test
void normalInput() {
assertEquals("olleh", helper.reverse("hello"));
}
@Test
void emptyString() {
assertEquals("", helper.reverse(""));
}
@Test
void singleCharacter() {
assertEquals("a", helper.reverse("a"));
}
@Test
void palindrome() {
assertEquals("racecar", helper.reverse("racecar"));
}
@Test
void nullInput() {
assertThrows(NullPointerException.class,
() -> helper.reverse(null));
}
@Test
void veryLongString() {
String longString = "a".repeat(10000);
assertEquals(longString, helper.reverse(longString));
}
}
6. Avoid Magic Numbers
// BAD
assertEquals(86400, session.getTimeout());
// GOOD
int ONE_DAY_IN_SECONDS = 86400;
assertEquals(ONE_DAY_IN_SECONDS, session.getTimeout());
7. Keep Tests Fast
- Use in-memory databases instead of real ones
- Mock external services
- Avoid Thread.sleep() โ use Awaitility instead
- Minimize I/O operations
8. Use @Nested for Readability
class OrderServiceTest {
@Nested
class WhenOrderIsValid {
@Test
void shouldProcessOrder() { }
@Test
void shouldSendConfirmation() { }
}
@Nested
class WhenOrderIsInvalid {
@Test
void shouldRejectOrder() { }
@Test
void shouldNotifyUser() { }
}
}
9. Test Behavior, Not Implementation
// BAD: Tests implementation details
@Test
void shouldCallInternalMethod() {
service.process();
verify(service).internalStep1();
verify(service).internalStep2();
}
// GOOD: Tests behavior
@Test
void shouldProcessOrder() {
Order order = new Order(items);
Result result = service.process(order);
assertEquals(Status.SUCCESS, result.getStatus());
assertNotNull(result.getOrderId());
}
10. Review Tests Like Code
Tests are code. They deserve the same attention to quality:
- Code review your tests
- Refactor tests that become hard to understand
- Keep test code as clean as production code
- Delete tests that no longer add value