Do you want to read better blog posts? If so, help me to improve my writing style!

Turning Assertions Into a Domain-Specific Language

Business definition in a dictionary

Assertions are an essential part of our unit tests. And yet, it so easy to neglect them.

That is a shame because if we overlook the importance of assertions, the assert section of our tests becomes long and messy. Sadly, most tests which I have seen (and written) suffer from this problem.

This blog post describes how we can get rid of messy assertions. We learn to write assertions by using the language understood by domain experts.

What Is Tested?

Let’s start by taking a quick look at the tested class.

The Person class is a class which contains the information of a single person. It has four fields (id, email, firstName, and lastName), and we can create new Person objects by using the builder pattern.

The source code of the Person class looks as follows:

public class Person {

    private Long id;

    private String email;

    private String firstName;

    private String lastName;

    private Person() {

    }

    public static PersonBuilder getBuilder(String firstName, String lastName) {
        return new PersonBuilder(firstName, lastName);
    }

	//Getters are omitted for the sake of clarity
	
    public static class PersonBuilder {

        Person build;

        private PersonBuilder(String firstName, String lastName) {
            build = new Person();
            build.firstName = firstName;
            build.lastName = lastName;
        }

        public PersonBuilder email(String email) {
            build.email = email;
            return this;
        }

        public PersonBuilder id(Long id) {
            build.id = id;
            return this;
        }

        public Person build() {
            return build;
        }
    }
}

Why Bother?

In order to understand what is wrong with using the standard JUnit assertions, we have to analyze a unit test which uses them. We can write a unit test which ensures that the construction of new Person objects is working properly by following these steps:

  1. Create a new Person object by using the builder class.
  2. Write assertions by using the assertEquals() method of the Assert class.

The source code of our unit test looks as follows:

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class PersonTest {

    @Test
    public void build_JUnitAssertions() {
        Person person = Person.getBuilder("Foo", "Bar")
                .email("foo.bar@email.com")
                .id(1L)
                .build();

        assertEquals(1L, person.getId().longValue());
        assertEquals("Foo", person.getFirstName());
        assertEquals("Bar", person.getLastName());
        assertEquals("foo.bar@email.com", person.getEmail());
    }
}

This unit test is short and pretty clean but the standard JUnit assertions have two big problems:

  • When the number of assertions grow, so does the length of the test method. This might seem obvious but a large assert section makes the test harder to understand. It becomes hard to understand what we want to achieve with this test.
  • The standard JUnit assertions speak the wrong language. The standard JUnit assertions speak the “technical” language. This means that the domain experts cannot understand our tests (and neither can we).

We can do better. A lot better.

AssertJ to the Rescue

AssertJ is a library which allows us to write fluent assertions in our tests. We can create a simple assertion with AssertJ 1.6.0 by following these steps:

  1. Call the static assertThat() method of the Assertions class, and pass the actual value as a method parameter. This method returns an assertion object. An assertion object is an instance of a class which extends the AbstractAssert class.
  2. Specify the assertion by using the methods of the assertion object. The methods which are are available to us depends from the type of the returned object (the assertThat() method of the Assertions class is an overloaded method, and the type of the returned object depends from the type of the method parameter).

When we rewrite our unit test by using AssertJ, its source code looks as follows:

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class PersonTest {

	@Test
	public void build_AssertJ()  {
		Person person = Person.getBuilder("Foo", "Bar")
			.email("foo.bar@email.com")
			.id(1L)
			.build();

		assertThat(person.getId()).isEqualTo(1L);
		assertThat(person.getFirstName()).isEqualTo("Foor");
		assertThat(person.getLastName()).isEqualTo("Bar");
		assertThat(person.getEmail()).isEqualTo("foo.bar@email.com");
	}
}

This is a bit more readable than the test which uses the standard JUnit assertions. And yet, it suffers from the same problems.

Another problem is that the default message which is shown if the assertion fails is not very readable. For example, if the first name of the user is ‘Bar’, the following message is shown:

expected:<"[Foo]"> but was:<"[Bar]">

We can fix this by adding custom messages to our assertions. Let’s see how this is done.

Specifying Custom Messages for AssertJ Assertions

We can write an assertion which has a custom error message by following these steps:

  1. Call the static assertThat() method of the Assertions class and pass the actual value as a method parameter. This method returns an assertion object. An assertion object is an instance of class which extends the AbstractAssert class.
  2. Call the overridingErrorMessage() method of the AbstractAssert class, and pass the error message “template” and its arguments as method parameters.
  3. Specify the assertion by using the methods of the assertion object. The methods which are are available to us depends from the type of the returned object (the assertThat() method of the Assertions class is an overloaded method, and the type of the returned object depends from the type of the method parameter).

The source code of our unit test looks as follows:

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class PersonTest {

	@Test
	public void build_AssertJ_CustomMessages() {
		Person person = Person.getBuilder("Foo", "Bar")
				.email("foo.bar@email.com")
				.id(1L)
				.build();

		assertThat(person.getId())
				.overridingErrorMessage("Expected id to be <%d> but was <%d>",
						1L,
						person.getId()
				)
				.isEqualTo(1L);

		assertThat(person.getFirstName())
				.overridingErrorMessage("Expected firstName to be <%s> but was <%s>",
						"Foo",
						person.getFirstName()
				)
				.isEqualTo("Foo");

		assertThat(person.getLastName())
				.overridingErrorMessage("Expected lastName to be <%s> but was <%s>",
						"Bar",
						person.getLastName()
				)
				.isEqualTo("Bar");

		assertThat(person.getEmail())
				.overridingErrorMessage(
						"Expected email to be <%s> but was <%s>",
						"foo.bar@email.com",
						person.getEmail()
				)
				.isEqualTo("foo.bar@email.com");
	}
}

If the first name of the user is ‘Bar’, the following message is shown:

Expected firstName to be <Foo> but was <Bar>

We fixed one problem but our fix caused another problem:

The test method looks messy and it is harder to read than our earlier attempts.

However, all hope is not lost. Let’s find out how we can create a domain specific language by using the information we have learned during this blog post.

Creating a Domain-Specific Language

Wikipedia defines the term domain-specific language as follows:

A domain-specific language (DSL) is a computer language specialized to a particular application domain.

When we follow this definition, we get the following requirements for our domain specific language:

  1. It must speak the language understood by domain experts. For example, a person’s first name is not equal to ‘Foo’. A person has a first name ‘Foo’.
  2. The assertions must have custom error messages which use the domain-specific language.
  3. It must have a fluent API. In other words, it must be possible to chain assertions.
If you want to get more information about implementing domain-specific language with Java, read articles titled An Approach to Internal Domain-Specific Languages in Java and The Java Fluent API Designer Crash Course .

We can create a domain-specific language for our unit tests by creating a custom assertion. We can do this by following these steps:

  1. Create a PersonAssert class.
  2. Extend the AbstractAssert class and provide the following type parameters:
    1. The type of the custom assertion class. Set the value of this type parameter to PersonAssert.
    2. The type of the actual value. Set the value of this type parameter to Person.
  3. Create a constructor which takes a Person object as a constructor argument. Implement this constructor by calling the constructor of the AbstractAssert class and passing the following objects as constructor arguments:
    1. The actual value. Pass the Person object given as a constructor argument forward to the constructor of the superclass.
    2. The class of the custom assertion. Set the value of this constructor argument to PersonAssert.class.
  4. Add an assertThatPerson() method to the created class. This method takes a Person object as a method parameter and returns a PersonAssert object. Implement this method by following these steps:
    1. Create a new PersonAssert object and pass the Person object as a constructor argument.
    2. Return the created PersonAssert object.
  5. Create the methods which are used to write assertions against the actual Person object. We need to create assertion methods for email, firstName, id, and lastName fields. We can implement each method by following these steps:
    1. Ensure that the actual Person object is not null by calling the isNotNull() method of the AbstractAssert class.
    2. Ensure that the value of the Person object’s field is equal to the expected value. We can do this by following these steps:
      1. Call the assertThat() method of the Assertions class and provide the actual field value as a method parameter.
      2. Override the default error message by calling the overridingErrorMessage() method of the AbstractAssert class. Pass the custom error message template and its arguments as method parameters.
      3. Ensure that the the actual property value is equal to expected value. We can do this by calling the isEqualTo() method of the AbstractAssert class and passing the expected value as a method argument.
    3. Return a reference to the PersonAssert object. This ensures that we can chain assertions in our unit tests.

The source code of the PersonAssert class looks as follows:

import org.assertj.core.api.AbstractAssert;

import static org.assertj.core.api.Assertions.assertThat;

public class PersonAssert extends AbstractAssert<PersonAssert, Person> {

    private PersonAssert(Person actual) {
        super(actual, PersonAssert.class);
    }

    public static PersonAssert assertThatPerson(Person actual) {
        return new PersonAssert(actual);
    }

    public PersonAssert hasEmail(String email) {
        isNotNull();

        assertThat(actual.getEmail())
                .overridingErrorMessage("Expected email to be <%s> but was <%s>",
                        email,
                        actual.getEmail()
                )
                .isEqualTo(email);

        return this;
    }

    public PersonAssert hasFirstName(String firstName) {
        isNotNull();

        assertThat(actual.getFirstName())
                .overridingErrorMessage("Expected first name to be <%s> but was <%s>",
                        firstName,
                        actual.getFirstName()
                )
                .isEqualTo(firstName);

        return this;
    }

    public PersonAssert hasId(Long id) {
        isNotNull();

        assertThat(actual.getId())
                .overridingErrorMessage( "Expected id to be <%d> but was <%d>",
                        id,
                        actual.getId()
                )
                .isEqualTo(id);

        return this;
    }

    public PersonAssert hasLastName(String lastName) {
        isNotNull();

        assertThat(actual.getLastName())
                .overridingErrorMessage( "Expected last name to be <%s> but was <%s>",
                        lastName,
                        actual.getLastName()
                )
                .isEqualTo(lastName);

        return this;
    }
}

We can now rewrite our unit test by using the PersonAssert class. The source of our unit test looks as follows:

import org.junit.Test;

import static net.petrikainulainen.junit.dsl.PersonAssert.assertThatPerson;

public class PersonTest {

	@Test
	public void build_AssertJ_DSL() {
		Person person = Person.getBuilder("Foo", "Bar")
				.email("foo.bar@email.com")
				.id(1L)
				.build();

		assertThatPerson(person)
				.hasId(1L)
				.hasFirstName("Foo")
				.hasLastName("Bar")
				.hasEmail("foo.bar@email.com");
	}
}

Why Does This Matter?

We have now turned messy assertions into a domain-specific language. This made our test a lot more readable but the changes we made are not entirely cosmetic.

Our solution has three major benefits:

  • We moved the actual assertion logic from the test method to the PersonAssert class. If the API of the Person class changes, we have to only make changes to the PersonAssert class. We just made our test less brittle and easier to maintain.
  • Because our assertion uses a language understood by domain experts, our tests become an important part of our documentation. Our tests define exactly how our application should behave in a specific situation. They are executable specifications which are always up-to-date.
  • Custom error messages and the more readable API ensure that we don’t have to waste time for trying to figure out why our test failed. We know immediately why it failed.

Implementing domain-specific languages takes some extra work but as we saw, it is worth the effort. A good unit test is both readable and easy to maintain, but a great unit test also describes the reason for its existence.

Turning assertions into a domain-specific language takes us one step closer to that goal.

AssertJ provides an assertions generator which helps you to generate custom assertions for your classes.

The example application of this blog post is available at Github.

About the Author

Petri Kainulainen 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.

About Petri Kainulainen →

4 comments… add one

  • Hi Petri,

    I wonder why having the
    public static PersonBuilder getBuilder(String firstName, String lastName)
    method ?

    Is it just for not having the
    private PersonBuilder(String firstName, String lastName)
    public ?

    Or is it so that the code is more explicit and readable ?

    Again, thanks for the cool read !

    Kind Regards,

    Stephane Eybert

    Reply
    • Hi Stephane,

      Thank you for your comment! About your question:

      I think that it is just a matter of preference. I cannot think any technical reason why the constructor of the builder class should be private. I just happen to think that Person.getBuilder("John", "Smith") looks better than new Person.PersonBuilder("John", "Smith").

      Reply
      • Indeed, it does :-)

        Reply
  • Hello Petri,

    Say we have a list of persons, with an assertion like:
    Page admins = adminRepository.findByFirstnameStartingWithOrLastnameStartingWith(“spirou”, new PageRequest(0, 10));
    assertEquals(10, admins.getContent().size());

    Should we need another class dedicated to the list of persons ? Like, an AdminsAssert

    Kind Regards,

    Stephane Eybert

    Reply

Leave a Comment