Mock大法:Fake it till u make it!

在单元测试时,我们希望测试环境尽可能单纯、可控。因此我们不希望依赖于用户输入,不希望连接无法独占的数据库或者第三方微服务等。这时候,我们需要通 mock 来模拟出这些外部接口。mock 可能是单元测试中最核心的技术。

在这里插入图片描述

无论是 unittest 还是 pytest,都是直接或者间接使用了 unittest 中的 mock 模块。所以,当你遇到 mock 相关的问题,请参阅 mock。我们接下来关于 mock 的介绍,也将以 Unittest 中的 mock 为主。不过,两个框架的 mock,在基本概念上都是相通的。

在这里插入图片描述

unittest.mock 模块提供了最核心的 Mock 类。当我们用 Mock 类的一个实例来替换被测试系统的某些部分之后,我们就可以对它的使用方式做出断言。这包括检查哪些方法(属性)被调用以及调用它们的参数如何。我们还可以设定返回值或者令其抛出异常,以改变执行路径。

除此之外,mock 模块还提供了 patch 方法和 MagicMock 子类。MagicMock 区别于 Mock 的地方在于,它自动实现了对 Python 中类对象中的魔法函数的 mock(这是它的名字的来源!),比如__iter__等。patch 则是一个带上下文管理的工具,它能自动复原我们对系统的更改。

在这里插入图片描述

基础概念

最基础的 mock 的概念可以通过下面的代码得到演示:

# 示例 7 - 9
from unittest.mock import MagicMock
thing = ProductionClass()
thing.method = MagicMock(return_value=3)
thing.method(3, 4, 5, key='value')

thing.method.assert_called_with(3, 4, 5, key='value')

这段代码假设我们有一个被测试类 ProductionClass,当我们调用它的 method 方法时,它有一些不便在单元测试下运行的情况(比如需要连接数据库),因此,我们希望能跳过对它的调用,而直接返回我们指定的一些值。

在这里我们能拿到 ProductionClass 实例对像的引用,所以,我们可以直接修改它的 method 属性,使之指向一个 MagicMock 对象。MagicMock 对象有一些重要的属性和方法。

这里出现的 return_value 是第一个重要的属性。它的意思时,当被替换的对象(这里是 method)被调用时,返回值应该是 3。与之类似的另一个属性是 side_effect。它同样也在 mock 被调用时,返回设置的值。但 return_value 与 side_effect 有重要区别:两者的返回值都可以设置为数组(或者其它可迭代对象),但通过 side_effect 设置返回值时,每次调用 mock,它都返回 side_effect 中的下一个迭代值;而 return_value 则会将设置值全部返回。另外,如果两者同时设置,side_effect 优先返回。请看下面的示例:

# 示例 7 - 10
import unittest.mock

side_effect = [1, 2, unittest.mock.DEFAULT, 4, 5]
m = unittest.mock.Mock(return_value="foo", side_effect=side_effect)

for _ in side_effect:
    print(m())

输出结果将是:

1
2
foo
4
5

我们给 side_effect 设置了 5 个值,在 5 次重复测试时,它分别依次返回下一个迭代值。注意这里我们通过 unittest.mock.DEFAULT,来让其中一次迭代,返回了 return_value 的设置值。当然,本质上,这仍然是对 side_effect 的一个迭代结果。

这里还出现了它的一个重要方法,assert_called_with,即检查被替换的方法是否被以期望的参数调用了。除此之外,还可以断言被调用的次数,等等。

在这里插入图片描述

这个例子非常简单。但它也演示了使用 Mock 的精髓,即生成 Mock 实例,设置行为(比如返回值),替换生产系统中的对象(方法、属性等),最后,检验结果。

很多时候,我们会通过 patch 的方式来使用 mock。又有两种主要的方式:

作为装饰器使用

假如我们有一个文件系统相关的操作,为了正常运行,必须在测试环境下构建目录,增加某些文件。为了简单起见,我们希望通过 mock 来模拟这个环境。

# 示例 7 - 11
import os

# FUNCTION UNDER TEST
class Foo:
    def get_files(self, dir_: str):
        return os.list_dir(dir_)

# TESTING CODE
from unittest.mock import patch
from unittest import TestCase

class FooTest(TestCase):
    @patch('__main__.Foo.get_files')
    def test_get_files(self, mocked):
        mocked.return_value = ["readme.md"]
        foo = Foo()
        self.assertListEqual(foo.get_files(), ["readme.md"])
        
test = FooTest()
test.test_get_files()

我们对关键代码进行一些解释。首先,通过装饰器语法进行 mock 时,我们的测试函数会多一个参数(这里是 mocked,但名字可以由我们任意指定)。这里使用多个 patch 装饰器也是可以的,每增加一个装饰器,测试函数就会多增加一个参数。

其次,我们要对 Foo.get_files 进行 mock,但我们在 Foo.get_files 之前,加上了一个__main__的前缀。这是由于类 Foo 的定义处在顶层模块中。在 Python 中,任何一个符号(类、方法或者变量)都处在某个模块(module)之下。如果这段代码存为磁盘文件 foo.py,那么模块名就是 foo;我们在别的模块中引入 Foo.get_files 时,应该使用 foo.Foo.get_files。但在这里,由于我们是同模块引用,因此它的前缀是__main__。

!!! info
使用 mock 的关键,是要找到引用被 mock 对象的正确方式。在 Python 中,一切都是对象。这些对象通过具有层次结构的命名空间来进行寻址。以 patch 方法为例,它处在 mock 模块之中,而 mock 模块又是包 unittest 的下级模块,因此,我们就使用 unittest.mock.patch 来引用它,这也与导入路径是一致的。

但是,像这里的脚本,如果一个对象不是系统内置对象,又不存在于任何包中,那么它的名字空间就是__main__,正如这里的示例__main__.Foo 一样。关于寻址,还存在其它的情况,我们会在后面介绍 builtin 对象以及错误的引用那两节中进行介绍。

通过装饰器语法传入进来的 mock 对象,它的行为是未经设置的。因此,我们要在这里先设置它的返回值,然后再调用业务逻辑函数 foo.get_files – 由于它已经被 mock 了,所以会返回我们设置的返回值。

在块级代码中使用

当我们通过装饰器来使用 mock 时,实际上它仍然是有上下文的,在函数退出之后,mock 对系统的更改就复原了。但是,有时候我们更希望使用代码块级别的 patch,一方面可以更精准地限制 mock 的使用范围,另一方面,它的语法会更简练,因为我们可以一行代码完成 mock 行为的设置。

# 示例 7 - 12
import os

# FUNCTION UNDER TEST
class Foo:
    def get_files(self, dir_: str):
        return os.list_dir(dir_)

# TESTING CODE
from unittest.mock import patch
from unittest import TestCase

class FooTest(TestCase):
    def test_get_files(self):
        with patch('__main__.Foo.get_files', return_value=["readme.md"]):
            foo = Foo()
            self.assertListEqual(foo.get_files(), ["readme.md"])
        
test = FooTest()
test.test_get_files()

这里仅用一行代码就完成了替换和设置。

在实践中,使用 mock 可能并不像看起来那么容易。有一些情景对初学者而言会比较难以理解。一旦熟悉之后,你会发现,你对 Python 的底层机制,有了更深入的理解。下面,我们就介绍这些场景下如何使用 mock。

特殊场合下的 mock

修改实例的属性

前面的例子中,我们给 patch 传入的 target 是一个字符串,显然,在 patch 作用域内,所有的新生成的对象都会被 patch。如果在 patch 之前,对象已经生成了,我们则需要使用patch.object来完成 patch。这样做的另一个好处是,我们可以有选择性地 patch 部分对象。

# 示例 7 - 19
def bar():
    logger = logging.getLogger(__name__)
    logger.info("please check if I was called")

    root_logger = logging.getLogger()
    root_logger.info("this is not intercepted")

# TEST_FOO.PY
from sample.core.foo import bar

logger = logging.getLogger('sample.core.foo')
with mock.patch.object(logger, 'info') as m:
    bar()
    m.assert_called_once_with("please check if I was called")

在 bar 方法里,两个 logger(root_logger 和’sample.core.foo’对应的 logger) 都被调用,但我们只拦截了后一个 logger 的info方法,结果验证它被调用,且仅被调用一次。

这里要提及 pytest 中 mocker.patch 与 unitest.mock.patch 的一个细微差别。后者进行 patch 时,可以返回 mock 对象,我们可以通过它进行更多的检查(见上面示例代码中的第 14,16 行);但 mocker.patch 的返回值是 None。

异步对象

从 3.8 起,unittest.mock 一般就不再区分同步和异步对象,比如:

# FUNCTION UNDER TEST

class Foo:
    async def bar():
        pass
        
# TESTING CODE
class FooTest(TestCase):
    async def test_bar(self):
        foo = Foo()
        with patch("__main__.Foo.bar", return_value="hello from async mock!"):
            res = await foo.bar()
            print(res)
            
test = FooTest()
await test.test_bar()

原函数 bar 的返回值为空。但输出结果是 “hello from async mock”,说明该函数被 mock 了。

被 mock 的方法 bar 是一个异步函数,如果我们只需要 mock 它的返回值的话,仍然是用同样的方法,直接给 return_value 赋值就好。如果我们要将其替换成另一个函数,也只需要将该函数声明成为异步函数即可。

但是,如果我们要 mock 的是一个异步的生成器,则方法会有所不同:

# FUNCTION UNDER TEST
from unittest import mock

class Foo:
    async def bar():
        for i in range(5):
            yield f"called {i}th"
    
# TESTING CODE
class FooTest(TestCase):
    async def test_bar(self):
        foo = Foo()
        with mock.patch(
            "__main__.Foo.bar"
        ) as mocked:
            mocked.return_value.__aiter__.return_value = [0, 2, 4, 6, 8]
            print([i async for i in foo.bar()])

            
test = FooTest()
await test.test_bar()

理解这段代码的关键是,我们要 mock 的对象是 bar 方法,它的返回值(即 mocked.return_value)是一个 coroutine。我们需要对该 coroutine 的__aiter__方法设置返回值,这样才能得到正确的结果。此外,由于__aiter__本身就是迭代器的意思,所以,即使我们设置它的 return_value,而不是 side_effect 为一个列表,它也会按次返回迭代结果,而不是整个 list。这是与我们前面介绍 return_value 和 side_effect 的区别时所讲的内容相区别的。

同样需要特别注意的是 async with 方法。你需要 mock 住它的__aexit__,将其替换成你要实现的方法。

builtin 对象

如果我们有一个程序,读取用户从控制台输入的参数,根据该参数进行计算。显然,我们需要 Mock 用户输入,否则单元测试没法自动化。

在 Python 中,接受用户控制台输入的函数是 input。要 mock 这个方法,按照前面学习中得到的经验,我们需要知道它属于哪个名字空间。在 Python 中,像 input, open, eval 等一类的函数大约有 80 个左右,被称为 builtin(内置函数)。

在 mock 它们时,我们使用 builtins 名字空间来进行引用:

with patch('builtins.input', return_value="input is mocked"):
    user_input = input("please say something:")
    print(user_input)

执行上述代码时,用户并不会有机会真正输入数据,input 方法被 mock,并且会返回"input is mocked"。

让时间就停留在这一刻

在这里插入图片描述

有时候我们会在代码中,通过 datetime.datetime.now() 来获取系统的当前时间。显然,在不同的时间测试,我们会得到不同的取值,导致测试结果无法固定。因此,这也是需要被 mock 的对象。

要实现对这个方法的 mock,可能比我们一开始以为的要难一些。我们的推荐是,使用 freezegun 这个库,而避开自己去 mock 它。

# 请使用 PYTEST 来运行,或者自行改写为 UNITTEST
from freezegun import freeze_time

import datetime
import unittest

# FREEZE TIME FOR A PYTEST STYLE TEST:

@freeze_time("2012-01-14")
def test():
    assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)

def test_case2():
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)
    with freeze_time("2012-01-14"):
        assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)

注意 Python 的时间库很多,如果您使用的是其它的库来获取当前时间,则 freeze_gun 很可能会不起作用。不过,对第三方的时间库,一般很容易实现 mock。

如何制造一场“混乱”?

假设我们有一个爬虫在抓取百度的热搜词。它的功能主要由 crawl_baidu 来实现。我们另外有一个函数在调用它,以保存 crawl_baidu 的返回结果。我们想知道,如果 crawl_baidu 中抛出异常,那么调用函数是否能够正确处理这种情况。

这里的关键是,我们要让 crawl_baidu 能抛出异常。当然,我们不能靠拔网线来实现这一点。

import httpx
from httpx import get, ConnectError
from unittest.mock import patch
from unittest import TestCase

def crawl_baidu():
    return httpx.get("https://www.baidu.com")
    
class ConnectivityTest(TestCase):
    def test_connectivity(self):
        with patch('httpx.get', side_effect=["ok", ConnectError("disconnected")]):
            print(crawl_baidu())

            with self.assertRaises(ConnectError):
                crawl_baidu()
        

case = ConnectivityTest()
case.test_connectivity()

crawl_baidu 依靠 httpx.get 来爬取数据。我们通过 mock httpx.get 方法,让它有时返回正常结果,有时返回异常。这是通过 side_effect 来实现的。

注意第 14 行,我们使用的是 self.assertRaises,而不是 try-except 来捕捉异常。两者都能够实现检查异常是否抛出的功能。但通过 self.assertRaises,我们强调了这里应该抛出一个异常,它是我们测试逻辑的一部分。而 try-except 则应该用来处理真正的异常。

消失的魔法

再强调一遍,“使用 mock 的关键,是要找到引用被 mock 对象的正确方式。”而正确引用的关键,则是这样一句“咒语”

!!! Warning
Mock an item where it is used, not where it came from

在对象被使用的地方进行 mock, 而不是在它出生的地方。

我们通过一个简单的例子来说明这一点:

from os import system
from unittest import mock
import pytest

def echo():
    system('echo "Hello"')

with mock.patch('os.system', side_effect=[Exception("patched")]) as mocked:
    with pytest.raises(Exception) as e:
        echo()

我们在 echo 方法中,调用了系统的 echo 命令。在测试中,我们试图 mock 住 os.system 方法,让它一被调用,就返回一个异常。然后我们通过 pytest 来检查,如果异常抛出,则证明 mock 成功,否则,mock 失败。

但是如果我们运行这个示例,只会得到一个友好的问候,No errors, No warnings! 为什么?

因为当我们在 echo() 函数中调用 system 函数时,此时的 system 存在于__main__名字空间,而不是 os 的名字空间。os 名字空间是 system 出生的地方,而__main__名字空间才是使用它的地方。因此,我们应该 patch 的对象是’main.system’,而不是’os.system’。

现在,让我们将os.system改为__main__.system,重新运行,你会发现,魔法又生效了!

在配套代码中,还有一个名为 where_to_patch 的示例,我们也来看一下。

# FOO.PY
def get_name():
    return "Alice"

# BAR.PY
from .foo import get_name

class Bar:
    def name(self):
        return get_name()

# TEST.PY
from unittest.mock import patch

from where_to_patch.bar import Bar

tmp = Bar()

with patch('where_to_patch.foo.get_name', return_value="Bob"):
    name = tmp.name()
    assert name == "Bob"

测试代码会抛出 AssertionError: assert "Alice" == "Bob"的错误。如果我们把where_to_patch.foo改为where_to_patch.bar`,则测试通过。这个稍微扩展了一下的例子,进一步清晰地演示了如何正确引用被 mock 对象。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/343490.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【DeepLearning-1】 注意力机制(Attention Mechanism)

1.1注意力机制的基本原理: 计算注意力权重: 注意力权重是通过计算输入数据中各个部分之间的相关性来得到的。这些权重表示在给定上下文下,数据的某个部分相对于其他部分的重要性。 加权求和: 使用这些注意力权重对输入数据进行加权…

Flink(十五)【Flink SQL Connector、savepoint、CateLog、Table API】

前言 今天一天争取搞完最后这一部分,学完赶紧把 Kafka 和 Flume 学完,就要开始做实时数仓了。据说是应届生得把实时数仓搞个 80%~90% 才能差不多找个工作,太牛马了。 1、常用 Connector 读写 之前我们已经用过了一些简单的内置连接器&#x…

用ChatGPT教学、科研!大学与OpenAI合作

亚利桑那州立大学(简称“ASU”)在官网宣布与OpenAI达成技术合作。从2024年2月份开始,为所有学生提供ChatGPT企业版访问权限,主要用于学习、课程作业和学术研究等。 为了帮助学生更好地学习ChatGPT和大语言模型产品,AS…

禅道的下载使用

文章目录 禅道的下载下载安装包 http://www.zentao.net/安装导南 禅道的使用创建用户产品经理将人员添加进禅道查看权限、产品经理使用禅道添加产品添加产品模块关联用例(测试主管)执行测试用例转bug 泳道图 禅道的下载 下载安装包 http://www.zentao.n…

电脑无法开机?重装系统教程在这!超详细

#电脑无法开机,怎么重装系统# 前言 本教程适合比较新的Windows电脑硬件。硬件的新旧并没有一个清晰的标准去判定,毕竟有些厂家生产的主板支持UEFI和Legacy两种引导方式,但部分厂家生产的硬件所使用的Bios并不支持Legacy,所以只能用UEFI引导来安装系统。 所以要使用哪种引…

容器原理之Union FS

一、前言 1.1 什么是 UnionFS 联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories in…

华为OD机试之阿里巴巴找黄金宝箱(IV) C++

题目背景 贫如洗的椎夫阿里巴巴在去砍柴的路上,无意中发现了强盗集团的藏宝地,藏宝地有编号从0-N的箱子,每个箱子上面有一人数字,箱子排列成一个环,编号最大的箱子的下一个是编号为0的箱子。请输出每个箱了贴的数字之…

【记一次线上事故的排查思路】- CPU飙升问题排查

问题描述 由于项目排期较紧,临时从其他组调来三个开发资源帮我一起做项目,难免上线的时候大家的需求一块上线。 问题来了,上线三天后,线上CPU总是莫名奇妙的突然飙升,飙升后CPU并未降下来,而是一直处在高点…

解密POM:提升自动化脚本稳定性和开发效率的正确姿势!

Page Objects是selenium的一种测试设计模式,主要将每个页面看作是一个class。class的内容主要包括属性和方法,属性不难理解,就是这个页面中的元素对象,比如输入用户名的输入框,输入登陆密码的输入框、登陆按钮、这个页…

《WebKit 技术内幕》学习之七(3): 渲染基础

3 渲染方式 3.1 绘图上下文(GraphicsContext) 上面介绍了WebKit的内部表示结构,RenderObject对象知道如何绘制自己,但是,问题是RenderObject对象用什么来绘制内容呢?在WebKit中,绘图操作被定…

【Leetcode】2765. 最长交替子数组

文章目录 题目思路代码结果 题目 2765. 最长交替子数组 题目:给你一个下标从 0 开始的整数数组 nums 。如果 nums 中长度为 m 的子数组 s 满足以下条件,我们称它是一个 交替子数组 : m 大于 1 。 s1 s0 1 。 下标从 0 开始的子数组 s 与…

Vue中$watch()方法和watch属性的区别

vue中$watch()和watch属性都是监听值的变化的,是同一个作用,但是有两个不同写法。 用法一: //注意:这种方法是监听不到对象的变化的。 this.$watch((newVal,oldVal)>{ }) 用法二: watch:{xxx:(newVal,oldVal)>…

SpringCloud Aliba-Seata【上】-从入门到学废【7】

目录 🧂.Seata是什么 🌭2.Seata术语表 🥓3.处理过程 🧈4.下载 🍿5.修改相关配置 🥞6.启动seata 1.Seata是什么 Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能…

硅像素传感器文献调研(八)

1977 平面单场限环器件的理论与击穿电压 摘要 使用一个或多个浮置场限制环减少了平面器件中结曲率对击穿电压的不利影响。虽然这已经知道了一段时间,但还没有一种方法可以准确地预测使用场环可以实现的改善量。本文提出了一种计算机算法,它使得有可能进…

残差连接是什么意思

残差连接是深度神经网络中一种用于缓解梯度消失问题的技术。它的核心思想是通过将网络的输入直接传递到网络的输出,从而构建了一条直达路径,使得梯度更容易通过整个网络传播。这有助于在训练深层网络时避免梯度消失或梯度爆炸的问题。 在残差连接中&…

Linux 一键部署grafana

grafana 前言 Grafana 是一款开源的数据可视化和监控仪表盘工具。它提供了丰富的数据查询、可视化和报警功能,可用于实时监控、数据分析和故障排除等领域。 通过 Grafana,您可以连接到各种不同的数据源,包括时序数据库(如 Prometheus、InfluxDB)和关系型数据库(如 MySQ…

题记(26)--Sharing(链表公共后缀)

目录 一、题目内容 二、输入描述 三、输出描述 四、输入输出示例 五、完整C语言代码 一、题目内容 To store English words, one method is to use linked lists and store a word letter by letter. To save some space, we may let the words share the same sublist if…

Mybatis----缓存

MyBatis是一个流行的Java持久化框架,它提供了一个灵活的缓存机制来提高查询性能。 MyBatis的缓存机制主要分为一级缓存和二级缓存。 一级缓存是指在同一个SqlSession中,查询结果会被缓存起来,当再次执行同样的查询时,直接从缓存中…

Python学习04—基本图形绘制

通过一个案例来初步认识Python的图形绘制 案例:绘制Python蟒蛇 #PythonDraw.py import turtle turtle.setup(650,350,200,200) turtle.penup() turtle.fd(-250) turtle.pendown() turtle.pensize(25) turtle.pencolor("purple") turtle.seth(-40) for i…

基于springboot+vue的“衣依”服装销售平台系统(前后端分离)

博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容:毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 研究背景…