codeception/assert-throws
Adds assertThrows-style assertions to Codeception, letting you easily verify exceptions in tests. Assert the exception type and optionally message/code when running a callable, making negative-path testing clearer and less verbose.
Installation
composer require --dev codeception/assert-throws
Ensure your project uses Codeception 4.0+ or PHPUnit 8.0+.
First Use Case
Replace a try-catch block or expectException() with:
use AssertThrows\AssertThrowsTrait;
class UserTest extends \Codeception\Test\Unit
{
use AssertThrowsTrait;
public function testInvalidAgeThrowsException()
{
$this->assertThrows(
InvalidArgumentException::class,
fn() => User::create(['age' => -5])
);
}
}
Where to Look First
Basic Exception Assertion
$this->assertThrows(
\InvalidArgumentException::class,
fn() => $this->userRepository->delete(-1)
);
Assertion with Custom Message
$this->assertThrows(
\RuntimeException::class,
fn() => $this->paymentService->process($invalidPayment),
'Payment amount must be positive.'
);
Dynamic Exception Classes
Useful for polymorphic exceptions (e.g., Laravel’s HttpResponseException):
$exceptionClass = $this->getExceptionClassForStatus(403);
$this->assertThrows($exceptionClass, fn() => $this->get('/admin'));
Combining with Laravel Testing
$this->assertThrows(
\Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException::class,
fn() => $this->actingAs($user)->post('/admin', ['name' => 'Test'])
);
$this->seeResponseCodeIs(401);
Negative Assertions Verify no exception is thrown:
$this->assertDoesNotThrow(
fn() => $this->userRepository->find(1)
);
Laravel TestCase Integration
Extend Laravel\Testing\TestCase and include the trait:
use Laravel\Testing\TestCase;
use AssertThrows\AssertThrowsTrait;
class CustomTestCase extends TestCase
{
use AssertThrowsTrait;
}
Mocking Exceptions
Use with Mockery or PHPUnit’s mocks:
$mock = $this->mock(SomeService::class);
$mock->shouldThrow(new \RuntimeException('Failed'));
$this->assertThrows(\RuntimeException::class, fn() => $mock->someMethod());
Codeception Modules Add to a custom module for reusable assertions:
namespace Modules;
use AssertThrows\AssertThrowsTrait;
use Codeception\Module;
class ExceptionModule extends Module
{
use AssertThrowsTrait;
}
PHPUnit Compatibility If not using Codeception, wrap the trait in a helper:
function assertThrows($exception, callable $callback, ?string $message = null)
{
$trait = new AssertThrowsTrait();
$trait->assertThrows($exception, $callback, $message);
}
Non-Halting Behavior
expectException(), assertThrows() does not stop test execution if the assertion fails. Subsequent assertions may run, leading to confusing test reports.assertDoesNotThrow() for negative cases or chain assertions carefully:
$this->assertThrows(\InvalidArgumentException::class, fn() => $this->fail());
$this->fail('This should not run if the above passes!');
Exception Message Matching
assertStringContainsString() for partial checks:
try {
$this->fail();
} catch (\InvalidArgumentException $e) {
$this->assertStringContainsString('invalid', $e->getMessage());
}
Closure Scope
$this).bindTo() or pass dependencies explicitly:
$this->assertThrows(
\RuntimeException::class,
fn() => $this->service->method(),
'Error message'
);
Laravel-Specific Exceptions
ValidationException, HttpResponseException) may not extend PHP’s base exceptions, causing false negatives.$this->assertThrows(
\Illuminate\Validation\ValidationException::class,
fn() => $this->post('/submit', ['invalid' => 'data'])
);
PHPUnit 12+ Attribute Conflicts
#[ExpectException], the package may interfere.#[ExpectException] for halting tests.Verbose Output
Run tests with --verbose to see the exact exception details:
phpunit --verbose
Custom Exception Classes For complex exceptions, create a helper:
function assertCustomException(callable $callback, string $expectedType, array $expectedData = [])
{
try {
$callback();
$this->fail("Expected {$expectedType} but no exception thrown.");
} catch (\Throwable $e) {
$this->assertInstanceOf($expectedType, $e);
if (!empty($expectedData)) {
$this->assertEquals($expectedData, $e->getData());
}
}
}
Logging Exceptions Temporarily log exceptions to debug:
$this->assertThrows(
\RuntimeException::class,
fn() => $this->riskyOperation(),
'Expected error'
);
// Log the actual exception for debugging
$this->debug((string) $this->getLastException());
Custom Assertion Logic Extend the trait to add Laravel-specific assertions:
trait LaravelAssertThrows extends AssertThrowsTrait
{
public function assertLaravelValidationException(callable $callback, string $field, string $message)
{
$this->assertThrows(
\Illuminate\Validation\ValidationException::class,
$callback,
"The {$field} field is required."
);
}
}
Global Assertion Overrides Override the trait in a base test case:
abstract class BaseTestCase extends \Codeception\Test\Unit
{
use AssertThrowsTrait {
assertThrows as protected traitAssertThrows;
}
protected function assertThrows($exception, callable $callback, ?string $message = null)
{
try {
$this->traitAssertThrows($exception, $callback, $message);
} catch (\Throwable $e) {
$this->fail("Assertion failed: " . $e->getMessage());
}
}
}
Integration with Laravel’s assertDatabase*
Combine with Laravel’s testing helpers:
$this->assertThrows(
\Illuminate\Database\QueryException::class,
fn() => $this->call('POST', '/create', ['invalid' => 'data'])
);
$this->assertDatabaseMissing('users', ['email' => 'invalid@example.com']);
Codeception Autoloading
Ensure the trait is autoloaded in codeception.yml:
modules:
enabled:
- AssertThrows\AssertThrowsTrait
PHPUnit Bootstrap
If using PHPUnit directly, require the trait in phpunit.xml:
<php>
<autoload>
<classmap>
<dir>vendor/codeception/assert-throws/src</dir>
</classmap>
</autoload>
</php>
IDE Support
Some IDEs (e.g., PHPStorm) may not recognize the trait. Add a @mixin annotation:
/**
* @mixin AssertThrowsTrait
*/
class CustomTestCase extends \Codeception\Test
How can I help you explore Laravel packages today?