由于需要过多的模拟而导致单元测试脆弱


21

关于我们在团队中实施的单元测试,我一直在遇到越来越烦人的问题。我们正在尝试将单元测试添加到设计不良的旧代码中,尽管我们在实际添加测试方面没有遇到任何困难,但是我们开始为测试的结果而苦恼。

作为问题的一个示例,假设您有一个方法在执行过程中调用了5个其他方法。此方法的测试可能是确认是否由于调用这5个其他方法之一而导致了行为。因此,由于单元测试应该仅出于一个原因和一个原因而失败,因此您希望消除调用这四种方法并对其进行模拟而导致的潜在问题。大!执行单元测试,忽略模拟的方法(它们的行为可以作为其他单元测试的一部分进行确认),并且验证有效。

但是存在一个新问题-单元测试对您如何确认将来行为和任何其他4种方法的签名变化或需要添加到“父方法”的任何新方法有深入了解。导致必须更改单元测试以避免可能的故障。

自然地,可以通过简单地使更多的方法完成更少的行为而在某种程度上缓解该问题,但是我希望可以找到一个更优雅的解决方案。

这是捕获问题的示例单元测试。

简要说明一下,“ MergeTests”是一个单元测试类,它继承自我们正在测试的类,并根据需要覆盖行为。这是我们在测试中采用的“模式”,允许我们覆盖对外部类/依赖项的调用。

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

其余的人如何处理此问题,或者没有很好的“简单”方法来处理它?

更新-感谢大家的反馈。不幸的是,这真的不足为奇,如果要测试的代码很差,似乎没有很好的解决方案,模式或实践可以在单元测试中遵循。我标记了最能抓住这个简单事实的答案。


哇,我只看到模拟设置,没有SUT实例化或任何东西,您在这里测试任何实际的实现吗?谁应该给StopSpinner打电话?OnMerge?您应该嘲笑它可能会引起的任何依赖关系,而不是事物本身
。.– Joppe

很难看,但是Mock <MergeTests>是SUT。我们设置Call​​Base标志以确保'OnMerge'方法在实际对象上执行,但是模拟出'OnMerge'调用的方法,这可能会由于依赖关系等原因导致测试失败。测试的目标是最后一行-确认在这种情况下我们停止了微调器。
PremiumTier

MergeTests听起来像是另一个仪器类,不是真正存在于生产中的东西,因此很混乱。
2013年


1
完全除了您的其他问题之外,对我来说您的SUT是Mock <MergeTests>似乎是错误的。你为什么要测试一个模拟?您为什么不测试MergeTests类本身?
埃里克·金

Answers:


18
  1. 修复代码以更好地进行设计。如果您的测试存在这些问题,那么当您尝试更改内容时,您的代码将遇到更严重的问题。

  2. 如果做不到,那么也许您就需要变得不太理想。根据方法的前提条件和条件进行测试。谁在乎您是否使用其他5种方法?他们大概有自己的单元测试,以便弄清测试失败时是什么原因导致了失败。

好的指导方针是“单元测试应该只有一个失败的原因”,但是根据我的经验,这是不切实际的。难以编写的测试不会被编写。脆弱的测试不可信。


我完全同意修改代码的设计,但是对于时间紧迫的大型公司而言,在开发过程中不太理想,很难为“还清”过去团队或决策不当所造成的技术债务提供理由。一旦。第二点,很多模拟不仅仅是因为我们只希望测试失败有一个原因-这是因为如果不首先处理该代码内部创建的大量依赖项,就不允许执行代码。很抱歉将目标杆移到该杆上。
PremiumTier13年

如果更好的设计不现实,我同意“谁在乎您是否使用其他5种方法?” 验证该方法执行所需的功能,而不是其执行方式。
Kwebble

@Kwebble-理解了,但是问题的目的是确定是否还有一种简单的方法来验证方法的行为,同时还必须模拟出该方法中调用的其他行为才能运行测试。我想删除“方法”,但我不知道如何:)
PremiumTier

没有神奇的银弹。没有测试不良代码的“简单方法”。需要重构被测代码,或者测试代码本身也很差。就像您遇到的那样,测试会很糟糕,因为它过于内部细节,或者如btilly所建议的那样,您可以在工作环境中运行测试,但是测试会变得慢得多,也更加复杂。无论哪种方式,测试都将更难编写,更难维护且容易出现假阴性。
史蒂文·多格特

8

将大型方法分解为更具针对性的小型方法绝对是最佳实践。您将其视为检验单元测试行为的痛苦,但同时也以其他方式经历了痛苦。

就是说,这是一个异端,但我个人是创建现实的临时测试环境的粉丝。也就是说,不要嘲笑那些其他方法中隐藏的所有内容,而是要确保有一个易于设置的临时环境(完整的私有数据库和架构-SQLite可能在这里提供帮助),该环境可以让您运行所有这些东西。知道如何构建/拆卸该测试环境的责任与要求该代码的代码一起存在,因此当它发生更改时,您不必更改所有依赖于其存在的单元测试代码。

但是我确实注意到这是我的异端。大量参与单元测试的人提倡“纯”单元测试,并将我所说的称为“集成测试”。我个人并不担心这种区别。


3

我会考虑简化模拟程序,只是制定可能包含其调用方法的测试。

不要测试方法,不能测试什么。重要的是结果,必要时包括子方法。

从另一个角度讲,您可以制定一个测试,使其通过一种大方法通过,进行重构,并在重构后得到一棵方法树。您无需单独测试每一个。最终结果至关重要。

如果子方法难以测试某些方面,请考虑将它们分解为单独的类,这样您就可以在其中进行更干净的模拟,而无需对要测试的类进行大量的检测/缝合。很难判断您是否在示例测试中实际测试了任何具体的实现。


问题是我们必须模拟“如何”来测试“什么”。这是代码设计所施加的限制。我当然不希望“嘲笑”如何使测试变得脆弱。
PremiumTier

查看方法名称,我认为您测试过的类仅承担过多的责任。阅读有关单一责任原则的信息。从MVC借用可能会有所帮助,您的课程似乎可以处理UI,基础结构和业务问题。
2013年

是的:(那就是我指的是设计不良的旧代码。我们正在重新设计和重构,但是我们认为最好先对源进行测试
。– PremiumTier
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.