In Test-Driven Development by Example, Kent Beck introduced "two simple rules". The first of those rules
Write a failing automated test before you write any code
(Emphasis added.)
The construction of the exception, and its message, certainly counts as code, so if you accept that this rule applies, there should definitely be a matching test.
Michael Feathers, in Working Effectively With Legacy Code, writes that a good unit test has these qualities
- They run fast
- They help us localize problems
The test you describe here isn't obviously a problem for speed (it's not obviously more expensive than payForCart
) -- we're really just looking at different in memory representations of the same information.
"Help us localize problems" is kind of interesting. There are actually a lot of different decisions being evaluated here, which might indicate that this design needs more modules (see Parnas 1971).
In other words, we could have a test that checks that the correct exception is thrown
assertThatThrownBy(() -> shoppingCartService.payForCart(command))
.isEqualTo(
PaymentException.forItem(
item
)
);
And then elsewhere write other tests to ensure that PaymentException::forId has the right behavior.
Of course, carving things apart this way means more investment, both in the tests and in the design. And you do want to ensure that effort invested is balanced by business value returned.
What's important in the message is that it clearly communicates what the problem is. But there must be hundreds of ways of constructing such a message, and no testing library can tell us if a plain text human language String is "clear" or "useful"
Ah - you may want to do some reading of Michael Bolton and James Back on Testing vs Checking. The automated thing we have here is a check; it's an automated confirmation that the behavior today matches the behavior documented by the programmer.
The test is when the human being (be it the programmer, or the reviewer) looks at the message to decide whether it is clear or useful.
If that's what you want to measure against, we'd probably write the test somewhat differently
assertThatThrownBy(() -> shoppingCartService.payForCart(command))
.isInstanceOfSatisfying(
PaymentException.class,
exception ->
assertThat(
exception.getMessage()
)
.isEqualTo(
"Cannot pay for ids [12345], status is not WAITING"
)
);
In this case, we deliberately add duplication to the test to make the intended behavior more explicit, and therefore easier to evaluate in review.
Another way of expressing the same idea; human beings are flexible pattern matching machines, but machines aren't. Therefore, we need to be designing our interfaces with those two different constraints in mind. That suggests a somewhat different test design (since tests are not flexible pattern matching machines).
assertThatThrownBy(() -> shoppingCartService.payForCart(command))
.isInstanceOfSatisfying(
PaymentException.class,
exception ->
assertThat(
PaymentException.cast(exception).ids()
)
.isEqualTo(
List.of(12345)
)
);
And then implement the check of the human readable message elsewhere.
I don't particularly consider "duplicating your messages file in the tests" to be a down side, in so far as the messages file itself is an implementation details that we might want to change without breaking the observable behavior. That's especially so when the messages file is going to be a bunch of templates for the machines to use, meaning that we are sacrificing some of the human readability there.
In practice, I would let this example slide by without comment during a review. Yes, it could be better, but as is it is unlikely to cause harm
- It's test code, therefore it isn't going to impact production at all
- The test is fast and isolated, so it won't be adding additional drag to development processes
- We're probably not changing the behaviors of our exception messaging very often, so over fitting a particular behavior is not likely to add drag.
In short, while the test as implemented doesn't achieve the Platonic ideal, it is Mostly Harmless, and our attention is better invested elsewhere.