· 5 min read

Writing Cleaner Test with Domain-Specific Assertions

Domain-specific assertions can help create an extremely clean test base. Learn how to create your own manually or with LLM.

Domain-specific assertions can help create an extremely clean test base. Learn how to create your own manually or with LLM.

Tests need to be as clean as the code we write. A clean test is faster to write, easier to understand, debug, maintainable, and extensible.

Personally, I see more care applied to code than tests. When I write new tests in a Test file that lacks specific encapsulations, I mostly spend 3-4 times more time writing tests and still being under-confident about the tests.

Like clean code, writing clean tests requires an initial investment in setting up the test infrastructure. Some of these techniques I have talked about before:

Apart from these, there are some patterns I have implemented which I’ll talk about in the upcoming blog posts:

  • Custom Mocks and Custom Bean Decorators for Tests (⛓️‍💥 TODO)
  • Encapsulating WireMock / Mock Server (⛓️‍💥 TODO)
  • Easy Random for Quick Data Generator (⛓️‍💥 TODO)
  • Custom Extensions for JUnit (⛓️‍💥 TODO)
  • Testing Red Flag: Too many acts. Too Many Assertions. (⛓️‍💥 TODO)

This blog post will talk about Domain Specific Assertions which will extend the blogpost on Fluent Assertions I wrote a while back.

blog DomainSpecifAssertions extends FluentAssertions 🙃

Introducing Domain Specific Assertions

When using AssertJ or any other testing library we typically get some value from expected and get some value from actual and try to match them.

Example:

// 64 characters
assertThat(movie.getName()).isEqualTo(expectedMovie.getName());

And we may have some assertions which are repeated often.

For example, suppose we always assert the actor names something like this.

var actors = movie.getActor().stream().map(a -> a.getName()).toList();
assertThat(actors).contains(”Tom Cruize”);
// 113 characters

Typically we create a helper method to help us assert. However, the typical way of making a helper method makes it complicated.

Luckily, AssertJ provides an AbstractAssert class that can be extended to create Domain Specific Assertions that can look something like this:

assertThat(movie)
 .hasName(expectedMovie.getName())
 .hasActorWithName(”Tom Cruize”);
// 86 characters 

Implementation

Use this implementation or use the LLM prompt I have provided in the next section.

Step 1. Create a class that extends AbstractAssert

class MovieAssert extends AbstractAssert<MovieAssert, Movie> {
   public MovieAssert(Movie actual){
    super(actual, MovieAssert.class);
 }

 public MovieAssert hasName(String expectedName){
  if(!expectedName.equals(actual.getName())){
   failWithMessage("Expected movie anme to be %s but was %s", name, actual.getName());
 }
  return this;
 }

 public MovieAssert hasActorWithName(String actorName){
  var actors = movie.getActor().stream().map(a -> a.getName()).toList();
  if(!actors.contains(actorName)){
   failWithMessage("Expected movie anme to be %s but was %s", name, actual.getName());
 }
 }

 ... more methods here...

 The more complex your assertions are, the better it is to encapsulate them within a method here.
}
Note: It's the same amount of characters
  • Even though it feels like we are writing more code, it's the same amount of code we would have written with a typical assertion helper method.
  • The beauty of AssertJ extension is how clean and domain-specific assertions become.

Step 2. Create a base class and add an assertThat method

class BaseAssertions extends Assertions {
 
 public MovieAssert assertThat(Movie movie){
  return new MovieAssert(movie)
 }

 ... add other Domain Specific Assertions here ...
}

Step 3. Extend base class

class MovieTest extends BaseAssertions{

 @Test
 void addActor_shouldAddActorToMovie(){
  // given
  Movie movie = new TestMovieBuilder().build();
  
  // when
  movie.addActor("NTR Jr");
  
  // then
  assertThat(movie)
 .hasActorWithName("NTR Jr")
 ... other methods ...
 }
}
Something to think about: How you'd do this in other languages
  • I think such a thing is also easy to create in typescript.
  • You just need to extend check the type and return the right value.

Step 4. Optional: Create a custom base class

You can create a base class out of common patterns of assertions. Assume the following method in a base class.

<T> void assertObjectEquals(String fieldName, T actual, T expected){
  boolean isEqual = Objects.equals(actual, expected);
  if(!isEqual){
       failWithMessage("Expected %s to be %s but was %s", fieldName, expected, actual);
 }
}

This way most of your Domain Specific Assertion class will just delegate to their methods in your parent class.

Here are some that I think can be added:

  • booleanEquals(String fieldName, Boolean object, Boolean object)
  • isNull(String fieldName, T object)
  • notNull(String fieldName, T object)
  • listContains(String fieldName, Collection<T> collection, T object)

What to generate Domain Specific Assertions

You can generate these domain-specific assertions for all things you assert which typically include:

  • entities
  • response classes
  • value objects

Autogenerating Domain-Specific Assertions

There are two ways to auto-generate these domain-specific assertions.

  1. Using LLMs like GitHub Copilot
  2. Using AssertJ Assertions generator
  • currently does not work with Java 17. They plan to launch a newer version soon.

For generating with LLM you can use the following prompt. Add more details to it as needed.

## Task

Generate Domain Specific Assertions

## Details: 

Generate a domain-specific assertion for this class for AssertJ. Extend AbstractAssert.

For each field in the source ensure these methods exist in the assertion class.

has{fieldName}() - if this method is present
notHas{fieldname} - fieldName not present
has{fieldName}(value) -> compare value

For fields that are other tables add another method that takes UUID.

has{fieldName}(UUID id) then compare by field.getId() with the id passed

Make sure to use String formatting.

Make use of the following base interface.

```java
interface DomainSpecificAssertionHelper {
  default <T> void assertObjectEquals(String fieldName, T actual, T expected){
    boolean isEqual = Objects.equals(actual, expected);
    if(!isEqual){
         failWithMessage("Expected %s to be %s but was %s", fieldName, expected, actual);
 }
 }
}

This can help you create a skeleton. But you’ll need to add the custom methods if your assertions are complex.

Recap

  • Writing a clean test is as important as writing clean code.
  • We can write cleaner assertions by creating our own domain-specific assertion
  • To create domain-specific assertion for AssertJ
  1. Create a class that extends AbstractAssert
  2. Create a base class with the assertThat method which will create our Domain Specific assertion class
  3. Extend your test class with the base class and use
  • Instead of writing the code yourself, use an LLM to generate the code for you.

Look out for more blog posts coming in the future.

Resources

I stand on the shoulders of experts.

🙋‍♂️ Question: How do you ensure your tests are clean?


Back to Blog