Can you add a custom message to AssertJ assertThat?
Asked Answered
A

4

150

We have a test suite that primarily uses JUnit assertions with Hamcrest matchers. One of our team started experimenting with AssertJ and impressed people with its syntax, flexibility and declarative-ness. There is one feature that JUnit provides that I can't find an equivalent for in AssertJ: adding a custom assert failure message.

We're often comparing objects that are not made for human readability and will have random-seeming Ids or UUIDs and it's impossible to tell what they're supposed to be by the data they contain. This is an unavoidable situation for our codebase, sadly, as part of the purpose it fulfills is mapping data between other services without necessarily understanding what it is.

In JUnit, the assertThat method provides a version with a String reason parameter before the Matcher<T> param. This makes it trivial to add a short debug string shedding some light on the problem, like what the comparison should mean to a human.

AssertJ, on the other hand, provides a jillion different genericized static assertThat methods which return some form of interface Assert or one of its many implementing classes. This interface does not provide a standard way of setting a custom message to be included with failures.

Is there any way to get this functionality from the AssertJ API or one of its extensions without having to create a custom assert class for every assert type we want to add messages to?

Alberik answered 11/3, 2015 at 18:14 Comment(1)
FYI this is documented in the new AssertJ website, see assertj.github.io/doc/#assertj-core-assertion-description.Peridot
A
188

And in classic fashion, I found what I was looking for moments after posting the question. Hopefully this will make it easier for the next person to find without first having to know what it's called. The magic method is the deceptively short-named as, which is part of another interface that AbstractAssert implements: Descriptable, not the base Assert interface.

public S as(String description, Object... args)

Sets the description of this object supporting String.format(String, Object...) syntax.
Example :

try {
  // set a bad age to Mr Frodo which is really 33 years old.
  frodo.setAge(50);
  // you can specify a test description with as() method or describedAs(), it supports String format args
  assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);
} catch (AssertionError e) {
  assertThat(e).hasMessage("[check Frodo's age] expected:<[33]> but was:<[50]>");
}

Where that quoted string in the catch block hasMessage is what appears in your unit test output log if the assertion fails.


I found this by noticing the failWithMessage helper in the custom assert page linked in the question. The JavaDoc for that method points out that it is protected, so it can't be used by callers to set a custom message. It does however mention the as helper:

Moreover, this method honors any description set with as(String, Object...) or overridden error message defined by the user with overridingErrorMessage(String, Object...).

... and the overridingErrorMessage helper, which completely replaces the standard AssertJ expected: ... but was:... message with the new string provided.

The AssertJ homepage doesn't mention either helper until the features highlights page, which shows examples of the as helper in the Soft Assertions section, but doesn't directly describe what it does.

Alberik answered 11/3, 2015 at 18:41 Comment(0)
R
60

To add another option to Patrick M's answer:

Instead of using Descriptable.as, you can also use AbstractAssert.withFailMessage():

try {
  // set a bad age to Mr Frodo which is really 33 years old.
  frodo.setAge(50);
  // you can specify a test description via withFailMessage(), supports String format args
  assertThat(frodo.getAge()).
    withFailMessage("Frodo's age is wrong: %s years, difference %s years",
      frodo.getAge(), frodo.getAge()-33).
    isEqualTo(33);
} catch (AssertionError e) {
  assertThat(e).hasMessage("Frodo's age is wrong: 50 years, difference 17 years");
}

The difference to using Descriptable.as is that it gives you complete control over the custom message - there is no "expected" and "but was".

This is useful where the actual values being tested are not useful for presentation - this method allows you to show other, possibly calculated values instead, or none at all.


Important Note that, just like Descriptable.as, you must call withFailMessage() before any actual assertions - otherwise it will not work, as the assertion will fire first. This is noted in the Javadoc.

Robledo answered 1/2, 2019 at 11:3 Comment(6)
"you must call withFailMessage() before any actual assertions" thanks, this tripped me. The order of calling withFailMessage matter; I like AssertJ, but this sucks.Pellegrini
How about custom success message?Poona
@Serob_b: What do you mean by "success message"? Assertions only report a message if they fail. Consider asking a new question where you explain in more detail what you are looking for.Robledo
@Robledo you can assert a failed test either in negative way "Result not found", or in a positive way: "Result as expected" + " failed". Messages built with .as() can potentially be used to build reports of passed/non-passed tests.Eada
@zakmck: Sorry, can't follow there. "Result as expected" + " failed" - what does that mean? If the result is as expected, the test is not failed, is it?Robledo
@Robledo you might want the message to be like: assertThat ( searchResult.size() ).as ( "The result size is as expected" ).is ( 100 ). Then, if it fails, you'll see something like "Test failed: The result size is as expected" (the final message is built by the framework). If it succeeds, you might see List of passed tests: "The result size is as expected", "...", "...", ... In other words, as() gives a way to state what you are asserting, while withFailedMessage() you're saying what fails.Eada
P
11

The two options mentioned so far are as and withFailMessage, so I won't go into the syntax or usage again. To see the difference between them, and how each can be useful, consider the use case where we are testing metrics being exported:

// map of all metrics, keyed by metrics name
Map<String, Double> invocations = ...

List.of(
    "grpc.client.requests.sent",
    "grpc.client.responses.received",
    "grpc.server.requests.received",
    "grpc.server.responses.sent"
).forEach { counter ->
    var meter = // create meter name using counter
    assertThat(invocations)
        .withFailMessage("Meter %s is not found", meter)
        .containsKey(meter)
    assertThat(invocations.get(meter))
        .as(meter)
        .isEqualTo(0.0)
}

I've used Java 11 syntax to reduce some boilerplate.

Without the withFailMessage, if the meter isn't present in the map, the default output contains a dump of all entries in the map, which clutters the test log. We don't care what other meters are present, only that the one we want is there.

Using withFailMessage, the output becomes:

java.lang.AssertionError: Meter blah is not found

As for as, it only appends the given message to the output, but retains the failed comparison, which is very useful. We get:

org.opentest4j.AssertionFailedError: [blah] 
Expecting:
 <1.0>
to be equal to:
 <0.0>
but was not.
Pellegrini answered 1/3, 2021 at 22:51 Comment(0)
Q
7

Use the inbuilt as() method in AssertJ. For example:

 assertThat(myTest).as("The test microservice is not active").isEqualTo("active");
Quinque answered 27/9, 2020 at 21:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.