Write BDD Unit Tests with BDDMockito and AssertJ

Write BDD Unit Tests with BDDMockito and AssertJ

In this post, I’ll show you how to write more readable Unit Tests using a Behavior-Driven Development style (BDD). This is a coding style that is very easy to adopt and, at the same time, brings a huge benefit: it increases your test readability (a lot). Besides, it’s a small change that may drive you to go full-BDD (i.e. writing your test scenarios before your code).

With a practical example, you’ll see how to make this change with two very popular libraries: Mockito (and its BDDMockito implementation) and AssertJ.

All the code in this post is available on GitHub: Tests with BDD Mockito and AssertJ. If you find it useful, please give it a star!

Why should I use BDD?

Reverse Engineering in non-BDD tests

The code in Unit Tests tends to be disorganized: some mocks here and there, interleaved with assertions and test method calls. If you have many unit tests (good!), maintaining them is hard. As a developer, I use Unit Tests to understand the functionality that someone else has written before. However, if the test is a mess, that task is much more complicated (or near impossible in some cases).

Mapping Functionality to Test Cases

There is even a worse scenario in which, apart from the tests, you also count with documentation for that specific functionality but you have no idea how the code maps to those requirements. The tests may be working perfectly and covering that functionality as it’s needed but, not being able to derive the application flow from their code is a clear symptom of bad readability of your Unit Tests, that makes them harder (and more expensive) to maintain.

Given-When-Then: Tests that a human can read

BDD brings to the table not only a human-friendly style of writing test scenarios but also the important idea that you should be doing that before writing the code that implements your functionality (thereby the name Behavior-Driven Development). In this article, I focus on the first part: the writing style. As I introduced before, it’s the easiest change and improves a lot your code. Also, switching to that coding style can enable the bigger move: all those given-when-then scenario descriptions may activate people’s willing to try to write the tests before the code. And that would come with some other advantages like a more efficient way to extract the application’s requirements, which avoids wasting time in implementing the wrong features.

BDD tests with BDDMockito and AssertJ
BDD tests with BDDMockito and AssertJ

To recap, the biggest two advantages are:

  1. Much better test readability, which leads to less time required to maintain your unit tests.
  2. Your test code is your documentation and will be maintained implicitly when the requirements change (otherwise your build will fail). There is no need for extra documentation on separate docs that get obsolete very quickly.

A Practical Example of BDD

As usual, I created a practical example to show you how to create BDD Unit Tests with Mockito and AssertJ. The functionality is very simple. To make the experiment yourself of how the code documents your application, you can navigate to the complete test class written in BDD style and try to derive it from there: you’ll get the idea instantly. For educational purposes, I’ll give you a text description of what this implementation should do anyway.

This first version of the app Population (in a very early stage) should allow inserting Cities and retrieving them. A city is no more than a name and a number that represents its population, and it has a unique identifier in the system. We need to write some business logic to cope with the following requirements:

  • A city will be passed to the system with only its name. Its population can be retrieved using an external service that, given a name, will return the number of inhabitants.
  • In the storage system, cities should be already enriched with the population. However, if the population is not known, they can be stored temporarily for a later enrichment.
  • When asking for the list of cities, they should be retrieved in alphabetical order, omitting any incomplete city (those without a population).

Let’s start coding.

Setup and Required Dependencies

I created the sample code using the TPD Basic Maven Archetype since creating a POM file from scratch is boring. To use the archetype you just need Maven. Using the command line, execute:

mvn archetype:generate -DgroupId=examples -DartifactId=test-bdd-assertj -DarchetypeGroupId=com.thepracticaldeveloper -DarchetypeArtifactId=archetype-java-basic-tpd -DarchetypeVersion=1.0.0 -Djava-version=9

To replicate the code exactly as it is on GitHub, use com.thepracticaldeveloper as groupId.

The Basic Java Archetype comes with some useful testing dependencies, which are actually the ones we need for this example: JUnit, Mockito, and AssertJ.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.thepracticaldeveloper</groupId>
    <artifactId>test-bdd-assertj</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>9</maven.compiler.source>
        <maven.compiler.target>9</maven.compiler.target>
        <maven.jar.plugin.version>3.0.2</maven.jar.plugin.version>
        <jacoco.maven.plugin.version>0.8.0</jacoco.maven.plugin.version>
        <junit.version>4.12</junit.version>
        <assertj.version>3.9.0</assertj.version>
        <mockito.version>2.13.0</mockito.version>
    </properties>
    <build>
        <plugins>
            <!-- Makes the JAR executable, by configuring the plugin to use the included manifest file -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>${maven.jar.plugin.version}</version>
                <configuration>
                    <archive>
                        <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
            <!-- JaCoCo configuration - provides test coverage reports -->
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.maven.plugin.version}</version>
                <executions>
                    <execution>
                        <id>default-prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>default-report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>${assertj.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
Get the book Practical Software Architecture

With this pom.xml file we also get Unit Test Coverage Reports (via JaCoCo) and JAR packaging. All we need to start.

Note that to use BDDMockito you don’t need any extra dependency since it’s just a class inside the core Mockito library containing method stubs.

Code

Remember: All the code in this post is available on GitHub: Tests with BDD Mockito and AssertJ. If you find it useful, please give it a star!

Domain model

To cover the requirements, we’ll need to model the City in the code, and also some actions. Let’s start with the simple POJO:

package com.thepracticaldeveloper.population;

import java.util.Objects;

public final class City {

  private final Long id;
  private final String name;
  private final Integer population;

  public City(final Long id, final String name, final Integer population) {
    this.id = id;
    this.name = name;
    this.population = population;
  }

  public Long getId() {
    return id;
  }

  public String getName() {
    return name;
  }

  public Integer getPopulation() {
    return population;
  }

  public City copyWithId(final Long id) {
    return new City(id, this.name, this.population);
  }

  @Override
  public boolean equals(final Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    final City city = (City) o;
    return Objects.equals(id, city.id) &&
      Objects.equals(name, city.name) &&
      Objects.equals(population, city.population);
  }

  @Override
  public int hashCode() {
    return Objects.hash(id, name, population);
  }

}

Nothing fancy there, just an immutable class with getters, hashCode and equals, and a method to copy the city with a new id.

Interfaces

Let’s create now two interfaces to model other functionalities apart from the main logic: the external service providing the city population (PopulationService) and the persistence layer to store and retrieve our cities (CityRepository).

package com.thepracticaldeveloper.population;

import java.util.Optional;

public interface PopulationService {
  /**
   * Retrieves the population for a given city name
   *
   * @param cityName the name of the city
   * @return an Optional containing the city population if available, otherwise empty.
   */
  Optional<Integer> forCity(final String cityName);
}
package com.thepracticaldeveloper.population;

import java.util.List;

public interface CityRepository {

  City save(City city);

  List<City> getAllCities();
}

The good thing about Test-Driven Development (TDD) and Behavior-Driven Development (BDD), is that we don’t need to implement those pieces of functionality to start verifying our app requirements (or the app behavior if you want to use BDD terms). That’s a great superpower: let’s focus first on the main requirements we got so you can validate and change them if needed (in a real scenario with your team, business analyst, etc.).

Business Logic Implementation

The code fulfilling the requirements is inside a CityService class, which is interacting with the other two interfaces and adding some logic on top of it:

package com.thepracticaldeveloper.population;

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class CityService {

  private final CityRepository repository;
  private final PopulationService populationService;

  public CityService(final CityRepository repository, final PopulationService populationService) {
    this.repository = repository;
    this.populationService = populationService;
  }

  public City enrichAndCreateCity(final City city) {
    // Validation
    if (city.getId() != null) {
      throw new IllegalArgumentException("City (" + city.getName() + ") can't be created with a predefined ID");
    }
    // External service call
    final Optional<Integer> population = populationService.forCity(city.getName());
    // Enrichment
    final City enrichedCity = new City(city.getId(), city.getName(), population.orElse(null));
    // Storing in repository
    return repository.save(enrichedCity);
  }

  public List<City> getAllValidCities() {
    // Retrieve from repository
    final List<City> storedCities = repository.getAllCities();
    // Remove those with empty population, and sort them alphabetically
    return storedCities.stream()
      // Comment only the following line to check how SoftAssertions work
      .filter(city -> city.getPopulation() != null)
      .sorted(Comparator.comparing(City::getName))
      .collect(Collectors.toList());
  }
}

Let’s go through it, just in case you got some questions:

  • The constructor's arguments are the interfaces needed within this service. As you can imagine, with some annotations you could use a dependency injection framework here.
  • The enrichAndCreateCity method covers one of the main requirements and allows to create a city with only a name in it. It calls the PopulationService, enriches the city by making a copy of it, and finally stores it using the CityRepository interface. This method won't work yet, given that we don't have implementations of those interfaces, but we can test it perfectly since the interfaces are clearly defined. There is also a very low-level validation there, which will throw an exception in case the city is passed with an id. I kept it like that for simplicity but, in real life, you could use a validation library which comes with some predefined annotations (for instance Hibernate or Spring).
  • Finally, the method getAllValidCities() retrieves all stored cities and returns a filtered, sorted list as per the requirements. This is something you could do directly as a query in a standard database, but let's add it here, knowing that we don't have any functionality to storing/retrieving anyways.

You can go ahead and try to code this yourself, or you can also clone the GitHub repository and check how tests work by running, from the project folder:

$ mvn clean test

You can also use your preferred IDE which surely provides a more visual way to check the results.

BDDMockito and AssertJ: Basics

It’s time to have a look at the Unit Test so you see where are the benefits. First, we’ll create a test method to verify that creating a city works as expected.

package com.thepracticaldeveloper.population;

import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;

import static org.assertj.core.api.BDDAssertions.then;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;

@RunWith(MockitoJUnitRunner.class)
public class CityServiceTest {

  @Mock
  private CityRepository cityRepository;

  @Mock
  private PopulationService populationService;

  private CityService cityService;
  private static final String MALAGA = "Malaga";
  private static final int MALAGA_POPULATION = 569_000;

  @Before
  public void setup() {
    cityService = new CityService(cityRepository, populationService);
    Assertions.useRepresentation(new CityRepresentation());
  }

  @Test
  public void createCity() {
    // Given
    final City inputCity = new City(null, MALAGA, null);
    given(populationService.forCity(MALAGA))
      .willReturn(Optional.of(MALAGA_POPULATION));
    given(cityRepository.save(any(City.class)))
      .willAnswer(answer -> ((City) answer.getArgument(0)).copyWithId(randomLong()));

    // When
    final City actualCity = cityService.enrichAndCreateCity(inputCity);

    // Then
    final City expectedCity = new City(null, MALAGA, MALAGA_POPULATION);
    then(actualCity.getId())
      .as("Check that City ID is set when stored.")
      .isNotNull();
    then(actualCity)
      .as("Check that City name is correct and city population is filled in.")
      .isEqualToIgnoringGivenFields(expectedCity, "id");
  }

  private long randomLong() {
    return ThreadLocalRandom.current().nextLong(1000L);
  }
}

Let’s review first some basic JUnit and Mockito configuration that we need as usual:

  • The @Test annotation tells JUnit that the method is actually a test, so it should be executed like that.
  • @RunWith is an optional JUnit annotation that we use here to replace the standard runner for MockitoJUnitRunner. This has some advantages like automatic Mock initialization (just by using the @Mock annotation), detection of unused stubs and validation of incomplete stubbing.
  • @Before is also a JUnit annotation and tells the framework to execute that method before each test method. We use it here to initialize the service to test and to configure the city representation, but we'll cover that a bit later.
  • The @Mock annotation is just another way to configure an object that will have the same methods available, but that we can configure to behave as we want by telling Mockito what to do when methods are called. This is a very brief explanation so, if you want to know more about Mockito basics, I recommend you to have a look first at an introductory tutorial.

BDD and the Given-When-Then triad

Given-When-Then Structure

Get the book Practical Software Architecture

As you see in the code above, the test is structured in three parts: Given, When and Then. I added comments for a better visualization but this is something you can skip when you get used to it.

  • The Given part entitles the test case setup. The assumptions, the preconditions, the requirements for this use case.
  • The When part is the action that you want to test. Normally, it's also the smallest part of the test since the execution we want to test is typically one or two lines of code.
  • The Then part is used to very what should happen after the execution of the action, which is represented usually by assertions to mocked classes and validation of returned results.

Advantages of using BDD’s Given-When-Then

What we’re doing here is writing our test cases following a natural language approach as introduced by BDD (Given this preconditions, When this action happens, Then these results should be obtained). And that comes with serious advantages:

  • This natural language is easy to read not only by other developers but also by business analysts. That means they can extract what the tests are doing and help to refine new scenarios and cases. Furthermore, if you start using this approach the idea is that you can eventually move from a starting point in which developers are writing the scenarios (Given, When, Then) to a much better way of working in which the business analysts are documenting the features using this syntax: that's the real superpower that you may get with this small change. If you want to see a more advanced example of BDD Testing using Cucumber, save this other article for a later reading: End-to-End Microservice Tests with Cucumber
  • Structuring your test classes in this easy way creates a soft, implicit convention that, if everybody follows and gets used to it, makes much faster to read what someone else's tests are doing, which parts are mocked and what are the assertions. When tests fail or need additional functionality, maintenance becomes easier if you know where to fix or insert the code.

BDDMockito vs. Standard Mockito

We don’t need to go for a full Gherkin syntax to start with and at this level (especially not for Unit Tests, that should be as simple as possible). All we need is to structure our code. So, you might be asking yourself, what is BDD Mockito and what are the differences with standard Mockito? It’s very simple, the standard Mockito implementation uses the following syntax:

when(methodCall).then(doSomething)

And that when there to set up the scenario (configure the mocks) is annoying for BDD purposes since When is used to trigger the action, not to set up the scenario. That little thing might be confusing so the great team behind Mockito decided to create a class with BDD-friendly aliases, BDDMockito. So you can now configure your mocks like this:

given(methodCall).will(doSomething)

It might look like a very subtle difference, but this last piece of code is much better for our goal.

Difference between Mockito’s willAnswer and willReturn

In the code sample above (also extracted below for reference) you can see two examples of BDDMockito, the first using willReturn and the second one the more flexible willAnswer. The difference is that the latter allows us to capture the argument to use it to generate the result:

given(populationService.forCity(MALAGA))
  .willReturn(Optional.of(MALAGA_POPULATION));
given(cityRepository.save(any(City.class)))
  .willAnswer(answer -> ((City) answer.getArgument(0)).copyWithId(randomLong()));

Where is the When in Mockito BDD?

Great! Now we have a good way to specify our scenarios in a BDD style using full-power Mockito. But you might be wondering, what about the When keyword?

As you can imagine, the When part is so simple -just calling the method you want to test- that is not really worth it to add anything there. Just call your method and store the result for later checks. What I recommend you here is to separate this method call with a blank line before and after so it’s easily recognized. You can also add the //When comment line if you have a big chunk of code before and/or after.

Assertions within the Then block

So that leaves us with only the Then part to be figured out. Let’s see how AssertJ can help us there.

Readable and Powerful Assertions with AssertJ

AssertJ vs. JUnit assertions

Why should you care about AssertJ if JUnit comes with assertions out of the box? Actually, JUnit assertions are quite limited to a few basic scenarios and -in my opinion- lead to confusion when reading them. For example, I’ve been coding years with the JUnit API for assertions and I still mix up the actual with the expected value…

AssertJ, apart from being fully compatible with JUnit, brings many benefits to your unit tests. Two important ones:

  • AssertJ comes with a lot of possibilities to execute assertions: elements in lists in any order, equality excluding specific fields, support for Optional, extraction of values, custom representations, etc. This makes your life easier since you don't have to preprocess the results to prepare them for the assertions; the assertion methods take care of that for you.
  • Assertions are much more readable (being this a bit subjective). You can judge yourself but, to me, there is a big difference in readability between these two versions:
    • assertEquals("Check that City ID is set when stored", inputCity.getId(), actualCity.getId()) (JUnit version, either you use meaningful variable names or it's difficult to remember actual and expected)
    • assertThat(actualCity.getId()).as("Check that City ID is set when stored").isEqualTo(inputCity.getId()) (AssertJ - Much more natural, fluent API). Note that, in this post, we'll use then, the BDD alias of assertThat.

AssertJ in practice

We’ll complete the missing part in this guide (the Then in Given-When-Then) with AssertJ assertions. You already saw that in our first test method createCity(), where we use two of them:

// Then
final City expectedCity = new City(null, MALAGA, MALAGA_POPULATION);
then(actualCity.getId())
  .as("Check that City ID is set when stored.")
  .isNotNull();
then(actualCity)
  .as("Check that City name is correct and city population is filled in.")
  .isEqualToIgnoringGivenFields(expectedCity, "id");

Basics

Vanilla AssertJ uses the method Assertions.assertThat(actual) (abbreviated as assertThat(actual) to create the assertion type using the static import). But AssertJ also has a BDD-friendly alias (like BDDMockito has): BDDAssertions.then(actual). Again, they behave exactly the same way and this is just a useful shortcut that we’ll use to make even more visible the distinction between the Given-When-Then parts.

Chaining and Assertion Descriptions

You can chain several assertions with some other useful methods in a fluent way after then(). In my case, I like using as(description) to provide a better error message in case the assertion fails. It adds even more readability to your tests as well, since they are at the same time your test documentation. You can also add the description in terms of "City ID should not be null when stored" if you prefer (actually, I prefer that as you’ll see in further examples).

Many useful assertions

The assertion isEqualToIgnoringGivenFields is very convenient for cases like the one shown above. You have a plain java object that your service layer enriches and you want to verify that the process worked fine, but you better avoid comparing field by field or creating yet another object just to use it as a reference for comparison. This method will ignore the passed field when checking for equality (therefore, AssertJ will ignore your equals() implementation).

In the next section, I’ll explain some other common, useful assertions coming with AssertJ that you can use in your Unit Tests depending on your scenario.

Overview of some useful AssertJ assertions

One of the best things about AssertJ is its documentation: full of practical examples. I check it everytime I’m wondering if there is an assertion for a scenario and almost always find it there.

What I’ll do in this article is putting some of them in practice in our sample code so you see a closer-to-real application of them.

Completing the example

Remember our requirements? We still need to write a couple of tests more:

  • Check that an exception is thrown when the input contains an ID (validation test).
  • Check that the list of cities only contains those with population.

Below you find the full version of the test, and then the description of the main features used.

package com.thepracticaldeveloper.population;

import org.assertj.core.api.Assertions;
import org.assertj.core.api.SoftAssertions;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;

import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.BDDAssertions.then;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;

@RunWith(MockitoJUnitRunner.class)
public class CityServiceTest {

  @Mock
  private CityRepository cityRepository;

  @Mock
  private PopulationService populationService;

  private CityService cityService;
  private static final String MALAGA = "Malaga";
  private static final String AMSTERDAM = "Amsterdam";
  private static final String BARCELONA = "Barcelona";
  private static final int MALAGA_POPULATION = 569_000;
  private static final int BARCELONA_POPULATION = 1_609_000;

  @Before
  public void setup() {
    cityService = new CityService(cityRepository, populationService);
    Assertions.useRepresentation(new CityRepresentation());
  }

  @Test
  public void createCity() {
    // Given
    final City inputCity = new City(null, MALAGA, null);
    given(populationService.forCity(MALAGA))
      .willReturn(Optional.of(MALAGA_POPULATION));
    given(cityRepository.save(any(City.class)))
      .willAnswer(answer -> ((City) answer.getArgument(0)).copyWithId(randomLong()));

    // When
    final City actualCity = cityService.enrichAndCreateCity(inputCity);

    // Then
    final City expectedCity = new City(null, MALAGA, MALAGA_POPULATION);
    then(actualCity.getId())
      .as("Check that City ID is set when stored.")
      .isNotNull();
    then(actualCity)
      .as("Check that City name is correct and city population is filled in.")
      .isEqualToIgnoringGivenFields(expectedCity, "id");
  }

  @Test
  public void createCityWithIdThrowsException() {
    // Given
    final City inputCity = new City(1L, MALAGA, null);

    // When
    final Throwable throwable = catchThrowable(() -> cityService.enrichAndCreateCity(inputCity));

    // Then
    then(throwable).as("An IAE should be thrown if a city with ID is passed")
      .isInstanceOf(IllegalArgumentException.class)
      .as("Check that message contains the city name")
      .hasMessageContaining(inputCity.getName());
  }

  @Test
  public void getCities() {
    // Given
    final City malaga = new City(1L, MALAGA, MALAGA_POPULATION);
    // let's say the service did not work for Amsterdam so it's stored without population...
    final City amsterdam = new City(2L, AMSTERDAM, null);
    final City barcelona = new City(3L, BARCELONA, BARCELONA_POPULATION);
    given(cityRepository.getAllCities()).willReturn(List.of(malaga, amsterdam, barcelona));

    // When
    final List<City> cities = cityService.getAllValidCities();

    // Then
    SoftAssertions.assertSoftly(softly -> {
      softly.assertThat(cities)
        .as("Should contain only two cities")
        .hasSize(2);
      softly.assertThat(cities).extracting("population")
        .as("Should not contain null populations")
        .doesNotContainNull();
      softly.assertThat(cities).extracting("name")
        .as("Should contain names in alphabetical order")
        .containsSequence(BARCELONA, MALAGA);
    });
  }

  private long randomLong() {
    return ThreadLocalRandom.current().nextLong(1000L);
  }
}

Flexible equality assertions

We already saw how isEqualToIgnoringFields can save us some time and code lines, but you also have some other alternatives for equality comparison:

  • isEqualToComparingFieldByField is very useful when you deal with objects for which you're not in control of (or simply don't want to add) theequals() implementation. Two different objects with equal field values will result in being equal with this method, while they would be asserted as different when using the standard isEqualTo assertion. There is also a recursive version of this one, isEqualToComparingFieldByFieldRecursively.
  • You can also decide to ignore fields from comparison with isEqualToIgnoringNullFields. However, I don't use this one too much since it's risky: a field may be null for an erroneous reason and it's better not to ignore that silently.

AssertJ Exception Assertions

In our second test method above, createCityWithIdWillThrowException we want to verify that an Exception is thrown. AssertJ has a smart alternative to deal with this by leveraging functions in our code. What we use is a function and then pass that function as an argument to the method callThrowable(), that will call it and capture the exception for us.

// When
final Throwable throwable = catchThrowable(() -> cityService.enrichAndCreateCity(inputCity));

// Then
then(throwable).as("An IAE should be thrown if a city with ID is passed")
  .isInstanceOf(IllegalArgumentException.class)
  .as("Check that message contains the city name")
  .hasMessageContaining(inputCity.getName());

Easy handling of Soft Assertions

When an assertion fails, the test case is interrupted and this one is the only one you’ll see in the output. Sometimes it’s useful to let the test continue and verify some other results since there might be a few things failing and you want to fix all of them together, avoiding a fix-one, next-error, fix-one situation.

This is achieved in an easy way with SoftAssertions in AssertJ, and you saw it in the test to check the list of cities:

// Then
SoftAssertions.assertSoftly(softly -> {
  softly.assertThat(cities)
    .as("Should contain only two cities")
    .hasSize(2);
  softly.assertThat(cities).extracting("population")
    .as("Should not contain null populations")
    .doesNotContainNull();
  softly.assertThat(cities).extracting("name")
    .as("Should contain names in alphabetical order")
    .containsSequence(BARCELONA, MALAGA);
});

I prefer this functional-style amongst the multiple options to use SoftAssertions: you don’t need to call softly.assertAll() and the block is nicely delimited by the function. There is even a BDDSoftAssertions that you can use, but it’s stateful so it doesn’t allow this way of coding.

If you want to see how the soft assertions work, comment the line in CityService that is taking care of null population filtering and run the test (as indicated in the code comment). You’ll get this error:

org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [Should contain only two cities] 
Expected size:<2> but was:<3> in:
<[{id:2, name:Amsterdam, population:null},
    {id:3, name:Barcelona, population:1609000},
    {id:1, name:Malaga, population:569000}]>
at CityServiceTest.lambda$getCities$2(CityServiceTest.java:96)
2) [Should not contain null populations] 
Expecting:
 <[null, 1609000, 569000]>
not to contain <null> elements
at CityServiceTest.lambda$getCities$2(CityServiceTest.java:99)

As you see, you get a detailed message for each assertion and what is exactly the error.

Custom Object Representations

Did you notice that the City class doesn’t have a toString() method? That’s annoying. You could simply add it but imagine that the class is inside an external library and you can’t access the code. Without any additional code, instead of the error shown above, you would get this one:

org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [Should contain only two cities] 
Expected size:<2> but was:<3> in:
<[[email protected],
    [email protected],
    [email protected]]>
at CityServiceTest.lambda$getCities$2(CityServiceTest.java:96)
2) [Should not contain null populations] 
Expecting:
 <[null, 1609000, 569000]>
not to contain <null> elements
at CityServiceTest.lambda$getCities$2(CityServiceTest.java:99)

Not human-friendly at all. How are you supposed to know which city is [email protected]? You better use a custom representation. AssertJ allows you to do that in a simple manner by extending StandardRepresentation and providing your own string representation for the objects you want by overriding the method fallbackToStringOf(object). So you can code this class to provide a JSON representation of City (why not?):

package com.thepracticaldeveloper.population;

import org.assertj.core.presentation.StandardRepresentation;

public class CityRepresentation extends StandardRepresentation {

  @Override
  protected String fallbackToStringOf(Object object) {
    if (object instanceof City) {
      final City city = (City) object;
      return "{id:" + city.getId() + ", name:" + city.getName() + ", population:" + city.getPopulation() + "}";
    }
    return super.fallbackToStringOf(object);
  }
}
Get the book Practical Software Architecture

Then, all you need is to tell AssertJ to use this new representation, which we included in the previous code inside the setup() method:

@Before
public void setup() {
  cityService = new CityService(cityRepository, populationService);
  Assertions.useRepresentation(new CityRepresentation());
}

Wrapping it up

I hope you enjoyed this guide and learned something new. As you saw, just with some basic code conventions you can improve a lot the way you write your Unit Tests. The adoption of BDD, even at a minor scale, can be an eye-opener for the rest of people, who can quickly find its benefits and maybe drive the change to a full-BDD environment. Also, we covered how AssertJ assertions can help you write more readable verification code blocks and learned some common usages.

Remember:

  • All the source code is available on GitHub.
  • If you want to know more about BDD using Cucumber and Gherkin, check this other blog guide.
  • You can also get my book to learn more TDD practices from scratch, in an incremental way, or
  • You can say thanks and contribute to the site by buying me a coffee.
Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Do you need help?

Amsterdam, The Netherlands https://thepracticaldeveloper.com