Struggling for Competence

Unit Test MsBuild Custom Task

We have a few custom MsBuild tasks in our build system at work. MsBuild tasks are not as easy to test as you might hope. My previous effort involved creating a process to call MsBuild and swearing a lot. I've had another go and come up with a much better solution; creating an MsBuild engine in code. Check it out:


using System.IO;
using System.Text;
using Microsoft.Build.BuildEngine;
using Microsoft.Build.Framework;
using NUnit.Framework;

namespace MsBuildDemo
{
    [TestFixture]
    public class TestMsBuild
    {
        const string projectXml =
@"<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003'>
  <Target Name='WriteToConsole'>
    <Message Text='Hullo World'/>
  </Target>
</Project>";
    
        public bool CallMsBuild(string buildProject, out string consoleOutput)
        {
            var engine = new Engine{DefaultToolsVersion = "3.5"};

            var project = new Project(engine);
            project.LoadXml(buildProject);

            var builder = new StringBuilder();
            var writer = new StringWriter(builder);
            WriteHandler handler = (x) => writer.WriteLine(x);
            var logger = new ConsoleLogger(LoggerVerbosity.Normal, handler, null, null);
            engine.RegisterLogger(logger);

            bool result = engine.BuildProject(project);

            engine.UnregisterAllLoggers();
            consoleOutput = builder.ToString();
            return result;
        }

        [Test]
        public void CallMsBuildAndCaptureOutput()
        {
            string consoleOutput;

            bool success = CallMsBuild(projectXml, out consoleOutput);

            Assert.IsTrue(success, consoleOutput);
            StringAssert.Contains("Hullo World", consoleOutput);
            StringAssert.DoesNotContain("MSB4056", consoleOutput);
        }
    }
}

The bit I am particularly pleased with is subverting the WriteHandler for the console logger to capture the output as a string.

I'm using the 3.5 version of the engine and framework. When I first tried to call the Message task in the test I got an error "MSB4127: The "Message" task could not be instantiated from the assembly". The solution was to create the engine with the property DefaultToolsVersion = "3.5".

I also spent quite a bit of time getting a warning "MSB4056: The MSBuild engine must be called on a single-threaded-apartment." The apartment is to do with COM interop, and so probably doesn't matter in this case. It is however annoying. I was able to make NUnit run in STA mode by creating a config file for the test assembly. NUnit has an attribute to set the apartment mode but this didn't seem to work, possibly because I am calling the tests via the ReSharper test runner, or possibly due to incompetence.

So that's calling an MsBuild project from inside a test. You can use this technique to unit test custom MsBuild tasks.