Mark Gilbert's Blog

Science and technology, served light and fluffy.

Test-Driven Development at Mock 1

As you may have read in my previous posts on “The Agile Path”, the .NET Team here at BlueGranite has been slowly adopting and adapting agile practices over the past 2 years.  The most current example of this came at the beginning of this year, when one of our project teams was starting a new custom .NET application.  I was associated with the project as a mentor/coach.

There were several areas of the project where there wasn’t going to be an associated user interface.  We still wanted a way to test these areas, and after some discussion decided that it would be a great time to try test-driven development (for those readers that aren’t familiar with this practice, I respectfully refer you to TestDriven.com for information, articles, tools, etc.).

One of the first things the team wanted to build was a strongly-typed wrapper class for the session object, so instead of making a reference to Current.Session(“UserID”), the developer would reference MySession.UserID.  The latter being strongly typed and a defined property of the class would allow the compiler to catch when the developer accidentally misspells the property name.  The MySession class would also allow us to build methods like “ClearUserInfo” and “InitializeOrder” – things that operated on multiple items in the session.

The challenge here is that Current.Session is normally managed by the web server.  When we’re running our tests via NUnit there is no web server, so the “Current” object comes back as “Nothing”.

First Attempt

Our first attempt at meeting this challenge was to create a flag called “m_blnInTestMode”.  This flag would default to False, but the unit tests would set it to “True” before running.  Then, we would create a generic collection call “MyCollection” which would hold the session values when the class was being exercised by the unit tests.  Finally, the class properties would take on the general form of:

If (m_blnInTestMode) Then
      MyCollection(“UserID”) = strNewValue
Else
      Current.Session(“UserID”) = strNewValue
End If

There are two glaring problems with this approach.  First, we’re writing almost twice as much code for everything.  Secondly – and even more importantly – we’re not getting any of the benefits of the unit tests because they aren’t testing the real code.  While my TestRunner metrics may report that I have 50-60% code coverage, it’s the other 40% that I set out to test in the first place.

Second Attempt

Ok, so like any good NT-personality I continued digging to see if there was a better way.  The basic challenge is in getting Current.Session to exist when the unit tests are running.  So, I started looking for a way to fake – or mock up – Current.Session, well enough to allow me to store data in it like the application would when being run by a web server.

If we could find a way to mock up Current.Session, then the MySession constructor could take care of creating it if it didn’t already exist, and we could eliminate the “If in-test-mode-do-this else…” constructs, and simply access Current.Sesssion.  This would mean that we could avoid the two criticisms of the first approach – we’re only writing the logic once, and the code that we’re writing is what is being tested.

First, I noticed that when the unit tests were running, the “Current” object would return “Nothing”.  When I ran it using the web server, this had a valid object behind it.  This gave me an easy way to detect when the code was being run from a web server versus the unit tests, so I could ditch my test-mode flag.

Then, I found a blog posting by James Geurts that showed how to mock up the HttpContext object.  Although I translated his example from C# to VB, his sample was exactly what I needed to create the “Current” object.

The next step was to mock up the Current.Session object.  For that, I turned to an MSDN article on the AddHttpSessionStateToContext method of SessionStateUtility.  After modifying their example a bit, I was able to mock up Current.Session.

The resulting MySession class constructor (and associated class declarations) is below:

Imports System.Web.HttpContext
Imports System.IO
Imports System.Web.Hosting

Public Class MySession

    Class SessionItem
        Friend Items As SessionStateItemCollection
        Friend StaticObjects As HttpStaticObjectsCollection
        Friend Expires As DateTime
    End Class

    Private m_siSessionData As SessionItem
    Private m_htSessionItems As Hashtable = New Hashtable()
    Private m_intTimeout As Integer
    Private m_hcmCookieMode As HttpCookieMode = HttpCookieMode.UseCookies
    Private m_rwlHashtableLock As Threading.ReaderWriterLock = New Threading.ReaderWriterLock()
    Private m_strSessionID As String

    Public Sub New()
        ‘ Check to see if we have a valid session already; if not, create a mock one
        If (IsNothing(Current)) Then
            Dim strPath, strVirtualDir As String
            Dim twCurrent As StringWriter
            Dim hwrCurrent As HttpWorkerRequest
            Dim blnIsNew As Boolean
            Dim hsscCurrent As HttpSessionStateContainer

            ‘ ***********************************************************************
            ‘ Mock up the HttpContext object
            ‘ Adapted from
http://www.biasecurities.com/blogs/jim/archive/2005/08/11/2058.aspx:
            strPath = “C:\Documents and Settings\mgilbert\Desktop\Session Test\SessionTest\”
            strVirtualDir = “/SessionTest”

            AppDomain.CurrentDomain.SetData(“.appDomain”, “*”)
            AppDomain.CurrentDomain.SetData(“.appPath”, strPath)
            AppDomain.CurrentDomain.SetData(“.appVPath”, strVirtualDir)
            AppDomain.CurrentDomain.SetData(“.hostingVirtualPath”, strVirtualDir)
            AppDomain.CurrentDomain.SetData(“.hostingInstallDir”, HttpRuntime.AspInstallDirectory)

            twCurrent = New StringWriter
            hwrCurrent = New SimpleWorkerRequest(“default.aspx”, “”, twCurrent)
            HttpContext.Current = New HttpContext(hwrCurrent)

            ‘ ***********************************************************************
            ‘ Mock up the HttpSessionState object to attach to the HttpContext.Current
            ‘ object.
            ‘ Adapted from http://msdn2.microsoft.com/en-us/library/system.web.sessionstate.sessionstateutility.addhttpsessionstatetocontext(VS.80).aspx

            blnIsNew = True
            Me.m_siSessionData = New SessionItem
            Me.m_siSessionData.Items = New SessionStateItemCollection()
            Me.m_siSessionData.StaticObjects = SessionStateUtility.GetSessionStaticObjects(HttpContext.Current)
            Me.m_siSessionData.Expires = DateTime.Now.AddMinutes(Me.m_intTimeout)
            Me.m_strSessionID = System.Guid.NewGuid.ToString

            Try
                Me.m_rwlHashtableLock.AcquireWriterLock(Int32.MaxValue)
                Me.m_htSessionItems(Me.m_strSessionID) = Me.m_siSessionData
            Finally
                Me.m_rwlHashtableLock.ReleaseWriterLock()
            End Try

            hsscCurrent = New HttpSessionStateContainer(Me.m_strSessionID, _
                                                        Me.m_siSessionData.Items, _
                                                        Me.m_siSessionData.StaticObjects, _
                                                        Me.m_intTimeout, _
                                                        blnIsNew, _
                                                        Me.m_hcmCookieMode, _
                                                        SessionStateMode.Custom, _
                                                        False)
            SessionStateUtility.AddHttpSessionStateToContext(HttpContext.Current, hsscCurrent)
        End If
    End Sub
End Class

Our constructor slight-of-hand allows the MySession properties and methods to remain blissfully unaware about the origins of Current.Session:

    Public Property UserID() As Integer
        Get
            Return
Current.Session.Item(“UserID”)
        End Get
        Set
(ByVal value As Integer)
            Current.Session.Item(“UserID”) = value
        End Set
    End Property

The constructor certainly could use some aditional polishing to make it more generic, but I think it demonstrates that mocking up Current.Session was a much more elegant way to handle testing our session wrapper.

Advertisements

March 26, 2007 - Posted by | Visual Studio/.NET

1 Comment

  1. using System;
    using System.Collections;
    using System.IO;
    using System.Threading;
    using System.Web;
    using System.Web.Hosting;
    using System.Web.SessionState;

    namespace TestUtilities
    {
    ///
    /// Creates a context and an HTTPSession for testing.
    ///
    ///
    /// Adapted from Mark Gilbert’s Blog https://markegilbert.wordpress.com/2007/03/26/test-driven-developthisnt-at-mock-1/
    ///
    public class MockSession
    {
    class SessionItem
    {
    internal SessionStateItemCollection Items;
    internal HttpStaticObjectsCollection StaticObjects;
    internal DateTime Expires;
    }

    #region Fields
    private SessionItem m_siSessionData;
    private Hashtable m_htSessionItems = new Hashtable();
    private int m_intTimeout;
    private HttpCookieMode m_hcmCookieMode = HttpCookieMode.UseCookies;
    private ReaderWriterLock m_rwlHashtableLock = new ReaderWriterLock();
    private string m_strSessionID;
    #endregion

    public MockSession( string strPath, string strVirtualDir )
    {
    //Check to see if we have a valid session already; if not, create a mock one
    if( HttpContext.Current == null )
    {
    StringWriter twCurrent;
    HttpWorkerRequest hwrCurrent;
    bool blnIsNew;
    HttpSessionStateContainer hsscCurrent;

    //***********************************************************************
    // Mock up the HttpContext object
    // Adapted from http://www.biasecurities.com/blogs/jim/archive/2005/08/11/2058.aspx:

    AppDomain.CurrentDomain.SetData( “.appDomain”, “*” );
    AppDomain.CurrentDomain.SetData( “.appPath”, strPath );
    AppDomain.CurrentDomain.SetData( “.appVPath”, strVirtualDir );
    AppDomain.CurrentDomain.SetData( “.hostingVirtualPath”, strVirtualDir );
    AppDomain.CurrentDomain.SetData( “.hostingInstallDir”, HttpRuntime.AspInstallDirectory );

    twCurrent = new StringWriter();
    hwrCurrent = new SimpleWorkerRequest( “default.aspx”, “”, twCurrent );
    HttpContext.Current = new HttpContext( hwrCurrent );

    //***********************************************************************
    // Mock up the HttpSessionState object to attach to the HttpContext.Current
    // object.
    // Adapted from http://msdn2.microsoft.com/en-us/library/system.web.sessionstate.sessionstateutility.addhttpsessionstatetocontext(VS.80).aspx

    blnIsNew = true;
    this.m_siSessionData = new SessionItem();
    this.m_siSessionData.Items = new SessionStateItemCollection();
    this.m_siSessionData.StaticObjects = SessionStateUtility.GetSessionStaticObjects(HttpContext.Current);
    this.m_siSessionData.Expires = DateTime.Now.AddMinutes( this.m_intTimeout );
    this.m_strSessionID = System.Guid.NewGuid().ToString();

    try
    {
    this.m_rwlHashtableLock.AcquireWriterLock(Int32.MaxValue);
    this.m_htSessionItems[this.m_strSessionID] = this.m_siSessionData;
    }
    finally
    {
    this.m_rwlHashtableLock.ReleaseWriterLock();
    }

    hsscCurrent = new HttpSessionStateContainer(this.m_strSessionID,
    this.m_siSessionData.Items,
    this.m_siSessionData.StaticObjects,
    this.m_intTimeout,
    blnIsNew,
    this.m_hcmCookieMode,
    SessionStateMode.Custom,
    false);
    SessionStateUtility.AddHttpSessionStateToContext(HttpContext.Current, hsscCurrent);
    }
    }
    }
    }

    Comment by Lew Wolf | March 27, 2008


Sorry, the comment form is closed at this time.

%d bloggers like this: