【问题标题】:How to mock a python class dependency using pytest-mock?如何使用 pytest-mock 模拟 python 类依赖项?
【发布时间】:2021-09-07 10:09:02
【问题描述】:

我正在努力理解如何从 Python 和 pytest 中的类依赖项中对类进行存根/模拟所有方法。下面的清单显示了我正在测试的类。它有两个内部依赖:OWMProxyPyOwmDeserializer

被测类

class OWM:
    def __init__(self, api_key: str, units: WeatherUnits) -> None:
        self._api_key = api_key
        self._units = units    
        self._pyorm = OWMProxy.from_api_key(api_key)
        self._deserializer = PyOwmDeserializer()

    def at(self, city: str, iso_datetime: str) -> ForecastModel:
        weather = self._pyorm.for_time(city, iso_datetime)

        return self._deserializer.deserialize(weather, self._units)

    def day(self, city: str, day: str) -> ForecastModel:
        weather = self._pyorm.for_day(city, day)

        return self._deserializer.deserialize(weather, self._units)

    def now(self, city: str) -> ForecastModel:
        weather = self._pyorm.now(city) 

        return self._deserializer.deserialize(weather, self._units)

我的问题是,是否可以在使用 PyTest 进行单元测试时模拟整个类依赖项?

目前,我的单元测试使用 mocker 来模拟每个类方法,包括 init 方法。

我可以使用依赖注入方法,即为内部反序列化器和代理接口创建一个接口,并将这些接口添加到被测类的构造函数中。

或者,我可以按照here 的建议使用unittest.mock 模块进行测试。 pytest-mock中是否有等效功能???

到目前为止的单元测试...

@pytest.mark.skip(reason="not implemented")
  def test_owm_initialises_deserializer(
      default_weather_units: WeatherUnits, mocker: MockFixture
  ) -> None:
      api_key = "test_api_key"
  
      proxy = OWMProxy(py_OWM(api_key))
  
      patch_proxy = mocker.patch(
          "wayhome_weather_api.openweathermap.client.OWMProxy.from_api_key",
          return_value=proxy,
      )   
  
      patch_val = mocker.patch(
          "wayhome_weather_api.openweathermap.deserializers.PyOwmDeserializer",
          "__init__",
          return_value=None,
      )   
  
      owm = OWM(api_key, default_weather_units)
  
      assert owm is not None

【问题讨论】:

    标签: python pytest pytest-mock


    【解决方案1】:

    您可以模拟整个类并控制其方法的返回值和/或副作用,就像它在 docs 中所做的那样。

    >>> def some_function():
    ...     instance = module.Foo()
    ...     return instance.method()
    ...
    >>> with patch('module.Foo') as mock:
    ...     instance = mock.return_value
    ...     instance.method.return_value = 'the result'
    ...     result = some_function()
    ...     assert result == 'the result'
    

    假设被测类位于src.py

    test_owm.py

    import pytest
    from pytest_mock.plugin import MockerFixture
    
    from src import OWM, WeatherUnits
    
    
    @pytest.fixture
    def default_weather_units():
        return 40
    
    
    def test_owm_mock(
          default_weather_units: WeatherUnits, mocker: MockerFixture
    ) -> None:
        api_key = "test_api_key"
    
        #  Note that you have to mock the version of the class that is defined/imported in the target source code to run. So here, if the OWM class is located in src.py, then mock its definition/import of src.OWMProxy and src.PyOwmDeserializer
        patch_proxy = mocker.patch("src.OWMProxy.from_api_key")
        patch_val = mocker.patch("src.PyOwmDeserializer")
    
        owm = OWM(api_key, default_weather_units)
    
        assert owm is not None
    
        # Default patch
        print("Default patch:", owm.day("Manila", "Today"))
    
        # Customizing the return value
        patch_proxy.return_value.for_day.return_value = "Sunny"
        patch_val.return_value.deserialize.return_value = "Really Sunny"
        print("Custom return value:", owm.day("Manila", "Today"))
        patch_proxy.return_value.for_day.assert_called_with("Manila", "Today")
        patch_val.return_value.deserialize.assert_called_with("Sunny", default_weather_units)
    
        # Customizing the side effect
        patch_proxy.return_value.for_day.side_effect = lambda city, day: f"{day} in hot {city}"
        patch_val.return_value.deserialize.side_effect = lambda weather, units: f"{weather} is {units} deg celsius"
        print("Custom side effect:", owm.day("Manila", "Today"))
        patch_proxy.return_value.for_day.assert_called_with("Manila", "Today")
        patch_val.return_value.deserialize.assert_called_with("Today in hot Manila", default_weather_units)
    
    
    def test_owm_stub(
          default_weather_units: WeatherUnits, mocker: MockerFixture
    ) -> None:
        api_key = "test_api_key"
    
        class OWMProxyStub:
            @staticmethod
            def from_api_key(api_key):
                return OWMProxyStub()
    
            def for_day(self, city, day):
                return f"{day} in hot {city}"
    
        class PyOwmDeserializerStub:
            def deserialize(self, weather, units):
                return f"{weather} is {units} deg celsius"
    
    
        patch_proxy = mocker.patch("src.OWMProxy", OWMProxyStub)
        patch_val = mocker.patch("src.PyOwmDeserializer", PyOwmDeserializerStub)
    
        owm = OWM(api_key, default_weather_units)
    
        assert owm is not None
    
        # Default patch
        print("Default patch:", owm.day("Manila", "Today"))
        # If you want to assert the calls made as did in the first test above, you can use the mocker.spy() functionality
    

    输出

    $ pytest -q -rP
    ================================================================================================= PASSES ==================================================================================================
    ______________________________________________________________________________________________ test_owm_mock ______________________________________________________________________________________________
    ------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
    Default patch: <MagicMock name='PyOwmDeserializer().deserialize()' id='139838844832256'>
    Custom return value: Really Sunny
    Custom side effect: Today in hot Manila is 40 deg celsius
    ______________________________________________________________________________________________ test_owm_stub ______________________________________________________________________________________________
    ------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
    Default patch: Today in hot Manila is 40 deg celsius
    2 passed in 0.06s
    

    如您所见,我们能够控制模拟依赖项的方法的返回值。

    【讨论】:

    • 非常感谢!这正是我想要的!接受答案,再次感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-17
    • 1970-01-01
    • 1970-01-01
    • 2021-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多