Functions That Throw
Many PHP functions refuse bad input by throwing an exception: on success they return the result, on failure they throw and never return at all.
A test for such a function must check both paths: the good input returns the right value without throwing, and the bad input actually throws — a function that silently accepts bad input is a bug too.
The Good Path
For a valid input the test simply calls the function and compares the value. No extra error check is needed: if the call throws unexpectedly, PHPUnit reports the exception and fails the test on its own.
Valid input, no exception
safeDivide(10, 2) must return 5.0 and throw nothing. One assertSame covers it — an unexpected exception would fail the test without any extra code.
Output: PHP script
<?php
use PHPUnit\Framework\TestCase;
function safeDivide(float $a, float $b): float {
if ($b == 0) {
throw new InvalidArgumentException("cannot divide by zero");
}
return $a / $b;
}
final class SafeDivideTest extends TestCase {
public function testSafeDivideGood(): void {
$this->assertSame(5.0, safeDivide(10, 2));
}
}
The Bad Path
The other half of the contract: bad input must throw. The test declares the expectation before the call with expectException, then calls the function.
If the call throws the declared exception, the expectation counts as a passed assertion. If the call returns normally instead, the test fails — the function accepted input it should have rejected.
Bad input must throw
Dividing by zero has to fail. The test expects an InvalidArgumentException; safeDivide(10, 0) throws it, so the run reports OK (1 test, 1 assertion). A safeDivide that returned a value here would fail the test.
Output: PHP script
<?php
use PHPUnit\Framework\TestCase;
function safeDivide(float $a, float $b): float {
if ($b == 0) {
throw new InvalidArgumentException("cannot divide by zero");
}
return $a / $b;
}
final class SafeDivideTest extends TestCase {
public function testSafeDivideByZero(): void {
$this->expectException(InvalidArgumentException::class);
safeDivide(10, 0);
}
}
A Table with throws
Both paths fit into one data provider: give each row a throws field saying whether an exception is expected.
The test method checks that field first — for a throwing row it declares expectException, calls the function and returns. For the good rows it compares the value as usual.
Good and bad cases in one table
Four rows: three normal divisions and one division by zero with throws set to true. One test method covers the whole contract of the function — run with --testdox, all four cases pass.
Output: PHP script
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
function safeDivide(float $a, float $b): float {
if ($b == 0) {
throw new InvalidArgumentException("cannot divide by zero");
}
return $a / $b;
}
final class SafeDivideTest extends TestCase {
public static function divideCases(): array {
return [
"simple division" => [10, 2, 5.0, false],
"negative result" => [-9, 3, -3.0, false],
"divide zero" => [0, 5, 0.0, false],
"divide by zero" => [10, 0, 0.0, true],
];
}
#[DataProvider('divideCases')]
public function testSafeDivide(float $a, float $b, float $want, bool $throws): void {
if ($throws) {
$this->expectException(InvalidArgumentException::class);
safeDivide($a, $b);
return;
}
$this->assertSame($want, safeDivide($a, $b));
}
}