2009-05-28 69 views
3

我有以下签名的扩展方法(在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 
} 

而且我想

[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); 
    } 
} 

当我运行这个测试,我得到:使用MSTest的和Rhino.Mocks(V3.4)与测试

System.InvalidOperationException

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

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

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

然后我注意到我对Rhino.Mocks截取这个的期望很可能是错误的。

问题是:我如何消除这种依赖关系并使Select方法可测试?

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

回答

3

你的扩展方法实际写得相当好。它是一个无副作用的方法,并且扩展了一个接口,而不是一个具体的类。你几乎在那里,但你只需要走得更远一点。您试图嘲笑.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实例,并以列表返回<它BuildAgent >,您可以有效地返回一个IEnumerable <BuildAgent>你的选择方法进行操作。这应该让你去。如果仅仅返回一个基本的BuildAgent实例是不够的,或者您需要多个实例,您可能需要做一些额外的模拟。当涉及到嘲讽结果被返回时,Rhino.Mocks可能成为后方的真正痛苦。如果遇到麻烦(根据我的经验,你很有可能),我建议你give Moq a try,因为它是一个更好,更友好的框架。它不需要存储库,并且消除了Rhino.Mocks所需的记录/重放和使用()语句重标记。 Moq还提供了其他框架还没有提供的额外功能,一旦你陷入更严重的嘲讽场景,你会爱上它(即It。*方法)。

希望这有助于你。

0

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

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

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

1

休息一段时间后,我发现我实际上在BuildAgentSelector类中混合了一些问题。我得到的代理商和选择他们。通过分离这两个问题,并让代理直接选择BuildAgentSelector构造函数(或委托/接口来实现),我可以分离问题,删除buildServer和teamProjectName参数的依赖关系,并简化接口正在进行中。它还实现了我在BuildAgentSelector类中寻找的可测试性结果。我也可以很好地分别测试扩展方法。

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

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

我会给MOQ一个尝试,并小心写入扩展方法太高兴。