Assertion After An Expected Exception
ID |
php.assertions_end_blocks_expecting_exception |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Php |
Tags |
reliability, testing, unused-code |
Description
Reports an assertion placed after the point where a test expecting an exception has already stopped running. The exception unwinds the method, so the assertion is never reached.
Rationale
The assertion reads as a check on the outcome and contributes nothing: the behaviour it was written to verify is not verified at all. Worse, it is invisible — the test passes, so nobody looks, and the next reader assumes the state after the failure has been checked.
Two positions are provably past the end of the block, and only those are reported.
After fail(…). fail raises unconditionally, so anything that follows it in the same statement
list is dead however the block is reached.
After the call the test declares will raise. In a test calling expectException(…), an assertion
following the first invocation of the subject is reached in neither outcome that matters: if the
call raises, as the test says it will, the method ends there; if it does not raise, the test has
already failed for the missing exception.
<?php
use PHPUnit\Framework\TestCase;
final class PaymentTest extends TestCase
{
private Payment $subject;
public function testPaymentRejectsANegativeAmount(): void
{
$this->expectException(NegativeAmount::class);
$this->subject->charge(-1);
$this->assertSame(0, $this->subject->total()); // FLAW — the charge raises, so this never runs
}
public function testPaymentRejectsAnUnknownCurrency(): void
{
try {
$this->subject->charge(100, 'zzz');
$this->fail('an unknown currency must be rejected');
$this->assertSame(0, $this->subject->total()); // FLAW — fail() always raises
} catch (UnknownCurrency $e) {
$this->assertSame('zzz', $e->getCurrency()); // OK — the catch is the reached path
}
}
public function testPaymentRecordsTheAttemptBeforeRaising(): void
{
$this->expectException(AccountClosed::class);
$this->assertSame(0, $this->subject->total()); // OK — checked before the raising call
$this->subject->charge(100);
}
}
Two neighbouring shapes are deliberately left alone, because neither is provable from the code at hand:
-
an assertion at the end of a
tryblock whosecatchis the expected path but which carries nofail(…)— that assertion does run whenever nothing is raised, so calling it dead would be wrong, even though it is skipped in the case the test is about; -
an assertion after a call whose raising is implied only by a comment or by a helper’s name.
An assertion inside the catch block is correct and is never reported: the catch is the path the
test expects to take.
Remediation
Move the assertion to where it can run. What the state should be after the failure belongs in the
catch block, or in a second test that exercises the same call and then inspects the object.
<?php
use PHPUnit\Framework\TestCase;
final class PaymentRejectionTest extends TestCase
{
private Payment $subject;
public function testPaymentRejectsANegativeAmount(): void
{
$this->expectException(NegativeAmount::class);
$this->subject->charge(-1);
}
public function testPaymentLeavesTheTotalUntouchedAfterARejection(): void
{
try {
$this->subject->charge(-1);
$this->fail('a negative amount must be rejected');
} catch (NegativeAmount $e) {
$this->assertSame(0, $this->subject->total());
}
}
}