Software Development

Writing Clean Tests – Naming Matters

It is pretty hard to figure out a good definition for clean code because everyone of us has our own definition for the word clean. However, there is one definition which seems to be universal:

Clean code is easy to read.

This might come as a surprise to some of you, but I think that this definition applies to test code as well. It is in our best interests to make our tests as readable as possible because:
 

  • If our tests are easy to read, it is easy to understand how our code works.
  • If our tests are easy to read, it is easy to find the problem if a test fails (without using a debugger).

It isn’t hard to write clean tests, but it takes a lot of practice, and that is why so many developers are struggling with it.

I have struggled with this too, and that is why I decided to share my findings with you.

This is the second part of my tutorial which describes how we can write clean tests. This time we will learn what kind of an effect naming has to the readability of our tests. We will also learn rules which help us to transform our test cases into executable specifications.

The Devil Is in the Details

It is relatively easy to write tests which seem clean. However, if we want to go the extra mile and change our tests into a executable specification, we have to pay extra attention to the naming of test classes, test methods, test class’ fields, and local variables.

Let’s find out what this means.

Naming Test Classes

When we think about the different test classes which we create in a typical project, we notice that these classes can be divided into two groups:

  • The first group contains tests which tests the methods of a single class. These tests can be either unit tests or integration tests written for our repositories.
  • The second group contains integration tests which ensure that a single feature is working properly.

A good name identifies the tested class or feature. In other words, we should name our test classes by following these rules:

  1. If the test class belongs to the first group, we should name it by using this formula: [The name of the tested class]Test. For example, if we are writing tests for the RepositoryUserService class, the name of our test class should be: RepositoryUserServiceTest. The benefit of this approach is that if a test fails, this rule helps us figure out which class is broken without reading the test code.
  2. If the class belongs to the second group, we should name it by using this formula: [The name of the tested feature]Test. For example, if we would be writing tests for the registration feature, the name of our test class should be RegistrationTest. The idea behind this rule is that if a test fails, using this naming convention helps us to figure out what feature is broken without reading the test code.

Naming Test Methods

I am big fan of the naming convention introduced by Roy Osherove. Its idea is to describe the tested method (or feature), expected input or state, and expected behavior in the name of a test method.

In other words, if we follow this naming convention, we should name our test methods as follows:

  1. If we write tests for a single class, we should name our test methods by using this formula: [the name of the tested method]_[expected input / tested state]_[expected behavior]. For example, if we write a unit test for a registerNewUserAccount() method which throws an exception when the given email address is already associated with an existing user account, we should name our test method as follows: registerNewUserAccount_ExistingEmailAddressGiven_ShouldThrowException().
  2. If we write tests for a single feature, we should name our test methods by using this formula: [the name of the tested feature]_[expected input / tested state]_[expected behavior]. For example, if we write an integration test which tests that an error message is shown when a user tries to create a new user account by using an email address which is already associated with an existing user account, we should name out test method as follows registerNewUserAccount_ExistingEmailAddressGiven_ShouldShowErrorMessage().

This naming convention ensures that:

  • The name of a test method describes a specific business or technical requirement.
  • The name of a test method describes expected input (or state) and the expected result for that input (state).

In other words, if we follow this naming convention we can answer to the following questions without reading the code of our test methods:

  • What are the features of our application?
  • What is the expected behavior of a feature or method when it receives an input X?

Also, if a test fails, we have a pretty good idea what is wrong before we read the source code of the failing test.

Pretty cool, huh?

Naming Test Class’ Fields

A test class can have the following fields:

  • Fields which contains Test doubles such mocks or stubs.
  • A field which contains a reference to the tested object.
  • Fields which contains the other objects (testing utilities) which are used in our test cases.

We should name these fields by using the same rules which we use when we name the fields found from the application code. In other words, the name of each field should describe the “purpose” of the object which is stored to that field.

This rule sounds pretty “simple” (naming is always hard), and it has been easy for me to follow this rule when I name the tested class and the other classes which are used my tests. For example, if I have to add a TodoCrudService field to my test class, I use the name crudService.

When I have added fields which contain test doubles to my test class, I have typically added the type of the test double to the end of the field name. For example, if I have added a TodoCrudService mock to my test class, I have used the name crudServiceMock.

It sounds like a good idea but I have come to conclusion that it is a mistake. It is not a major problem but the thing is that a field name should describe the “purpose” of the field, not its type. Thus, we should not add the type of the test double to the field name.

Naming Local Variables

When we name the local variables used in our test methods, we should follow the same principles used when we name the variables found from our application code.

In my opinion, the most important rules are:

  • Describe the meaning of the variable. A good rule of thumb is that the variable name must describe the content of the variable.
  • Don’t use shortened names which aren’t obvious for anyone. Shortened names reduces readability and often you don’t gain anything by using them.
  • Don’t use generic names such as dto, modelObject, or data.
  • Be consistent. Follow the naming conventions of the used programming language. If your project has its own naming conventions, you should honor them as well.

Enough with theory. Let’s put these lessons into practice.

Putting Theory into Practice

Let’s take a look at a modified unit test (I made it worse) which is found from the example application of my Spring Social tutorial.

This unit test is written to test the registerNewUserAccount() method of the RepositoryUserService class, and it verifies that this method is working correctly when a new user account is created by using a social sign provider and a unique email address.

The source code of our test class looks as follows:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

    private RepositoryUserService service;

    @Mock
    private PasswordEncoder passwordEncoderMock;

    @Mock
    private UserRepository repositoryMock;

    @Before
    public void setUp() {
        service = new RepositoryUserService(passwordEncoderMock, repositoryMock);
    }


    @Test
    public void registerNewUserAccountByUsingSocialSignIn() throws DuplicateEmailException {
        RegistrationForm form = new RegistrationForm();
        form.setEmail("john.smith@gmail.com");
        form.setFirstName("John");
        form.setLastName("Smith");
        form.setSignInProvider(SocialMediaService.TWITTER);

        when(repositoryMock.findByEmail("john.smith@gmail.com")).thenReturn(null);
       
        when(repositoryMock.save(isA(User.class))).thenAnswer(new Answer<User>() {
            @Override
            public User answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                return (User) arguments[0];
            }
        });

        User modelObject = service.registerNewUserAccount(form);

        assertEquals("john.smith@gmail.com", modelObject.getEmail());
        assertEquals("John", modelObject.getFirstName());
        assertEquals("Smith", modelObject.getLastName());
        assertEquals(SocialMediaService.TWITTER, modelObject.getSignInProvider());
        assertEquals(Role.ROLE_USER, modelObject.getRole());
        assertNull(modelObject.getPassword());

        verify(repositoryMock, times(1)).findByEmail("john.smith@gmail.com");
        verify(repositoryMock, times(1)).save(modelObject);
        verifyNoMoreInteractions(repositoryMock);
        verifyZeroInteractions(passwordEncoderMock);
    }
}

This unit test has quite many problems:

  • The field names are pretty generic, and they describe the types of the test doubles.
  • The name of the test method is “pretty good” but it doesn’t describe the given input or the expected behavior.
  • The variable names used in the test method are awful.

We can improve the readability of this unit test by making the following changes to it:

  1. Change the name of the RepositoryUserService field to registrationService (the name of the service class is a bit bad but let’s ignore that).
  2. Remove the the word ‘mock’ from field names of the PasswordEncoder and UserRepository fields.
  3. Change the name of the test method to: registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider().
  4. Change the name of the form variable to registration.
  5. Change the name of the modelObject variable to createdUserAccount.

The source code of our “modified” unit test looks as follows:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;


@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

    private RepositoryUserService registrationService;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserRepository repository;

    @Before
    public void setUp() {
        registrationService = new RepositoryUserService(passwordEncoder, repository);
    }


    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationForm();
        registration.setEmail("john.smith@gmail.com");
        registration.setFirstName("John");
        registration.setLastName("Smith");
        registration.setSignInProvider(SocialMediaService.TWITTER);

        when(repository.findByEmail("john.smith@gmail.com")).thenReturn(null);

        when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
            @Override
            public User answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                return (User) arguments[0];
            }
        });

        User createdUserAccount = registrationService.registerNewUserAccount(registration);

        assertEquals("john.smith@gmail.com", createdUserAccount.getEmail());
        assertEquals("John", createdUserAccount.getFirstName());
        assertEquals("Smith", createdUserAccount.getLastName());
        assertEquals(SocialMediaService.TWITTER, createdUserAccount.getSignInProvider());
        assertEquals(Role.ROLE_USER, createdUserAccount.getRole());
        assertNull(createdUserAccount.getPassword());

        verify(repository, times(1)).findByEmail("john.smith@gmail.com");
        verify(repository, times(1)).save(createdUserAccount);
        verifyNoMoreInteractions(repository);
        verifyZeroInteractions(passwordEncoder);
    }
}

It is clear that this test case still has some problems but I think that our changes improved its readability. I think that the most dramatic improvements are:

  1. The name of test method describes the expected behavior of the tested method when a new user account is created by using a social sign in provider and a unique email address. The only way we could get this information from the “old” test case was to read the source code of the test method. This is obviously a lot slower than reading just the method name. In other words, giving good names to test methods saves time and helps us to get a quick overview about the requirements of the tested method or feature.
  2. the other changes transformed a generic CRUD test into a “use case”. The “new” test method describes clearly
    1. What steps does this use case have.
    2. What the registerNewUserAccount() method returns when it receives a registration, which is made by using a social sign in provider and has a unique email address.

    In my opinion, the “old” test case failed to do this.

I am not entirely happy with the name of the RegistrationForm object but it is definitely better than the original name.

Summary

We have now learned that naming can have a huge positive effect to the readability of our test cases. We have also learned a few basic rules which helps us to transform our test cases into executable specifications.

However, our test case still has some problems. These problems are:

  • The code which creates new RegistrationForm objects simply sets the property values of the created object. We can make this code better by using test data builders.
  • The standard JUnit assertions, which verify that the information of the returned User object is correct, are not very readable. Another problem is that they only check that the property values of the returned User object are correct. We can improve this code by turning assertions into a domain-specific language.

I will describe both techniques in the future.

In the meantime, I would love to hear what kind of naming conventions do you use.

Reference: Writing Clean Tests – Naming Matters from our JCG partner Petri Kainulainen at the Petri Kainulainen blog.

Petri Kainulainen

Petri is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Frank
Frank
10 years ago

Great article! I have some refactoring to do!

Petri Kainulainen
10 years ago
Reply to  Frank

Thanks. It is great to hear that you enjoyed this blog post.

Back to top button