Tests-behind: Tests as Code-Behind Files

One the first issues that you have to deal with when writing unit tests is – where do I put the the tests? Here’s where I prefer to have them: as close to the code that’s being tested as possible, like so:

tests behind 1

The tests class is hooked up to the original source file in the same way that ASP.NET code-behind files are, as a dependent project item.

You can do this by hacking the *csproj file directly using the <DependentUpon> tag, but to automate it I’ve written a Visual Studio macro (works in Visual Studio 2008, should work in Visual Studio 2005) that creates an appropriately-named tests class that can be invoked like so:

tests behind 2

Here’s the macro code (copy and paste into a code module in the Visual Studio Macros IDE: Tools > Macros > Macros IDE)

Sub AddTestsFile()
   Dim item As ProjectItem = DTE.SelectedItems.Item(1).ProjectItem
   Dim fileName As String = item.FileNames(1)
   Dim dir As String = System.IO.Path.GetDirectoryName(fileName)
   Dim bareName As String = System.IO.Path.GetFileNameWithoutExtension(fileName)
   Dim newItemPath As String = dir & "\" & bareName & ".Tests.cs"

   Dim codeClass As CodeClass = findClass(item.FileCodeModel.CodeElements)
   Dim namespaceName As String = codeClass.Namespace.FullName

   System.IO.File.WriteAllText(newItemPath, "" _
     & "#if DEBUG" & vbCrLf _
     & "using System;" & vbCrLf _
     & "using System.Diagnostics;" & vbCrLf _
     & "using NUnit.Framework;" & vbCrLf _
     & "" & vbCrLf _
     & "namespace " & namespaceName & vbCrLf _
     & "{" & vbCrLf _
     & "	[TestFixture]" & vbCrLf _
     & "	public class " & codeClass.Name & "_Tests" & vbCrLf _
     & "	{" & vbCrLf _
     & "		" & vbCrLf _
     & "	}" & vbCrLf _
     & "}" & vbCrLf _
     & "#endif" & vbCrLf _
    )

   ' Add as sub-item and show
   Dim newItem As ProjectItem = item.ProjectItems.AddFromFile(newItemPath)
   newItem.Open().Activate()

End Sub

' Utility used by AddTestsFile
Function findClass(ByVal items As System.Collections.IEnumerable) As CodeClass
   For Each codeEl As CodeElement In items
      If codeEl.Kind = vsCMElement.vsCMElementClass Then
         Return codeEl
      ElseIf codeEl.Children.Count &gt; 0 Then
         Dim cls As CodeClass = findClass(codeEl.Children)
         If cls IsNot Nothing Then
            Return findClass(codeEl.Children)
         End If
      End If
   Next
   Return Nothing
End Function

The right-click Project Item context menu shortcut can be wired up to the macro with the help of Sara Ford’s tip about customizing Visual Studio context menus.

Update 11 March 2008: Fixed findClass subroutine which resulted in null reference error, it now recurses correctly.