Unit testing in Azure Service Fabric

Unit testing in Service Fabric can be difficult. For example, how can you test remoting communication between services? And how do you even create an instance of your service type, without relying on a Service Fabric cluster being present on the machine? Fortunately, there’s help available.

In this article you’ll find a description of some of the solutions provided by the open source library named ‘ServiceFabric.Mocks’.

Please note that all examples below are also available as unit tests on Github.

ServiceFabric.Mocks

ServiceFabric.Mocks contains many Mock and helper classes to facilitate and simplify unit testing of Service Fabric Actors and Services.

Useful links

Download:
https://www.nuget.org/packages/ServiceFabric.Mocks/

Contribute:
https://github.com/loekd/ServiceFabric.Mocks

Unit Testing Actors

Let’s first create an Actor definition to test.

Define Actor under test

[StatePersistence(StatePersistence.Persisted)]
public class MyStatefulActor : Actor, IMyStatefulActor
{
   public MyStatefulActor(ActorService actorSerice, ActorId actorId)
   : base(actorSerice, actorId)
   {
   }

   public async Task InsertAsync(string stateName, Payload value)
    {
       await StateManager.AddStateAsync(stateName, value);
    }
}

[DataContract]
public class Payload
{
    [DataMember]
    public readonly string Content;

    public Payload(string content)
    {
       Content = content;
    }
}

Create test

You can create an instance of your Actor, by using an ActorService combined with the MockActorStateManager and MockStatefulServiceContextFactory.Default

private const string StatePayload = "some value";

[TestMethod]
public async Task TestActorState()
{
   var actorGuid = Guid.NewGuid();
   var id = new ActorId(actorGuid);
   var actor = CreateActor(id);
   var stateManager = (MockActorStateManager)actor.StateManager;
   const string stateName = "test";
   var payload = new Payload(StatePayload);
   //create state
   await actor.InsertAsync(stateName, payload);
   //get state
   var actual = await stateManager.GetStateAsync(stateName);
   Assert.AreEqual(StatePayload, actual.Content);
}

internal static MyStatefulActor CreateActor(ActorId id)
{
    Func actorFactory = (service, actorId) => new                MyStatefulActor(service, id);
    var svc = MockActorServiceFactory.CreateActorServiceForActor      (actorFactory);
    var actor = svc.Activate(id);
    return actor;
}

Unit Testing Stateful Services

Let’s now create a Service that we can test.

Define Stateful Service under test

public class MyStatefulService : StatefulService, IMyStatefulService
{
    public const string StateManagerDictionaryKey = "dictionaryname";
    public const string StateManagerQueueKey = "queuename";
    public const string StateManagerConcurrentQueueKey = "concurrentqueuename";

    public MyStatefulService(StatefulServiceContext serviceContext) : base(serviceContext)
    {
    }

    public MyStatefulService(StatefulServiceContext serviceContext,    IReliableStateManagerReplica reliableStateManagerReplica)
       : base(serviceContext, reliableStateManagerReplica)
    {
    }

    public async Task InsertAsync(string stateName, Payload value)
    {
       var dictionary = await StateManager.GetOrAddAsync(StateManagerDictionaryKey);
       using (var tx = StateManager.CreateTransaction())
       {
          await dictionary.TryAddAsync(tx, stateName, value);
          await tx.CommitAsync();
       }
    }

   public async Task EnqueueAsync(Payload value)
   {
       var queue = await StateManager.GetOrAddAsync(StateManagerQueueKey);
       using (var tx = StateManager.CreateTransaction())
       {
          await queue.EnqueueAsync(tx, value);
          await tx.CommitAsync();
       }
   }

   public async Task ConcurrentEnqueueAsync(Payload value)
   {
       var concurrentQueue = await StateManager.GetOrAddAsync(StateManagerConcurrentQueueKey);
       using (var tx = StateManager.CreateTransaction())
       {
          await concurrentQueue.EnqueueAsync(tx, value);
          await tx.CommitAsync();
       }
   }
}

Create an instance using with the MockReliableStateManager and MockStatefulServiceContextFactory.Default

Test ReliableDictionary:

[TestMethod]
public async Task TestServiceState_Dictionary()
{
   var context = MockStatefulServiceContextFactory.Default;
   var stateManager = new MockReliableStateManager();
   var service = new MyStatefulService(context, stateManager);
   const string stateName = "test";
   var payload = new Payload(StatePayload);
   //create state
   await service.InsertAsync(stateName, payload);
   //get state
   var dictionary = await stateManager.TryGetAsync(MyStatefulService.StateManagerDictionaryKey);
   var actual = (await dictionary.Value.TryGetValueAsync(null, stateName)).Value;
   Assert.AreEqual(StatePayload, actual.Content);
}

Test ReliableQueue:

[TestMethod]
public async Task TestServiceState_Queue()
{
   var context = MockStatefulServiceContextFactory.Default;
   var stateManager = new MockReliableStateManager();
   var service = new MyStatefulService(context, stateManager);   
   var payload = new Payload(StatePayload);
   //create state
   await service.EnqueueAsync(payload);
   //get state
   var queue = await stateManager.TryGetAsync(MyStatefulService.StateManagerQueueKey);
   var actual = (await queue.Value.TryPeekAsync(null)).Value;
   Assert.AreEqual(StatePayload, actual.Content);
}

Test ReliableConcurrentQueue

[TestMethod]
public async Task TestServiceState_ConcurrentQueue()
{
   var context = MockStatefulServiceContextFactory.Default;
   var stateManager = new MockReliableStateManager();
   var service = new MyStatefulService(context, stateManager);
   var payload = new Payload(StatePayload);
   //create state
   await service.ConcurrentEnqueueAsync(payload);
   //get state
   var queue = await stateManager.TryGetAsync(MyStatefulService.StateManagerConcurrentQueueKey);
   var actual = (await queue.Value.DequeueAsync(null));
   Assert.AreEqual(StatePayload, actual.Content);
}

Communication between Actors and Services

Testing remoting interaction, by injecting IServiceProxyFactory and/or IActorProxyFactory Mocks into Actors and Services. The factories will create Mock Proxies.

Mocking out called Actors

Let’s create Service Under Test

public class ActorCallerService : StatelessService
{
   public static readonly Guid CalledActorId = Guid.Parse("{1F263E8C-78D4-4D91-AAE6-C4B9CE03D6EB}");
   public IActorProxyFactory ProxyFactory { get; }

   public ActorCallerService(StatelessServiceContext serviceContext, IActorProxyFactory proxyFactory = null) 
       : base(serviceContext)
   {
       ProxyFactory = proxyFactory ?? new ActorProxyFactory();
   }

   public async Task CallActorAsync()
   {
      var proxy = ProxyFactory.CreateActorProxy(new ActorId(CalledActorId));
      var value = new Payload("some other value");
      await proxy.InsertAsync("test", value);
   }
}

Create Service Test

[TestMethod]
public async Task TestActorProxyFactory()
{
   //mock out the called actor
   var id = new ActorId(ActorCallerService.CalledActorId);
   Func actorFactory = (service, actorId) => new MockTestStatefulActor(service, id);
   var svc = MockActorServiceFactory.CreateActorServiceForActor(actorFactory);
   var actor = svc.Activate(id);
   //prepare the service:
   var mockProxyFactory = new MockActorProxyFactory();
   mockProxyFactory.RegisterActor(actor);
   var serviceInstance = new ActorCallerService(MockStatelessServiceContextFactory.Default, mockProxyFactory);
   //act:
   await serviceInstance.CallActorAsync();
   //assert:
   Assert.IsTrue(actor.InsertAsyncCalled);
}

private class MockTestStatefulActor : Actor, IMyStatefulActor
{
   public bool InsertAsyncCalled { get; private set; }

   public MockTestStatefulActor(ActorService actorService, ActorId actorId) : base(actorService, actorId)
   {
   }

   public Task InsertAsync(string stateName, Payload value)
   {
       InsertAsyncCalled = true;
       return Task.FromResult(true);
   }
}

Mocking out called Services

If you have an Actor that calls a Service, you can test it like this:

Create Actor Under Test

public class ServiceCallerActor : Actor, IMyStatefulActor
{
   public static readonly Uri CalledServiceName = new Uri("fabric:/MockApp/MockStatefulService");
   public IServiceProxyFactory ServiceProxyFactory { get; }
   public ServiceCallerActor(ActorService actorService, ActorId actorId, IServiceProxyFactory serviceProxyFactory) 
      : base(actorService, actorId)
   {
      ServiceProxyFactory = serviceProxyFactory ?? new ServiceProxyFactory();
   }

   public Task InsertAsync(string stateName, Payload value)
   {
      var serviceProxy = ServiceProxyFactory.CreateServiceProxy(CalledServiceName, new ServicePartitionKey(0L));
      return serviceProxy.InsertAsync(stateName, value);
   }
}

Create Actor Test

[TestMethod]
public async Task TestServiceProxyFactory()
{
   //mock out the called service
   var mockProxyFactory = new MockServiceProxyFactory();
   var mockService = new MockTestStatefulService();
   mockProxyFactory.RegisterService(ServiceCallerActor.CalledServiceName, mockService);
   //prepare the actor:
   Func actorFactory = (service, actorId) => new ServiceCallerActor(service, actorId, mockProxyFactory);
   var svc = MockActorServiceFactory.CreateActorServiceForActor(actorFactory);
   var actor = svc.Activate(ActorId.CreateRandom());
   //act:
   await actor.InsertAsync("test", new Payload("some other value"));
   //assert:
   Assert.IsTrue(mockService.InsertAsyncCalled);
}
 
private class MockTestStatefulService : IMyStatefulService
{
   public bool InsertAsyncCalled { get; private set; }

   public Task ConcurrentEnqueueAsync(Payload value)
   {
      throw new NotImplementedException();
   }

   public Task EnqueueAsync(Payload value)
   {
      throw new NotImplementedException();
   }

   public Task InsertAsync(string stateName, Payload value)
   {
      InsertAsyncCalled = true;
      return Task.FromResult(true);
   }
}

Mocking out called Actors

So what if an Actor calls another Actor, without knowing in advance which one it will be? You can use an event handler to provide the proper target at test-runtime.

Create Actor Dynamically Within Another Actor

An actor is created from the ActorCallerActor and we need to test that it is there and that its state was set.

public class ActorCallerActor : Actor, IMyStatefulActor
{
   public static readonly Uri CalledServiceName = new Uri("fabric:/MockApp/MyStatefulActor");
   public const string ChildActorIdKeyName = "ChildActorIdKeyName";
   public IActorProxyFactory ActorProxyFactory { get; }

   public ActorCallerActor(ActorService actorService, ActorId actorId, IActorProxyFactory actorProxyFactory) 
       : base(actorService, actorId)
   {
       ActorProxyFactory = actorProxyFactory ?? new ActorProxyFactory();
   }

   public Task InsertAsync(string stateName, Payload value)
   {
      var actorProxy = ActorProxyFactory.CreateActorProxy(CalledServiceName, new ActorId(Guid.NewGuid()));
      this.StateManager.SetStateAsync(ChildActorIdKeyName, actorProxy.GetActorId());
      return actorProxy.InsertAsync(stateName, value);
   }
}

Create Actor Test
Here a callback is used when the actor is requested and not found that will allow you to create it with the identifier defined in the original actor. So you can test it.

[TestMethod]
public async Task TestServiceProxyFactory()
{
   //mock out the called service
   var mockProxyFactory = new MockActorProxyFactory();
   mockProxyFactory.MissingActor += MockProxyFactory_MissingActorId;
   //prepare the actor:
   Func actorFactory = (service, actorId) => new ActorCallerActor(service, actorId, mockProxyFactory);
   var svc = MockActorServiceFactory.CreateActorServiceForActor(actorFactory);
   var actor = svc.Activate(ActorId.CreateRandom());
   //act:
   await actor.InsertAsync("test", new Payload("some other value"));
   //check if the other actor was called
   var statefulActorId = await actor.StateManager.GetStateAsync(ActorCallerActor.ChildActorIdKeyName);
   Func statefulActorFactory = (service, actorId) => new MyStatefulActor(service, actorId);
   var statefulActor = mockProxyFactory.CreateActorProxy(ActorCallerActor.CalledServiceName, statefulActorId);
   var payload = await ((MyStatefulActor)statefulActor).StateManager.GetStateAsync("test");
   //assert:
   Assert.AreEqual("some other value", payload.Content);
}

private void MockProxyFactory_MissingActorId(object sender, MissingActorEventArgs args)
{
   var registrar = (MockActorProxyFactory)sender;
   Func actorFactory = (service, actorId) => new MyStatefulActor(service, actorId);
   var svc = MockActorServiceFactory.CreateActorServiceForActor(actorFactory);
   var actor = svc.Activate(args.Id);
   registrar.RegisterActor(actor);
}

Test Actor Reminders and timers

Actor timers and reminders are quite tricky to test. They won’t trigger during tests. But you can test if they are registered correctly.

Actor under test:

public class ReminderTimerActor : Actor, IRemindable, IReminderTimerActor
{
   public ReminderTimerActor(ActorService actorService, ActorId actorId) : base(actorService, actorId)
   {
   }

   public Task RegisterReminderAsync(string reminderName)
   {
      return RegisterReminderAsync(reminderName, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(-1));
   }

   public Task ReceiveReminderAsync(string reminderName, byte[] context, TimeSpan dueTime, TimeSpan period)
   {
      //will not be called automatically.
      return Task.FromResult(true);
   }

   public Task RegisterTimerAsync()
   {
       RegisterTimer(TimerCallbackAsync, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(-1));
       return Task.FromResult(true);
   }
 
   private Task TimerCallbackAsync(object state)
   {
      //will not be called automatically.
      return Task.FromResult(true);
   }
}

Test code:

[TestMethod]
public async Task TestActorReminderRegistration()
{
   var svc = MockActorServiceFactory.CreateActorServiceForActor();
   var actor = svc.Activate(new ActorId(Guid.NewGuid()));
   string reminderName = "reminder";
   //setup
   await actor.RegisterReminderAsync(reminderName);
   //assert
   var reminderCollection = actor.GetActorReminders(); //extension method
   bool hasReminder = reminderCollection.Any();
   Assert.IsTrue(hasReminder);
}

[TestMethod]
public async Task TestActorTimerRegistration()
{
   var svc = MockActorServiceFactory.CreateActorServiceForActor();
   var actor = svc.Activate(new ActorId(Guid.NewGuid()));
   //setup
   await actor.RegisterTimerAsync();
   //assert
   var timers = actor.GetActorTimers(); //extension method
   bool hasTimer = timers.Any();
   Assert.IsTrue(hasTimer);
}

Using your custom ActorService implementations

You can use a custom ActorService implementations to create your Actor instances. You won’t be able to use `CreateActorServiceForActor`. Instead, you can use `CreateCustomActorServiceForActor`.

Custom ActorService

public class CustomActorService : ActorService
{
   //no additional constructor parameters
   public CustomActorService(StatefulServiceContext context, ActorTypeInformation actorTypeInfo, Func actorFactory = null, Func stateManagerFactory = null, 
      IActorStateProvider stateProvider = null, ActorServiceSettings settings = null) : base(context, actorTypeInfo, 
       actorFactory, stateManagerFactory, stateProvider, settings)
   {
   }
}

Test code

//an ActorService with a standard constructor can be created by the MockActorServiceFactory
var customActorService = MockActorServiceFactory.CreateCustomActorServiceForActor();
var actor = customActorService.Activate(new ActorId(123L));

Assert.IsInstanceOfType(customActorService, typeof(CustomActorService));
Assert.IsInstanceOfType(actor, typeof(OnActivateActor));
Assert.AreEqual(123L, actor.Id.GetLongId());

ActorService with non standard constructor
In this situation you can’t use the MockActorServiceFactory, so you’ll need to create an instance directly.

An ActorService with a NON standard constructor can be created by passing Mock arguments:

var stateManager = new MockActorStateManager();
Func stateManagerFactory = (actr, stateProvider) => stateManager;

IActorStateProvider actorStateProvider = new MockActorStateProvider();
actorStateProvider.Initialize(ActorTypeInformation.Get(typeof(OnActivateActor)));
var context = MockStatefulServiceContextFactory.Default;
var dummy = new object(); //this argument causes the 'non standard' ctor.
var customActorService = new AnotherCustomActorService(dummy, context, ActorTypeInformation.Get(typeof(OnActivateActor)));

var actor = customActorService.Activate(new ActorId(123L));
Assert.IsInstanceOfType(actor, typeof(OnActivateActor));
Assert.AreEqual(123L, actor.Id.GetLongId());

Testing service configuration

If your service relies on configuration, you can test this by using `MockCodePackageActivationContext`.

To inject a configuration section into the MockCodePackageActivationContext, you can use this code:

[TestClass]
public class ConfigurationPackageTests
{
   [TestMethod]
   public void ConfigurationPackageAtMockCodePackageActivationContextTest()
   {
      //build ConfigurationSectionCollection
      var configSections = new ConfigurationSectionCollection();
      //Build ConfigurationSettings
      var configSettings = CreateConfigurationSettings(configSections);
      //add one ConfigurationSection
      ConfigurationSection configSection = CreateConfigurationSection(nameof(configSection.Name));
         configSections.Add(configSection);
      //add one Parameters entry
      ConfigurationProperty parameter = CreateConfigurationSectionParameters(nameof(parameter.Name), nameof(parameter.Value));
         configSection.Parameters.Add(parameter);
      //Build ConfigurationPackage
      ConfigurationPackage configPackage = CreateConfigurationPackage(configSettings, nameof(configPackage.Path));
      [..]
   }
}

More info will be added here as features are added. Want to help? Contribute at Github!

Useful links
Download:
https://www.nuget.org/packages/ServiceFabric.Mocks/

Contribute:
https://github.com/loekd/ServiceFabric.Mocks

Advertisements

Author: loekd

Loek is a Technical Trainer, Cloud solution architect at Xpirit, a public speaker and Microsoft Azure MVP. He focuses on creating secure, scalable and maintainable systems. To help companies make the most efficient transition into the Cloud, he is always looking for even better ways to leverage the Microsoft Azure Platform. As an active member of open source projects, Loek likes to exchange knowledge with other community members. Let’s engage! https://xpirit.com/loek

2 thoughts on “Unit testing in Azure Service Fabric”

  1. Can you please expand your last example for mocking out configuration ? It uses ‘var’ for variables and it’s not clear what their types should be. It also uses methods that the user is presumably meant to implement for their own individual circumstances (CreateConfigurationSettings, CreateConfigurationSection, CreateConfigurationSectionParameters, CreateConfigurationPackage) but if one is a beginner with Service Fabric and ServiceFabric.Mocks, then they have no idea how to implement these, which makes this example useless. Please expand it and show us how to implement these methods. You’ve clearly made a very functional library, but its ease of usability is very low.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s