通过

为有状态服务创建单元测试

单元测试 Service Fabric 有状态服务可发现常规应用程序或特定于域的单元测试不一定捕获的常见错误。 在为有状态服务开发单元测试时,应牢记一些特殊注意事项。

  1. 每个副本均执行应用程序代码,但在不同的上下文下。 如果服务使用三个副本,则在不同上下文/角色下并行在三个节点上执行服务代码。
  2. 在有状态服务中存储的状态应在所有副本间保持一致。 状态管理器和可靠集合提供现成的一致性。 但是,内存中状态需要由应用程序代码管理。
  3. 每个副本在群集上运行时在某个时间点更改角色。 如果托管主副本的节点变得不可用或重载,次要副本将成为主副本。 这是 Service Fabric 典型的行为,因此服务必须计划最终在不同角色下运行。

本文假设已阅读对 Service Fabric 中的有状态服务进行单元测试

ServiceFabric.Mocks 库

从版本 3.3.0 开始,ServiceFabric.Mocks 提供了一套 API,用于模拟副本的编排和状态管理。 此示例中使用了这一点。

NuGetGitHub

ServiceFabric.Mocks 不由Microsoft拥有或维护。 但是,这是当前 Azure 建议用于对有状态服务进行单元测试的库。

设置模拟编排和状态

作为测试的排列部分的一部分,将创建模拟副本集和状态管理器。 然后,副本集将自己为每个副本创建已测试服务的实例。 它还将自己执行生命周期事件,例如 OnChangeRoleRunAsync。 模拟状态管理器可确保针对状态管理器执行的任何操作都能够像在实际状态管理器上那样运行并保持。

  1. 创建一个服务工厂代理,用于实例化正在测试的服务。 这应该与通常在 Program.cs 中找到的 Service Fabric 服务或角色的服务工厂回调类似或完全相同。 这应遵循以下签名:
    MyStatefulService CreateMyStatefulService(StatefulServiceContext context, IReliableStateManagerReplica2 stateManager)
    
  2. 创建 MockReliableStateManager 类的实例。 这将模拟与状态管理器的所有交互。
  3. 创建 MockStatefulServiceReplicaSet<TStatefulService> 的实例,其中 TStatefulService 是要测试服务的类型。 这需要步骤 #1 中创建的委托,以及在步骤 #2 中实例化的状态管理器。
  4. 将副本添加到副本集。 指定角色(如 Primary、ActiveSecondary、IdleSecondary)和副本的 ID

    保留副本 ID! 这些将有可能在单元测试的行为和断言部分中使用。

//service factory to instruct how to create the service instance
var serviceFactory = (StatefulServiceContext context, IReliableStateManagerReplica2 stateManager) => new MyStatefulService(context, stateManager);
//instantiate a new mock state manager
var stateManager = new MockReliableStateManager();
//instantiate a new replica set with the service factory and state manager
var replicaSet = new MockStatefulServiceReplicaSet<MyStatefulService>(CreateStatefulService, stateManager);
//add a new Primary replica with id 1
await replicaSet.AddReplicaAsync(ReplicaRole.Primary, 1);
//add a new ActiveSecondary replica with id 2
await replicaSet.AddReplicaAsync(ReplicaRole.ActiveSecondary, 2);
//add a second ActiveSecondary replica with id 3
await replicaSet.AddReplicaAsync(ReplicaRole.ActiveSecondary, 3);

执行服务请求

可以使用便捷属性和查找在特定副本上执行服务请求。

const string stateName = "test";
var payload = new Payload(StatePayload);

//execute a request on the primary replica using
await replicaSet.Primary.ServiceInstance.InsertAsync(stateName, payload);

//execute a request against replica with id 2
await replicaSet[2].ServiceInstance.InsertAsync(stateName, payload);

//execute a request against one of the active secondary replicas
await replicaSet.FirstActiveSecondary.InsertAsync(stateName, payload);

执行服务迁移

模拟副本集公开了多种便捷方法来触发不同类型的服务迁移。

//promote the first active secondary to primary
replicaSet.PromoteNewReplicaToPrimaryAsync();
//promote the secondary with replica id 4 to primary
replicaSet.PromoteNewReplicaToPrimaryAsync(4);

//promote the first idle secondary to an active secondary
PromoteIdleSecondaryToActiveSecondaryAsync();
//promote idle secondary with replica id 4 to active secondary
PromoteIdleSecondaryToActiveSecondaryAsync(4);

//add a new replica with randomly assigned replica id and promote it to primary
PromoteNewReplicaToPrimaryAsync()
//add a new replica with replica id 4 and promote it to primary
PromoteNewReplicaToPrimaryAsync(4)

汇总

以下测试演示如何设置一个三节点副本集,并验证数据在角色变更后是否可以从从节点获取。 出现的典型问题可能是,在未运行 CommitAsync 的情况下,通过 InsertAsync 添加的数据被保存到了内存中或可靠集合中。 在任一情况下,辅助项都将与主要项不同步。 这将导致服务移动后的响应不一致。

[TestMethod]
public async Task TestServiceState_InMemoryState_PromoteActiveSecondary()
{
    var stateManager = new MockReliableStateManager();
    var replicaSet = new MockStatefulServiceReplicaSet<MyStatefulService>(CreateStatefulService, stateManager);
    await replicaSet.AddReplicaAsync(ReplicaRole.Primary, 1);
    await replicaSet.AddReplicaAsync(ReplicaRole.ActiveSecondary, 2);
    await replicaSet.AddReplicaAsync(ReplicaRole.ActiveSecondary, 3);

    const string stateName = "test";
    var payload = new Payload(StatePayload);

    //insert data
    await replicaSet.Primary.ServiceInstance.InsertAsync(stateName, payload);
    //promote one of the secondaries to primary
    await replicaSet.PromoteActiveSecondaryToPrimaryAsync(2);
    //get data
    var payloads = (await replicaSet.Primary.ServiceInstance.GetPayloadsAsync()).ToList();

    //data should match what was inserted against the primary
    Assert.IsTrue(payloads.Count == 1);
    Assert.IsTrue(payloads[0].Content == payload.Content);

    //verify the data was saved against the reliable dictionary
    var dictionary = await StateManager.GetOrAddAsync<IReliableDictionary<string, Payload>>(MyStatefulService.StateManagerDictionaryKey);
    using(var tx = StateManager.CreateTransaction())
    {
        var payload = await dictionary.TryGetValue(stateName);
        Assert.IsTrue(payload.HasValue);
        Assert.IsTrue(payload.Value.Content == payload.Content);
    }
}

后续步骤

了解如何测试服务间通信使用受控的混沌模拟故障