【问题标题】:How to validate a unit test with random values如何使用随机值验证单元测试
【发布时间】:2014-11-23 08:09:51
【问题描述】:

如何使用随机值验证单元测试? 我需要保证gen_age 返回一个介于 15 和 99 之间的整数,但是这段代码不正确。

import random
import unittest


def gen_age():
    # generate integer between 15 and 99
    return random.randint(15, 99)


class AgeTest(unittest.TestCase):

    def setUp(self):
        self.a = gen_age()

    def test_choice(self):
        element = random.choice(self.a)
        self.assertTrue(element in self.a)

    def test_sample(self):
        for element in random.sample(self.a, 98):
            self.assertTrue(element in self.a)

if __name__ == '__main__':
    unittest.main()

【问题讨论】:

  • test_choicesetUp 方法结合使用没有意义,random.choicelist 作为参数,self.a 是int
  • 对于生成随机值的测试,单个测试只能提供发现问题的机会。您通常需要进行多次测试,以确保您已经验证了足够多的可能结果集。

标签: python unit-testing random


【解决方案1】:

测试类似行为的最佳方法是为 Random 对象设置种子。

random 包提供了一个 Random 类。 Random 的实例具有与 random 包相同的方法; random(), randint(), sample(), ... 此外,Random 接受一个种子。将种子添加到 Random 使其输出确定性。例如,

from random import Random
random = Random(666)
assert random.randint(0, 1000) == 467  # will never break

因此,您想测试您的功能

from random import Random
import unittest

random = Random()

def gen_age():
    # generate integer between 15 and 99
    return random.randint(15, 99)


class AgeTest(unittest.TestCase):

    def setUp(self):
        global random
        random = Random(666)

    def test_gen_age(self):
        self.assertEqual(gen_age(), 53)

if __name__ == '__main__':
    unittest.main()

请注意,如果您的测试不在同一个文件中,则需要使用unittest.mock.patch 随机修补。类似的东西应该可以工作

from random import Random
from package.file import gen_age
import unittest


class AgeTest(unittest.TestCase):

    def setUp(self):
        self.random = Random(666)

    @patch('package.file.random')
    def test_gen_age(self, random):
        random.randint._mock_side_effect = self.random.randint
        self.assertEqual(gen_age(), 53)

【讨论】:

    【解决方案2】:

    应该是这样的:

    def test_GenAge_ReturnIsBetween15And99(self):
        self.assertTrue(self.a >=15 and self.a <= 99);
    

    但您目前并不需要测试 gen_age 功能。您正在尝试测试 python 随机生成器 API,但原因是什么?我没有看到。

    【讨论】:

    • 实际上他正在尝试测试参数是否正确传递给API,而不是API本身。
    • 一个随机结果落在指定范围内这一事实并不能保证它们都将如此。我认为 OP 正在寻找一些保证,即最大值和最小值在范围内,很难确保结果是随机的。
    • @almanegra,“测试参数是否正确传递”?在 Python 中的哪些情况下可能会错误地传递某些常量(例如,您传递了数字 15,但函数确实收到了 23)?它不是 C/C++,所有东西都可以被预处理器覆盖。
    • @pjs,但他为什么要测试呢?此库 API 已由供应商测试。它工作正常。这只是对第三方 API 的一次调用,没有任何逻辑,没有任何非约束值。它只是 100% 的存根函数。不应测试存根。
    • 在这种情况下,我同意您的逻辑,但肯定存在算法依赖随机性并且难以测试的情况。我对这些案例很感兴趣,这就是我浏览这个问题的原因。
    【解决方案3】:

    我认为在@user983716 的答案之上还有更多内容可以构建,因为:

    1. 这个答案通过强制种子改变了源代码的工作方式,而真正的源是不确定的
    2. 该测试可以做更多的事情来记录预期的行为,而该测试是模糊的,可能会让大多数人感到困惑

    真正的愿望不是测试Random.randint 的功能,而是输出是否正确。这是@rufanov 提出的建议,尽管该解决方案可以改进。

    在整个答案中,我们假设实现是在独立于测试的package.file 中。

    让我们从以下开始:

    import unittest
    
    from package.file import get_age
    
    class AgeTest(unittest.TestCase):
    
        def test_gen_age_generates_a_number_between_15_and_99(self):
            age = gen_age()
            self.assertGreaterEqual(age, 15)
            self.assertLessEqual(age, 99)
    

    这是一个很好的开始测试,因为如果发生故障,它会提供清晰的输出:

    AssertionError: 14 not greater than or equal to 15
    AssertionError: 100 not less than or equal to 99
    

    好的,但我们还想确保它是一个随机数,因此我们可以添加另一个测试,以确保我们按预期从randint 获取它:

    @unittest.mock.patch('package.file.random')
    def test_gen_age_gets_a_random_integer_in_the_range(self, mock_random):
        gen_age()
        mock_random.randint.assert_called_with(15, 99)
    

    我们在这里做了两件重要的事情:

    • random 对象(来自定义 gen_age 的文件)已修补,以便我们可以对其执行测试,而无需依赖实际实现
    • 做出一个断言,确认预期的 15 和 99 参数已提供给 randint,因此给出了正确的范围

    可以编写额外的测试来断言真实的返回值,确认给定的数字始终是随机的。这将提供该方法直接返回随机值的信心,因为可以想象该方法可以做更多的事情,甚至返回一些任意值,即使它在内部仍然进行randint 调用。

    例如,假设有人将gen_age() 更改为如下:

    def gen_age():
        age = random.randint(15, 99) # still doing what we're looking for
        return age + 1               # but now we get a result of 16-100
    

    哦,现在只有 randint 返回 99 的情况下,我们的第一个测试会失败,而第二个测试仍然会通过。这是一个等待发生的生产错误...

    那么,一个简单但有效的确认结果的方法可能如下:

    @unittest.mock.patch('package.file.random')
    def test_returns_age_as_generated(mock_random):
        mock_random.return_value = 27
        age = get_age()
        self.assertEqual(age, 27)
    

    这里还有最后一个缺陷……如果返回的值变成这样会怎样:

    def gen_age():
        age = random.randint(15, 99)
        return 27
    

    现在所有的测试都通过了,但我们仍然没有得到我们真正想要的随机结果。为了解决这个问题,我们也需要随机化测试值......

    事实证明,我们的源代码指向了答案 - 只需将实际实现用作第二次测试的一部分。为此,我们首先需要导入原始的random

    from package.file import get_age, random
    

    然后我们将修改我们编写的最后一个测试,结果如下:

    @unittest.mock.patch('package.file.random')
    def test_returns_age_as_generated(mock_random):
        random_age = random.randint(15, 99)
        mock_random.randint.return_value = random_age
        age = get_age()
        self.assertEqual(age, random_age)
    

    因此,我们最终会得到以下全套测试:

    import unittest
    
    from package.file import get_age, random
    
    
    class AgeTest(unittest.TestCase):
    
        def test_gen_age_generates_a_number_between_15_and_99(self):
            age = gen_age()
            self.assertGreaterEqual(age, 15)
            self.assertLessEqual(age, 99)
    
        @unittest.mock.patch('package.file.random')
        def test_gen_age_gets_a_random_integer_in_the_range(self, mock_random):
            gen_age()
            mock_random.randint.assert_called_with(15, 99)
    
        @unittest.mock.patch('package.file.random')
        def test_returns_age_as_generated(mock_random):
            random_age = random.randint(15, 99)
            mock_random.randint.return_value = random_age
            age = get_age()
            self.assertEqual(age, random_age)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-09-25
      • 1970-01-01
      • 2014-02-08
      • 2012-07-18
      • 2015-10-01
      • 1970-01-01
      • 2017-05-23
      相关资源
      最近更新 更多