Question
How can I write a PHPUnit test that verifies code throws an exception under a specific condition? Is there a PHPUnit assertion or expectation for testing whether an exception was thrown by the code under test?
Short Answer
You will learn how PHPUnit tests expected exceptions with expectException(), how to check an exception message or code, and how to ensure the exception comes from the intended line of code.
Concept
Exceptions represent abnormal or invalid situations that normal code cannot successfully complete, such as invalid input, a missing record, or an unavailable service.
A test for an exception has two jobs:
- Arrange a situation that should fail.
- Declare which exception is expected, then run the code.
In PHPUnit, the usual approach is to declare an expectation with expectException(). If the tested code throws an exception matching that class, the test passes. If it throws nothing, or throws a different exception type, the test fails.
Testing exceptions matters because failure behavior is part of an application's contract. For example, callers may rely on an invalid email address causing an InvalidArgumentException rather than silently creating a broken user record.
Mental Model
Think of an exception expectation as telling the test runner: “When I press this button, an alarm must sound.”
expectException()describes the alarm you expect to hear.- The method call is pressing the button.
- PHPUnit passes the test only if the matching alarm occurs.
Declare the expected alarm immediately before pressing the button. If you declare it too early, an unrelated earlier line could trigger the expected exception and make the test pass for the wrong reason.
Syntax and Examples
The basic PHPUnit syntax is:
$this->expectException(ExceptionClass::class);
$object->methodThatShouldThrow();
For example, this function rejects an empty name:
function createGreeting(string $name): string
{
if ($name === '') {
throw new InvalidArgumentException('Name cannot be empty.');
}
return "Hello, {$name}!";
}
A PHPUnit test can verify that behavior:
use PHPUnit\Framework\TestCase;
final class GreetingTest extends TestCase
{
public function ():
{
->(::);
();
}
}
Step by Step Execution
Consider this test:
use PHPUnit\Framework\TestCase;
final class AgeValidatorTest extends TestCase
{
public function testNegativeAgeIsRejected(): void
{
$validator = new AgeValidator();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Age cannot be negative.');
$validator->validate(-1);
}
}
Assume validate() is implemented as follows:
final class AgeValidator
{
public function validate( ):
{
( < ) {
();
}
}
}
Real World Use Cases
Exception tests are useful whenever code rejects invalid input or cannot complete an operation safely.
- API validation: Reject a request with a missing required field.
- Authentication: Throw an exception when a token is expired or malformed.
- Domain rules: Prevent an order from being paid twice.
- File processing: Report an error when an uploaded file has an unsupported format.
- Database lookups: Throw a
UserNotFoundExceptionwhen a required user does not exist. - Configuration: Fail early when a required environment variable is missing.
For example, a service might reject a duplicate email address:
$this->expectException(DomainException::class);
$registrationService->register('existing@example.com', 'secret-password');
Real Codebase Usage
In real projects, exception tests usually focus on the public behavior of a class rather than its internal implementation.
Validate at boundaries
Validate data as it enters a controller, service, command, or domain object:
final class PasswordPolicy
{
public function assertValid(string $password): void
{
if (strlen($password) < 12) {
throw new InvalidArgumentException('Password must contain at least 12 characters.');
}
}
}
Test the boundary rule directly:
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('at least 12 characters');
$policy->assertValid('short');
Use custom exceptions for meaningful business failures
Common Mistakes
Setting the expectation too early
This test can pass for the wrong reason if createService() throws the expected exception:
// Risky: setup might satisfy the expectation.
$this->expectException(InvalidArgumentException::class);
$service = createService();
$service->register('');
Prefer this:
$service = createService();
$this->expectException(InvalidArgumentException::class);
$service->register('');
Catching the exception without making an assertion
This test passes even if no exception is thrown:
try {
$validator->validate(-1);
} catch (InvalidArgumentException $exception) {
}
Comparisons
| Approach | Best use | Main limitation |
|---|---|---|
expectException() | The code should throw a specific exception type | Does not itself inspect custom exception fields |
expectExceptionMessage() | The message is part of the expected behavior | Exact messages can make tests fragile |
expectExceptionMessageMatches() | Only a stable message pattern matters | Regular expressions can be harder to read |
expectExceptionCode() | An exception code is a defined contract | Many projects do not use exception codes consistently |
Manual try/catch | You need to inspect custom properties, previous exceptions, or multiple details | Requires to avoid a false-positive test |
Cheat Sheet
// Expect an exception type
$this->expectException(InvalidArgumentException::class);
$service->run($input);
// Also require an exact message
$this->expectExceptionMessage('Input is invalid.');
// Require a message that matches a regular expression
$this->expectExceptionMessageMatches('/input.+invalid/i');
// Require an exception code
$this->expectExceptionCode(422);
- Declare expectations before the call that should throw.
- Put expectations as close as possible to that call.
- Prefer specific exception classes over
Exception::classorThrowable::class. - An expected parent exception class also matches child exceptions.
- Statements after an exception is thrown are not executed.
- Use manual
try/catchplus$this->fail()only when you need custom inspection.
FAQ
How do I assert that an exception is thrown in PHPUnit?
Call $this->expectException(ExceptionClass::class) immediately before invoking the code that should throw.
Does PHPUnit use assertThrows()?
The standard PHPUnit pattern is expectException(), not assertThrows(). Declare the expectation in the test, then call the method under test.
How do I test an exception message in PHPUnit?
Use $this->expectExceptionMessage('Expected message') along with expectException(). Use expectExceptionMessageMatches() when a regular expression is more appropriate.
Can I expect a parent exception class?
Yes. If you expect RuntimeException::class, an exception class that extends RuntimeException satisfies the expectation. Prefer the most specific type that your code promises to throw.
What happens if no exception is thrown?
The test fails because PHPUnit was told that an exception was expected.
Why did my exception test pass even though the wrong method threw?
The expectation was likely set before setup code or another operation that could throw the same exception. Move expectException() immediately before the intended method call.
Mini Project
Description
Build a small TemperatureConverter class that converts Celsius to Fahrenheit while rejecting temperatures below absolute zero. This demonstrates using an exception to enforce a business rule and testing both successful and failing behavior.
Goal
Create PHPUnit tests that verify valid conversions and confirm invalid temperatures throw a precise exception.
Requirements
Create a TemperatureConverter class with a celsiusToFahrenheit() method.
Reject Celsius values below -273.15.
Throw an InvalidArgumentException for invalid values.
Write one test for a valid conversion.
Write one test that expects the exception type and a helpful message.
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.