Integration Tests in Service Fabric – Communication Listener

In the previous post in my series on integration tests in Service Fabric I walked my readers through the AbstractTestRunner whose responsibility it is to discover tests in combination with the communication listener and exposes all the discovered tests to the client to be able to run individual tests over Service Fabric Remoting.

In this post, I will introduce the communication listener which contains the actual test runner and discovery process which uses NUnit. Service Fabric SDK provides an interface called ICommunicationListener. By implementing a communication listener it is possible to implement custom communication protocols. Communication listeners go through a specific lifecycle in the cluster. For example by default OpenAsync is only called in the communication listener instance on the primary. For testing purposes, it is enough to only have the primary replica inspecting the assemblies for tests. Let’s have a look at the communication listener definition below.

class CommunicationListener<TService> : ICommunicationListener
    where TService : StatefulService {
    public CommunicationListener(TService statefulService) {
        this.statefulService = statefulService;
    }

    public Task<string> OpenAsync(CancellationToken cancellationToken) {
        return Task.Run(() => {
            runner = new NUnitTestAssemblyRunner(new DefaultTestAssemblyBuilder());
            var settings = new Dictionary<string, object>
            {
                {"SynchronousEvents", true} // crucial to run listeners sync
            };
            var testSuite = runner.Load(typeof(TService).Assembly, settings);
            var testNameCache = new HashSet<string>();
            CacheTests(testNameCache, testSuite);
            cachedTestNames = Task.FromResult(testNameCache.ToArray());

            return "";
        });
    }

    static void CacheTests(HashSet<string> testNameCache, ITest test) {
        var testIsSuite = test.IsSuite;
        if (testIsSuite) {
            foreach (var child in test.Tests) {
                CacheTests(testNameCache, child);
            }
        }

        if (testIsSuite || test.RunState != RunState.Runnable) {
            return;
        }

        testNameCache.Add(test.FullName);
    }

    NUnitTestAssemblyRunner runner;
    Task<string[]> cachedTestNames;
    TService statefulService;
}

The communication listener retrieves the concrete instance of the stateful service passed into the constructor. The OpenAsync method uses the service to detect the assembly which contains the tests with typeof(TService).Assembly. Furthermore, OpenAsync uses the NunitTestAssemblyRunner from NUnitLite to spin up a unit test runner. NUnitLite is basically a library which contains all the bits and pieces to discover and run tests but you are in charge of setting up the runner infrastructure. When loading the test assembly into the runner it is crucial to specify SynchronousEvents with true in the settings. When the tests are run in the Run method (covered in the next post) we require test listeners to be able to intercept the result of the run tests. By specifying SynchronousEvents to true NUnit will make sure that the listeners are not executed in a dedicated background thread and basically synchronously complete with the tests.

When the test suite is loaded and returned by the Load method the code caches all the found tests. This is done by recursively iterating over the deeply nested ITest structure and cache all full names of all tests found in a HashSet. This is done once and cached as a synchronous and already completed task in the communication listener. Subsequent calls that want to acquire the tests will get the cached results and no further discovery is needed during the lifetime of the stateful service.

In the next post, I’ll cover how the NunitTestAssemblyRunner is used in combination with test listeners and NUnit filters to executed a test.

About the author

Daniel Marbach

1 comment

Recent Posts