状态/交互测试以及混淆(或滥用)它们的混淆

时间:2009-08-19 14:10:33

标签: unit-testing tdd mocking

我认为理解基于状态/交互的测试的定义(阅读Fowler的东西等)。我发现我开始以状态为基础,但一直在做更多的交互,我对如何测试某些事情感到有些困惑。

我在MVC中有一个控制器,一个动作调用服务来拒绝一个包:

public ActionResult Deny(int id)
{
    service.DenyPackage(id);

    return RedirectToAction("List");
}

这对我来说似乎很清楚。提供模拟服务,验证它是否已正确调用,已完成。

现在,我有一个视图操作,允许用户将证书与包关联:

public ActionResult Upload(int id)
{
    var package = packageRepository.GetPackage(id);
    var certificates = certificateRepository.GetAllCertificates();

    var view = new PackageUploadViewModel(package, certificates);

    return View(view);
}

这个我有点难过。我正在进行Spec样式测试(可能不正确)所以为了测试这个方法,我有一个类然后进行两个测试:验证调用包存储库,验证是否调用了证书库。我实际上想要第三个测试来验证构造函数是否被调用但不知道如何做到这一点!我觉得这是完全错误的。

因此,对于基于状态的测试,我会传入id,然后测试ActionResult的视图。好的,这是有道理的。但是我不会对PackageUploadViewModel构造函数进行测试吗?因此,如果我对构造函数进行了测试,那么我的一部分只是想验证我是否调用了构造函数,并且操作返回与构造函数返回的内容相匹配。

现在,我能想到的另一个选择是我有一个PackageUploadViewModelBuilder(或同样愚蠢的名字)依赖于两个存储库然后我只是将id传递给CreateViewModel方法或其他东西。然后,我可以模拟这个对象,验证一切,并开心。但是......好吧......看起来很奢侈。我做的很简单......不简单。另外,controller.action(id)返回builder.create(id)似乎无缘无故地添加一个层(控制器负责构建视图模型..对吗?)

我不知道......我在想更多基于状态的测试是必要的,但我担心如果我开始测试返回值,那么如果方法A可以在8个不同的上下文中调用,那么我将会进行测试爆炸有很多重复。我一直在使用基于交互的测试将一些上下文传递给方法B,这样我所要做的就是验证方法A,称为方法B,我测试了方法B,因此方法A可以信任处理这些上下文。因此,基于交互的测试正在构建这种测试层次结构,但基于状态的测试将使其变得扁平化。

我不知道这是否有意义。

哇,这很长......

2 个答案:

答案 0 :(得分:5)

我认为Roy Osherove最近认为,根据经验,你的测试应该是95%基于状态和5%基于交互。我同意。

最重要的是,您的API可以满足您的需求, 是您需要测试的内容。如果你测试它如何实现它需要做的机制,你很可能最终得到Overspecified Tests,它会在可维护性方面咬你。

在大多数情况下,您可以设计API,以便基于状态的测试是自然的选择,因为这样做更容易。

检查上传示例:调用GetPackage和GetAllCertificates是否重要?这真的是Upload方法的预期结果吗?

我猜不会。我的猜测是,上传方法的目的 - 它是存在的原因 - 是填充和提供正确的视图。

因此,基于状态的测试将检查返回的ViewResult及其ViewModel,并验证它是否具有所有正确的值。

当然,正如代码现在所说的那样,你需要为packageRepository和certificateRepository提供测试双打,因为否则会抛出异常,但看起来本身并不重要的是调用存储库方法

如果您使用Stubs而不是Mocks作为存储库,则您的测试不再与内部实现细节相关联。如果您稍后决定更改Upload方法的实现以使用缓存的包(或其他)实例,则不会调用Stub,但这没关系,因为它无论如何都不重要 - 重要的是什么是返回的视图包含预期的数据。

这比测试中断要好得多,即使所有返回的数据都是应该的。

有趣的是,你的Deny示例看起来仍然是基于交互的测试的一个主要示例,因为只有通过检查间接输出才能验证方法是否执行了正确的操作(DenyPackage方法返回void)。 / p>

所有这些以及更多内容在优秀的书xUnit Test Patterns中得到了很好的解释。

答案 1 :(得分:1)

要问的问题是“如果这段代码有效,我该怎么说?”这可能意味着测试一些交互或某些状态,这取决于什么是重要的。

在第一次测试中,Deny改变了目标类之外的世界。它需要服务的协作,因此测试交互是有意义的。在你的第二次测试中,你正在对邻居进行查询(不改变目标类之外的任何东西),因此对它们进行删除更有意义。

这就是为什么我们在http://www.mockobjects.com/book中有“Stub Queries,Mock Actions”的启发式算法