单元测试应该复制功能还是测试输出?

时间:2010-03-20 08:02:43

标签: ruby unit-testing testing tdd

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

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

简化示例(红宝石):

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

据我所知,大多数情况下,您测试的方法不够简单,无法通过这种方式进行测试。但我的问题仍然存在; 在某些情况下这是一种有效的方法(为什么或为什么不这样做)

7 个答案:

答案 0 :(得分:9)

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

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

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

选择输入值时,大多数时候使用随机值不是很有效。我希望随时都能选择精心挑选的价值观。在你给出的例子中,我想到了连接时不符合字符串的空值和极长值。

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

答案 1 :(得分:4)

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

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

使用Derived Values来测试结果会在测试输入和预期输出之间建立一个更强大的更清晰的关系

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

另一个好处是你可以use fewer unit tests to cover the desired functionality,甚至可以让它们同时更具沟通性。最终的结果是更简洁,更可维护的单元测试。

答案 2 :(得分:2)

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

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

答案 3 :(得分:1)

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

测试功能是否随时间没有变化可能(暂时)在重构期间有用。但是你经常重构这么小的方法呢?

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

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

答案 4 :(得分:1)

单元测试应该运用您的代码,而不是您正在使用的语言的一部分。

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

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

答案 5 :(得分:0)

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

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

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

答案 6 :(得分:0)

是。它也困扰我...虽然我会说它在非平凡的计算中更为普遍。为了避免代码更改时更新测试,一些程序员编写了一个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)更改为tripler,则上述测试不会失败。 def doubler(x); x * 3; end

但是这个会:

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

选择静态代表性数据点代替随机数据集进行测试,并使用xUnit RowTest / TestCase运行带有diff数据输入的测试。如果单位的n个输入集相同,请选择1。 OP中的测试可以用作探索性测试/或确定其他代表性输入集。单元测试需要是可重复的(参见q#61400) - 使用随机值会破坏这个目标。