zoukankan      html  css  js  c++  java
  • 5、pytest -- 猴子补丁

    有时候,测试用例需要调用某些依赖于全局配置的功能,或者这些功能本身又调用了某些不容易测试的代码(例如:网络接入)。fixture monkeypatch可以帮助你安全的设置/删除一个属性、字典项或者环境变量,甚至改变导入模块时的sys.path路径。

    monkeypatch提供了以下方法:

    monkeypatch.setattr(obj, name, value, raising=True)
    monkeypatch.delattr(obj, name, raising=True)
    monkeypatch.setitem(mapping, name, value)
    monkeypatch.delitem(obj, name, raising=True)
    monkeypatch.setenv(name, value, prepend=False)
    monkeypatch.delenv(name, raising=True)
    monkeypatch.syspath_prepend(path)
    monkeypatch.chdir(path)
    

    所有的修改将在测试用例或者fixture执行完成后撤销。raising参数表明:当设置/删除操作的目标不存在时,是否上报KeyErrorAttributeError异常。

    1. 修改函数功能或者类属性

    使用monkeypatch.setattr()可以将函数或者属性修改为你希望的行为,使用monkeypatch.delattr()可以删除测试用例使用的函数或者属性;

    参考以下三个例子:

    • 在这个例子中,使用monkeypatch.setattr()修改Path.home方法,在测试运行期间,它一直返回的是固定的Path("/abc"),这样就移除了它在不同平台上的依赖;测试运行完成后,对Path.home的修改会被撤销;

      # src/chapter-5/test_module.py
      
      from pathlib import Path
      
      
      def getssh():
          return Path.home() / ".ssh"
      
      
      def test_getssh(monkeypatch):
          def mockreturn():
              return Path("/abc")
      
          # 替换 Path.home
          # 需要在真正的调用之前执行
          monkeypatch.setattr(Path, "home", mockreturn)
          
          # 将会使用 mockreturn 代替 Path.home
          x = getssh()
          assert x == Path("/abc/.ssh")
      
    • 在这个例子中,使用monkeypatch.setattr()结合类,模拟函数的返回对象;

      假设我们有一个简单的功能,访问一个url返回网页内容:

      # src/chapter-5/app.py
      
      from urllib import request
      
      
      def get(url):
          r = request.urlopen(url)
          return r.read().decode('utf-8')
      

      我们现在要去模拟r,它需要一个.read()方法返回的是bytes的数据类型;我们可以在测试模块中定义一个类来代替r

      # src/chapter-5/test_app.py
      
      from urllib import request
      
      from app import get
      
      
      # 自定义的类模拟 urlopen 的返回值
      class MockResponse:
      
          # 永远返回一个固定的 bytes 类型的数据
          @staticmethod
          def read():
              return b'luizyao.com'
      
      
      def test_get(monkeypatch):
          def mock_urlopen(*args, **kwargs):
              return MockResponse()
      
          # 使用 request.mock_urlopen 代替 request.urlopen
          monkeypatch.setattr(request, 'urlopen', mock_urlopen)
      
          data = get('https://luizyao.com')
          assert data == 'luizyao.com'
      

      你可以继续为实际的场景构建更具有复杂度的MockResponse;例如,你可以包含一个总是返回Trueok属性,或者根据输入的字符串为read()返回不同的值;

      我们也可以通过fixture跨用例共享:

      # src/chapter-5/test_app.py
      
      import pytest
      
      
      # monkeypatch 是 function 级别作用域的,所以 mock_response 也只能是 function 级别,
      # 否则会报 ScopeMismatch 
      @pytest.fixture
      def mock_response(monkeypatch):
          def mock_urlopen(*args, **kwargs):
              return MockResponse()
      
          # 使用 request.mock_urlopen 代替 request.urlopen
          monkeypatch.setattr(request, 'urlopen', mock_urlopen)
      
      
      # 使用 mock_response 代替原先的 monkeypatch
      def test_get_fixture1(mock_response):
          data = get('https://luizyao.com')
          assert data == 'luizyao.com'
      
      
      # 使用 mock_response 代替原先的 monkeypatch
      def test_get_fixture2(mock_response):
          data = get('https://bing.com')
          assert data == 'luizyao.com'
      

      注意:

      • 测试用例使用的fixture由原先的mock_response替换为monkeypatch
      • 因为monkeypatchfunction级别作用域的,所以mock_response也只能是function级别,否则会报ScopeMismatch: You tried to access the 'function' scoped fixture 'monkeypatch' with a 'module' scoped request object 错误;
      • 如果你想让mock_response应用于所有的测试用例,可以考虑将它移到conftest.py里面,并标记autouse=True
    • 在这个例子中,使用monkeypatch.delattr()删除urllib.request.urlopen()方法;

      # src/chapter-5/test_app.py
      
      @pytest.fixture
      def no_request(monkeypatch):
          monkeypatch.delattr('urllib.request.urlopen')
      
      
      def test_delattr(no_request):
          data = get('https://bing.com')
          assert data == 'luizyao.com'
      

      执行:

      λ pipenv run pytest --tb=native --assert=plain --capture=no src/chapter-5/test_app.
      py::test_delattr
      =============================== test session starts ================================ 
      platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
      rootdir: D:Personal FilesProjectspytest-chinese-doc
      collected 1 item
      
      srcchapter-5	est_app.py F
      
      ===================================== FAILURES ===================================== 
      ___________________________________ test_delattr ___________________________________ 
      Traceback (most recent call last):
        File "D:Personal FilesProjectspytest-chinese-docsrcchapter-5	est_app.py", line 78, in test_delattr
          data = get('https://bing.com')
        File "D:Personal FilesProjectspytest-chinese-docsrcchapter-5app.py", line 26, in get
          r = request.urlopen(url)
      AttributeError: module 'urllib.request' has no attribute 'urlopen'
      ================================ 1 failed in 0.04s =================================
      

      注意:

      • 避免删除内置库中的方法,如果一定要这么做,最好加上--tb=native --assert=plain --capture=no

      • 修改pytest使用到的库,可能会污染pytest本身,建议使用MonkeyPatch.context(),它返回一个MonkeyPatch对象,结合with限制这些修改只发生在包裹的代码中。

        def test_stdlib(monkeypatch):
        with monkeypatch.context() as m:
            m.setattr(functools, "partial", 3)
            assert functools.partial == 3
        

    2. 修改环境变量

    使用monkeypatchsetenv()delenv()方法,可以在测试中安全的设置/删除环境变量;

    # src/chapter-5/test_env.py
    
    import os
    
    import pytest
    
    
    def get_os_user():
        username = os.getenv('USER')
    
        if username is None:
            raise IOError('"USER" environment variable is not set.')
    
        return username
    
    
    def test_user(monkeypatch):
        monkeypatch.setenv('USER', 'luizyao')
        assert get_os_user() == 'luizyao'
    
    
    def test_raise_exception(monkeypatch):
        monkeypatch.delenv('USER', raising=False)
        pytest.raises(IOError, get_os_user)
    

    monkeypatch.delenv()raising要设置为False,否则可能会报KeyError

    你也可以使用fixture,实现跨用例共享:

    import pytest
    
    
    @pytest.fixture
    def mock_env_user(monkeypatch):
        monkeypatch.setenv("USER", "TestingUser")
    
    
    @pytest.fixture
    def mock_env_missing(monkeypatch):
        monkeypatch.delenv("USER", raising=False)
    
    
    # notice the tests reference the fixtures for mocks
    def test_upper_to_lower(mock_env_user):
        assert get_os_user_lower() == "testinguser"
    
    
    def test_raise_exception(mock_env_missing):
        with pytest.raises(OSError):
            _ = get_os_user_lower()
    

    3. 修改字典

    使用monkeypatch.setitem()方法可以在测试期间安全的修改字典中特定的值;

    DEFAULT_CONFIG = {"user": "user1", "database": "db1"}
    
    
    def create_connection_string(config=None):
        config = config or DEFAULT_CONFIG
        return f"User Id={config['user']}; Location={config['database']};"
    

    我们可以修改数据库的用户或者使用其它的数据库:

    import app
    
    
    def test_connection(monkeypatch):
        monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
        monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
    
        expected = "User Id=test_user; Location=test_db;"
    
        result = app.create_connection_string()
        assert result == expected
    

    可以使用monkeypatch.delitem删除指定的项:

    import pytest
    
    import app
    
    
    def test_missing_user(monkeypatch):
        monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
    
        with pytest.raises(KeyError):
            _ = app.create_connection_string()
    

    GitHub仓库地址:https://github.com/luizyao/pytest-chinese-doc

  • 相关阅读:
    MySQL回顾
    mysql多表查询
    通过JDBC进行简单的增删改查(以MySQL为例)
    如何正确学习JavaScript
    List集合遍历时修改元素出现并发修改异常总结
    国内有哪些质量高的JAVA社区?
    【题解】【链表】【Leetcode】Add Two Numbers
    【题解】【字符串】【Leetcode】Valid Palindrome
    【题解】【DP】【Leetcode】Climbing Stairs
    【题解】【数组】【Leetcode】Merge Sorted Array
  • 原文地址:https://www.cnblogs.com/luizyao/p/11698702.html
Copyright © 2011-2022 走看看