【问题标题】:Mocking python function based on input arguments基于输入参数模拟python函数
【发布时间】:2013-04-16 05:38:04
【问题描述】:

一段时间以来,我们一直在使用Mock for python。

现在,我们要模拟一个函数

def foo(self, my_param):
    #do something here, assign something to my_result
    return my_result

通常,模拟它的方法是(假设 foo 是对象的一部分)

self.foo = MagicMock(return_value="mocked!")

即使我调用 foo() 几次我也可以使用

self.foo = MagicMock(side_effect=["mocked once", "mocked twice!"])

现在,我面临一种情况,当输入参数具有特定值时,我想返回一个固定值。因此,如果假设“my_param”等于“something”,那么我想返回“my_cool_mock”

这似乎在mockito for python 上可用

when(dummy).foo("something").thenReturn("my_cool_mock")

我一直在寻找如何通过 Mock 实现同样的目标,但没有成功?

有什么想法吗?

【问题讨论】:

  • 这个答案可能会有所帮助 - stackoverflow.com/a/7665754/234606
  • @naiquevin 这完美解决了老兄的问题,谢谢!
  • 我不知道您可以将 Mocktio 与 Python 一起使用,为此 +1!
  • 如果您的项目使用 pytest,出于这样的目的,您可能需要利用 monkeypatch。 Monkeypatch 更多的是“为了测试而替换此功能”,而 Mock 是您在还想检查 mock_calls 或断言调用它的内容等时使用的。两者都有一席之地,我经常在给定的测试文件中的不同时间同时使用两者。

标签: python unit-testing mocking mockito


【解决方案1】:

如果side_effect_func 是一个函数,那么该函数返回的是 什么叫模拟返回。 side_effect_func 函数被调用 与模拟相同的论点。这允许您改变回报 根据输入动态调用的值:

>>> def side_effect_func(value):
...     return value + 1
...
>>> m = MagicMock(side_effect=side_effect_func)
>>> m(1)
2
>>> m(2)
3
>>> m.mock_calls
[call(1), call(2)]

http://www.voidspace.org.uk/python/mock/mock.html#calling

【讨论】:

  • 只是为了让答案更容易,您可以将 side_effect 函数重命名为其他名称吗? (我知道,我知道,这很简单,但提高了可读性,因为函数名和参数名不同:)
  • 这些年后只是为了迂腐,但side effect是准确的术语:en.wikipedia.org/wiki/Side_effect_(computer_science)
  • @Ish 他们并没有抱怨CallableMixin.side_effect 的名称,而是示例中定义的单独函数具有相同的名称。
【解决方案2】:

Python Mock object with method called multiple times所示

解决办法是自己写side_effect

def my_side_effect(*args, **kwargs):
    if args[0] == 42:
        return "Called with 42"
    elif args[0] == 43:
        return "Called with 43"
    elif kwargs['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect

这就是诀窍

【讨论】:

  • 这让我比选择的答案更清楚,所以谢谢你回答你自己的问题:)
【解决方案3】:

副作用需要一个函数(也可以是 lambda 函数),所以对于简单的情况,您可以使用:

m = MagicMock(side_effect=(lambda x: x+1))

【讨论】:

    【解决方案4】:

    如果您“想在输入参数具有特定值时返回一个固定值”,也许您甚至不需要模拟并且可以使用dict 及其@987654322 @方法:

    foo = {'input1': 'value1', 'input2': 'value2'}.get
    
    foo('input1')  # value1
    foo('input2')  # value2
    

    当你的假货的输出是输入的映射时,这很有效。当它是输入的函数时,我建议按照Amber的回答使用side_effect

    如果您想保留Mock 的功能(assert_called_oncecall_count 等),也可以将两者结合使用:

    self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
    

    【讨论】:

    • 这很聪明。
    【解决方案5】:

    我最终在这里寻找“如何根据输入参数模拟函数”,我终于解决了这个问题,创建了一个简单的辅助函数:

    def mock_responses(responses, default_response=None):
      return lambda input: responses[input] if input in responses else default_response
    

    现在:

    my_mock.foo.side_effect = mock_responses(
      {
        'x': 42, 
        'y': [1,2,3]
      })
    my_mock.goo.side_effect = mock_responses(
      {
        'hello': 'world'
      }, 
      default_response='hi')
    ...
    
    my_mock.foo('x') # => 42
    my_mock.foo('y') # => [1,2,3]
    my_mock.foo('unknown') # => None
    
    my_mock.goo('hello') # => 'world'
    my_mock.goo('ey') # => 'hi'
    

    希望这会对某人有所帮助!

    【讨论】:

      【解决方案6】:

      虽然side_effect可以达到目的,但是为每个测试用例设置side_effect函数并不是那么方便。

      我写了一个轻量级的Mock(叫做NextMock)来增强内置的mock来解决这个问题,这里是一个简单的例子:

      from nextmock import Mock
      
      m = Mock()
      
      m.with_args(1, 2, 3).returns(123)
      
      assert m(1, 2, 3) == 123
      assert m(3, 2, 1) != 123
      

      它还支持参数匹配器:

      from nextmock import Arg, Mock
      
      m = Mock()
      
      m.with_args(1, 2, Arg.Any).returns(123)
      
      assert m(1, 2, 1) == 123
      assert m(1, 2, "123") == 123
      

      希望这个包可以让测试更愉快。随时提供任何反馈。

      【讨论】:

        【解决方案7】:

        如果您想使用带参数的函数但您要模拟的函数不带参数,也可以使用 functools 中的 partial。例如。像这样:

        def mock_year(year):
            return datetime.datetime(year, 11, 28, tzinfo=timezone.utc)
        
        @patch('django.utils.timezone.now', side_effect=partial(mock_year, year=2020))
        

        这将返回一个不接受参数的可调用对象(如 Django 的 timezone.now()),但我的 mock_year 函数可以。

        【讨论】:

        • 感谢这个优雅的解决方案。我想补充一点,如果您的原始函数有其他参数,则需要在生产代码中使用关键字调用它们,否则这种方法将不起作用。你得到错误:got multiple values for argument.
        【解决方案8】:

        只是为了展示另一种方法:

        def mock_isdir(path):
            return path in ['/var/log', '/var/log/apache2', '/var/log/tomcat']
        
        with mock.patch('os.path.isdir') as os_path_isdir:
            os_path_isdir.side_effect = mock_isdir
        

        【讨论】:

          【解决方案9】:

          你也可以使用@mock.patch.object:

          假设一个模块my_module.py 使用pandas 从数据库中读取数据,我们想通过模拟pd.read_sql_table 方法(以table_name 作为参数)来测试这个模块。

          您可以做的是(在您的测试中)创建一个 db_mock 方法,该方法根据提供的参数返回不同的对象:

          def db_mock(**kwargs):
              if kwargs['table_name'] == 'table_1':
                  # return some DataFrame
              elif kwargs['table_name'] == 'table_2':
                  # return some other DataFrame
          

          然后在您的测试函数中执行以下操作:

          import my_module as my_module_imported
          
          @mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
          def test_my_module(mock_read_sql_table):
              # You can now test any methods from `my_module`, e.g. `foo` and any call this 
              # method does to `read_sql_table` will be mocked by `db_mock`, e.g.
              ret = my_module_imported.foo(table_name='table_1')
              # `ret` is some DataFrame returned by `db_mock`
          

          【讨论】:

            猜你喜欢
            • 2015-09-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-10-29
            • 2020-06-20
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多