Integration Tests in Service Fabric – Server side runner

In the last post in my series on integration tests in Service Fabric I showed how it is possible to use the C# API of the Service Fabric SDK to deploy the test applications dynamically to the cluster. The approach showed also used a ServiceProxy client that was able to interact with the server-side hosted inside the cluster over Service Fabric remoting. The service proxy is responsible for querying all tests that are hosted inside the test application and then instruct the service to run them one by one while the test cases get expanded on the client side.

So far I haven’t talked about the server side that provides the test names to the client as well as executes the test whenever the client proxy requests to do so. In this post, I will show how the AbstractTestRunner service uses the power of communication listeners to host an NUnit test runner inside the cluster and how remoting is used to run individual tests.

Like shown briefly in the service fabric test definition blog post the stateful service project that will host the tests needs to define a stateful service as an entry point that inherits from AbstractTestRunner<TSelf>.

sealed class Tests : AbstractTestRunner<Tests> {
    public Tests(StatefulServiceContext context) : base(context) { }
 
    protected override Tests Self => this;
}

The AbstractTestRunner base class has two responsibilities. The first responsibility is that it exposes the test definitions and the ability to run a specified test over Service Fabric Remoting to the clients. The second responsibility is exposing the communication listeners required to introspect the tests once that are hosted in the current assembly. Below is the interface that defines the first responsibility.

using Microsoft.ServiceFabric.Services.Remoting;

public interface ITestRunner : IService {
   Task<string[]> Tests();
   Task<Result> Run(string testName);
}

In order to leverage Service Fabric Remoting the interface needs to implement the marker interface IService. For simplicity reasons, the test names are returned as strings only since this doesn’t require a declaration of data contracts. Running a test needs a data contract definition since the runner needs to report back the state of the test as well as console outputs or exceptions that have happened during the execution of the test. For more details please refer to the Client Side Test Cases blog post.

The AbstractTestRunner implements the ITestRunner interface and delegates all the calls to the communication listener. Let’s see below how the runner definition looks like:

public abstract class AbstractTestRunner<TSelf> : StatefulService, ITestRunner
    where TSelf : StatefulService
{
    protected AbstractTestRunner(StatefulServiceContext context) : base(context) { }

    protected abstract TSelf Self { get; }

    protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners() {
        return new[] { new ServiceReplicaListener(context =>
        {
            communicationListener = new CommunicationListener<TSelf>(Self);
            return new CompositeCommunicationListener(communicationListener, 
                this.CreateServiceRemotingListener(context));
        }) };
    }

    public Task<string[]> Tests() {
        return communicationListener.Tests();
    }

    public Task<Result> Run(string testName) {
        return communicationListener.Run(testName);
    }

    CommunicationListener<TSelf> communicationListener;
}

The CreateServiceReplicaListeners method creates an NUnit aware CommunicationListener and wraps that one together with the ServiceRemotingListener inside a composite communication listener. The call to CreateServiceRemotingListener instructs Service Fabric to create the necessary remoting infrastructure so that the implementation of IService will be automatically exposed to clients.

I’ve chosen to use a CommunicationListener for hosting the NUnit tests because communication listeners have a nifty property. By default, they are executed once on the primary replica of the service. This allows caching the heavy lifting of introspecting the current service assembly for tests while the service is being bootstrapped inside the cluster.

How the communication listener achieves this and why it requires a generic parameter is a topic for another post.

About the author

Daniel Marbach

1 comment

Recent Posts