Art Of Programming

musings by Dmytrii Nagirniak

Unit Test - Granular Expected Exception

Most common .NET Unit testing frameworks (referring to NUnit, MbUnit) use ExpectedException attribute to set the expectation that test should throw an exception. (There are some with another approach, but I forgot their names).

While this is easy to use, it is not clear at which line we really do expect the exception to be raise. Let’s have a look at an example (just imaging it):

   1: [Test]
   2: [ExpectedException(typeof(InvalidCreditCardException))]
   3: public void PayWithInvalidCreditCard_ProducesError()
   4: {    
   5:     Product product = GetProductUnderTest();
   6:     Customer customer = GetCustomer();
   7:     CreditCardPayment payment = CreateCreditCardPayment("1234567812345678", "05", "1999");
   8:     customer.BuyProduct(product, payment)
   9: }

In .NET 1.1 I usually rewrite this code like so:

   1: [Test]   
   2: // Note: the attribute is removed   
   3: public void PayWithInvalidCreditCard_ProducesError()   
   4: {   
   5:     Product product = GetProductUnderTest();      
   6:     Customer customer = GetCustomer();      
   7:     CreditCardPayment payment = CreateCreditCardPayment("1234567812345678", "05", "1999");      
   8:     // Explicitly define the line with expected exception   
   9:     Exception wrongError = null;  
  10:     bool isRaised = true;  
  11:     try {  
  12:         customer.BuyProduct(product, payment);  
  13:         isRaised = false;  
  14:     } catch(InvalidCreditCardException) {  
  15:         // That's what we expect.  
  16:     } catch(Exception ex) {  
  17:         // Wrong exception raised  
  18:         wrongError = ex;  
  19:     };  
  20:     // Assert exception  
  21:     Assert.IsTrue(isRaised && wrongError == null,   
  22:         "Wrong exception raised: {0}.",   
  23:         wrongError == null ? "<null>" : wrongError.ToString());  
  24: }

 

While this really defines exact point at which we expect exception (line 12), there’s much code for it.

With .NET 3.5 we can greatly simplify this just by adding one simple helper method:

   1: public void ExpectException<TException>(Action failureBlock) where TException: Exception {
   2:     try {
   3:         failureBlock.Invoke();                
   4:     } catch (TException) {
   5:         // Good. That's what we expect
   6:         return;
   7:     } catch (Exception ex) {
   8:         Assert.Fail("Expected exception: {0}, but was {1}.", typeof(TException).FullName, ex.ToString());
   9:     }
  10:     Assert.Fail("Expected exception {0} has not been raised.", typeof(TException).FullName);
  11: }

Let’s see how we can improve our test using this helper method. Please note that it is easily reusable everywhere.

   1: [Test]   
   2: public void PayWithInvalidCreditCard_ProducesError()   
   3: {       
   4:     Product product = GetProductUnderTest();   
   5:     Customer customer = GetCustomer();   
   6:     CreditCardPayment payment = CreateCreditCardPayment("1234567812345678", "05", "1999");   
   7:  
   8:     ExpectException<InvalidCreditCardException>(() => {
   9:         customer.BuyProduct(product, payment); // Very explicit and easy to define expectation
  10:     });       
  11: }

Note how we define expected exception in line 8.  We only verify for an error explicitly in code block (line 9).

First thing to improve is to add some overrides to the ExpcetException method to support message formatting. It is some minutes work stuff, so you’ll do it :)

Enjoy the code!

Comments

Chad
Very nice. Thank you!

Comments