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!