Are these Singleton Unit Tests actually working as expected?

448 views Asked by At

I have a bootstrapper object that I'm trying to test (using xunit). The tests appear to pass, but I'm seeing some weird things in one of the test runners I use (ncrunch). I use both ncrunch and the resharper xunit runner. My idea was to take the assembly that the singleton is in, load it into a new appdomain, run my tests using reflection, then unload the app domain. As I said, the tests pass in both ncrunch and resharper, but ncrunch is not showing the execution paths that I expect. Here's the code:

public class Bootstrapper
{
    private static Bootstrapper booted;

    public Bootstrapper()
    {
        // performs boot tasks
    }

    public static void Boot()
    {
        if (booted == null)
        {
            var staticboot = new Bootstrapper();
            Booted = staticboot;
        }
    }

    public static Bootstrapper Booted
    {
        get
        {
            if (booted == null) throw new InvalidOperationException("Should call Boot() before accessing the booted object");
            return booted;
        }
        set { booted = value; }
    }
}

public class Tests
{
    [Fact]
    public void TryingToAccessBootedKernelBeforeBootThrowsException()
    {
        var setup = this.SetupTestingDomainWithAssembly("StackOverflowQuestion.Tests.dll");
        var kernelType = setup.Item2.GetType("StackOverflowQuestion.Tests.Bootstrapper");
        var bootedkernelProperty = kernelType.GetProperty("Booted");
        try
        {
            bootedkernelProperty.GetValue(null);
        }
        catch (Exception e)
        {
            Assert.IsType(typeof(InvalidOperationException), e.InnerException);
        }

        AppDomain.Unload(setup.Item1);
    }

    [Fact]
    public void CanAccessKernelAfterBooting()
    {
        var setup = this.SetupTestingDomainWithAssembly("StackOverflowQuestion.Tests.dll");
        var kernelType = setup.Item2.GetType("StackOverflowQuestion.Tests.Bootstrapper");
        var bootMethod = kernelType.GetMethod("Boot");
        bootMethod.Invoke(null, new object[] { });
        var bootedkernelProperty = kernelType.GetProperty("Booted");

        Assert.DoesNotThrow(() => bootedkernelProperty.GetValue(null));

        AppDomain.Unload(setup.Item1);
    }

    [Fact]
    public void BootIsIdempotent()
    {
        var setup = this.SetupTestingDomainWithAssembly("StackOverflowQuestion.Tests.dll");
        var kernelType = setup.Item2.GetType("StackOverflowQuestion.Tests.Bootstrapper");
        var bootMethod = kernelType.GetMethod("Boot");
        bootMethod.Invoke(null, new object[] {});
        var bootedkernelProperty = kernelType.GetProperty("Booted");

        var bootedKernel = (Bootstrapper)bootedkernelProperty.GetValue(null);

        bootMethod.Invoke(null, new object[] {});

        var secondCall = (Bootstrapper)bootedkernelProperty.GetValue(null);

        Assert.Equal(bootedKernel, secondCall);

        AppDomain.Unload(setup.Item1);
    }

    private Tuple<AppDomain, Assembly> SetupTestingDomainWithAssembly(string assemblyPath)
    {
        // we guarantee that each domain will have a unique name.
        AppDomain testingDomain = AppDomain.CreateDomain(DateTime.Now.Ticks.ToString());
        var pancakesAssemblyName = new AssemblyName();
        pancakesAssemblyName.CodeBase = assemblyPath;
        var assembly = testingDomain.Load(pancakesAssemblyName);

        return new Tuple<AppDomain, Assembly>(testingDomain, assembly);
    }
}

Now, I recognize that there is some cleanup that needs to happen code-wise, but I was happy to see them all green. If I fiddle with them to make them fail, that works as expected. The only thing that's kind of smelly is that ncrunch is reporting weird execution paths. Specifically, ncrunch is showing that the line that throws the invalid operation exception is never executed.

I suppose it's possible that ncrunch has a bug when dealing with other application domains, but it's more likely that I don't actually understand what's going on with the app domains, but I'm not sure where to continue from here.

Also, I do know that singletons are bad, but I believe bootstrappers are one place where they actually are useful. You want to guarantee that they are only booted once.

1

There are 1 answers

1
Tallek On BEST ANSWER

Unless I'm missing something here.. it doesn't look like you are actually invoking anything in your other app domain. Your reflection is occurring in the current app domain. Take a look at the DoCallback method: http://msdn.microsoft.com/en-us/library/system.appdomain.docallback.aspx

public class Tests
{
    [Fact]
    public void TryingToAccessBootedKernelBeforeBootThrowsException()
    {
        var appDomain = AppDomain.Create(Guid.NewGuid());
        try
        {
            appDomain.DoCallBack(new CrossAppDomainDelegate(TryingToAccessBootedKernelBeforeBootThrowsException_AppDomainCallback));
        }
        catch (Exception e)
        {
            Assert.IsType(typeof(InvalidOperationException), e.InnerException);
        }

        AppDomain.Unload(appDomain);
    }

    public static void TryingToAccessBootedKernelBeforeBootThrowsException_AppDomainCallback()
    {
        var bootstrapper = BootStrapper.Booted;
    }
}