Search code examples
servicestackhttplistenerservicestack-bsd

How do I get around "HttpListenerBase.Instance has already been set" in my ServiceStack-hosted tests?


I've got a project build on ServiceStack 3.9, with extensive test coverage that uses ServiceStack's self-hosting API facility.

Lots of my test fixtures look like this:

private const string URL_BASE = "http://localhost:60666/";

[TestFixtureSetUp]
public void TestFixtureSetUp() {
    AppHost = new HttpTestableAppHost(FakeServices.Register);
    AppHost.Init();
    AppHost.Start(URL_BASE);
}

[TestFixtureTearDown]
public void TestFixtureTearDown() {
    AppHost.Dispose();
    AppHost = null;
}

The problem is that if when tests fail, the TearDown doesn't always seem to be running cleanly - and then every other test on the project fails with the same errors, either

System.IO.InvalidDataException : HttpListenerBase.Instance has already been set

or less frequently

Failed to listen on prefix http://localhost:60666/ because it conflicts with an existing registration on the machine

When this happens, the entire test suite becomes un-runnable for a couple of minutes - presumably whilst some underlying piece of network binding times out or gets disposed? - and then after a few minutes it all starts working again.

How can I handle this more gracefully? Is there some way I can forcibly dispose/unregister that http://localhost:60666/ endpoint before initialising my AppHost, so it'll kill any existing service hosts before starting a fresh one? It's increasingly difficult to work out what's going on when one 'proper' failing test results in 1,000+ test failures because the other tests can't initialise their HTTP listener.


Solution

  • Unload the AppDomain:

    The only way to cleanly restart the ServiceStack host is to unload the application domain it is running in and start a fresh instance in a new application domain.

    Your issue is related to this question & answer.

    1: Create your AppHost (as per normal):

    An example AppHost which registers an object called MyTest with the IoC container.

    public class AppHost : AppSelfHostBase
    {
        public AppHost(): base("My ServiceStack Service", typeof(AppHost).Assembly)
        {
        }
    
        public override void Configure(Funq.Container container)
        {
            container.Register<MyTest>(c => new MyTest());
        }
    }
    
    public class MyTest
    {
        public string Name { get { return "Hello"; } }
    }
    

    2: Create an IsolatedAppHost Class:

    The IsolatedAppHost class is used to start your application host, that will be run in the isolated AppDomain. You can start and configure whatever AppHost you need here, such as your HttpTestableAppHost.

    public class IsolatedAppHost : MarshalByRefObject
    {
        readonly AppHost Host;
    
        public IsolatedAppHost()
        {
            // Start your HttpTestableAppHost here
            Host = new AppHost();
            Host.Init();
            Host.Start("http://*:8090/");
            Console.WriteLine("ServiceStack is running in AppDomain '{0}'", AppDomain.CurrentDomain.FriendlyName);
        }
    
        public void RunTest(Action<AppHost> test)
        {
            test.Invoke(Host);
        }
    
        public void Teardown()
        {
            if(Host != null)
            {
                Console.WriteLine("Shutting down ServiceStack host");
                if(Host.HasStarted)
                    Host.Stop();
                Host.Dispose();
            }
        }
    }
    

    3: Create your TestFixture:

    Your TestFixureSetup method will need to create an instanced of the IsolatedAppHost in a new AppDomain. And the TestFixtureTearDown will ensure the AppHost and the domain are correctly shutdown.

    [TestFixture]
    public class Test
    {
        AppDomain ServiceStackAppDomain;
        IsolatedAppHost IsolatedAppHost;
    
        [TestFixtureSetUp]
        public void TestFixtureSetup()
        {
            // Get the assembly of our host
            var assemblyName = typeof(IsolatedAppHost).Assembly.GetName();
    
            // Create new AppDomain
            ServiceStackAppDomain = AppDomain.CreateDomain("ServiceStackAppDomain");
    
            // Load our assembly
            ServiceStackAppDomain.Load(assemblyName);
    
            // Create instance
            var handle = ServiceStackAppDomain.CreateInstance(assemblyName.FullName, "MyApp.Tests.IsolatedAppHost");
    
            // Unwrap so we can access methods
            IsolatedAppHost = (IsolatedAppHost)handle.Unwrap();
        }
    
        [TestFixtureTearDown]
        public void TestFixtureTearDown()
        {
            // Tell ServiceStack to stop the host
            IsolatedAppHost.Teardown();
    
            // Shutdown the ServiceStack application
            AppDomain.Unload(ServiceStackAppDomain);
            ServiceStackAppDomain = null;
        }
    
        // Tests go here
    }
    

    4: Running tests:

    As the test runner AppDomain and the AppHost AppDomain are now different, we cannot access the AppHost from our tests directly. So we pass our test to the RunTest method instance of our IsolatedAppHost.

    IsolatedAppHost.RunTest(appHost => {
        // Your test goes here
    });
    

    For example:

    [Test]
    public void TestWillPass()
    {
        IsolatedAppHost.RunTest(appHost => {
            var t = appHost.TryResolve<MyTest>();
            Assert.That(t.Name, Is.EqualTo("Hello"));
        });
    }
    
    [Test]
    public void TestWillFail()
    {
        IsolatedAppHost.RunTest(appHost => {
            var t = appHost.TryResolve<MyTest>();
            Assert.That(t.Name, Is.EqualTo("World"));
        });
    }
    

    The full source code is here. I hope that helps.