I think it's worth keeping in mind that, if you are running tests as part of your development/design loop, it doesn't matter very much how to describe a test failure. You don't need a lot of information emitted by the test when you know the root cause is the line of code you just changed.
Where it tends to matter is when a bunch of new code gets merged in as a block, and we need the detailed signals to track down the problem(s) that were just introduced.
When performing tests where you have more than one assertion you can perform, is it recommended to provide a single test for each assertion, or group the assertions into a single test?
So let's assume for the start that we're talking specifically about tests that follow the Arrange-Act-Assert recipe. So we create a system, and then we send that system zero of more messages, and finally we want to measure whether the system now satisfies some constraint.
That constraint usually has the form of "extract some value from the system, and compare it to some expected value".
Such a comparison might have N different checks to be applied.
One option is to have a different test for each check; the tests will duplicate the arrange and act phases, but will have slightly different assert phases. The good news is that all of the checks are run, and don't interfere with each other. If the test is properly isolated, the tests can be run concurrently. The bad news is that, without additional structure, your test harness won't necessarily understand that the tests should be bundled together, and you'll need to look in several places to get a complete understanding of
what's going on when things fail.
Another option is to put all of the checks into a single test. The good news is that any given scenario is in one place, so the code describes the specification you are trying to satisfy in one place, rather than leaving it scattered everywhere. The bad news is that a single failed check trips the assert; the subsequently scheduled checks are never evaluated, leaving us with an incomplete picture of what's going on.
A third option is to treat each check as a predicate, AND the results, and assert on the outcome. That gives you a single test with all of the logic, and all of the checks being evaluated, but it gives you only a weak signal about which check was the problem.
A fourth option is similar - collect the results of the check together, but assert on the results, rather than simple boolean predicates. Here's a ordered list of expected outcomes, here's an ordered list of actual outcomes, assert if they don't match. This gives you a lot of everything -- all of the checks are in one place AND all of the checks run AND you get clear messages about all of the failures.
The bad news is that this fourth approach isn't how SUnit was designed, and therefore isn't how JUnit was designed, and therefore isn't how all of the descendants of those tools were designed.
Functional programmers might be more comfortable with this last approach; we create a list of checks, and then map to create a list of check results, and then fold those check results together.
Note that none of these approaches does particularly well if the test happens to trip a precondition in the arrange or act phase.
Best Practice? Your choice
1) Document what you know, document what you guess, document your decision, document the outcomes you expect, and review that document from time to time, re-evaluating the call.
2) Experiment, accumulate evidence, and improve your ability to guess which external factors are meaningful in a given context.