When you're using PHPUnit to test for an exception you can use $this->expectException(EvatrInvalidVatNumberException::class); for setting up your test to listen to the exception. If the expected exception is not thrown your test will fail and PHPUnit will report the issue.
When you only need to assert the message (or a substring) you would use $this->expectExceptionMessage().
In most test cases this works very well. But sometimes you might want to assert some more aspects of the exception. For example if it contains some debug infos.
Of course you could try to use $this->expectExceptionObject() but this is only a shortcut for expecting the class, message and code. It won't compare properties on the exception for example (!).
You could assert that the exception is thrown when you write the whole test by hand like this:
public function testAnExceptionIsThrown(): void { try { $this->start(); } catch (Domain\Exception $e) { $this->assertEquals('some debug info', $e->debugInfo); return; } $this->fail('Expected Exception is not thrown'); } This works. But its not very nice. The fail now does not provide any information anymore which exception is thrown instead. Therefore does not convey why the tests fails. And the return in the catch is hard to read and easy to miss.
Looking closer at it, there is another issue. The exception count is wrong: assertContains when the test passes, but actually you have two assertions
- The correct exception is thrown
- The exception contains the debug info
A Better Approach
public function testAnExceptionIsThrownWithDebugInfo(): void { $this->expectException(Domain\Exception::class); try { $this->start(); } catch (Domain\Exception $e) { $this->assertEquals('some debug info', $e->debugInfo); throw $e; } } Testing the exception like this has some advantages:
- Assertion count is now correct
- Your test will still be reporting correctly if the exception is changed
- It's more readable
- When you miss to add the throw the test will still fail
- The failing test (wrong exception thrown) will report nicely which exception is caught instead and skip the assertEquals
Insights from the comments
Be extra careful, when using something like
class Domain\Exception extends \RuntimeException { public function __construct( string $message = '', int $code = 0, \Throwable $previous = null, public string $debugInfo = ''; ) {} } // and in the test: $this->expectExceptionObject( new Domain\Exception( message: 'this went wrong', debugInfo: 'someDebugInfo', ), ); Because the phpunit implementation (til 15) is:
final protected function expectExceptionObject(\Exception $exception): void { $this->expectException($exception::class); $this->expectExceptionMessage($exception->getMessage()); $this->expectExceptionCode($exception->getCode()); } So the test would not compare the $debugInfo, even if it looks like it SHOULD.
Originally published on ps-webforge.com on March 3rd, 2013, updated Dec 2025
Top comments (8)
If you re-upload an article, I suggest you add updated information.
expectExceptionMessageis available since 2015. Which reduces the test code toHey @xwero did you maybe read an earlier version of the article? Because I went ahead and updated it exactly because of that. So thanks for the great hint! Maybe you didn't see my edit yet?
I added the 'some debug info' as a property of the exception, to show that this is only needed if you have special exceptions.
First i thought "oh there is expectExceptionObject", so that it can be:
But this wont work as well!
Because if you look at the phpunit implementation
Hope you read the article again, does this explain it better for you now?
I can still see the
assertEqualsand thetry catch, so I'm not sure which post you changed?If you want to use
expectExceptionObjectthe test code will be.I prefer to use Pest, then the code is
expect($this-start())->toThrow( Domain\Exception::class, 'some debug info');. Having to read from the bottom up is not the way I'm taught to read text.My point is: the try catch is still needed.
Given the class
You cannot test this without using try catch. The code
would error:
github.com/sebastianbergmann/phpun...
this only accepts string not an object. And expectExceptionObject() (i guess this is what you had in mind) wouldn't compare the public property $debugInfo.
Here is a demo:
here is the output from phpunit 12:
Ok I get it now, but why would an exception class should have an extra property? Is there a real world situation you needed this?
PS: yes the expectException was meant to be expectExceptionObject in the example.
Yeah would say so, e.g. the debug info, which is maybe really really long, you want to transport that in the exception to later render it somehow, but dont want to add it to the exception message - cause it's way too long.
Another one I can think of is e.g. BranchAlreadyExistsException and you want to pass in the branch name, so you don't have to parse it from the exception. Sometimes we pass the translation key and parameters into exceptions, so that we can render a message to the user later (where the exception message is used only internally).
I think the expectExceptionObject() is really a misleading API! I just learned this yesterday, when I updated the article and had to immediately search my codebase if I used it somewhere :D
If I really wanted debug information I would create a separate log entry with it. The exception should not be extended with a property that is only useful occasionally.
That is a code smell in my book. The message should end up in the logs, and if it only has translation metadata, the log is useless.
But if a property is needed I think the
try catchshouldn't be in the test methods. That should be the job of a helper method.The example is also a test that could fail on multiple points:
I would create an
expectExceptionPropertyValuemethodIn the test you can now do
$this->expectExceptionPropertyValue( fn() => $this->start(), 'debugInfo', 'some debug info').This helper will fail when changes are made by people with less knowledge of the code.
It does what it says, the thrown exception is checked against all the exception asserts of the added exception.
I do think that the exception asserts should allow custom extensions.