Turning Assertions Into a Domain-Specific Language
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:
- Create a new Person object by using the builder class.
- 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.
FEST-Assert to the Rescue
FEST-Assert is a library which allows us to write fluent assertions in our tests. We can create a simple assertion with FEST-Assert 1.4 by following these steps:
- 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 GenericAssert class.
- 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 FEST-Assert, its source code looks as follows:
import org.junit.Test; import static org.fest.assertions.Assertions.assertThat; public class PersonTest { @Test public void build_FESTAssert() { Person person = Person.getBuilder("Foo", "Bar") .email("foo.bar@email.com") .id(1L) .build(); assertThat(person.getId()).isEqualTo(1L); assertThat(person.getFirstName()).isEqualTo("Foo"); 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 FEST-Assert Assertions
We can write an assertion which has a custom error message by following these steps:
- Create an error message by using the format() method of the String class.
- 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 GenericAssert class.
- Call the overridingErrorMessage() method of the GenericAssert class and pass the created error message as a method parameter.
- 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.fest.assertions.Assertions.assertThat; public class PersonTest { @Test public void build_FESTAssert_CustomMessages() { Person person = Person.getBuilder("Foo", "Bar") .email("foo.bar@email.com") .id(1L) .build(); String idMessage = String.format("Expected id to be <%d> but was <%d>", 1L, person.getId()); assertThat(person.getId()) .overridingErrorMessage(idMessage) .isEqualTo(1L); String firstNameMessage = String.format("Expected firstName to be <%s> but was <%s>", "Foo", person.getFirstName()); assertThat(person.getFirstName()) .overridingErrorMessage(firstNameMessage) .isEqualTo("Foo"); String lastNameMessage = String.format("Expected lastName to be <%s> but was <%s>", "Bar", person.getLastName()); assertThat(person.getLastName()) .overridingErrorMessage(lastNameMessage) .isEqualTo("Bar"); String emailMessage = String.format("Expected email to be <%s> but was <%s>", "foo.bar@email.com", person.getEmail()); assertThat(person.getEmail()) .overridingErrorMessage(emailMessage) .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 is unreadable! It is a lot worse than our earlier tests!
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:
- 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’.
- The assertions must have custom error messages which use the domain-specific language.
- It must have a fluent API. In other words, it must be possible to chain assertions.
Note: 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:
- Create a PersonAssert class.
- Extend the GenericAssert class and provide the following type parameters:
- The first type parameter specifies the type of the custom assertion class. Set the value of this type parameter to PersonAssert.
- The second type parameter specifies the type of the actual value. Set the value of this type parameter to Person.
- Create a constructor which takes a Person object as a constructor argument. Implement this constructor by calling the constructor of the GenericAssert class and passing the following objects as constructor arguments:
- The first constructor argument specifies the class of the custom assertion. Set the value of this constructor argument to PersonAssert.class.
- The second constructor argument is the actual value. Pass the Person object given as a constructor argument forward to the constructor of the superclass.
- Add an assertThat() 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:
- Create a new PersonAssert object and pass the Person object as a constructor argument.
- Return the created PersonAssert object.
- 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:
- Ensure that the actual Person object is not null by calling the isNotNull() method of the GenericAssert class.
- Create a custom error message by using the format() method of the String class.
- Ensure that the value of the Person object’s field is equal to the expected value. We can do this by following these steps:
- Call the assertThat() method of the Assertions class and provide the actual field value as a method parameter.
- Override the default error message by calling the overridingErrorMessage() method of the GenericAssert class. Pass the custom error message as a method argument.
- Ensure that the the actual property value is equal to expected value. We can do this by calling the isEqualTo() method of the GenericAssert class and passing the expected value as a method argument.
- 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:
package example; import org.fest.assertions.Assertions; import org.fest.assertions.GenericAssert; public class PersonAssert extends GenericAssert<PersonAssert, Person> { protected PersonAssert(Person actual) { super(PersonAssert.class, actual); } public static PersonAssert assertThat(Person actual) { return new PersonAssert(actual); } public PersonAssert hasEmail(String email) { isNotNull(); String errorMessage = String.format( "Expected email to be <%s> but was <%s>", email, actual.getEmail() ); Assertions.assertThat(actual.getEmail()) .overridingErrorMessage(errorMessage) .isEqualTo(email); return this; } public PersonAssert hasFirstName(String firstName) { isNotNull(); String errorMessage = String.format( "Expected first name to be <%s> but was <%s>", firstName, actual.getFirstName() ); Assertions.assertThat(actual.getFirstName()) .overridingErrorMessage(errorMessage) .isEqualTo(firstName); return this; } public PersonAssert hasId(Long id) { isNotNull(); String errorMessage = String.format( "Expected id to be <%d> but was <%d>", id, actual.getId() ); Assertions.assertThat(actual.getId()) .overridingErrorMessage(errorMessage) .isEqualTo(id); return this; } public PersonAssert hasLastName(String lastName) { isNotNull(); String errorMessage = String.format( "Expected last name to be <%s> but was <%s>", lastName, actual.getLastName() ); Assertions.assertThat(actual.getLastName()) .overridingErrorMessage(errorMessage) .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.assertThat; public class PersonTest { @Test public void build_FESTAssert_DSL() { Person person = Person.getBuilder("Foo", "Bar") .email("foo.bar@email.com") .id(1L) .build(); assertThat(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.
- P.S. The example application of this blog post is available at Github.
Hello,
nice article.
I would recommend to use assertJ https://github.com/joel-costigliola/assertj-core, a forked project of FEST assert. It provides many great features like a maven plugin to generate assert classes.
Just try it ;-).
I confirm that ! ;-) I was a Fest commiter for years and the initiator of the assertions generator at the time. I forked Fest to create Assertj because it was not possible to contribute to Fest for too much time (due to a major refactoring that never ended). The current version of the generator (1.1.0) is there : https://github.com/joel-costigliola/assertj-assertions-generator#latest-news And a new version is coming that will also generates an Assertions class which acts as an entry point to all generated assertions. As clearly mentioned in this article, the main reason I created the generator is to have domain… Read more »
Hi,
I had no idea about this project. It looks very interesting and I promise to take a closer look at it. The assertion generator looks like a good addition because writing those custom assertions manually takes a lot of time (one of the biggest objections which I have heard about this approach).
Hi,
I hadn’t heard about this project before but it looks very interesting. One of the biggest objections which I have heard about this approach is that writing custom assertions takes a lot of time. It seems that assertion generator could solve this issue. I will promise to take a closer look at this.
Nice article. I really like custom Assert.
Do you know if it exists some plugins (for eclispe, Netbeans, intellij …) to generate this kind of assert from beans?
Hi Pierre,
an eclipse exists but is not yet ready.
William Delanoue is working on a plugin for IntelliJ Idea
Regards,
Joel
Take a look at assertJ and its maven plugin