Three Essential Properties For Writing Maintainable Unit Tests


Since last three months I am running a course in my office where I am teaching junior developers how to write clean and maintainable code. In my experience, you can’t make people write clean and maintainable code until you make them write automated unit tests. For the purpose of this discussion, I don’t care much whether you write test first or last. The goal should be to write quality tests.

Writing unit tests is easy but writing clean and maintainable unit tests is difficult. Over time I have realised there are properties of the test that if satisfied can help you write maintainable tests. I will not cover FIRST(Fast, Isolated, Repeatable, Self-validating, and Timely) properties as they are covered at a lot of places.

I am going to cover three properties that I find useful but are not written about.

Property 1: Structural independence

To be structural independent your test result should not change if the structure of the code changes.

To understand this property, let’s look at an example

@Test
public void shouldCreateArticleWhenProvidedValidData() {
    ArticleRepository mockedRepo = Mockito.mock(ArticleRepository.class);
    ArticleService articleService = new ArticleService(mockedRepo);    

    Article article = new Article(
        "My blog",
        "Today, we will learn following topic"
    );
    articleService.save(article);
    Mockito.verify(mockedRepo).save(article);
    Article articleWithDraftStatus = new Article(
        "My blog",
        "Today, we will learn following topic",
        Status.DRAFT
    );
    Mockito.verify(mockedRepo).save(articleWithDraftStatus);
}

In the test above, we are testing the save method of ArticleService. We are using Mockito library to mock our dependencies.

The save method is shown below.

public void save(Article article){
    Article saved = articleRepository.save(article);
    saved.setStatus(Status.DRAFT);
    articleRepository.save(saved);
}

The developer first saved the article and then changed the status to DRAFT and then saved it again. I know this looks like a contrived example but I have seen many code bases written something like that.

The above test case will fail when the developer changes the code to below. It is still the same behaviour.

public Article save(Article article){
    article.setStatus(Status.DRAFT);
    return articleRepository.save(article);
}

The test written above fails to meet structural independence property as its result changed when we restructured our code. The behavior of both the save method implementations are same. They both end up saving article in the DRAFT state but because we were tightly coupled to the internal implementation of the save method we have to spend time fixing our test.

This is a common problem with mocking. When you use mocking a lot your tests become structural dependent. You should try to minimise the use of mocking in your code if you want your tests to be structural independent.

Property 2: Behavioural dependence

To be behavioural dependent a test case result should change when the behavior of the code under test change.

To understand this property, let’s look at the following example.

@Test
public void shouldCreateArticleWhenProvidedValidData() {
    ArticleService articleService = new ArticleService();
    Article article = new Article(
        "My blog",
        "Today, we will learn following topic"
    );
    Article saved = articleService.save(article);
    assertThat(saved).isNotNull();
}

The above is a test that checks if the article is saved in the database. We assert hat saved article should be not be null.

The above is the test case for the save method shown below.

public Article save(Article article){
    article.setStatus(Status.DRAFT);
    return articleRepository.save(article);
}

Lets’ assume a developer changes the behavior of the code to below. So, rather than saving the article as draft we are saving the article in the published state.

public Article save(Article article){
    article.setStatus(Status.PUBLISH);
    return articleRepository.save(article);
}

Our test case will start pass even though we have completely changed our article publishing workflow.

Our test case breaks behavioural dependence property.

To make test abide behavioural dependence property we should assert the status of the article as shown below.

@Test
public void shouldCreateArticleWhenProvidedValidData() {
    ArticleService articleService = new ArticleService();
    Article article = new Article(
        "My blog",
        "Today, we will learn following topic"
    );
    Article saved = articleService.save(article);
    assertThat(saved).isNotNull();
    assertThat(saved.getStatus()).isEqualTo(Status.DRAFT);
}

Property 3: Refactoring safety

This property means you have a belief that you or anyone in your team can safely refactor the code and your test results will change in case refactoring breaks a functionality. The first two properties structural independence and behavioural dependence enable this property.

You need to have a good test coverage to be completely sure that refactoring does not break functionality. If you are refactoring a code base and tests does not exist then you should first write a test cases for it.

One thought on “Three Essential Properties For Writing Maintainable Unit Tests”

  1. I love the way your write the first 2 properties. I find it very clear.
    I agree that overuse of mocks leads to a mess. I call it Mock Hell: when the mocking you put in place in order to test your code in order to be able to refactor it actually prevent you from refactoring! I’ve written a series of post about the techniques I used to get rid of mocks in a code base (https://philippe.bourgau.net/careless-mocking-considered-harmful/). For example, using an in-memory DB keeps tests fast, let you test that things were really saved correctly and make refactoring a lot easier.
    Thanks for your post!

Leave a Reply to Philippe Bourgau Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s