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 try block whose catch is the expected path but which carries no fail(…) — 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());
        }
    }
}

© 2026 Xygeni. All rights reserved