Test Without Assertion

ID

php.test_without_assertion

Severity

critical

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Php

Tags

assertion, reliability, testing

Description

Reports a test method that states no expectation. It exercises the code under test and then accepts whatever came back, so it passes as long as nothing throws — it cannot fail on a wrong answer, only on a crash.

Rationale

A test with no assertion is worse than no test at all: the suite counts it, the coverage report counts the lines it runs, and both report success while nothing about the result is checked. It will keep passing after the behaviour it was written for is broken.

<?php

class InvoiceTotalsTest extends TestCase
{
    public function testTotalsAreRounded(): void // FLAW — nothing is checked
    {
        $this->subject->addLine(new Line('books', 12.345));
        $this->subject->round();
    }

    public function testTotalsAreRoundedToTwoDecimals(): void // OK
    {
        $this->subject->addLine(new Line('books', 12.345));

        $this->assertSame(12.35, $this->subject->round());
    }

    public function testRejectsANegativeLine(): void // OK — declaring the exception is the expectation
    {
        $this->expectException(InvalidLine::class);

        $this->subject->addLine(new Line('books', -1.0));
    }
}

"States an expectation" is answered widely, because a wrong "no" would report a test that does check. Every assert…, expect…, should… and verify… call counts, whatever its receiver — PHPUnit’s $this→assertSame(…), self::assertSame(…) and bare assertSame(…) forms, the fluent assertions of framework test clients, the Mockery and Prophecy expectation styles, a mock expectation such as $ledger→expects($this→once())→method('record'), and the hand-written assertOrderIsValid(…) helpers a suite grows. A call to a method declared in the same file whose own body carries one of those counts too, which covers a suite that delegates its checking to a local helper. That single hop is the limit: a helper living in another file is beyond a per-file rule, and a suite whose assertions all come from an imported trait is the known false positive.

These are not reported, each being complete or a different defect:

  • a test declaring the exception it expects — expectException(…) and its relatives;

  • expectNotToPerformAssertions(), markTestSkipped(…) and markTestIncomplete(…);

  • the #[DoesNotPerformAssertions] attribute and its @doesNotPerformAssertions annotation;

  • an empty body, which php.empty_function_body reports;

  • a body that only throws — the not-implemented stub.

The enclosing class must itself be a fixture, extending a TestCase base or following the …Test naming convention. Without that guard a production class with a testConnection() method of its own would be reported.

Remediation

State what the test is meant to prove. If the point really is that the call does not raise, say so explicitly, so the next reader is not left guessing whether an assertion was forgotten.

<?php

public function testTotalsAreRounded(): void
{
    $this->subject->addLine(new Line('books', 12.345));

    $this->assertSame(12.35, $this->subject->round());
}

#[DoesNotPerformAssertions]
public function testAddingALineDoesNotBlowUp(): void
{
    $this->subject->addLine(new Line('books', 1.0));
}

© 2026 Xygeni. All rights reserved