ChatGPT解决这个技术问题 Extra ChatGPT

Can you add a custom message to AssertJ assertThat?

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?

FYI this is documented in the new AssertJ website, see assertj.github.io/doc/#assertj-core-assertion-description.

S
Stefan Birkner

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.


s
sleske

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.

Do 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.


"you must call withFailMessage() before any actual assertions" thanks, this tripped me. The order of calling withFailMessage matter; I like AssertJ, but this sucks.
T
Testilla

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

 assertThat(myTest).as("The test microservice is not active").isEqualTo("active");

A
Abhijit Sarkar

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.