One of the things that unit tests are best for is checking error conditions. While we want wider coverage of full happy flows, error conditions are sometimes hard to simulate in a big system, and unit tests excel at minimal setup to make sure our code handles the problem correctly.
We have class A that operates class B, and B is the code we’re writing now. Basically, there are three things our code can do when an error occurs:
Do nothing
Sometimes our code is not supposed to handle errors. Someone else will take care of it, in our case A. For this kind of behavior, we don’t need a test for B. Let’s say we have a party in our house, and we’re using a GuestTracker to track how many guests come in. The internal list, if not initialized, can throw an exception, when AddGuests is called.
public class GuestTracker { private List<int> guests; public void Init() { guests = new List<int>(); } public void AddGuests(int newGuests) { guests.Add(newGuests); } }
If we write a test for the AddGuests method it will look like this (I’m using NUnit’s Assert.Throws because of its conciseness, but this works with other mechanics and test frameworks as well):
[Test] public void ThrowNullException_WhenUninitialized() { GuestTracker tracker = new GuestTracker(); Assert.Throws<NullReferenceException>(()=> { tracker.AddGuests(3); }); }
What are we checking here? Since our GuestTracker doesn’t handle exceptions by design, we’re actually checking the .Net framework works as expected. Answering “what bug will it find?” will point to an unlikely error in the .Net runtime. If we later decide to change the behavior of the AddGuests method to handle errors, this test will fail and will need rewriting.
Bottom line, if the code is not supposed to handle errors, don’t write tests for the error handling part.
Transform and throw
This is not Optimus Prime’s call to action, but an exception handling mechanism. Our code may need to translate a system exception to an application specific one. For example, our GuestTracker will try to catch the Null exception, and replace it with a friendlier UninitializedTracker exception:
public void AddGuests(int newGuests) { try { guests.Add(newGuests); } catch (NullReferenceException e) { throw new UninitializedTrackerException(); } }
While not much of a “handling”, the main application that uses our class, may be able to handle the error better this. Sometimes the translation between exceptions can have some custom code inside the catch clause, like logging, which might be another thing we’d like check.
(By the way, don’t use this pattern to just log a Null exception, and re-throw it. It’s tempting to do it, but if you want to add some logging, do it in the main application handler. One place to code and maintain with all its benefits. Now, Back to our usual broadcast).
Handle it
The final option is to do some actual error handling. That means there’s going to be some behavior on the part of the class that we can observe and check. In our case, let’s say that our responsible GuestTracker understands that it’s not really initialized, and initializes itself:
public void AddGuests(int newGuests) { try { guests.Add(newGuests); } catch (NullReferenceException e) { guests = new List<int>(); guests.Add(newGuests); } }
Yowza! Maybe we can call it a SmartGuestTracker. Well, if the error is caught internally, the observable behavior is that no exception is being thrown, right?
[Test] public void NoException_WhenUninitialized() { GuestTracker tracker = new GuestTracker(); Assert.DoesNotThrow(() => { tracker.AddGuests(3); }); }
Yes, but that’s not a test you want. We’d like to check what actually happens, not what doesn’t. After all, we wouldn’t test that the world doesn’t end every time a guest arrives, right?
In addition, “what bug will it find?” If DoesNotThrow fails the test somewhere in the future, that means that there was an exception thrown, which moves us back to square one, only with less stack information.
A better test would look like this:
[Test] public void AddGuests_WhenUninitialized() { GuestTracker tracker = new GuestTracker(); tracker.AddGuests(3); Assert.AreEqual(3, tracker.GuestsSoFar()); }
This actually checks what we expect as observable behavior, after the error has occurred. In this case – that we can count the number of guests, even if the tracker was not initialized.
If you want to know more about error handling and checking exceptions, read ahead.
Check out the following workshops where I talk about error handling in unit tests:
0 Comments