在调用参数之一的成员函数时如何对函数进行单元测试

How to unit test a function when it is calling a member function of one of the argument

我想对 Python 中的以下函数进行单元测试:

def get_params(env, secret_fetcher):
    try:
        url = env['API_URL']
    except KeyError:
        raise
    try:
        key = secret_fetcher.get_secret('CLIENT-KEY')
        secret = secret_fetcher.get_secret('CLIENT-SECRET')
    except:
        raise
    return url, key, secret

它从环境中读取一个参数,同时使用 KeyVault class secret_fetcher 的对象从 Key Vault 中检索另外两个参数。我在我的主要功能中调用它,如下所示:

secret_fetcher = SecretFetcher(vault_url)
url, key, secret = get_params(os.environ, secret_fetcher)

我正在为此功能编写单元测试。对于 env 我在测试中使用了字典。但是,我该如何处理其成员函数在要测试的函数内部被调用的函数的第二个参数?

class TestApp():
    def test_get_params(self):
        env = {'WrongField': 'http://test.com/123'}
        <mock secret_fetcher>
        self.assertRaises(KeyError, get_params, env, secret)

我嘲笑 secret_fetcher 还是 secret_fetcher.get_secret?尤其是当 get_secret returns 不同的值被喂养它自己的不同参数时。我是否应该模拟 class SecretFetcher 并实现一个 get_secret 函数 returns 期望输出这两个不同值的参数?

如果你只是打算按原样测试异常,那么在这个阶段模拟 secret_fetcher 参数基本上是无关紧要的,因为一个简单的 None 值就可以了,因为它永远不会被触及,但这里有一个开始的例子:

# include the `get_param` function by import or inline here

import unittest
from unittest.mock import Mock

class TestApp(unittest.TestCase):

    def test_get_params_missing_url(self):
        env = {'missing': 'fail'}
        secret_fetcher = Mock()
        with self.assertRaises(KeyError):
            get_params(env, secret_fetcher)

(请注意,我更喜欢使用 assertRaises 作为上下文管理器,以确保以更自然的方式编写函数调用;请注意 with 块中的第一个异常将防止后续代码在该块中执行,因此建议 assertRaises 上下文管理器中只包含一个逻辑表达式,或者至少是最后一行;即一次只能测试一个异常)

运行这一项测试:

$ python -m unittest demo.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

但是,鉴于在单元测试上下文中使用模拟背后的理论是使用最少的外部依赖项(即不使用其他真实模块、方法或 classes 来测试代码; 这使得只针对相关单元进行测试),使用 unittest.mock.Mock 和朋友提供的其他功能可以简化这个目标。

如果提供了正确的 env,您可能希望确保使用正确的参数调用 get_secret 并返回预期的结果。还测试错误处理是否按预期处理。可以附加到上面 TestApp class 的其他方法:

    def test_get_params_success(self):
        env = {'API_URL': 'https://api.example.com'}
        def get_secret(arg):
            return arg
        secret_fetcher = Mock()
        secret_fetcher.get_secret.side_effect = get_secret

        url, key, secret = get_params(env, secret_fetcher)
        self.assertEqual(url, 'https://api.example.com')
        self.assertEqual(key, 'CLIENT-KEY')
        self.assertEqual(secret, 'CLIENT-SECRET')

        # Test that the secret_fetcher.get_secret helper was called
        # with both arguments
        secret_fetcher.get_secret.assert_any_call('CLIENT-KEY')
        secret_fetcher.get_secret.assert_any_call('CLIENT-SECRET')
        self.assertEqual(
            secret_fetcher.get_secret.call_args[0], ('CLIENT-SECRET',))

    def test_get_params_failure(self):
        env = {'API_URL': 'https://api.example.com'}
        secret_fetcher = Mock()
        secret_fetcher.get_secret.side_effect = ValueError('wrong value')

        with self.assertRaises(ValueError):
            get_params(env, secret_fetcher)

        # Test secret_fetcher.get_secret helper was only called with
        # the first CLIENT-KEY argument
        # Python 3.8 can check secret_fetcher.get_secret.call_args.args
        self.assertEqual(
            secret_fetcher.get_secret.call_args[0], ('CLIENT-KEY',))

正在测试:

$ python -m unittest demo.py 
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

请注意,虽然我对您的 SecretFetcher class 所做或拥有的信息绝对为零,但三个测试用例一起测试了提供的 get_params 函数以确保其行为如下预期的,包括测试它应该如何使用 secret_fetcher.get_secret,它按预期处理错误,并且所有提供的测试用例都测试了 [中的每个 代码行=45=]问题中提供的示例。

希望这可以作为一个综合示例,说明如何使用模拟来满足单元测试的目标。