编写单元测试的好方法

时间:2010-06-13 17:01:10

标签: unit-testing

所以,我之前并没有真正参与编写单元测试 - 现在我有点喜欢,我需要检查一下我是否在正确的轨道上。

假设您有一个处理数学计算的课程。

class Vector3
{
public:  // Yes, public.
  float x,y,z ;
  // ... ctors ...
} ;

Vector3 operator+( const Vector3& a, const Vector3 &b )
{
  return Vector3( a.x + b.y /* oops!! hence the need for unit testing.. */,
                  a.y + b.y,
                  a.z + b.z ) ;
}

我有两种方法可以真正想到在Vector类上进行单元测试:

1)手工解决一些问题,然后将数字硬编码到单元测试中,只有在等于你的手和硬编码结果时传递

bool UnitTest_ClassVector3_operatorPlus()
{
  Vector3 a( 2, 3, 4 ) ;
  Vector3 b( 5, 6, 7 ) ;

  Vector3 result = a + b ;

  // "expected" is computed outside of computer, and
  // hard coded here.  For more complicated operations like
  // arbitrary axis rotation this takes a bit of paperwork,
  // but only the final result will ever be entered here.
  Vector3 expected( 7, 9, 11 ) ;

  if( result.isNear( expected ) )
    return PASS ;
  else
    return FAIL ;
}

2)在单元测试中非常仔细地重写计算代码。

bool UnitTest_ClassVector3_operatorPlus()
{
  Vector3 a( 2, 3, 4 ) ;
  Vector3 b( 5, 6, 7 ) ;

  Vector3 result = a + b ;

  // "expected" is computed HERE.  This
  // means all you've done is coded the
  // same thing twice, hopefully not having
  // repeated the same mistake again
  Vector3 expected( 2 + 5, 6 + 3, 4 + 7 ) ;

  if( result.isNear( expected ) )
    return PASS ;
  else
    return FAIL ;
}

还是有其他办法可以做这样的事吗?

10 个答案:

答案 0 :(得分:9)

方式#1是普遍接受的单元测试方式。通过重写代码,您可以将错误的代码重写到测试中。很多时候,每个测试方法只需要一个真实的测试用例,所以这不是太费时间。

答案 1 :(得分:4)

它总是取决于用例。我会永远选择那个版本,这使得经过测试的想法更加明显。出于这个原因,我也不会使用isNear方法。我会检查

expected.x == 7;
expected.y == 9;
expected.z == 11;

使用一个好的xUnit库,您将收到一条干净的错误消息,其中预期的组件是错误的。在您的示例中,您将不得不搜索错误的真正来源。

答案 2 :(得分:2)

我对此的处理方法非常简单:从不模仿生产代码以获得测试结果。如果你的算法存在缺陷,那么你的单元测试就会重现缺陷的传递。花点时间考虑一下!错误的代码和错误的传递测试。我认为它不会变得更糟。想象一下,您在代码中找到了错误并进行了更改;测试现在会失败,但看起来是正确的。 IMO不仅可以进行容易出错的测试,而且可以让您根据算法考虑结果。对于像数学这样的东西,你不应该关心算法是什么,只是答案是正确的。我甚至可以说我从根本上不信任那些模仿生产代码逻辑的测试。

测试应该尽可能具有声明性,这意味着对完全计算的结果进行硬编码。对于数学测试​​,我通常使用尽可能简单的值来计算纸张/计算器上的结果,但并不简单。例如。如果我想测试一个normalize方法,我会选择一些众所周知的值。大多数人都知道45的sin / cos是超过根2的一个,所以归一化(1,-1,0)将给出一个容易识别的值。您可以使用许多其他众所周知的数字/技巧。您可以使用命名良好的常量对结果进行编码,以提高可读性。

我还建议对数学类型使用数据驱动测试,因为您可以快速添加新的测试用例。

答案 3 :(得分:1)

我认为写出数字(你的第二种方法)是正确的选择。这使得你的意图对于阅读测试的人来说更加明显。

假设您没有超载+运算符,而是有一个可怕的命名函数f花了两个Vector3。你也没有记录它,所以我看了你的测试,看看f应该做什么。

如果我看到Vector3 expected( 7, 9, 11 ),我必须返回并反向设计 7,9和11是“预期”的结果。但是,如果我看到Vector3 expected( 2 + 5, 6 + 3, 4 + 7 ),那么我很清楚f会将参数的各个元素添加到新的Vector3中。


你没有在你的问题中提出这个问题,但我想在另一方面提出另一点意见。至于要编写哪些测试,您确实希望确保覆盖边缘情况。

会发生什么
Vector3 a(INT_MAX, INT_MAX, INT_MAX);
Vector3 b(INT_MAX, INT_MAX, INT_MAX);

Vector3 result = a + b;

// What is expected?  Simple overflow?  Exception?  Default to invalid value?

如果你正在进行分工,你必须确保以零为掩护。尽量记住这些边缘情况。

答案 4 :(得分:1)

复制这种逻辑并不会真正有用。你理解阅读你对#2 :)的评论。除非它非常复杂,否则我会使用方法#1。

可能需要一些工作来确定一些测试数据;但这通常很容易确定。

答案 5 :(得分:1)

在测试中使用与代码中相同的计算是完全没有意义的。如果你要特别小心,为什么在实际编写代码时不要格外小心?使用手工计算的示例是最好的方法,但更好的方法是在编写代码之前编写测试,这样你就不会懒惰并编写一个你知道会通过的测试并避免边缘情况你“我不完全确定。

答案 6 :(得分:0)

方式1将是更好的选择。主要的是你如何选择将测试代码的魔术数据。

另一种方式可以是,有些时候而不是硬编码值到单元测试中,我们可以有输入集(魔术数据)和一组预期结果相应的输入。因此,单元测试将从输入集读取值,执行代码并根据预期结果进行测试。

答案 7 :(得分:0)

添加向量时,选择哪种方法并不重要,因为这是一个非常简单的操作。一个更好的例子可能是测试说,一个规范化方法:

Vector3 a(7, 9, 11); 
Vector3 result = a.normalize(); 

Vector3 hand_solved(0.4418, 0.5680, 0.6943);
Vector3 reproduced(7/sqrt(7*7+9*9+11*11), 9/sqrt(7*7+9*9+11*11), 
    11/sqrt(7*7+9*9+11*11));

请参阅?读者不清楚是否正确。重现的计算是可验证的,但它很麻烦,难以阅读。在单元测试中重写每个计算也是不切实际的。手动求解计算不能向读者保证它是正确的(读者必须手工解决并比较答案)。

解决方案是选择更简单的输入。使用向量,您可以仅在基础向量( i j k )上测试所有操作。因此,在这种特殊情况下,可以更清楚地说出类似的内容:

Vector3 i(1, 0, 0);
Vector3 result = i.normalize();
Vector3 expected(1, 0, 0);

这里很清楚你在测试什么以及你期望得到什么结果。如果读者知道normalize应该做什么,那么答案是正确的。

答案 8 :(得分:0)

无论如何,您应该执行数字1以验证您的代码是否正确 - 单元测试应该假设在创建计算的过程中完成。使用这些知识,您可以创建单元测试以使用您已创建的代码(即,不要复制它)。

单元测试应该测试已知的成功案例,已知的故障情况,边界情况(上限/下限范围,如果适用)和任何罕见的情况(在运行时调试很少和昂贵,但在构建时测试非常便宜,假设您知道它们是什么:)

你会发现直接计算是最简单的单元测试,因为逻辑流程(希望)是自包含的。

答案 9 :(得分:0)

要遵循的简单规则:

  1. 始终使用安排,行动和断言(AAA 模式) - 谷歌找到更多相关信息。
  2. 你应该从不     if / else阻止你的单元测试
  3. 你应该从不     单元测试中的任何计算/逻辑
  4. 不要测试多件事 你单元测试。例如:如果我编写了一个方法:public int Sum(int number1, int number2)我将进行4-5个单元测试,看起来像这样

    Test_Sum_Number1IsOneNumer2IsTwo_ReturnsThree

    Test_Sum_Number1IsZeroNumer2IsZero_Returns0

    Test_Sum_Number1IsNegativeOneNumer2IsNegativeThree_ReturnsNegativeFour ....等等等等

  5. 或者也许不是编写四种不同的方法,而是可以在MBUnit中使用RowTest属性或在NUnit中使用TestCase(2.5.5以后)来进行参数化测试 - 在这里你只需编写一个方法并传入通过将它们指定为属性来指定不同的参数。