Geeks With Blogs
Chris Falter .NET Design and Best Practices
Suppose you have to write a test to verify that an edit form will throw a certain exception under certain conditions.  If you don't use VB 9 features, you might end up with this effusion of verbosity:
 
        Try
            editForm.Save(MyDataContext)
            Throw New AssertFailedException("Save should have thrown an exception")
        Catch ex As Exception
            If Not (ex.GetType().Equals(GetType(BusinessOperationException))) Then
                Dim errorMessage = String.Format("Save should should have thrown BusinessOperationException; threw {0} instead", ex.GetType())
                Throw New AssertFailedException(errorMessage)
            End If
            Dim bopException = DirectCast(ex, BusinessOperationException)
            Assert.IsTrue(bopException.DataFormErrorInfo.SQLErrorInfo.ErrorMessage.Contains("ERR_MEMBERSPECIFIEDMEMBERS_INVALID"))
        End Try
 
Of course, you could make the code a little simpler by not handling any exceptions of the wrong type, which would implicitly cause the test to fail if an exception of the wrong type is thrown:
 
         Try
             editForm.Save(MyDataContext) 
             Throw New AssertFailedException("Save should have thrown an exception")
         Catch ex As BusinessOperationException
             ' exceptions of other types are not handled 
             Assert.IsTrue(ex.DataFormErrorInfo.SQLErrorInfo.ErrorMessage.Contains("ERR_MEMBERSPECIFIEDMEMBERS_INVALID"))
         End Try
 
While this is better, the code is not without problems:
  • The reader of this code has to infer that exceptions of a type other than BusinessOperationException will fail cause a test failure.  Yes, you can mitigate this by including a helpful comment--if you remember.  I'd rather not have to remember.
  • The intention of the test is buried inside the mechanics of the try/catch block.
  • The assertion about the state of the BusinessOperationException instance is not easy to follow, since the reader of the code has to walk a lengthy property hierarchy (DataFormErrorInfo => SQLErrorInfo => ErrorMessage).  This is a glaring violation of the Law of Demeter.  In addition, it is not self-evident from reading the code that DataFormErrorInfo.SQLErrorInfo.ErrorMessage would naturally be the location where one would find the argument to a SQL raiserror call.
Prior to VB 9 it would have been very difficult to write this code with a smoother syntax due to the limitations of the language.  But now we can solve these problems by using lambda expressions and extension methods, as follows:
 
        Dim saveForm As Action = Function() Me.Save(editForm)
        saveForm.ShouldThrow(Of BusinessOperationException).Where(Function(e) e.DBExceptionCode.Contains("ERR_MEMBERSPECIFIEDMEMBERS_INVALID"))
 
Here the fact that the form's .Save() method should throw a BusinessOperationException with a "ERR_MEMBERSPECIFIEDMEMBERS_INVALID" exception code walks up to the reader and punches him in the face.  And that's what we want all of our code to do; pugnacious code is good code!  Moreover, the ShouldThrow() and Where() methods will emit useful, detailed messages for any error conditions.  Let's see how VB 9 helps us to work this magic.

Extension Methods

An extension method acts as if it belongs to a type; thus you may create an extension method for a type even if you do not possess the type's source code.  Without extension methods, your only choice is to create a shared helper method that takes a type instance as a parameter.  For example, in earlier VB versions you could have written a shared Throws() method that takes an Action delegate as a parameter.  However, the extension method has greater clarity and readability:
 
Extension Methods vs. Shared/Static Methods
Coding Technique Extension Method Shared Method
Syntax Subject + Verb + arguments/modifiers Verb + list of arguments
Example saveForm.ShouldThrow(Of BusinessOperationException) Throws(Of BusinessOperationException)(saveForm)
Readability Close to standard English - very readable

Completely at odds with English grammar.  And when there are many arguments, which is the subject and which are modifiers?

 
 
Ultimately an extension method is syntactical sugar; then again, a little sugar can help you bake some wonderful recipes.  So let's check it out.
 
In the solution code above, the generic ShouldThrow(Of TException) method is as an extension method to the .NET Action delegate.  Let me pause for a moment to let that concept sink in; we are writing code that extends a built-in .NET type.  And that type isn't even a class; it's a delegate!  Clearly, extension methods open up a new realm of possibilities to us.  Decorating a standard shared method with the System.Runtime.CompilerServices.ExtensionAttribute turns it into a method that behaves as if it were a member of the type of its first argument.  Thus the first argument to the ShouldThrow method in Blackbaud.AppFx.UnitTesting/AssertExtensions.vb is of type Action delegate:
 
    <Extension()> _
    Public Function ShouldThrow(Of TException As Exception)(ByVal workToDo As Action) As TException
        Dim errorMessage As New StringBuilder("An exception of type ")
        errorMessage.Append(GetType(TException))
        errorMessage.Append(" should have been thrown, but wasn't.  Test method info follows...")
        errorMessage.AppendLine()
        errorMessage.AppendCallingMethodInfo()
        Return ShouldThrow(Of TException)(workToDo, errorMessage.ToString())
    End Function
 
    <Extension()> _
    Public Function ShouldThrow(Of TException As Exception)(ByVal workToDo As Action, ByVal errorMessage As String) As TException
        Try
            workToDo.Invoke()
            Throw New AssertFailedException(errorMessage)
        Catch ex As Exception
            If (GetType(TException).Equals(ex.GetType)) Then
                Return ex
            End If
            Throw
        End Try
    End Function
 
The .ShouldThrow() method encapsulates all of the try/catch logic that had obfuscated the earlier versions of our test code.  Just specify the type of the exception that should be thrown when you call it, and it will do one of three things:
  • return the thrown exception of the desired type,
  • rethrow a thrown exception of the wrong type, or
  • throw its own exception if an exception is not thrown.
In VB an extension method must belong to a module.  (C# uses a static class.)  After you compile the module containing the ShouldThrow() method, the .NET languages will allow you to use it as if it were actually a member of the Action delegate type.  An extension method does have a limitation that true members of a type do not have: it cannot access the non-public members of the type.  Alas, even magic has limits.
 
Another nice aspect of writing an extension method is that it will even show up in Intellisense!   Already the advantage of the the extension method is clear; if you were using an older-style shared method, Intellisense would not list it as a candidate for use with the Action delegate saveAction.  
 
While an overload of the ShouldThrow() method is available for those who wish to provide a custom error message, the default (parameterless) overload uses the AppendCallingMethodInfo() method to incorporate the name, source file, and line number of the calling test method into a useful error message.  I recommend using the default overload of ShouldThrow() for most situations, since it produces a very useful error message while keeping your code's syntax as simple as possible.  The alert reader will note that AppendCallingMethodInfo() is implemented as an extension method to the StringBuilder class; the curious may read through AssertExtensions.vb source file to see how the info is extracted from a stack trace.
 
The AssertExtensions module also contains the .Where() extension method overloads which, as we see from their first parameter, can be called on a generic exception.
 
 
    <Extension()> _
    Public Function Where(Of TException As Exception) _
    (ByVal ex As TException, ByVal predicate As Func(Of TException, Boolean)) _
    As Boolean
 
        Dim errorMessage As New StringBuilder("The " + GetType(TException).ToString() + " did not satisfy the Where predicate.  Test method info follows...")
        errorMessage.AppendLine()
        errorMessage.AppendCallingMethodInfo()
        Return Where(ex, predicate, errorMessage.ToString())
 
    End Function
 
    <Extension()> _
    Public Function Where(Of TException As Exception) _
    (ByVal ex As TException, ByVal predicate As Func(Of TException, Boolean), ByVal errorMessage As String) _
    As Boolean
 
        If Predicate(ex) Then
            Return True
        Else
            Throw New AssertFailedException(errorMessage)
        End If
 
    End Function
 
The Func keyword, new in VB 9, denotes a generic function delegate whose signature is determined by the types of the parameters.  The last parameter is the return type of the delegate, and the leading parameters before it (if any) specify the types of the input parameters. Applying this syntax to our code, we see that the parameter ByVal predicate As Func(Of TException, Boolean) denotes a delegate for a function that takes a generic TException argument and returns Boolean.   The Where() method invokes the delegate and takes care of throwing an AssertFailedException with an appropriate message if the delegate returns False.  You will recall that the sample code used a lambda expression to create the delegate invoked by Where().
 
Finally, the ability to write an extension method allows us to obey the Law of Demeter, even though we cannot modify the BBSolder-generated BusinessOperationException class.  In the unit test file, just define an extension method that traverses the properties:
 
Friend Module BusinessOperationExceptionExtensions
 
    <Extension()> _
    Friend Function DBExceptionCode(ByVal ex As BusinessOperationException) As String
        Return ex.DataFormErrorInfo.SQLErrorInfo.ErrorMessage
    End Function
 
End Module

Now for any BusinessOperationException ex, you can just call ex.DbExceptionCode instead of traversing the various properties.  Sweet!

Lambda Expressions

According to MSDN, a lambda expression is "a function without a name that calculates and returns a single value."  Think of a lambda expression as an executable block of code that is defined in-line, rather than in a function that must be defined somewhere else.  A lambda expression can also make use of variables that are in scope at the point where it is defined.  Because a lambda expression helps you keep the flow of logic in one place and avoid the ceremony of passing parameters, it can serve as a powerful tool for simplifying your code.
 
You will recall that in our sample code, the .Where extension method processes a lambda expression:
 
.Where(Function(e) e.DBExceptionCode.Contains("ERR_MEMBERSPECIFIEDMEMBERS_INVALID"))
 
The Function keyword indicates that the expression which follows is a lambda.  Behind the scenes, the VB compiler will do the following:
  • build an anonymous function that takes a parameter of type BusinessOperationException and returns Boolean; the lambda expression then becomes the body of the function. 
  • infer that the generic type TException declared in the .Where() method definition will be of type BusinessOperationException in this block of code. 
  • build a delegate to that anonymous function, and then pass that delegate as an argument to the .Where extension method, which has a ByVal predicate As Func(Of TException, Boolean) parameter. 
And you thought you worked hard!  Well, why not let the compiler do the hard work for you?  You could think of a lambda expression as a way to get the compiler to handle all the plumbing chores of function bodies and delegates, while leaving just the essence of the logic visible in your code. 

Conclusion

Extension methods and lambda expressions can help you write code that is more succinct, more readable, and more powerful.  According to Microsoft's Eric White, "Functional Programming (FP) has the potential to reduce program line count by 20% to 50%, reduce bugs and increase robustness, and move us in the direction of taking advantage of multiple core CPUs."  In this article we saw how extension methods and lambda expressions, which are important parts of the VB functional programming toolkit, made the sample test code more readable and succinct.  Of course these new techniques are not a golden hammer that can be used everywhere; nevertheless, be alert for opportunities to use them in the test code you write.

Further Reading

Source Listing for AssertExtensions.vb

Imports System.Diagnostics
Imports System.Runtime.CompilerServices
Imports System.Text
 
Public Module AssertExtensions
 
    ''' <summary>
    ''' Verifies that a specific method call throws an exception of a specific type
    ''' </summary>
    ''' <typeparam name="TException">The type of exception the method call is expected to throw</typeparam>
    ''' <param name="workToDo">delegate to method call which is expected to throw an exception</param>
    ''' <returns>
    ''' instance of exception thrown by the Action
    ''' </returns>
    ''' <remarks>
    ''' This method will re-throw any exceptions of a type other than that designated by the generic TException.
    ''' If the Action does not throw any exceptions, this method will throw an AssertFailedException.
    ''' </remarks>
    <Extension()> _
    Public Function ShouldThrow(Of TException As Exception)(ByVal workToDo As Action) As TException
        Dim errorMessage As New StringBuilder("An exception of type ")
        errorMessage.Append(GetType(TException))
        errorMessage.Append(" should have been thrown, but wasn't.  Test method info follows...")
        errorMessage.AppendLine()
        errorMessage.AppendCallingMethodInfo()
        Return ShouldThrow(Of TException)(workToDo, errorMessage.ToString())
    End Function
 
    ''' <summary>
    ''' Verifies that a specific method call throws an exception of a specific type
    ''' </summary>
    ''' <typeparam name="TException">The type of exception the method call is expected to throw</typeparam>
    ''' <param name="workToDo">delegate to method call which is expected to throw an exception</param>
    ''' <param name="errorMessage">user-defined error message if expected exception not thrown</param>
    ''' <returns>
    ''' instance of exception thrown by the Action
    ''' </returns>
    ''' <remarks>
    ''' This method will re-throw any exceptions of a type other than that designated by the generic TException.
    ''' If the Action does not throw any exceptions, this method will throw an AssertFailedException.
    ''' </remarks>
    <Extension()> _
    Public Function ShouldThrow(Of TException As Exception)(ByVal workToDo As Action, ByVal errorMessage As String) As TException
        Try
            workToDo.Invoke()
            Throw New AssertFailedException(errorMessage)
        Catch ex As Exception
            If (GetType(TException).Equals(ex.GetType)) Then
                Return ex
            End If
            Throw
        End Try
    End Function
 
    <Extension()> _
    Public Sub AppendCallingMethodInfo(ByVal sb As StringBuilder, Optional ByVal howManyFramesBack As Integer = 1)
        ' how far back is from the perspective of the caller of this method. +1 gets us back to the frame of 
        ' this method's caller, then we continue from there
        howManyFramesBack += 1
        Dim st As New StackTrace(fNeedFileInfo:=True)
        Dim sf = st.GetFrame(howManyFramesBack)
        sb.Append("Test method: ")
        sb.Append(sf.GetMethod())
        sb.AppendLine()
        sb.Append("FileName: ")
        sb.Append(sf.GetFileName())
        sb.AppendLine()
        sb.Append("Line Number: ")
        sb.Append(sf.GetFileLineNumber())
    End Sub
 
    ''' <summary>
    ''' Verifies that an exception satisfies a predicate
    ''' </summary>
    ''' <typeparam name="TException">The type of exception against which the predicate is applied</typeparam>
    ''' <param name="ex">The exception against which the predicate is applied (passed by compiler)</param>
    ''' <param name="predicate">The condition which the exception must satisfy</param>
    ''' <returns>True if exception satisfies predicate, otherwise an AssertFailedException is thrown</returns>
    ''' <remarks>
    ''' Unlike the IEnumerable(Of T).Where extension method, this method does not return a subset from a set.  
    ''' Instead it verifies that a particular instance of an exception satisfies a predicate.  This method is 
    ''' designed to be used in conjunction with ShouldThrow to provide a fluent expression for a test.
    ''' <example>
    ''' Dim myAction As Action = (Function() MyMethod(myData))
    ''' myAction.ShouldThrow(Of SpecializedException)().Where(Function(ex) ex.SpecialProperty = "Hello")
    ''' </example>
    ''' </remarks>
    <Extension()> _
    Public Function Where(Of TException As Exception) _
    (ByVal ex As TException, ByVal predicate As Func(Of TException, Boolean)) _
    As Boolean
 
        Dim errorMessage As New StringBuilder("The " + GetType(TException).ToString() + " did not satisfy the Where predicate.  Test method info follows...")
        errorMessage.AppendLine()
        errorMessage.AppendCallingMethodInfo()
        Return Where(ex, predicate, errorMessage.ToString())
 
    End Function
 
    ''' <summary>
    ''' Verifies that an exception satisfies a predicate, and throws exception with user-supplied message if not
    ''' </summary>
    ''' <typeparam name="TException">The type of exception against which the predicate is applied</typeparam>
    ''' <param name="ex">The exception against which the predicate is applied (passed by compiler)</param>
    ''' <param name="predicate">The condition which the exception must satisfy</param>
    ''' <param name="errorMessage">Message of AssertFailedException thrown if predicate fails</param>
    ''' <returns>True if exception satisfies predicate, otherwise an AssertFailedException is thrown</returns>
    ''' <remarks>
    ''' Unlike the IEnumerable(Of T).Where extension method, this method does not return a subset from a set.  
    ''' Instead it verifies that a particular instance of an exception satisfies a predicate.  This method is 
    ''' designed to be used in conjunction with ShouldThrow to provide a fluent expression for a test.
    ''' <example>
    ''' Dim myAction As Action = (Function() MyMethod(myData))
    ''' myAction.ShouldThrow(Of MyException)().Where(Function(ex) ex.MyProp = "Hello", "MyProp wasn't 'Hello')
    ''' </example>
    ''' </remarks>
    <Extension()> _
    Public Function Where(Of TException As Exception) _
    (ByVal ex As TException, ByVal predicate As Func(Of TException, Boolean), ByVal errorMessage As String) _
    As Boolean
 
        If Predicate(ex) Then
            Return True
        Else
            Throw New AssertFailedException(errorMessage)
        End If
 
    End Function
 
End Module

 

Posted on Wednesday, December 30, 2009 3:23 PM Coding Practices and Design Patterns , Testing & Debugging | Back to top


Comments on this post: How to Use Extension Methods and Lambda Expressions to Write Elegant Unit Tests

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © Chris Falter | Powered by: GeeksWithBlogs.net