在Python中重用不同函数的单元测试的良好实践

时间:2012-03-22 14:56:23

标签: python unit-testing

我是单元测试的新手,想要从nose framework开始。但是,使用unittestpytest的答案也是受欢迎的。当然还有一般建议。

我认为我得到了基本概念,但我缺乏设置良好测试的练习。我也在努力如何布局测试。 我特别不确定如何处理我想在模块的不同功能上运行几个测试用例的情况:

例如:我可能有一个名为diceroller.py的模块,它包含一些模拟滚动骰子的功能,修改和测试结果等等。滚动骰子的所有函数都应该通过相同的测试运行(它们是否返回整数列表,具有正确的值,是范围内的值等)。但其中一些还应针对其他一些案例进行操作。

所以我得到了一个子目录test,并希望在那里设置我的测试代码。我该如何处理?

# a section of `diceroller.py`

def roll_a_dice(sides=6):
  """Return an integer x where `x >= 1 <= sides`"""
  return random.randint(1, sides)

def roll_dice(sides=6, count=1):
  """Return a list of integers (most function except this)"""
  rolls = list()
  while count:
    rolls.append(random.randint(1, sides))
    count -= 1
  return rolls

def roll_some_dice(sides=6, count=1, times=1):
  """Return a list of list containing integers"""
  some_rolls = list()
  while times:
    some_rolls.append(roll_dice(sides, count))
    times -= 1
  return some_rolls

def rolling_dice(sides=6, count=1):
  """Yielding integers `count` times"""
  while count:
    count -= 1
    yield random.randint(1, sides)

小更新

Simeon Visser有一个很好的观点。但上面的代码只是函数也给我的问题提供了一些上下文,它只是:我如何(重新)在不同的函数上使用测试用例?

我想编写check_xyz之类的测试然后从test_atest_b调用它是最简单的解决方案吗?或者这是不好的做法?

来自Rik Poggi的解决方案似乎完全正在尝试完成(将在输入后立即使用它)。但我有点感觉它“复杂化”了很多东西......可能不是在技术方面,但它可能会“太多”。

2 个答案:

答案 0 :(得分:6)

我不会解决您要测试的示例代码明显存在的问题。我将专注于您重复使用测试代码的问题。

在开始之前要记住的重要事项:

  1. 测试必须简单(内部没有任何逻辑,或非常少)。
  2. 测试应该是可读的(它们就像代码文档一样)。
  3. 测试应该是可维护的。
  4. 测试的简单性是必须的,同时保持你需要在最后两点之间找到适当的平衡。

    最后一个警告:在重复使用测试代码时要非常小心,因为如果你做错了,你的测试就会被窃听。测试错误很难被发现,但是从现在开始一年你可能会找到一个,怀疑的种子将被种植,你的测试中的信任可能会减少。你不信任的测试(“哦,那个失败但是没关系,因为另一个也失败了,等等......”)完全没有价值。


    现在我们已经清除了我们的方式,让我们看看你的代码。通常,测试的动态部分是通过上下文(setUptearDown方法)实现的,但在您的情况下,事情要复杂得多。

      

    我想在模块的不同功能上运行几个测试用例。

    您实际上并不希望相同的测试用例使用不同的函数运行,而只是使用相同的代码。一个好的测试环境(至少)每个功能都有一个测试用例。

    由于您正在寻找能够针对另一个函数的部分输出运行先前的测试用例/套件,因此您需要functools.partial来使用默认参数包装您的函数。

    这意味着你应该从最简单的测试开始:

    def check_valid_range(value, sides):
        """Check that value is a valid dice rolling"""
        assert 0 < value <= sides
    
    def check_is_int(value):
        """Check that value is an integer"""
        assert type(value) is int
    

    然后在它们之上构建(具有一点可插拔性):

    class TestRollADice:
       """roll_a_dice basic tests"""
    
        @staticmethod
        def func_to_test(*args, **kwargs):
            return diceroller.roll_a_dice(*args, **kwargs)
    
        @staticmethod    
        def check_valid_output(value, sides):
            """Check that value is the self.function is valid"""
            yield check_valid_range, value, sides
            yield check_is_int, value
    
        def test_3sides(self):
            """Check valid result for a 3 sides dice"""
            yield self.check_valid_output, self.func_to_test(3), 3
    
        def test_list_valid_sides(self):
            """Check valid result for a list of valid sides (sides >= 3)"""
            sides = list(range(3, 13))
            for s in sides:
                yield self.check_valid_output, self.func_to_test(s), s
    
        def test_0_sides_raises_ValueError(self):
            """0 side dice raise ValueError"""
            assert_raises(ValueError, self.func_to_test, 0)
    
        def test_1_sides_raises_ValueError(self):
            """1 side dice raise ValueError"""
            assert_raises(ValueError, self.func_to_test, 1)
    
        def test_2_sides_raises_ValueError(self):
            """2 sides dice raise ValueError"""
            assert_raises(ValueError, self.func_to_test, 2)
    
        def test_minus1_sides_raises_ValueError(self):
            """-1 side dice raise ValueError"""
            assert_raises(ValueError, self.func_to_test, -1)
    

    这些是roll_a_dice函数应具有的最低测试量。尝试在你的包中运行nosetests,你会看到其中两个失败,看看测试的重要性! :)

    我确定你已经注意到测试名称是如何详细的,这是为了便于阅读。

    现在对于roll_dice主要测试类应该测试这三个,四个基本值和baisc错误,因为一个好的应该是简单的并且控制正在测试的东西,我会说你可能需要一些功能如:

    def check_list_length(self, lst, count):
        """Check that the len(lst) == count"""
        assert len(lst) == count
    
    def check_valid_count(self, count):
        """Check count > 0"""
        assert count > 0
    
    class TestRollDice:
        # ...
    

    现在如果你想重用旧代码,你可以继承TestRollADice

    from functools import partial
    
    class TestRollDice_Count1(TestRollADice):
    
        @staticmethod
        def func_to_test(*args, **kwargs):
            return partial(diceroller.roll_dice, count=1)(*args, **kwargs) 
    

    瞧,几乎是免费的,你将有两次以前的测试:)

    注意:

    1. 上面的所有代码都可以用unittest语言编写,但是因为你问过我写的nose
    2. 有一个小问题:通过测试的文档字符串是相同的,所以在详细输出中它们会出现两次。这不是一个大问题,因为如果其中一个测试失败,您将被提示测试地址,并且该测试地址是唯一的(并且很清楚,因为幸运的是我们的测试名称是详细的)。我似乎也记得文档字符串可能被禁用或其他东西。因此,再一次,这不是一个大问题,如果需要,应该很容易找到解决方法。

答案 1 :(得分:4)

您的代码可以重构,以减少代码量,从而减少您需要测试的代码量。让我们来看看你想要什么:

  • 掷骰子(roll_a_dice
  • 多次掷骰子(roll_dice
  • 获取多次掷骰子的掷骰(roll_some_dice

让我们先重写roll_dice。我们基本上想多次调用roll_a_dice并将结果放在一个列表中:

def roll_dice(sides=6, count=1):
  """Return a list of integers (most function except this)"""
  return [roll_a_dice(sides) for i in xrange(count)]

让我们对roll_some_dice做同样的事情:我们想多次调用roll_dice并将结果放在一个列表中:

def roll_some_dice(sides=6, count=1, times=1):
  """Return a list of list containing integers"""
  return [roll_dice(sides, count) for i in xrange(times)]

最后,rolling_dice仍包含一些逻辑:

def rolling_dice(sides=6, count=1):
  """Yielding integers `count` times"""
  while count:
    count -= 1
    yield roll_a_dice(sides)

现在,代码更容易检查错误并更容易进行单元测试:

import random

def roll_a_dice(sides=6):
  """Return an integer x where `x >= 1 <= sides`"""
  return random.randint(1, sides)

def roll_dice(sides=6, count=1):
  """Return a list of integers (most function except this)"""
  return [roll_a_dice(sides) for i in xrange(count)]

def roll_some_dice(sides=6, count=1, times=1):
  """Return a list of list containing integers"""
  return [roll_dice(sides, count) for i in xrange(times)]

def rolling_dice(sides=6, count=1):
  """Yielding integers `count` times"""
  while count:
    count -= 1
    yield roll_a_dice(sides)

您现在可以编写测试用例来检查掷骰子是否正确(roll_a_dice)。一旦测试完毕,就不再需要为其他功能执行此操作了。对于这些函数,您只需要测试是否获得了正确的结果数(因为roll_a_dice的测试用例应该捕获任何不正确的值。)

同样,当您测试roll_some_dice时,您可以假设roll_dice正常工作,因为您已经为该函数编写了测试。如果出现问题,您可以为roll_dice的测试用例添加测试,而不是roll_some_dice(除非问题特定于roll_some_dice)。