V3 Documentation
Search

Tests That Build Their Own Application Domains

Cross-process and cross-appdomain tests require special attention when working with NCrunch.

Because these tests build their own environments inside NCrunch workspaces, they are often coded with certain assumptions about the structure of their environment and the location of the assemblies within it.

Because NCrunch will only record code coverage information within its test runner application domain, there will often be limited code coverage information available for these tests.

If your tests make assumptions about having their referenced assemblies inside their working directory, you may experience problems with these assemblies not being present in their expected places when NCrunch runs these tests. A simple solution is to enable the copy referenced assemblies to workspace setting for all the projects involved, but this will massively decrease build performance. A better solution is to look at other ways of resolving the locations of assemblies required in the domain.

The NCrunchEnvironment.GetAllAssemblyLocations() method is a useful way to retrieve a list of the file locations of every assembly that can be potentially loaded into the test runtime application domain. This method loads its data from an environment variable that will always be present in every application domain or process launched from the NCrunch test environment. A unfortunate drawback is that the method is only available when working with NCrunch - it will not be available under other test runners. This means that code making use of it needs to be conditional on NCrunch execution.

The following example contains code that creates a custom application domain during test execution. This code has been updated to use the NCrunchEnvironment.GetAllAssemblyLocations() method to find assemblies needed inside its application domain:

using System;
using System.IO;
using System.Reflection;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ReferencedProject;

namespace MyTestProject
{
    [TestClass]
    public class Fixture
    {
        [TestMethod]
        public void TestThatCreatesAnApplicationDomainIncludingAReferencedAssembly()
        {
            var setup = new AppDomainSetup
            {
                ApplicationName = "Application",
                ApplicationBase = Directory.GetCurrentDirectory(),
            };

            var evidence = new Evidence();
            evidence.AddHostEvidence(new Zone(SecurityZone.MyComputer));
            var appDomain = AppDomain.CreateDomain(
                "MyDomain",
                evidence,
                setup,
                new PermissionSet(PermissionState.Unrestricted)
                );

            var classInAppDomain = (ClassInApplicationDomain)appDomain.CreateInstanceFromAndUnwrap(
                                                    typeof(ClassInApplicationDomain).Assembly.Location,
                                                    typeof(ClassInApplicationDomain).FullName
                                                    );
            classInAppDomain.RunCodeInApplicationDomain();
        }
    }

    public class ClassInApplicationDomain : MarshalByRefObject
    {
        public void RunCodeInApplicationDomain()
        {
#if NCRUNCH
            // This code isn't needed for other test runners or NCrunch with the 
            // 'Copy Referenced Assemblies To Workspace' setting enabled, because they 
            // will have the ReferencedProject.dll sitting in the current directory.

            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
#endif

            runCodeInReferencedAssembly();
        }

        private void runCodeInReferencedAssembly()
        {
            // When no effort is made to find required assemblies via the AssemblyResolve method,
            // NCrunch will blow up here, as it won't be able to find ReferencedProject.dll.

            var c = new ClassInReferencedProject();
            c.DoStuff();
        }

        private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            // Search through the known assembly locations returned from the NCrunchEnvironment.GetAllAssemblyLocations method, 
            // and load any assembly with a name matching the one we're looking for

            var shortAssemblyName = getShortAssemblyName(args.Name);

            foreach (var knownAssemblyLocation in NCrunch.Framework.NCrunchEnvironment.GetAllAssemblyLocations())
                if (string.Compare(Path.GetFileNameWithoutExtension(knownAssemblyLocation), shortAssemblyName, true) == 0)
                    return Assembly.LoadFrom(knownAssemblyLocation);

            return null;
        }

        private static string getShortAssemblyName(string assemblyName)
        {
            // The CLR can attempt to resolve assemblies using long names - so we truncate the name to make matching easier.

            if (assemblyName.Contains(","))
                return assemblyName.Substring(0, assemblyName.IndexOf(','));

            return assemblyName;
        }
    }
}