【问题标题】:Should a Unit-test replicate functionality or Test output?单元测试应该复制功能还是测试输出?
【发布时间】:2011-01-29 18:08:30
【问题描述】:

我曾多次遇到这种困境。我的单元测试是否应该复制他们正在测试的方法的功能以验证其完整性?或者单元测试是否应该努力使用大量手动创建的输入和预期输出实例来测试方法?

我主要是针对您正在测试的方法相当简单并且可以通过查看代码一分钟来验证其正确操作的情况提出问题。

简化示例(在 ruby​​ 中):

def concat_strings(str1, str2)
  return str1 + " AND " + str2
end

上述方法的简化功能复制测试:

def test_concat_strings
  10.times do
    str1 = random_string_generator
    str2 = random_string_generator
    assert_equal (str1 + " AND " + str2), concat_strings(str1, str2)
  end
end

我了解大多数情况下,您正在测试的方法不够简单,不足以证明这样做是合理的。但我的问题仍然存在; 在某些情况下这是一种有效的方法(为什么或为什么不)

【问题讨论】:

  • 我对第一个场景不太熟悉——单元测试复制了一个方法的功能,你能详细说明一下吗?
  • 在测试驱动开发中,我经常创建一个测试来验证(尚未编写的)方法是否按预期工作。在简单的场景中,我经常想以我最熟悉的方式编写这些规范,以编程方式从输入值中获得所需的输出。有时我觉得我在手动计算和输入预期结果时更容易出错,而不仅仅是编写一些代码来完成工作。
  • 好问题 - 我想这是 OnceAndOnlyOnce 规则的一个例外......除非我不知道如何正确重构它。
  • 我应该给出一个更接近实际提示我提出问题的代码的示例。我正在测试的代码是一个简单的例程,它转换了哈希,将一些键更改为大写,组合了一些键/值对,删除了一些条目......所以 Mark Seemann 的建议听起来是个好主意。跨度>

标签: ruby unit-testing testing tdd


【解决方案1】:

使用相同的实现来测试功能,不会测试任何东西。如果其中一个有错误,另一个也会有。

但是通过与替代实现进行比较进行测试是一种有效的方法。例如,您可以通过将计算斐波那契数的迭代(快速)方法与相同方法的普通递归但缓慢的实现进行比较来测试它。

这种情况的一种变体是使用一种仅适用于特殊情况的实现。当然,在这种情况下,您只能将其用于此类特殊情况。

在选择输入值时,大多数情况下使用随机值并不是很有效。我更喜欢随时精心挑选的价值观。在您给出的示例中,想到连接时不适合字符串的空值和极长值。

如果您使用随机值,请确保您有办法使用相同的随机值重新创建准确的运行,例如通过记录种子值,并有办法在开始时设置该值。

【讨论】:

    【解决方案2】:

    这是一个有争议的立场,但我相信unit testing using Derived Values 远优于使用任意硬编码输入和输出。

    问题在于,当算法变得稍微复杂时,如果用硬编码值表示,输入和输出之间的关系就会变得模糊不清。单元测试最终成为一个假设。它可能在技术上有效,但会损害测试的可维护性,因为它会导致Obscure Tests

    使用Derived Values 对结果进行测试可以在测试输入和预期输出之间建立更清晰的关系

    这不测试任何东西的论点根本不正确,因为任何测试用例都只会运行通过 SUT 的路径的一部分,因此没有单个测试用例会重现被测试的整个算法,但是测试会这样做。

    另一个好处是您可以use fewer unit tests to cover the desired functionality,甚至同时让他们更具交流性。最终结果是更简洁、更易于维护的单元测试。

    【讨论】:

      【解决方案3】:

      在单元测试中,您绝对应该手动提出测试用例(输入、输出以及您期望的副作用 - 这些将是您的模拟对象的期望)。您以某种方式提出这些测试用例,以便它们涵盖您的类的所有功能(例如,涵盖所有方法,所有 if 语句的所有分支等)。通过显示所有可能的用法来更多地考虑创建类的文档。

      重新实现类不是一个好主意,因为您不仅会得到明显的代码/功能重复,而且您很可能会在这个新实现中引入相同的错误。

      【讨论】:

        【解决方案4】:

        为了测试方法的功能,我会尽可能使用输入和输出对。否则,您可能会复制和粘贴功能以及其实现中的错误。那你在测试什么?您将测试功能(包括其所有错误)是否随时间变化。但您不会测试实现的正确性。

        测试功能是否随时间变化可能(暂时)在重构期间很有用。但是你多久重构一次这样的小方法?

        也可以将单元测试视为文档以及方法输入和预期输出的规范。两者都应该尽可能简单,以便其他人可以轻松阅读和理解它。一旦你在测试中引入额外的代码/逻辑,它就会变得更难阅读。

        您的测试实际上看起来像fuzz test。模糊测试可能非常有用,但在单元测试中,由于可重复性,应避免随机性。

        【讨论】:

          【解决方案5】:

          单元测试应该执行您的代码,而不是作为您使用的语言的一部分。

          如果代码的逻辑是以特殊方式连接字符串,您应该对此进行测试 - 否则您需要依赖您的语言/框架。

          最后,您应该先创建单元测试以“有意义地”失败。换句话说,不应该使用随机值(除非您正在测试您的随机数生成器没有返回相同的随机值集!)

          【讨论】:

            【解决方案6】:

            永远不要使用随机数据作为输入。如果您的测试报告失败,您将如何复制它?并且不要使用相同的函数来生成预期的结果。如果您的方法中有错误,您可能会将相同的错误放入测试中。通过其他方法计算预期结果。

            硬编码值非常好,并确保选择输入来代表所有正常和边缘情况。至少测试预期的输入以及格式错误或大小错误的输入(例如:空值)。

            这真的很简单——单元测试必须测试函数是否有效。这意味着您需要提供一系列具有已知输出的已知输入并对此进行测试。没有普遍的正确方法可以做到这一点。但是,对方法和验证使用相同的算法只能证明您擅长复制/粘贴。

            【讨论】:

              【解决方案7】:

              是的。它也困扰着我.. 虽然我会说它在非平凡的计算中更为普遍。为了避免在代码更改时更新测试,一些程序员编写了一个 IsX=X 测试,无论 SUT 如何,它总是成功

              • 关于复制功能

              您不必这样做。您的测试可以说明预期的输出是什么,而不是您如何得出它。 尽管在某些不平凡的情况下,它可能会使您的测试更易读,以了解您如何得出预期值 - 测试作为规范。你不应该重构掉这个重复

              def doubler(x); x * 2; end
              
              def test_doubler()
                input, expected = 10, doubler(10)
              
                assert_equal expected, doubler(10)
              end
              

              现在,如果我将 doubler(x) 更改为三倍频,则上述测试不会失败。 def doubler(x); x * 3; end

              但是这个会:

              def test_doubler()
                 assert_equal(20, doubler(10))
              end
              
              • 单元测试中的随机性 - 不要。

              选择静态代表性数据点进行测试,而不是随机数据集,并使用 xUnit RowTest/TestCase 使用差异数据输入运行测试。如果单元的 n 个输入集相同,则选择 1。 OP 中的测试可用作探索性测试/或确定额外的代表性输入集。单元测试需要可重复(参见 q#61400)- 使用随机值无法实现这一目标。

              【讨论】:

              • 我不是在问是否要重复使用完全相同的方法,而只是为了重新实现 SUT 的部分或全部功能,以一种可以理解的方式生成测试的预期值方式。
              • @Daniel - 正如我在第二段中提到的,为了测试的可读性,您必须复制逻辑。例如EXPECTED_TOTAL_PRICE = lineitems.inject{|sum, item| sum += (item.price * item.quantity)} * (1-DISCOUNT_RATE)
              • 同意。我认为我将您的第一个示例误解为建议做什么,而不是不做什么:-)
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-11-17
              相关资源
              最近更新 更多