Testing Practices for Exceptions in unit tests

2013-05-15

Back in 2011 I published a post claiming that using anything but try/catch in unit tests was a bad idea. Since then, after having more discussions with others in the field and reading Chris Hartjes's post about try/catch being a bad practice in unit testing, I've realized my initial post on the topic was a bit short-sighted and didn't explain my views as well as I thought. It also contained no examples. I've decided to post some clarifications here, as well as sum up the arguments on both sides based on Chris's post and the comments.

Original clarifications

Much of what I originally wrote was based around the idea of someone writing a test for a very broad class of exception, such as the base Exception class or one with a number of permutations like PDOException. In these situations, it's possible that some part of the code under test would throw an exception that matched the test's acceptance criteria without actually testing the intended behavior.

I've had to write a lot of tests for legacy code that was not written with testability in mind. Many of those tests ended up being functional in nature even though they were written using PHPUnit. Because of this, I tended to err on the side of caution when writing my assertions.

Chris was correct in calling me out as contradicting myself. My original reasoning was based on test specificity, but a truly specific unit test, testing only a single unit of code, should not be able to throw an unplanned exception. Situations where one finds unexpected exceptions during their unit testing should be very rare.

Situations where the technique applies

If you're testing attributes of a thrown exception other than the class and message, this technique can help, but you may also want to consider writing a custom assertion instead.

If you're writing pseudo-functional tests for legacy code that has a deep inheritance hierarchy or other things that prevent you from testing true units of code, this technique can provide a bit of a safety net against false positives. It shouldn't be an end result though.

Summary

  • For TDD, use $this->setExpectedException right before you execute the command that should throw the exception, or the @expectedException annotation
  • For greenfield work, use TDD (see above)
  • Use the most specific Exception class that you can to avoid false positives
  • For legacy code, write functional tests to allow you to refactor safely to (unit) testable code

So in conclusion, in my original post I had latched on to a new idea and presented it as a best practice without thoroughly vetting it from all angles. It was valid for me when I wrote it, and it's still valid in some situations, but as a "pure" best practice, I no longer think it's valid.

Tags: php, phpunit

Comments