如何保持我的单元测试干燥时嘲笑不';不起作用

本文关键字:不起作用 我的 何保持 单元测试 | 更新日期: 2023-09-27 18:16:24

编辑:

我试图为自己的问题提供一些解决方案,似乎模糊了整个问题。所以我稍微修改一下这个问题

假设我有这样的课程:

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返回标头、null或异常的情况。因此,主要的问题是:我应该在GetProtocolHeader将真正执行的地方编写测试(有些测试将重复,因为它们将测试与GetProtocolHeader本身的测试相同的代码(,还是应该使用@adrift和@Eric Nicholson描述的mocking方法,在这种方法中,我不会运行真正的GetProtoclHeader,而只是将mock配置为在调用该方法时返回header、null或exception?

如何保持我的单元测试干燥时嘲笑不';不起作用

在对GetProtocolHeaderValue的调用中,您真的需要知道它是否调用了GetProtocolHeader吗?

当然,只要知道它从正确的标头中获得了正确的值就足够了。它实际上是如何得到它的,这与单元测试无关。

您正在测试功能单元,GetProtocolHeaderValue的功能单元是在给定标头名称的情况下是否返回预期值。

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

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

编辑更新的问题:

  1. 如果GetProtocolHeader工作迅速、可靠并且是幂等的,那么我仍然认为没有必要嘲笑它。这三个方面中的任何一个方面的不足都是嘲笑的主要原因。

    如果(正如我从问题标题中怀疑的那样(,您希望嘲笑它的原因是设置适当状态以返回实际值所需的序言太过冗长,并且您不想在两个测试中重复它,为什么不在设置阶段进行呢?

  2. 优秀的单元测试所扮演的角色之一是文档

    如果有人想知道如何使用你的类,他们可以检查测试,并可能复制和更改测试代码以适应他们的目的。如果真正的用法习惯被模仿的创造和注入所掩盖,这就变得很困难。

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

    假设GetProtocolHeader在名称为空时抛出异常。您相应地创建了一个mock,并确保GetProtocolHeaderValue适当地处理该异常。稍后,您决定GetProtocolHeader应该为空名称返回null。如果您忘记更新mock,GetProtocolHeaderValue("")现在在现实生活中的行为将与测试套件不同。

  4. 如果mock没有设置那么冗长,那么mock可能会带来优势,但首先要充分考虑以上几点。

    尽管您给出了GetProtocolHeaderValue需要测试的三个不同的GetProtocolHeader响应(标头、null或异常(,但我认为第一个可能是"一系列标头"。(例如,它对存在但为空的标头做什么?它如何处理前导和尾随空白?非ASCII字符怎么办?数字?(。如果所有这些的设置都非常冗长,那么模拟可能会更好。

我经常使用分部mock(在Rhino中(或等效方法(如FakeItEasy中的CallsBaseMethod(来模拟我正在测试的实际类。然后,你可以将GetProtocolHeader设为虚拟的,并嘲笑你对它的调用。你可以说它违反了单一责任原则,但这显然仍然是非常有凝聚力的代码。

或者,你可以制作一种类似的方法

internal static string GetProtocolHeaderValue(string name, IHeader header )

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

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

我可能遗漏了一些东西,但考虑到列出的代码,在我看来,你不需要担心它是否被调用。

存在两种可能性:

  1. GetProtocolHeader()方法需要是公共的,在这种情况下,您可以编写一组测试来告诉您它是否按预期工作
  2. 这是一个实现细节,不需要公开,除非你想直接测试它,但在这种情况下,你真正关心的是一组测试,告诉你GetProtocolHeaderValue()是否按要求工作

在任何一种情况下,您都在测试暴露的功能,最终这才是最重要的。如果它是一个依赖项,那么是的,你可能会担心它是否被调用,但如果不是,那么它肯定是一个实现细节,不相关?

使用Moq,您可以使用CallBase在Rhino Mocks中执行部分模拟。

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

有关更多详细信息,请参阅moq quickstart的"自定义模拟行为"部分。

为什么不简单地更改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);
}

尝试最简单的方法。

  • 如果真正的GetProtocolHeader((实现快速且易于控制(例如,模拟标头、null和异常情况(,只需使用它
  • 如果不是(即,要么实际实现很耗时,要么你可以很容易地模拟这3种情况(,那么就考虑重新设计,以减轻限制

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

这都是一个机会的问题,纯粹的tdd列表会说没有mock,mocker会嘲笑这一切。

老实说,您编写的代码有问题,GetProtocolHeader似乎足够重要,不能作为实现细节丢弃,因为您将其定义为公共。

这里的问题在于第二个方法GetProtocolHeaderValue,它不可能使用IHeader 的现有实例

我建议在IHeader接口上使用GetValue(字符串名称(