【问题标题】:How to unit test a function when it is calling a member function of one of the argument调用参数之一的成员函数时如何对函数进行单元测试
【发布时间】:2021-01-21 04:11:59
【问题描述】:

我想在 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 类 secret_fetcher 的对象从密钥库中检索另外两个参数。我在我的主函数中调用它,如下所示:

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 在输入不同的参数时返回不同的值时。我是否应该模拟 SecretFetcher 类并实现一个函数 get_secret 以返回具有这两个不同值的参数的预期输出?

【问题讨论】:

  • 如果您只是在测试get_params,我不确定您为什么要测试ValueError,因为它不是由get_params 提出的。我要做的唯一测试是确保使用 unittest.mock.Mock 实例使用适当的参数调用 secret_fetcher.get_secret
  • 但是,如果您将raise 行更改为raise ValueError(并且可能限制except 行以捕获特定的异常类型),那么您应该提供一个模拟secret_fetcher.get_secret引发适当的异常。
  • 详情请咨询this thread
  • @metatoaster 我打算测试 assertRaises(KeyError... 那是一个错误

标签: python unit-testing mocking patch


【解决方案1】:

如果您只打算按原样测试异常,则在此阶段模拟 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

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

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

    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 类所做或拥有的内容的信息绝对为零,但三个测试用例一起测试了提供的 get_params 函数以确保它按预期运行,包括测试它应该如何使用secret_fetcher.get_secret,它按预期处理错误,并且所有提供的测试用例都测试了问题中提供的 get_params 示例中的每一行代码。

希望这是一个全面的示例,说明如何使用模拟来满足单元测试的目标。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-01-18
    • 1970-01-01
    • 2019-04-15
    • 2018-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-16
    相关资源
    最近更新 更多