如何在模拟不起作用时保持我的单元测试DRY?

时间:2011-12-02 16:48:08

标签: c# .net unit-testing tdd mocking

修改

似乎通过尝试为我自己的问题提供一些解决方案,我模糊了整个问题。所以我正在修改这个问题。

假设我有这门课程:

public class ProtocolMessage : IMessage
{
    public IHeader GetProtocolHeader(string name)
    {
        // Do some logic here including returning null 
        // and throw exception in some cases

        return header;
    }

    public string GetProtocolHeaderValue(string name)
    {
        IHeader header = GetProtocolHeader(name);

        // Do some logic here including returning null
        // and throw exception in some cases

        return value;
    }
}

这些方法的实际情况并不重要。重要的是我有多个单元测试来覆盖涵盖所有情况的GetProtocolHeader方法(返回正确的标题,null或异常),现在我正在为GetProtocolHeaderValue编写单元测试。

如果GetProtocolHeaderValue依赖于外部依赖,我将能够模拟它并注入它(我正在使用Moq + NUnit)。然后我的单元测试将测试期望外部依赖被调用并返回预期值。外部依赖将通过自己的单元测试进行测试,我会完成但是如何在这个示例中正确进行,其中方法不是外部依赖?

澄清问题:

我相信我的GetProtocolHeaderValue测试套件必须测试GetProtocolHeader返回header,null或exception的情况。所以主要的问题是:我应该编写测试,其中GetProtocolHeader将被真正执行(一些测试将被复制,因为它们将测试相同的代码作为GetProtocolHeader本身的测试)或者我应该使用描述的模拟方法@adrift和@Eric Nicholson我不会运行真正的GetProtoclHeader但只是配置mock以在调用此方法时返回header,null或exception?

7 个答案:

答案 0 :(得分:4)

在致电GetProtocolHeaderValue时,您是否真的需要知道它是否会调用GetProtocolHeader

当然知道它从正确的标题中获取正确的值就足够了。实际如何获得它与单元测试无关。

您正在测试功能单元,GetProtocolHeaderValue的功能单位是它是否返回给定标题名称的预期值。

确实,您可能希望防止不适当的缓存或交叉污染或从不同的标头中获取值,但我不认为测试它已调用GetProtocolHeader是最好的方法做这个。你可以推断它以某种方式从它返回标题的预期值的事实中获取了正确的标题。

只要您以确保重复标题不会掩盖错误的方式制作测试和测试数据,那么一切都应该很好。

编辑更新的问题:

  1. 如果GetProtocolHeader快速,可靠并且是幂等的,那么我仍然认为没有必要嘲笑它。这三个方面中的任何一个缺陷都是(IMO)嘲弄的主要原因。

    如果(正如我怀疑问题标题所示),您希望模仿它的原因是设置适当状态以返回实际值所需的前导码过于冗长,并且您不希望重复它两个测试,为什么不在设置阶段进行测试?

  2. 良好的单元测试所执行的角色之一是文档

    如果有人想知道如何使用您的课程,他们可以检查测试,并可能复制和更改测试代码以符合他们的目的。如果通过创造和注入嘲笑来掩盖使用的真实习语,这就变得很困难。

  3. 模拟可以掩盖潜在的错误。

    假设name为空,GetProtocolHeader抛出异常。您可以相应地创建一个模拟,并确保GetProtocolHeaderValue正确处理该异常。稍后,您决定GetProtocolHeader应返回null以获取空名称。如果您忘记更新模拟,GetProtocolHeaderValue("")现在在现实生活中与测试套件的行为会有所不同。

  4. 如果模拟比设置更简洁,则模拟可能会带来优势,但首先要考虑上述要点。

    虽然您提供了GetProtocolHeader需要测试的三个不同的GetProtocolHeaderValue响应(标题,空或异常),但我认为第一个可能是“一系列标题”。 (例如,它对存在的标题有什么作用,但是空?它如何处理前导和尾随空格?非ASCII字符怎么样?数字?)。如果所有这些的设置非常详细,那么最好模拟。

答案 1 :(得分:3)

我经常使用部分模拟(在Rhino中)或等效模拟(如FakeItEasy中的CallsBaseMethod)来模拟我正在测试的实际类。然后,您可以使GetProtocolHeader虚拟化并模拟您的调用。你可以说它违反了单一的责任主体,但这仍然是非常有凝聚力的代码。

或者你可以制作像

这样的方法
internal static string GetProtocolHeaderValue(string name, IHeader header )

并独立测试处理。公共GetProtocolHeaderValue方法不会有任何/很多测试。

编辑:在这种特殊情况下,我还考虑将GetValue()添加为IHeader的扩展方法。这将非常容易阅读,你仍然可以进行空检查。

答案 2 :(得分:1)

我可能错过了一些东西,但鉴于列出的代码,在我看来,你不需要担心它是否被调用。

存在两种可能性:

  1. GetProtocolHeader()方法需要公开,在这种情况下,您编写一组测试,告诉您它是否按预期工作。
  2. 它是一个实现细节,不需要公开,除非你希望能够直接测试它,但在这种情况下,你真正关心的是一组测试,告诉你是否{{ 1}}按要求工作。
  3. 在任何一种情况下,您都在测试暴露的功能,并在一天结束时这一切都很重要。如果它是一个依赖那么是的你可能会担心它是否被调用,但它是否肯定是它的实现细节而不相关?

答案 3 :(得分:1)

使用Moq,您可以使用CallBase来完成Rhino Mocks中的部分模拟。

在您的示例中,将GetProtocolHeader更改为virtual,然后创建ProtocolMessage的模拟,设置CallBase = true。然后,您可以根据需要设置GetProtocolHeader方法,并调用GetProtocolHeaderValue的基类功能。

有关详细信息,请参阅moq快速入门的Customizing Mock Behavior部分。

答案 4 :(得分:1)

为什么不简单地更改GetProtocolHeaderValue(字符串名称)以便它调用2个方法,第二个方法接受IHeader?这样,您可以通过RetrieveHeaderValue方法在单独的测试中测试所有//做一些逻辑部分,而不必担心Mocks。类似的东西:

public string GetProtocolHeaderValue(string name)
{
   IHeader header = GetProtocolHeader(name);
   return RetrieveHeaderValue(IHeader header);
}

现在您可以非常轻松地测试GetProtocolHeaderValue的两个部分。现在,您仍然遇到测试该方法的相同问题,但其中的逻辑量已降至最低。

按照同样的思路,可以在IHeaderParser接口中提取这些方法,并且GetProtocol方法将接受IHeaderParser,这对于测试/模拟来说是微不足道的。

public string GetProtocolHeaderValue(string name, IHeaderParser parser)
{
    IHeader header = parser.GetProtocolHeader(name);
    return parser.HeaderValue(IHeader header);
}

答案 5 :(得分:1)

这都是意见问题,纯粹的tdd-ists会说没有嘲笑,嘲弄者会嘲笑它。

在我诚实的观点中,您编写的代码存在问题,GetProtocolHeader似乎非常重要,不能作为实现细节丢弃,因为您公开定义它。

这里的问题在于第二种方法GetProtocolHeaderValue,它无法使用现有的IHeader实例

我建议IHeader接口上的GetValue(字符串名称)

答案 6 :(得分:0)

尝试最简单的方法。

  • 如果真正的GetProtocolHeader()实现快速且易于控制(例如模拟标题,null和异常情况),只需使用它。
  • 如果不是(即实际实施既耗时又可以轻松模拟3种情况),那么请重新设计,以便减轻约束。

除非绝对必要,否则我不会使用Mocks(例如文件/网络/外部依赖),但您可能知道这只是个人选择而非规则。确保选择值得测试的额外认知开销(可读性下降)。