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(…)andmarkTestIncomplete(…); -
the
#[DoesNotPerformAssertions]attribute and its@doesNotPerformAssertionsannotation; -
an empty body, which
php.empty_function_bodyreports; -
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));
}