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
    public class TestMsBuild
        const string projectXml =
@"<Project xmlns=''>
  <Target Name='WriteToConsole'>
    <Message Text='Hullo World'/>
        public bool CallMsBuild(string buildProject, out string consoleOutput)
            var engine = new Engine{DefaultToolsVersion = "3.5"};

            var project = new Project(engine);

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

            bool result = engine.BuildProject(project);

            consoleOutput = builder.ToString();
            return result;

        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.