你如何使依赖于扩展方法的方法可测试?

时间:2009-05-28 01:23:26

标签: unit-testing c#-3.0 tdd extension-methods rhino-mocks

我有一个带有以下签名的扩展方法(在BuildServerExtensions类中)::

public static IEnumerable<BuildAgent> GetEnabledBuildAgents(
                                          this IBuildServer buildServer,
                                          string teamProjectName)
{
    // omitted agrument validation and irrelevant code
    var buildAgentSpec = buildServer.CreateBuildAgentSpec(teamProjectName);
}

另一个调用第一个(在BuildAgentSelector类中)的方法:

public BuildAgent Select(IBuildServer buildServer, string teamProjectName)
{
    // omitted argument validation
    IEnumerable<BuildAgent> serverBuildAgents = 
        buildServer.GetEnabledBuildAgents(teamProjectName);

    // omitted - test doesn't get this far
}

我正在尝试使用MSTest和Rhino.Mocks(v3.4)测试它:

[TestMethod]
public void SelectReturnsNullOnNullBuildAgents()
{
    Mocks = new MockRepository();
    IBuildServer buildServer = Mocks.CreateMock<IBuildServer>();

    BuildAgentSelector buildAgentSelector = new BuildAgentSelector();
    using (Mocks.Record())
    {
        Expect.Call(buildServer.GetEnabledBuildAgents(TeamProjectName)).Return(null);
    }

    using (Mocks.Playback())
    {
        BuildAgent buildAgent = buildAgentSelector.Select(buildServer, TeamProjectName);

        Assert.IsNull(buildAgent);
    }
}

当我运行此测试时,我得到:

  

System.InvalidOperationException

     

上一个方法IBuildServer.CreateBuildAgentSpec("TeamProjectName");需要返回值或抛出异常。

这显然是调用真正的扩展方法而不是测试实现。我的下一个倾向是尝试:

Expect.Call(BuildServerExtensions.GetEnabledBuildAgents(buildServer, TeamProjectName))
      .Return(null);

然后我注意到我对Rhino.Mocks拦截这一点的期望可能是错位的。

问题是:如何消除此依赖性并使Select方法可测试?

请注意,扩展方法和BuildAgentSelector类在同一个程序集中,我宁愿避免更改它或者不得不转向除扩展方法之外的东西,尽管如果我知道它会处理这个,我会考虑另一个模拟框架情况。

3 个答案:

答案 0 :(得分:4)

您的扩展方法实际上写得非常好。它是一个无副作用的方法,并且扩展了一个接口,而不是一个具体的类。你几乎就在那里,但你只需要走得更远一点。你试图模拟.GetEnabledBuildAgents(...)扩展方法......但是这实际上并不是可模拟的(除了TypeMock Isolator之外的任何东西,这是目前唯一可以实际模拟静态的东西......但它相当昂贵。)

您实际上有兴趣在IBuildAgent上模拟扩展方法在内部调用的方法:.CreateBuildAgentSpec(...)。如果您仔细考虑,模拟CreateBuildAgentSpec方法将解决您的问题。扩展方法是“纯粹的”,所以实际上不需要嘲笑。它没有状态,也没有副作用。它在IBuildAgent接口上调用单个方法...这是指导您真正需要模拟的第一个线索。

尝试以下方法:

[TestMethod]
public void SelectReturnsNullOnNullBuildAgents()
{
    Mocks = new MockRepository();
    IBuildServer buildServer = Mocks.CreateMock<IBuildServer>();

    BuildAgent agent = new BuildAgent { ... }; // Create an agent
    BuildAgentSelector buildAgentSelector = new BuildAgentSelector();
    using (Mocks.Record())
    {
        Expect.Call(buildServer.CreateBuildAgentSpec(TeamProjectName)).Return(new List<BuildAgent> { agent });
    }

    using (Mocks.Playback())
    {
        BuildAgent buildAgent = buildAgentSelector.Select(buildServer, TeamProjectName);

        Assert.IsNull(buildAgent);
    }
}

通过创建BuildAgent实例并将其返回到List&lt; BuildAgent&gt;中,您可以有效地返回IEnumerable&lt; BuildAgent&gt;您的Select方法可能会运行。这应该让你去。如果只是返回一个基本的BuildAgent实例是不够的,或者如果你需要多个实例,你可能需要做一些额外的模拟。当涉及到要返回的模拟结果时,Rhino.Mocks可能是后方的真正痛苦。如果你遇到麻烦(根据我的经验,你很可能),我建议你give Moq a try,因为它是一个更好,更适合测试的框架。它不需要存储库,并且消除了Rhino.Mocks所需的Record / Playback和using()语句重符号。 Moq还提供了其他框架尚未提供的其他功能,一旦你进入更重的模拟场景,你就会爱上它(即It。*方法。)

希望这有帮助。

答案 1 :(得分:1)

休息了一会儿后,我意识到我实际上在BuildAgentSelector类中混淆了一些问题。我 代理选择。通过分离这两个问题并传递代理直接选择BuildAgentSelector构造函数(或代理/接口),我能够分离关注点,删除buildServer和teamProjectName参数的依赖关系,并简化界面进行中。它还实现了我在BuildAgentSelector类中寻找的可测试性结果。我也可以很好地单独测试扩展方法。

然而,最后,它只是将测试问题转移到其他地方。这是更好的,因为关注点更好,但无论问题在何处,jrista的答案都能解决问题。

在测试代码下面模拟第二层仍然有点难看。我基本上必须从我的扩展方法测试中获取成功路径的模拟,并在我的其他测试中重用此代码 - 并不困难,但有点烦人。

我会尝试MOQ,并且对编写扩展方法感到非常满意。

答案 2 :(得分:0)

测试调用真正的扩展方法,因为这是唯一的方法。模拟IBuildServer时不会创建测试实现,因为该方法不是IBuildServer的成员。

使用您现在的设置,没有干净的解决方案。

理论上,TypeMock将模拟静态类,但重构扩展方法将提供更大的可测试性。