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

4

我有一个扩展方法,签名如下(在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个回答

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<BuildAgent>中返回它,您有效地返回了一个IEnumerable<BuildAgent>,供您的Select方法操作。这应该可以让您开始工作了。如果简单地返回一个基本的BuildAgent实例不足够,或者您需要更多的实例,则可能需要进行一些额外的模拟。当涉及到模拟要返回的结果时,Rhino.Mocks可能会非常麻烦。如果遇到问题(根据我的经验,您很有可能会遇到),我建议您尝试使用Moq,因为它是一个更好、更适合测试人员使用的框架。它不需要仓库,并且消除了Rhino.Mocks所需的Record/Playback和using()语句的繁琐符号。Moq还提供其他一些其他框架尚未提供的功能,一旦您进入更复杂的模拟场景,您将爱上它(例如It.* 方法)。 希望这可以帮助到你。

1

经过休息并重新开始,我意识到在BuildAgentSelector类中实际上混合了一些关注点。我正在获取代理并选择它们。通过将这两个关注点分开,并直接将要选择的代理传递给BuildAgentSelector构造函数(或委托/接口来执行此操作),我能够分离关注点,消除对buildServer和teamProjectName参数的依赖,并简化接口。这也实现了我在BuildAgentSelector类上寻找的可测试性结果。我也可以很好地单独测试扩展方法。

然而,最终,它只是将测试问题转移到其他地方。它更好,因为关注点更好,但jrista的答案无论关注点放在哪里都可以解决问题。

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

我将尝试使用MOQ,并小心不要过于热衷于编写扩展方法。


0

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

使用您当前的设置,没有清洁的解决方案。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接