DeploymentItemAttribute for NUnit

In a previous post I talked about the proper use of the DeploymentItemAttribute when running MSTests. If you do move to NUnit and have gotten used to depending on this attribute then worry not, I have you covered. Below I have code samples showing the custom attribute I have created to imitate the MSTest behaviour and usage samples. A good scenario of when you might want to use this is when you have a resource used by multiple tests and can be modified by any of the tests, in which case you want each test to have its own fresh copy.


    public class SampleTest : TestBase
    {
        [DeploymentItemAttribute(@"resources\file.txt", "TestName")]
        [Test]
        public void TestName()
        {
        }
    }

    public class TestBase
    {
        [SetUp]
        public void Setup()
        {
            this.DeployItems();
        }

        private void DeployItems()
        {
            var currentType = this.GetType();
            var publicMethodsWithTestAttr = currentType.GetMethods(BindingFlags.Instance | BindingFlags.Public)
                .Where(x => x.CustomAttributes.Any(y=>y.AttributeType == typeof(TestAttribute)));

            //Iterate through each test method and deploy files as necessary
            foreach (MethodInfo testMethodInfo in publicMethodsWithTestAttr)
            {
                var deploymentItemAttrs = testMethodInfo.GetCustomAttributes(typeof(DeploymentItemAttribute));
                foreach (DeploymentItemAttribute deploymentItemAttr in deploymentItemAttrs)
                {
                    this.DeployFile(deploymentItemAttr.Path,deploymentItemAttr.OutputDirectory);
                }
            }
        }

        private void DeployFile(string path, string outputDirectory = null)
        {
            string filePath = path.Replace("/", "\\");

            string originalItemPath = new Uri(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), filePath)).LocalPath;
            string originalItemName = Path.GetFileName(originalItemPath);

            string runFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            Debug.WriteLine("DeploymentItem: Copying " + originalItemPath + " to " + runFolderPath);

            string itemPathInBin;
            if (string.IsNullOrEmpty(outputDirectory))
            {
                itemPathInBin = new Uri(Path.Combine(runFolderPath, originalItemName)).LocalPath;
            }
            else if (!string.IsNullOrEmpty(Path.GetPathRoot(outputDirectory)))
            {
                itemPathInBin = new Uri(Path.Combine(outputDirectory, originalItemName)).LocalPath;
            }
            else
            {
                itemPathInBin = new Uri(Path.Combine(runFolderPath, outputDirectory, originalItemName)).LocalPath;
            }

            if (File.Exists(originalItemPath)) // It's a file
            {
                string parentFolderPathInBin = new DirectoryInfo(itemPathInBin).Parent.FullName;

                if (!Directory.Exists(parentFolderPathInBin))
                {
                    Directory.CreateDirectory(parentFolderPathInBin);
                }

                File.Copy(originalItemPath, itemPathInBin, true);

                FileAttributes fileAttributes = File.GetAttributes(itemPathInBin);
                if ((fileAttributes & FileAttributes.ReadOnly) != 0)
                {
                    File.SetAttributes(itemPathInBin, fileAttributes & ~FileAttributes.ReadOnly);
                }
            }
            else if (Directory.Exists(originalItemPath)) // It's a folder
            {
                if (Directory.Exists(itemPathInBin))
                {
                    Directory.Delete(itemPathInBin, true);
                }

                Directory.CreateDirectory(itemPathInBin);

                foreach (string dirPath in Directory.GetDirectories(originalItemPath, "*", SearchOption.AllDirectories))
                {
                    Directory.CreateDirectory(dirPath.Replace(originalItemPath, itemPathInBin));
                }

                foreach (string sourcePath in Directory.GetFiles(originalItemPath, "*.*", SearchOption.AllDirectories))
                {
                    string destinationPath = sourcePath.Replace(originalItemPath, itemPathInBin);
                    File.Copy(sourcePath, destinationPath, true);

                    FileAttributes fileAttributes = File.GetAttributes(destinationPath);
                    if ((fileAttributes & FileAttributes.ReadOnly) != 0)
                    {
                        File.SetAttributes(destinationPath, fileAttributes & ~FileAttributes.ReadOnly);
                    }
                }
            }
            else
            {
                Debug.WriteLine("Warning: Deployment item does not exist - \"" + originalItemPath + "\"");
            }
        }
    }

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
    public class DeploymentItemAttribute : Attribute
    {
        public DeploymentItemAttribute(string path, string outputDirectory = null)
        {
            this.Path = path;
            this.OutputDirectory = outputDirectory;
        }

        public string Path { get; set; }

        public string OutputDirectory { get; set; }
    }

Proper user of DeploymentItemAttribute

One of the necessary evils of working with MS Test is knowing how to make use of DeployItemAttribute to ensure proper deployment of files when unit tests are run.

One thing that makes it challenging for programmers to appreciate this attribute is the inconsistency of how unit tests are executed, making it possible for some file accesses to work depending on how the test is run then suddenly when you deploy to team city you have to argue that it worked on your machine.

The DeploymentItemAttribute allows you to indicate which files you want to be available when running the unit tests with an option to have files copied to a unique folder for each test to ensure a clean copy for each tests. The DeploymentItemAttribute applies to test methods and can be applied multiple times.

    [TestClass]
    public class UnitTest1:UnitTest
    {

        [TestMethod]
        [DeploymentItem(@"Resources\sample.txt", "TestMethod1")]
        //Second parameters is optional and if omitted files will be deployed to the same TestMethod folder
        public void TestMethod1()
        {
            var path = this.GetDeploymentItemPath("sample.txt");
            Assert.IsTrue(true);
        }
    }

The code sample above makes use of an extension method to resolve the path to the deployed file during test run and a base class for all unit tests.

   public class UnitTest
    {
        public TestContext TestContext { get; set; }
    }

    public static class UnitTestExtensions
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string GetDeploymentItemPath(this UnitTest unitTest, string filename, int skipFrames = 1)
        {
            var stackTrace = new StackTrace(1, false);
            var method = stackTrace.GetFrame(0).GetMethod();
            var attributes = method.GetCustomAttributes(typeof(DeploymentItemAttribute), false);

            var deploymentItemAttributes = attributes.Cast<DeploymentItemAttribute>().ToList();

            var targetDeploymentItem = deploymentItemAttributes.FirstOrDefault(x => x.Path.IndexOf(filename, StringComparison.Ordinal) != -1);
            string path = Path.Combine(unitTest.TestContext.TestDeploymentDir, targetDeploymentItem.OutputDirectory, Path.GetFileName(targetDeploymentItem.Path));
            return path;
        }
    }