如何使依赖扩展方法的方法可测试?
我有一个具有以下签名的扩展方法(在 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 类位于同一个程序集中,我希望避免更改此方法或必须转向扩展方法之外的其他方法,尽管如果我知道另一个模拟框架可以处理这种情况,我会考虑它。
I have an extension method with the following signature (in BuildServerExtensions class)::
public static IEnumerable<BuildAgent> GetEnabledBuildAgents(
this IBuildServer buildServer,
string teamProjectName)
{
// omitted agrument validation and irrelevant code
var buildAgentSpec = buildServer.CreateBuildAgentSpec(teamProjectName);
}
And another method which calls the first (in BuildAgentSelector class):
public BuildAgent Select(IBuildServer buildServer, string teamProjectName)
{
// omitted argument validation
IEnumerable<BuildAgent> serverBuildAgents =
buildServer.GetEnabledBuildAgents(teamProjectName);
// omitted - test doesn't get this far
}
And I am trying to test it using MSTest and Rhino.Mocks (v3.4) with:
[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);
}
}
When I run this test I get:
System.InvalidOperationException
:Previous method
IBuildServer.CreateBuildAgentSpec("TeamProjectName");
requires a return value or an exception to throw.
This is obviously calling the real extension method rather than the test implementation. My next inclination was to try:
Expect.Call(BuildServerExtensions.GetEnabledBuildAgents(buildServer, TeamProjectName))
.Return(null);
Then I noticed that my expectations for Rhino.Mocks to intercept this were probably misplaced.
The question is: How do I eliminate this dependency and make the Select method testable?
Note that the extension method and BuildAgentSelector classes are in the same assembly and I would prefer avoiding changing this or having to turn to something besides an extension method, though another mocking framework is something I would consider if I knew it would handle this situation.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
您的扩展方法实际上写得相当好。 它是一种无副作用的方法,并且扩展一个接口,而不是一个具体的类。 你已经快到了,但你只需要走得更远一点。 您正在尝试模拟 .GetEnabledBuildAgents(...) 扩展方法...但是这实际上并不是可模拟的(除了 TypeMock Isolator 之外的任何东西,这是目前唯一可以实际模拟静态的东西...但是它相当昂贵.)
您实际上有兴趣模拟扩展方法在内部调用的 IBuildAgent 上的方法:.CreateBuildAgentSpec(...)。 如果您仔细考虑,模拟 CreateBuildAgentSpec 方法将解决您的问题。 扩展方法是“纯粹的”,所以真的不需要被嘲笑。 它没有状态,也不会产生副作用。 它调用 IBuildAgent 接口上的单个方法...这是引导您真正需要模拟的内容的第一个线索。
尝试以下操作:
通过创建 BuildAgent 实例并将其返回到 List中,您可以有效地返回 IEnumerable实例。 您的 Select 方法可以对其进行操作。 这应该能让你继续前进。 如果仅返回一个基本的 BuildAgent 实例还不够,或者您需要多个实例,您可能需要进行一些额外的模拟。 当涉及到要返回的模拟结果时,Rhino.Mocks 可能是一个真正的麻烦。 如果您遇到麻烦(根据我的经验,您很可能会遇到麻烦),我建议您尝试一下 Moq,因为它是一个更好、更适合测试人员使用的框架。 它不需要存储库,并且消除了 Rhino.Mocks 所需的记录/回放和 using() 语句繁重符号。 Moq 还提供其他框架尚未提供的附加功能,一旦您进入较重的模拟场景,您就会爱上(即 It.* 方法)。
希望这会有所帮助。
Your extension method is actually written fairly well. Its a side-effect free method, and is extending an interface, rather than a concrete class. Your almost there, but you just need to go a little farther. You are trying to mock the .GetEnabledBuildAgents(...) extension method...however thats not actually mockable (by anything except TypeMock Isolator, which is the only thing that can actually mock statics at the moment...however its fairly pricy.)
You are actually interested in mocking the method on IBuildAgent that your extension method calls internally: .CreateBuildAgentSpec(...). If you think it through, mocking the CreateBuildAgentSpec method will solve your problem. The extension method is "pure", and so really doesn't need to be mocked. It has no state and causes no side effects. It calls a single method on the IBuildAgent interface...which is the first clue that directs you to what really needs to be mocked.
Try the following:
By creating a BuildAgent instance, and returning it in a List<BuildAgent>, you effectively return an IEnumerable<BuildAgent> that your Select method may operate on. That should get you going. You may need to do some additional mocking in case simply returning a basic BuildAgent instance isn't sufficient, or in case you need more than one. When it comes to mocking results to be returned, Rhino.Mocks can be a REAL pain in the rear to work with. If you run into troubles (which, given my experience with it, you are quite likely to), I recommend you give Moq a try, as it is a much nicer and more tester-friendly framework to work with. It doesn't require a repository, and eliminates the Record/Playback and using() statement heavy notation that Rhino.Mocks required. Moq also provides additional capabilities that other frameworks don't offer yet that, once you get into heavier mocking scenarios, you will fall in love with (i.e. the It.* methods.)
Hope this helps.
休息一下并带着全新的头脑回来后,我意识到我实际上在 BuildAgentSelector 类中混合了一些关注点。 我正在获取代理并选择他们。 通过分离这两个问题并将代理直接传递给 BuildAgentSelector 构造函数(或委托/接口来执行此操作),我能够分离问题,删除对 buildServer 和 teamProjectName 参数的依赖项,并简化接口正在进行中。 它还实现了我在 BuildAgentSelector 类上寻找的可测试性结果。 我也可以很好地单独测试扩展方法。
但最终,只是将测试问题转移到了别处。 更好是因为关注点放在了更好的位置,但无论关注点放在哪里,jrista 的答案都解决了问题。
必须模拟被测代码下面的第二层仍然有点难看。 我本质上必须从我的扩展方法测试中模拟成功路径,并在我的其他测试中重用此代码 - 并不困难,但有点烦人。
我会尝试最小起订量,并小心不要对编写扩展方法过于满意。
After a break and coming back with a fresh head, I realized that I am actually mixing concerns in the BuildAgentSelector class a little. I am getting the agents and selecting them. By separating these two concerns and passing the agents to select directly to the BuildAgentSelector constructor (or a delegate/interface to do so), I am able to separate the concerns, remove the dependencies on both the buildServer and teamProjectName parameters, and simplify the interface in the process. It also achieved the testability results I was looking for on the BuildAgentSelector class. I can test the extension method separately nicely as well.
However, in the end, it merely transferred the testing problem elsewhere. It is better because the concern is better placed, but jrista's answer resolves the issue no matter where the concern is placed.
It is still a bit ugly to have to mock the second layer beneath the code under test. I essentially have to take the mock of the successful path from my extension method testing and reuse this code in my other tests - not difficult, but a bit annoying.
I will give MOQ a try and be careful about getting too happy with writing extension methods.
该测试调用真正的扩展方法,因为这是唯一的方法。 当您模拟 IBuildServer 时,不会创建任何测试实现,因为该方法不是 IBuildServer 的成员。
使用您现在的设置,没有干净的解决方案。
理论上,TypeMock 将模拟静态类,但重构扩展方法将提供更好的可测试性。
The test calls the real extension method because that is the only one. No test implementation is created when you mock an IBuildServer because the method is not a member of IBuildServer.
There is no clean solution to this using the setup you have now.
Theoretically TypeMock will mock the static class, but refactoring out the extension methods would provide greater testability.