Unit Testing Custom MSBuild Tasks

I'm starting to really groove on the Team Edition of VS. I can now get rid of updating NAnt, NUnit, NCover, NProf, and FxCop (well, FxCop comes with Team Edition, and one can argue that MSTest is just NUnit, etc., etc.). I've got nothing against these tools - they're awesome open-source tools. But, if you can use Team Edition, you might as well use you get out of the box, right?

Anyway, one of my recent activities was writing a custom MSBuild task. This morning I thought of what it would take to unit test a custom task. I was worried because as soon as a class need some kind of contextural harness (like ASP.NET), things can get hairy. So I started by doing a Google search, and I found this. I understand his reasoning, but what I didn't like about this approach is that you either had to check to see if the build engine was set (which it really should be given that this is a MSBuild task) or you have to fire up msbuild.exe to ensure the correct context exists. I commented that it might be possible to mock a IBuildEngine class just so you could test your task, and sure enough, you can. First, here's an example of a simple logging task:

Imports Microsoft.Build.Framework
Imports Microsoft.Build.Utilities

Public Class LoggingTask
    Inherits Task

    Private _message As String

    <Required()> _
    Public Property Message() As String
        Get
            Return _message
        End Get
        Set(ByVal value As String)
            _message = value
        End Set
    End Property

    Public Overrides Function Execute() As Boolean
        Log.LogMessage(_message)
        Return True
    End Function
End Class

Now here's my test class to exercise that task:

Imports LoggingTask
Imports System
Imports System.Text
Imports System.Collections.Generic
Imports Microsoft.VisualStudio.TestTools.UnitTesting

<TestClass()> _
Public Class LoggingTaskTests
    Dim _task As LoggingTask

    <TestInitialize()> _
    Public Sub Initialize()
        _task = New LoggingTask()
        _task.BuildEngine = New MockBuildEngine()
    End Sub

    <TestMethod()> _
    Public Sub TestLogTask()
        _task.Message = "Log this!"
        Assert.IsTrue(_task.Execute())
    End Sub

    <TestMethod(), ExpectedException(GetType(ArgumentNullException))> _
    Public Sub TestLogTaskWithNoMessage()
        _task.Execute()
    End Sub
End Class

Note that I'm setting the BuildEngine to an instance of MockBuildEngine - here's what that class looks like:

Imports Microsoft.Build.Framework
Imports System.Collections

Public Class MockBuildEngine
    Implements IBuildEngine

    Public Function BuildProjectFile(ByVal projectFileName As String, _
        ByVal targetNames() As String, ByVal globalProperties As IDictionary, _
        ByVal targetOutputs As IDictionary) As Boolean _
        Implements IBuildEngine.BuildProjectFile
        Throw New NotImplementedException()
    End Function

    Public ReadOnly Property ColumnNumberOfTaskNode() As Integer _
        Implements IBuildEngine.ColumnNumberOfTaskNode
        Get
            Throw New NotImplementedException()
        End Get
    End Property

    Public ReadOnly Property ContinueOnError() As Boolean _
        Implements IBuildEngine.ContinueOnError
        Get
            Throw New NotImplementedException()
        End Get
    End Property

    Public ReadOnly Property LineNumberOfTaskNode() As Integer _
        Implements IBuildEngine.LineNumberOfTaskNode
        Get
            Throw New NotImplementedException()
        End Get
    End Property

    Public Sub LogCustomEvent(ByVal e As CustomBuildEventArgs) _
        Implements IBuildEngine.LogCustomEvent
    End Sub

    Public Sub LogErrorEvent(ByVal e As BuildErrorEventArgs) _
        Implements IBuildEngine.LogErrorEvent
    End Sub

    Public Sub LogMessageEvent(ByVal e As BuildMessageEventArgs) _
        Implements IBuildEngine.LogMessageEvent
    End Sub

    Public Sub LogWarningEvent(ByVal e As BuildWarningEventArgs) _
        Implements IBuildEngine.LogWarningEvent
    End Sub

    Public ReadOnly Property ProjectFileOfTaskNode() As String _
        Implements IBuildEngine.ProjectFileOfTaskNode
        Get
            Throw New NotImplementedException()
        End Get
    End Property
End Class

It's pretty bare-bones, but at least my tests will pass with this approach. A more "robust" mock of IBuildEngine may be needed for other tasks, but this is far easier than what I had to deal with to mock an HttpContext class!

* Posted at 07.19.2006 01:34:44 PM CST | Link *

Blog History