6

I'm using Topshelf to host a Windows Service written in C# and I now want to write some integration tests. My initialisation code is held in a launcher class like the following:

public class Launcher
{
    private Host host;

    /// <summary>
    /// Configure and launch the windows service
    /// </summary>
    public void Launch()
    {
        //Setup log4net from config file
        log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo(DEFAULT_CONFIG));

        //Setup Ninject dependency injection
        IKernel kernel = new StandardKernel(new MyModule());

        this.host = HostFactory.New(x =>
        {
            x.SetServiceName("MyService");
            x.SetDisplayName("MyService");
            x.SetDescription("MyService");

            x.RunAsLocalSystem();
            x.StartAutomatically();

            x.Service<MyWinService>(s =>
            {
                s.ConstructUsing(() => kernel.Get<MyWinService>());
                s.WhenStarted(w => w.Start());
                s.WhenStopped(w => w.Stop());
            });
        });

        this.host.Run(); //code blocks here
    }

    /// <summary>
    /// Dispose the service host
    /// </summary>
    public void Dispose()
    {
        if (this.host != null && this.host is IDisposable)
        {
            (this.host as IDisposable).Dispose();
            this.host = null;
        }
    }
}

I want to write some integration tests to make sure that log4net and Ninject get set up properly and Topshelf launches my service. The problem is, once you call Run() on the Topshelf host, the code just blocks so my test code never gets run.

I thought of calling Launch() in a separate thread in the SetUp section of my tests but then I need a bit of a hack to put in a Thread.Sleep(1000) to make sure the tests don't run before Launch() has finished. I can't use a proper sync on it (like a ManualResetEvent) because the Launch() never returns. The current code is:

private Launcher launcher;
private Thread launchThread;

[TestFixtureSetUp]
public void SetUp()
{
    launcher = new Launcher();
    launchThread = new Thread(o => launcher.Launch());
    launchThread.Start();
    Thread.Sleep(2500); //yuck!!
}

[TestFixtureTearDown]
public void TearDown()
{
    if (launcher != null)
    {
        launcher.Dispose(); //ouch
    }
}

Ideally what I'm looking for is a non-blocking way of launching the service and a programmatic way of stopping it again to put in my TearDown. At the moment my TearDown just disposes the launcher (so the TearDown literally tears it down!).

Does anyone have experience in testing Topshelf services in this way? I can do the above relatively easily using the standard ServiceHost but I much prefer the explicit configuration and ease of installation in Topshelf.

Adam Rodger
  • 3,152
  • 3
  • 29
  • 43

2 Answers2

2

https://github.com/Topshelf/Topshelf/blob/v2.3/src/Topshelf/Config/Builders/RunBuilder.cs#L113 I think is what you want. AfterStartingService could be used to set a ManualResetEvent from a different thread.

Now this is might work out for you, but this feels like overly complicated and could be validated just by deploying to dev/staging and doing smoke tests to your system. However, without more understanding of your environment, that might not be possible.

Travis
  • 9,959
  • 1
  • 25
  • 46
0

I had the same issue today and I chose to isolate the service instanciation and interaction from the actual Topshelf hosting (which consists of nothing more than resolving your service using Ninject in your case).

Adam Rodger has a fair point and after having a quick look to the Run method for the ConsoleRunHost, it will actually hang to wait for a ManualResetEvent and won't give you the control back until the service terminates.

At your place, to code your smoke/regression tests, just get the kernel and the module into your SetUp method, resolve the service, do your tests and dispose it in the TearDown

arobin
  • 11
  • 3