如何避免在具有配置选项的函数中进行脆弱测试?

How to avoid brittle tests in a function that has configuration options?

问题陈述

我正在尝试为对接收到的输入执行一系列算术运算的函数构建单元测试。这些操作是在函数外部配置的(在这种情况下是在包常量中)。

我的问题是 我可以编写在应该通过时通过的测试,但是如果外部配置发生变化,测试将会中断,或者测试将主要是函数中代码的重复已测试。

我能想到的两种编写测试的方法是:

  1. “假定”特定配置的测试。问题是测试很脆弱,如果我更改配置,它将停止工作。 (见下文test_example1)

    • 创建一个假设一些操作并应用于输入的预期结果
    • 调用函数进行测试
    • 将预期结果与要测试的函数返回的实际结果进行比较
  2. 我可以构建一个使用配置计算预期结果的测试。问题是,首先,测试依赖于函数外部的配置常量,其次,测试代码与被测试代码非常相似,感觉不对。 (见下文test_example2)

    • 根据配置常量创建处理输入的预期结果
    • 调用函数进行测试
    • 将预期结果与要测试的函数返回的实际结果进行比较

你能帮我弄清楚对执行外部配置操作的函数进行单元测试的正确方法是什么吗?

示例代码

import unittest
import operator

OPS = {'+':operator.add,
       '-':operator.sub}

DIRECTIVES = {'calculated_field':[('+','field1'),
                                  ('+','field2')]}

def example(input_dict):
    output_dict = {}
    
    for item,calcs in DIRECTIVES.items():
        output_dict[item] = 0
        for operation, field in calcs:
            output_dict[item] = OPS[operation](output_dict[item],input_dict[field])
        
    return output_dict    

class TestExample(unittest.TestCase):

    item1  = 'field1'
    value1 = 10
    item2  = 'field2'
    value2 = 20
    item3  = 'field3'
    value3 = 5    

    def setUp(self):
        self.input_dict = {self.item1:self.value1,
                           self.item2:self.value2,
                           self.item3:self.value3}
    
    def test_example_option1(self):
        expected_result = {'calculated_field':self.value1+self.value2}
        
        actual_result = example(self.input_dict)
        
        self.assertDictEqual(expected_result,actual_result)
        
    def test_example_option2(self):
        expected_result = {}

        for item,calcs in DIRECTIVES.items():
            expected_result[item] = 0
            for operation, field in calcs:
                expected_result[item] = OPS[operation](expected_result[item],self.input_dict[field])
                
        actual_result = example(self.input_dict)
        
        self.assertDictEqual(expected_result,actual_result)

这有点见仁见智,但与其严格依赖全局变量,我可能会让你的函数(example(),在这种情况下,我假设)允许将可选的覆盖传递给它依赖的外部数据。

例如

def example(ops=OPS, directives=DIRECTIVES):
    ...

这有两个优点:为了测试,您可以为该数据传递一些虚拟值(大概会比真实数据更小更简单),然后测试给定的已知正确输出数据越简单。

另一个优点是它使您的代码总体上更具可扩展性。

如果您不想这样做,另一个示例(因为您使用的是 unittest 模块)是使用 unittest.mock.patch

在这种情况下,您可以将测试编写为:

class TestExample(...):
    @patch('yourmodule.OPS', TEST_OPS)
    @patch('yourmodule.DIRECTIVES', TEST_DIRECTIVES)
    def text_example_option(self):
        # call example() and test against a known--good value
        # given TEST_OPS and TEST_DIRECTIVES

这本质上是同一件事,但提供了一种方法来测试您的函数针对(临时分配的)新配置而不更改默认值。