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

Bru*_*tus 3 python unit-testing

我是单元测试的新手,想要从鼻子框架开始.但是使用unittest,pytest的答案也是受欢迎的.当然还有一般建议.

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

例如:我可能有一个模块,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)
Run Code Online (Sandbox Code Playgroud)

小更新

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

我猜写像测试check_xyz,然后从调用它test_atest_b例如,是最简单的解决方案?或者这是不好的做法?

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

Rik*_*ggi 7

我不会解决您的示例代码要清楚地测试的问题.我将重点关注重用测试代码的问题.

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

  1. 测试必须简单(内部没有任何逻辑,或非常少).
  2. 测试应该是可读的(它们就像代码文档).
  3. 测试应该是可维护的.

测试的简单性是必须的,同时保持你需要在最后两点之间找到适当的平衡.

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


现在我们清除了我们的方式让我们看看你的代码.通常,测试的动态部分是通过上下文(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
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

这些是您的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:
    # ...
Run Code Online (Sandbox Code Playgroud)

现在如果你想重用旧代码,你可以继承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) 
Run Code Online (Sandbox Code Playgroud)

而且,几乎免费,你将有两倍的测试:)

注意:

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