Unit Tests

Keep tests clean

Test code should be maintained to the same standards of quality as their production code.

Focus on readability

Readability is even more important in unit tests than it is in production code.

What makes tests readable? The same thing that makes all code readable: clarity, simplicity, and density of expression. In a test, you want to say a lot with as few expressions as possible.

class UserRepository {
    public User save(UserDto dto) {
    }
    
    public User update(UserDto dto) {
    }
    
    public boolean delete(Long userId){
    }
}

@Test
public void testSaveUser() {
    try {
        // Given
        String userDtoJsonFile = "path/to/file/user-dto.json";
        Gson gson = new Gson();
        JsonReader reader = new JsonReader(new FileReader(userDtoJsonFile));
        UserDto dto = gson.fromJson(reader, UserDto.class);

        // When
        User actualUser = userRepository.save(dto);
        
        // Then
        // Expected user
        String userJsonFile = "path/to/file/user.json";
        reader = new JsonReader(new FileReader(userJsonFile));
        User expectedUser = gson.fromJson(reader, User.class);
        
        // Verify saved user
        assertNotNull(actualUser);
        assertNotNull(actualUser.getId());
        assertEquals(expectedUser.getEmail(), actualUser.getEmail());
        assertEquals(expectedUser.getPassword(), actualUser.getPassword());
    } catch(FileNotFoundException ex) {
        fail("The given user data json file is not found", ex);
    }
}
class JsonUtils {
    public <T> T fromJsonFile(String jsonFile, Class<T> tClass) throws FileNotFoundException {
        Gson gson = new Gson();
        JsonReader reader = new JsonReader(new FileReader(jsonFile));
        return gson.fromJson(reader, tClass);
    }
}
   
@Test
void givenUser_WhenSave_ShouldSaveSuccessfully() throws FileNotFoundException {
    // Given
    UserDto dto = createUserDto();
       
    // When 
    User actualUser = userRepository.save(dto);
    
    // Then
    User expectedUser = createExpectedUser();
    verifySavedUser(expectedUser, actualUser);
}

private UserDto createUserDto() throws FileNotFoundException {
    String userDtoJsonFile = "path/to/file/user-dto.json";
    return JsonUtils.fromJsonFile(userDtoJsonFile, UserDto.class);
}

private User createExpectedUser() throws FileNotFoundException {
    String userJsonFile = "path/to/file/user.json";
    return JsonUtils.fromJsonFile(userJsonFile, User.class);
}

private void verifySavedUser(UserDto expectedUser, User actualUser) {
    assertNotNull(actualUser);
    assertNotNull(actualUser.getId());
    assertEquals(expectedUser.getEmail(), actualUser.getEmail());
    assertEquals(expectedUser.getPassword(), actualUser.getPassword());
}

One Assert per Test

Every test function in a JUnit test should have one and only one assert statement.

@Test
public void testUserCRUD() {
    // Given
    UserDto userDto1 = new UserDtoBuilder()
        .setEmail("cleancode@gpcoder.com")
        .setPassword("secret")
        .build();
    // When
    User user1 = userRepository.save(userDto1);
    // Then
    assertNotNull(user1);

    // Do other assert statements ...
    
    // Given 
    UserDto updatedUser1 = user1.setPassword("new secret").build();
    // When 
    user1 = userRepository.update(user1);
    // Then
    assertNotNull(user1);
    
    // Given 
    long userId = 1;
    // When 
    boolean isDeleted = userRepository.delete(1);
    // Then
    assertTrue(isDeleted);
}
@Test
public void givenUser_WhenSave_ShouldSaveSuccessfully() {
    // Given
    UserDto userDto1 = new UserDtoBuilder()
        .setEmail("cleancode@gpcoder.com")
        .setPassword("secret")
        .build();
        
    // When
    User user1 = userRepository.save(userDto1);
    
    // Then
    assertNotNull(user1);
}

@Test
public void givenUser_WhenUpdate_ShouldUpdateSuccessfully() {
    // Given
    User user1 = new UserBuilder()
        .id(1)
        .setEmail("cleancode@gpcoder.com")
        .setPassword("new secret")
        .build();
    
    // When 
    User user1 = userRepository.update(user1);
    
    // Then
    assertNotNull(user1);
}

@Test
public void givenUserId_WhenDelete_ShouldDeleteSuccessfully() {
    // Given 
    long userId = 1;
    
    // When 
    boolean isDeleted = userRepository.delete(1);
    
    // Then
    assertTrue(isDeleted);
}

The names of the functions should use the common given-when-then convention. This makes the tests even easier to read.

Use a single concept per test

Should minimize the number of asserts per concept and test just one concept per test function.

public static final int MIN = 10;
public static final int MAX = 100;

boolean isInRange(int value) {
    return MIN <= value && value <= MAX;
}

@Test
public void testInRange() {
    assertFalse(isInRange(9));
    assertTrue(isInRange(10));
    assertTrue(isInRange(11));
    assertTrue(isInRange(99));
    assertTrue(isInRange(100));
    assertFalse(isInRange(101));
}

You can split it into multiple test cases, e.g. test less than, equal to, greater than MIN/MAX value. However, you can minimize it as follows:

@Test
public void givenValueInRange_WhenCheckInRange_ShouldReturnTrue() {
    assertTrue(isInRange(10));
    assertTrue(isInRange(11));
    assertTrue(isInRange(99));
    assertTrue(isInRange(100));
}

@Test
public void givenValueOutRange_WhenCheckInRange_ShouldReturnFalse() {
    assertFalse(isInRange(9));
    assertFalse(isInRange(101));
}

Follow five rules that form the acronym F.I.R.S.T.

Fast

Tests should be fast. They should run quickly. When tests run slow, you won’t want to run them frequently. If you don’t run them frequently, you won’t find problems early enough to fix them easily. You won’t feel as free to clean up the code. Eventually the code will begin to rot.

Independent

Tests should not depend on each other. One test should not set up the conditions for the next test. You should be able to run each test independently and run the tests in any order you like. When tests depend on each other, then the first one to fail causes a cascade of downstream failures, making diagnosis difficult and hiding downstream defects.

Repeatable

Tests should be repeatable in any environment. You should be able to run the tests in the production environment, in the QA environment, and on your laptop while riding home on the train without a network. If your tests aren’t repeatable in any environment, then you’ll always have an excuse for why they fail. You’ll also find yourself unable to run the tests when the environment isn’t available.

Self-Validating

The tests should have a boolean output. Either they pass or fail. You should not have to read through a log file to tell whether the tests pass. You should not have to manually compare two different text files to see whether the tests pass. If the tests aren’t self-validating, then failure can become subjective, and running the tests can require a long manual evaluation.

Timely

The tests need to be written in a timely fashion. Unit tests should be written just before the production code that makes them pass. If you write tests after the production code, then you may find the production code to be hard to test. You may decide that some production code is too hard to test. You may not design the production code to be testable.

Last updated