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.
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.
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"; } }
}
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();
}
}
}
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
}
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.