【问题标题】:Is there an easier way to handle unit testing a method with too many conditions?是否有更简单的方法来处理具有太多条件的方法的单元测试?
【发布时间】:2017-01-25 18:00:52
【问题描述】:

我有一个方法,里面有很多条件:

public bool IsLegalSomething(Order order)
{
    var item0 = order.Items.SingleOrDefault(x => x.ItemCode == "ItemCode0");
    var item1 = order.Items.SingleOrDefault(x => x.ItemCode == "ItemCode1");
    ...
    var itemN = order.Items.SingleOrDefault(x => x.ItemCode == "ItemCodeN");

    return ((item0.Status == Status.Open) && (item1.Status == Status.Closed)
         && ...
         && (itemN.Status == Status.Canceled));
}

我想对这个函数进行单元测试,但是如果你考虑每个组合,单元测试的数量太多了,以至于单元测试的数量是疯狂的。该返回语句中有 16 个条件,并且由于每个条件都是真/假,即 2^16 种不同的组合,我需要检查。我真的需要在这里创建 2^16 个不同的单元测试来确保每个条件都被利用吗?请注意,这是一个简单的例子。由于法律要求,我的一些功能具有复杂的条件:

return (condition0 && condition1 && (condition2 || condition3)
     && (condition4 || (condition5 && condition6)) ...)

根据我的一些函数的数学计算,条件可以产生的不同组合的数量是数百万!我研究了数据驱动单元测试 (DDUT) 以及参数化单元测试 (PUT),但这只是让单元测试成为“填空”风格。我仍然必须提供所有各种组合和预期的结果!例如:

// Parameterized Unit Test
[TestCase(..., Result = true)]  // Combination 0
[TestCase(..., Result = true)]  // Combination 1
[TestCase(..., Result = false)] // Combination 2
public bool GivenInput_IsLegalSomething_ReturnsValidResult(...) { }

如果我使用 MSTest 来提取数据源(例如 csv),我仍然会遇到同样的问题。我有太多的组合会产生不同的结果。有没有我不知道的替代方法?

【问题讨论】:

  • 设计可能需要审查和重构。创建单元测试的复杂性/难度是被测试代码干净程度的一个指标。正在测试的方法做得太多了。
  • @Nkosi,可以理解,但我不确定如何“简化”具有这么多组合的法律要求。在一天结束时,某些类将包含“16 个条件驱动一个布尔值”的逻辑。
  • @Nkosi 是正确的。
  • 你可以编写一个单一的测试方法来循环生成的组合。否则,我会考虑专注于业务用例。在这种情况下,您可能希望将“IsLegalSomething”重构为多个方法,每个方法都基于这些业务用例测试单个事物。
  • @PeterRitchie:这正是问题所在,IsLegalSomething 是一个代码单元,它需要 16 个条件并返回 1 个结果。如何重构说 16 个输入导致 1 个输出的代码?

标签: c# unit-testing data-driven-tests parameterized-unit-test


【解决方案1】:

虽然我同意 cmets 关于重构代码的观点,但我认为有必要提供更简洁的答案来解释 “设计可能需要审查和重构”的确切含义。

让我们看看下面的语句:A && B && (C || D);。通常,您会说您有 4 个输入 @ 2 个选项/每个,或 16 个组合。但是,如果你重构一些东西,你可以降低复杂性。这将根据您的业务域而有所不同,因此我将使用购物网站域作为示例(我们实际上并不知道您的业务域)。

  • 答:是新秩序
  • B:所有商品都有库存吗
  • C:是客户精英会员
  • D:是否使用了折扣代码 FREESHIP

我选择这个场景的原因是为了证明 C/D 实际上可能会提到是否应该免费包含运费。

  • E:免运费

现在,我们使用 A && B && E 代替 A && B && (C || D),即 3 个条件 @ 2 个选项/每个,或 8 个组合。当然,E的组成也要测试,但是C || D只有2个选项@2个选项/每个,或者4个组合。我们已将总组合的数量从 16 个减少到 12 个。虽然这可能看起来不多,但这种情况的规模要小得多。如果您能够将逻辑条件组合在一起并进一步减少事物,则可以将数百万个组合减少到数百个,这更容易维护。

此外,作为奖励,有时您的域逻辑会在某些方面发生变化,而在其他方面则不会。想象一下,您决定商业客户也可以在某一天获得免费送货服务,而不是在极其复杂的条件语句中添加另一个条件,基本上将单元测试的数量增加一倍,而是将该条件添加到 Is Free Shipping,这会将较小单元的组合数量从 4 个增加到 8 个,但它比具有 16 个条件(即 65536 个组合)到 17 个条件(即 131072 个组合)的函数要好。


此时,除非我们知道并了解您的确切领域,否则我们只能提出将您的类和方法重新设计成更小的部分的广泛建议。此外,虽然mart 使用字符串长度 > 5 不需要测试每个长度大于 5 的字符串,但我确实认为,一旦您将实际条件降低为真/假,则需要测试组合条件.例如:(String Length > 5) && B && C && D && E 有 5 个条件或 32 个组合。你应该测试所有 32 个。你不应该做的是想出 100 种不同的方法来证明 String Length > 5 是正确的。我之所以说要测试所有 32 种组合,是因为虽然可以重构和测试条件,但您仍然想测试您使用的是 A && B && (C || D),因此您可以确定没有人打错 A && B || (C && D)。或者,换句话说,证明单个条件并不能保证这些条件的组合被正确编码。

【讨论】:

    【解决方案2】:

    您应该重构代码以正确测试它,每个条件块应该是一个函数本身,并且该函数应该单独测试,然后您的主函数只需要测试来检查集成。

    单元测试并不意味着涵盖所有选项,事实上,如果您正在测试 true && true、false && false 等等,您实际上是在测试 && 运算符而不是您的逻辑,您应该涵盖您的功能作为“单位”不是所有可能的输入组合,只是想在一个检查字符串长度是否大于 5 的函数中,你会创建世界上所有可能的字符串吗?还是你只测试一个等于 5,一个超过 5 个字符,一个更低,也许你也应该测试 null,但不要超过那个。

    无论如何,如果您能够将 NUnit 添加到您的项目中,您可以使用https://github.com/nunit/docs/wiki/Combinatorial-Attribute 来生成您想要的所有测试,从而为您提供一个真正的替代方案。

    【讨论】:

      猜你喜欢
      • 2014-06-08
      • 1970-01-01
      • 2014-04-17
      • 2010-11-17
      • 1970-01-01
      • 1970-01-01
      • 2021-10-21
      • 1970-01-01
      • 2019-02-03
      相关资源
      最近更新 更多