NUnit DeploymentItem

In MsTest if I need some file from another project for my test, I can specify DeploymentItem attribute. Is there anything similar in NUnit?

Answers


You should check out another thread that contrasts the capabilities of NUnit and MSTest.

The accepted answer here is misleading. NUnit does not offer the [DeploymentItem("")] attribute at all which is what @Idsa wanted an equivalent solution for in NUnit.

My guess is that this kind of attribute would violate the scope of NUnit as a "unit" testing framework as requiring an item to be copied to the output before running a test implies it has a dependency on this resource being available.

I'm using a custom attribute to copy over a localdb instance for running "unit" tests against some sizeable test data that I'd rather not generate with code everytime.

Now using the attribute [DeploymentItem("some/project/file")] will copy this resource from file system into the bin again effectively refreshing my source data per test method:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Struct, 
    AllowMultiple = false, 
    Inherited = false)]
public class DeploymentItem : System.Attribute {
    private readonly string _itemPath;
    private readonly string _filePath;
    private readonly string _binFolderPath;
    private readonly string _itemPathInBin;
    private readonly DirectoryInfo _environmentDir;
    private readonly Uri _itemPathUri;
    private readonly Uri _itemPathInBinUri;

    public DeploymentItem(string fileProjectRelativePath) {
        _filePath = fileProjectRelativePath.Replace("/", @"\");

        _environmentDir = new DirectoryInfo(Environment.CurrentDirectory);
        _itemPathUri = new Uri(Path.Combine(_environmentDir.Parent.Parent.FullName
            , _filePath));

        _itemPath = _itemPathUri.LocalPath;
        _binFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

        _itemPathInBinUri = new Uri(Path.Combine(_binFolderPath, _filePath));
        _itemPathInBin = _itemPathInBinUri.LocalPath;

        if (File.Exists(_itemPathInBin)) {
            File.Delete(_itemPathInBin);
        }

        if (File.Exists(_itemPath)) {
            File.Copy(_itemPath, _itemPathInBin);
        }
    }
}

Then we can use like so:

[Test]
[DeploymentItem("Data/localdb.mdf")]
public void Test_ReturnsTrue() 
{
    Assert.IsTrue(true);
}

I've done a few improvements to Alexander Pasha's solution: I've given the attribute the same signature as the MSTest one, such that the first parameter is the absolute or relative file or folder to deploy, and the optional second parameter is the absolute or relative path to which it is to be deployed. In both cases 'relative' means to the executing program. Also I've removed any read-only attribute from the deployed file. This is important - if a previously deployed file can't be overwritten the attribute will throw. It is also worth noting that MSTest and NUnit have very different strategies when it comes to deploying the files that are to be used during testing. MSTest may or may not copy files to a deployment folder - see here. NUnit uses the ShadowCopyFiles property of the AppDomain, which deploys to a very obscure location in the user's temp folder. While shadow copying can be turned on and off in NUnit itself, I don't know how to manipulate it when using Test Explorer in Visual Studio. In this regard it is important to note that the Visual Studio NUnit Test Adapter prior to Version 2 has shadow copying turned on but in version 2 onwards it is turned off. This can have a major impact on tests that use deployment items and is well worth being aware of. Here is my version of the DeploymentItemAttribute:-

namespace NUnitDeploymentItem
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
    public class DeploymentItemAttribute : Attribute
    {
        /// <summary>
        /// NUnit replacement for Microsoft.VisualStudio.TestTools.UnitTesting.DeploymentItemAttribute
        /// Marks an item to be relevant for a unit-test and copies it to deployment-directory for this unit-test.
        /// </summary>
        /// <param name="path">The relative or absolute path to the file or directory to deploy. The path is relative to the build output directory.</param>
        /// <param name="outputDirectory">The path of the directory to which the items are to be copied. It can be either absolute or relative to the deployment directory.</param>
        public DeploymentItemAttribute(string path, string outputDirectory = null)
        {
            // Escape input-path to correct back-slashes for Windows
            string filePath = path.Replace("/", "\\");

            // Look up where we are right now
            DirectoryInfo environmentDir = new DirectoryInfo(Environment.CurrentDirectory);

            // Get the full path and name of the deployment item
            string itemPath = new Uri(Path.Combine(environmentDir.FullName, filePath)).LocalPath;
            string itemName = Path.GetFileName(itemPath);

            // Get the target-path where to copy the deployment item to
            string binFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            // NUnit uses an obscure ShadowCopyCache directory which can be hard to find, so let's output it so the poor developer can get at it more easily
            Debug.WriteLine("DeploymentItem: Copying " + itemPath + " to " + binFolderPath);

            // Assemble the target path
            string itemPathInBin;
            if (string.IsNullOrEmpty(outputDirectory))
            {
                itemPathInBin = new Uri(Path.Combine(binFolderPath, itemName)).LocalPath;
            }
            else if (!string.IsNullOrEmpty(Path.GetPathRoot(outputDirectory)))
            {
                itemPathInBin = new Uri(Path.Combine(outputDirectory, itemName)).LocalPath;
            }
            else
            {
                itemPathInBin = new Uri(Path.Combine(binFolderPath, outputDirectory, itemName)).LocalPath;
            }

            // Decide whether it's a file or a folder
            if (File.Exists(itemPath)) // It's a file
            {
                // Assemble the parent folder path (because the item might be in multiple sub-folders.
                string parentFolderPathInBin = new DirectoryInfo(itemPathInBin).Parent.FullName;

                // If the target directory does not exist, create it
                if (!Directory.Exists(parentFolderPathInBin))
                {
                    Directory.CreateDirectory(parentFolderPathInBin);
                }

                // copy source-file to the destination
                File.Copy(itemPath, itemPathInBin, true);

                // We must allow the destination file to be deletable
                FileAttributes fileAttributes = File.GetAttributes(itemPathInBin);
                if ((fileAttributes & FileAttributes.ReadOnly) != 0)
                {
                    File.SetAttributes(itemPathInBin, fileAttributes & ~FileAttributes.ReadOnly);
                }
            }
            else if (Directory.Exists(itemPath)) // It's a folder
            {
                // If it already exists, remove it
                if (Directory.Exists(itemPathInBin))
                {
                    Directory.Delete(itemPathInBin, true);
                }

                // Create target directory
                Directory.CreateDirectory(itemPathInBin);

                // Now Create all of the sub-directories
                foreach (string dirPath in Directory.GetDirectories(itemPath, "*", SearchOption.AllDirectories))
                {
                    Directory.CreateDirectory(dirPath.Replace(itemPath, itemPathInBin));
                }

                //Copy all the files & Replace any files with the same name
                foreach (string sourcePath in Directory.GetFiles(itemPath, "*.*", SearchOption.AllDirectories))
                {
                    string destinationPath = sourcePath.Replace(itemPath, itemPathInBin);
                    File.Copy(sourcePath, destinationPath, true);

                    // We must allow the destination file to be deletable
                    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 - \"" + itemPath + "\"");
            }
        }
    }
}

I've picked up the solution from @Matthew cleaned it up a bit and extended it to support multiple attribute usages for one test, and entire directories that can be used as DeploymentItems (including directories that contains sub-directories).

namespace NUnitDeploymentItem
{
    using System;
    using System.IO;
    using System.Reflection;

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
    public class DeploymentItem : Attribute
    {
        /// <summary>
        /// Marks an item to be relevant for a unit-test and copies it to deployment-directory for this unit-test.
        /// </summary>
        /// <param name="fileProjectRelativePath">The project-relative path to a file or a folder that will be copied into the deployment-directory of this unit-test.</param>
        public DeploymentItem(string fileProjectRelativePath)
        {
            // Escape input-path to correct back-slashes for Windows
            string filePath = fileProjectRelativePath.Replace("/", "\\");

            // Look up, where we are right now
            DirectoryInfo environmentDir = new DirectoryInfo(Environment.CurrentDirectory);

            // Get the full item-path of the deployment item
            string itemPath = new Uri(Path.Combine(environmentDir.Parent.Parent.FullName, filePath)).LocalPath;

            // Get the target-path where to copy the deployment item to
            string binFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            // Assemble the target path
            string itemPathInBin = new Uri(Path.Combine(binFolderPath, filePath)).LocalPath;

            // Decide whether it's a file or a folder
            if (File.Exists(itemPath)) // It's a file
            {
                // If it already exists, remove it
                if (File.Exists(itemPathInBin))
                {
                    File.Delete(itemPathInBin);
                }

                // Assemble the parent folder path (because the item might be in multiple sub-folders.
                string parentFolderPathInBin = new DirectoryInfo(itemPathInBin).Parent.FullName;

                // If the target directory does not exist, create it
                if (!Directory.Exists(parentFolderPathInBin))
                {
                    Directory.CreateDirectory(parentFolderPathInBin);
                }

                // If the source-file exists, copy it to the destination
                if (File.Exists(itemPath))
                {
                    File.Copy(itemPath, itemPathInBin);
                }
            }
            else if (Directory.Exists(itemPath)) // It's a folder
            {
                // If it already exists, remove it
                if (Directory.Exists(itemPathInBin))
                {
                    Directory.Delete(itemPathInBin, true);
                }

                // If the source-directory exists, copy it to the destination
                if (Directory.Exists(itemPath))
                {
                    // Create target directory
                    Directory.CreateDirectory(itemPathInBin);

                    // Now Create all of the sub-directories
                    foreach (string dirPath in Directory.GetDirectories(itemPath, "*", SearchOption.AllDirectories))
                    {
                        Directory.CreateDirectory(dirPath.Replace(itemPath, itemPathInBin));
                    }

                    //Copy all the files & Replaces any files with the same name
                    foreach (string newPath in Directory.GetFiles(itemPath, "*.*", SearchOption.AllDirectories))
                    {
                        File.Copy(newPath, newPath.Replace(itemPath, itemPathInBin), true);
                    }
                }
            }
        }
    }
}

This is actually a solution that was build from answers to these questions: Check if Path is a file or directory, Copy entire content of a directory and Create file if target folder does not exist.


I tried out the solution of implementing DeploymentItemAttribute, but found it problematic in that the class would be instantiated when tests were loaded. This resulted in deployment logic trying to execute as the Visual Studio NUnit Test Adapter was loading test classes, in its discovery phase. This isn't such a great idea.

I've opted instead to implement a static method, ItemDeployment.DeployItems, for deploying items, which you can call when setting up your test fixture:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

/// <summary>
/// Logic for deploying items for tests.
/// </summary>
internal static class ItemDeployment
{
    /// <summary>
    /// Call in subclass to deploy items before testing.
    /// </summary>
    /// <param name="items">Items to deploy, relative to project root.</param>
    /// <param name="retainDirectories">Retain directory structure of source items?</param>
    /// <exception cref="FileNotFoundException">A source item was not found.</exception>
    /// <exception cref="DirectoryNotFoundException">The target deployment directory was not found</exception>
    public static void DeployItems(IEnumerable<string> items, bool retainDirectories=false)
    {
        var environmentDir = new DirectoryInfo(Environment.CurrentDirectory);
        var binFolderPath = GetDeploymentDirectory();

        foreach (var item in items)
        {
            if (string.IsNullOrWhiteSpace(item))
            {
                continue;
            }

            string dirPath = retainDirectories ? Path.GetDirectoryName(item) : "";
            var filePath = item.Replace("/", @"\");
            var itemPath = new Uri(Path.Combine(environmentDir.Parent.Parent.FullName,
                filePath)).LocalPath;
            if (!File.Exists(itemPath))
            {
                throw new FileNotFoundException(string.Format("Can't find deployment source item '{0}'", itemPath));
            }

            if (!Directory.Exists(binFolderPath))
                throw new DirectoryNotFoundException(string.Format("Deployment target directory doesn't exist: '{0}'", binFolderPath));
            var dirPathInBin = Path.Combine(binFolderPath, dirPath);
            if (!Directory.Exists(dirPathInBin))
                Directory.CreateDirectory(dirPathInBin);
            var itemPathInBin = new Uri(Path.Combine(binFolderPath, dirPath, Path.GetFileName(filePath))).LocalPath;
            if (File.Exists(itemPathInBin))
            {
                File.Delete(itemPathInBin);
            }
            File.Copy(itemPath, itemPathInBin);
        }
    }

    /// <summary>
    /// Get directory test is deployed in.
    /// </summary>
    /// <returns></returns>
    public static string GetDeploymentDirectory()
    {
        return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    }
}

Then, in your test fixture, you can deploy items for your tests like so:

[TestFixture]
public class TestDatabaseService
{
    /// <summary>
    /// This is run once before any tests in this fixture.
    /// </summary>
    [TestFixtureSetUp]
    public void SetUpFixture()
    {
        ItemDeployment.DeployItems(new[] { @"App_Data\database.mdf" });
    }
}

Need Your Help

iOS 8 UIActionSheet ignores view controller supportedInterfaceOrientations and shouldAutorotate

ios rotation orientation ios8 uiactionsheet

I have an application that is configured via the plist file to support portrait, landscape left, and landscape right orientations (i.e. UISupportedInterfaceOrientations is set to