[12.x] Add `assertThrown()`
This adds a testing helper for asserting a closure throws an exception/error, ensures the type, and then returns the exception. This is similar to how JUnit's assertThrows works.
The big difference from Laravel's assertThrows() is that you can make expectations against the exception.
Code examples
This is taken from tests/Auth/AuthAccessResponseTest.php.
The way its written currently
public function testAuthorizeMethodThrowsAuthorizationExceptionWhenResponseDenied_original()
{
$response = Response::deny('Some message.', 'some_code');
try {
$response->authorize();
} catch (AuthorizationException $e) {
$this->assertSame('Some message.', $e->getMessage());
$this->assertSame('some_code', $e->getCode());
$this->assertEquals($response, $e->response());
}
}
Rewritten to use the assertThrows
public function testAuthorizeMethodThrowsAuthorizationExceptionWhenResponseDenied_assertThrows()
{
$response = Response::deny('Some message.', 'some_code');
$this->assertThrows(
fn() => $response->authorize(),
function(AuthorizationException $e) use ($response) {
$this->assertSame('some_code', $e->getCode());
$this->assertEquals($response, $e->response());
return true;
},
"Some message"
);
}
Using the newly added method
public function testAuthorizeMethodThrowsAuthorizationExceptionWhenResponseDenied_assertThrown()
{
$response = Response::deny('Some message.', 'some_code');
$e = $this->assertThrown(fn () => $response->authorize(), AuthorizationException::class);
$this->assertSame('Some message.', $e->getMessage());
$this->assertSame('some_code', $e->getCode());
$this->assertEquals($response, $e->response());
}
My belief is: The first way (try-catch and perform assertions in the catch block) is most common and works fine.
The second way (assertThrows()) is cumbersome to write, requires you to remember to add return true to the end, and the nested closure makes it difficult to parse. If you don't add return true to the end, you can just convert the second closure into a series of boolean statements, but then you can't really leverage the PHPUnit assertion methods which is a big loss.
The third way (assertThrown()) breaks down a layer of indentation, doesn't require adding any use ($otherVariablesFromOuterScope), nor does it have the gotcha of remembering to return true.
I do not love the name of this method, but couldn't think of anything better. I have added this as a trait in a work project, and figured I would see if it was of interest to the Laravel community at large.
Seems very good method to have :)
If it's of any help, PHPUnit has assertions for exceptions:
public function test_authorize(): void
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('Some message');
Response::deny('Some message', 'some_code')->authorize();
}
If it's of any help, PHPUnit has assertions for exceptions:
public function test_authorize(): void { $this->expectException(AuthorizationException::class); $this->expectExceptionMessage('Some message'); Response::deny('Some message', 'some_code')->authorize(); }
That's a good point! I think what this approach still lacks is being able to perform additional assertions against the exception. For instance:
class PaymentFailed extends RuntimeException
{
public PaymentMethodEnum $method;
public User $receivingUser;
public function setPaymentMethod(PaymentMethodEnum $method): void
{
$this->method = $method;
}
public function setReceivingUser(User $user): void
{
$this->receivingUser = $user;
}
}
I may not render both of those pieces of information in the exception message, but I may want to perform assertions against them.
Doesn't this already solved with https://github.com/laravel/framework/pull/50704
Doesn't this already solved with #50704
I did not know this existed. Thanks for mentioning it and nice work @nunomaduro!
- This would be unavailable in tests which don't boot the application (ie, extending from PHPUnit's TestCase)
- It won't work for a case like this:
public function test_uniqueEmail() {
Exceptions::fake();
// Given
Org::factory()->create(['id' => 12]);
$userFactory = fn () => User::factory()->create(['email' => '[email protected]', 'org_id' => 12]);
// And the user exists
$userFactory();
// When we attempt to create a user with the same email address
$userFactory();
// Then
Exceptions::assertReported(function(UniqueConstraintViolationException $e) {
self::assertStringContainsString(
"Duplicate entry '[email protected]' for key 'users.users_email_org_id_unique'",
$exception->getMessage()
);
return true;
});
}
☝️ this could be solved with PHPUnit's expectException/expectExceptionMessage of course.
I'm not sure if I'm doing something wrong, but it doesn't seem to work here either?
public function test_ModelNotFound()
{
// Given a route
Route::get('/some-example', function () {
return User::findOrFail(1);
});
// And we are faking exception handling
Exceptions::fake([ModelNotFoundException::class]);
// $this->withoutExceptionHandling(); <-- also tried this but it didn't seem to change anything
// When we request the endpoint
$response = $this->get('/some-example');
// Then the response is not found
$response->assertNotFound();
// And the exception looks like this
Exceptions::assertReported(function (ModelNotFoundException $e) {
self::assertStringContainsString('No query results for model [App\Models\User] 1', $e->getMessage());
return true;
});
}
- It requires knowing which exceptions are set on
Handler::$dontReportInternal(I think?) as well as which exceptions your application has indicated to not report. - It requires returning true at the end of the expectation closure (which isn't indicated in the docblock)
- It has the problems of being a closure (additional indentation, more cumbersome to read/write data to the outside scope)
Obviously anyone is free to add their own AssertsExceptionsTrait, this isn't a bug, but a quality of life improvement.
As I was working with this method today, I realized that this causes a failing test due to no assertions:
#[Test]
public function throws_an_exception(): void
{
$myFunction = static fn () => throw new \RuntimeException();
self::assertThrown($myFunction, \RuntimeException::class);
}
Just making mention of it.
@crynobone thanks for updating the target branch. Do you know if there's anything I can do to get this re-considered?