DarkMatter in Cyberspace
  • Home
  • Categories
  • Tags
  • Archives

Python Mock Notes


Python mock 对象的基本思想是:用一个假对象 (mock_obj) 代替参与测试过程的某个 真实对象 tested_module.myobj,在执行测试 (test_function()) 前, 可以调整假对象属性的值 (myobj.prop1 = 'val1') 或者方法的返回值 (myobj.method1.return_value = 'val2'), 然后对测试目标或者假对象在测试过程中的行为进行验证:

from tested_module import myobj

@mock.patch('myobj')
def test_target(self, mock_obj):
    myobj.prop1 = 'val1'
    tested_function()
    self.assertFalse(mock_obj.method1.called, "Failed to avoid running.")

上面的例子演示了:当假对象的属性 prop1 被设为 val1 时, 测试过程中它的 method1 方法不应该被调用。

下面按照作用域从大到小的顺序分别说明不同级别对象的 mock 方法。

mock 第三方库

通过mock Python 标准库避免副作用:

# mymodule.py:
import os
def rm(filename):
    if os.path.isfile(filename):
        os.remove(filename)


# mytests.py:
from mymodule import rm
from unittest import TestCase, mock

class RmTestCase(TestCase):
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os):
        rm("any path")
        mock_os.remove.assert_called_with("any path")

真实的 os.remove() 没有执行,但我们可以验证这个方法被调用过了。

mock 类实例

如果 mock 目标不是模块级对象,而是函数参数:

# mymodule.py:
class MyOS:
    def __init__(self, path):
        self.path = path

    def remove(self, target):
        return 'remove file %s, and %s' % (self.path, target)

def rm(myos, filepath):
    return myos.remove(filepath)


# mytests.py:
class RmTestCase(TestCase):
    def test_myos(self):
        mock_os = mock.create_autospec(MyOS)
        mock_os.remove.return_value = "this is mocked"
        print(rm(mock_os, 'mno'))

这里我们创建了真实 MyOS 类的替代品,然后定义了它的方法的返回值: mock_os.remove.return_value = ...,然后验证被测试函数在此情况下的执行结果。

mock 函数

如果进一步将上面的 myos 从函数参数变成内部对象,可以通过 mock 对象方法实现替代:

# mymodule.py:
class MyOS:
    def __init__(self, path):
        self.path = path

    def remove(self, target):
        return 'remove file %s, and %s' % (self.path, target)

def rm(filepath):
    myos = MyOS('linux')
    return myos.remove(filepath)


# mytests.py:
class RmTest(TestCase):
    def test_rm(self):
        print(rm('aabbc'))

    @mock.patch('mymodule.MyOS.remove')
    def test_mock_rm(self, mock_remove):
        mock_remove.return_value = 'this is mocked'
        print(rm('xyz'))

第一个测试用例是真实函数的运行结果,第2个测试用例是 mock 之后函数的运行结果。

多个 patch 的顺序

patch多个mock对象时,patch顺序和参数顺序要相反:

@mock.patch('mymodule.sys')
@mock.patch('mymodule.os')
@mock.patch('mymodule.os.path')
def test_something(self, mock_os_path, mock_os, mock_sys):
    pass

这是因为 Python 的 decorator 是按顺序包装函数的: patch_sys(patch_os(patch_os_path(test_something))), 导致最外层的 patch_sys 最后执行,所以其参数要放到最后。

Ref:

  • An Introduction to Mocking in Python

  • Mocking Has A Weakness, Speccing Removes It

  • Getting Started with Mocking in Python

  • What the mock? — A cheatsheet for mocking in Python

  • Python Mocks: a gentle introduction - Part 1



Published

Nov 27, 2018

Last Updated

Nov 27, 2018

Category

Tech

Tags

  • mock 1
  • python 136
  • test 4

Contact

  • Powered by Pelican. Theme: Elegant by Talha Mansoor