Java Unit Testing Guidelines

TL;DR: Guidelines for writing effective unit tests in Java projects, covering test organization, mocking external dependencies, and naming conventions.

Introduction

This document describes guidelines for writing unit tests in Java projects. Following these conventions ensures consistency, maintainability, and effective test coverage.

1. Test Data Management

All mock inputs and outputs should be managed using one of these approaches:

External Data Files

Store structured test data in JSON files under src/test/resources:

public class TestDataLoader {
    public static <T> T loadTestData(String fileName, Class<T> type) {
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                    TestDataLoader.class.getResourceAsStream("/testdata/" + fileName)))) {
            return new ObjectMapper().readValue(reader, type);
        }
    }
}

Builder/ConfigUtil Pattern

Use builder classes to construct test fixtures:

public class UserTestBuilder {
    private String name = "Test User";
    private String email = "test@example.com";
    
    public UserTestBuilder withName(String name) {
        this.name = name;
        return this;
    }
    
    public User build() {
        return new User(name, email);
    }
}

2. Test Organization

Package Structure

Mirror the main source structure in your test directory:

src/
├── main/java/com/example/service/
│   └── AccessManager.java
└── test/java/com/example/service/
    └── AccessManagerTest.java

Suite Runner

Create a suite runner to execute all tests:

@RunWith(Suite.class)
@Suite.SuiteClasses({
    AccessManagerTest.class,
    UserServiceTest.class,
    ValidationUtilTest.class
})
public class AllTestsSuite {
}

3. Naming Conventions

Test Class Names

Append Test to the class under test:

ClassTest Class
AccessManager.javaAccessManagerTest.java
UserService.javaUserServiceTest.java

Test Method Names

Use descriptive names that specify the scenario:

// Good - Describes what is being tested
@Test
public void createUser_withValidInput_returnsNewUser() { }

@Test
public void createUser_withNullEmail_throwsValidationException() { }

// Avoid - Vague naming
@Test
public void testCreateUser() { }

4. Mocking External Dependencies

All outbound calls must be mocked - databases, APIs, message queues, etc.

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private NotificationService notificationService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void getUser_existingId_returnsUser() {
        // Arrange
        when(userRepository.findById("123"))
            .thenReturn(Optional.of(new User("123", "Test")));
        
        // Act
        User result = userService.getUser("123");
        
        // Assert
        assertNotNull(result);
        assertEquals("Test", result.getName());
    }
}

5. Scenario Coverage

Cover both positive and negative scenarios:

Positive Scenarios

  • Valid inputs return expected outputs
  • Edge cases within valid bounds
  • Different valid data combinations

Negative Scenarios

@Test
void createUser_withNullName_throwsException() {
    // Using JUnit 5 expected exception
    assertThrows(ValidationException.class, () -> {
        userService.createUser(null, "email@test.com");
    });
}

// Or using expected attribute
@Test(expected = ValidationException.class)
public void createUser_withNullName_throwsException() {
    userService.createUser(null, "email@test.com");
}

6. Scope Guidelines

Include in Test Suite

  • Manager/Service classes (business logic)
  • Utility classes
  • Controller classes
  • Validation logic
  • Data transformation

Exclude from Test Suite

  • POJO/DTO classes (getters/setters only)
  • Repository interfaces (test via integration tests)
  • Configuration classes
  • Auto-generated code

Test Coverage Targets

Service TypeTarget Coverage
Core Business Logic70%+
Utility Classes80%+
Controllers50%+
Overall Project40-50%

Best Practices Summary

  1. ✅ One test class per source class
  2. ✅ Descriptive method names
  3. ✅ External test data in resources
  4. ✅ Mock all external dependencies
  5. ✅ Cover positive and negative scenarios
  6. ✅ Use suite runners for organized execution
  7. ❌ Don’t test POJOs or repositories directly
  8. ❌ Don’t make actual network/database calls