对象 替换_读懂Python的Mock对象库(1)

本文介绍了Python的unittest.mock库,用于在测试中使用Mock对象以控制代码行为,提高测试质量。Mock对象可以模拟真实对象,用于替换难以测试的部分,如外部依赖。文章详细讲解了Mock的特性,如惰性成员创建、断言检查、返回值管理、侧面影响和配置,并介绍了patch()函数的使用,包括作为装饰器、上下文管理器、替换对象属性以及确定打补丁的位置。通过学习,读者将学会如何利用unittest.mock创建和管理Mock对象,提升测试效率。
摘要由CSDN通过智能技术生成

7f02f31be9df8620fb14dee4bb108177.png

当你希望编写健壮的代码时,使用测试对于验证程序逻辑是否正确,可靠以及高效至关重要。然而,测试的价值取决于它能达成这些标准的程度。比如复杂的逻辑和预料之外的依赖等障碍都会对编写高质量的测试造成困难。Python中的mock对象库unittest.mock可以帮助你解决这些障碍。

读完本篇文章,你将收获很多:

    1. 使用Mock创建Python mock对象

    2. 断言你使用的对象是你期望的

    3. 校验并使用Python mock的数据

    4. 针对Python mock对象进行配置

你将从mock是什么以及它如何优化你的测试程序开始。

Mock是什么

一个在测试环境中模仿并替换真实对象的mock对象,它是一个强大且功能丰富的工具,可以提高你的测试代码质量。

使用Python模拟对象的一个原因是在测试期间控制代码的行为。

例如,如果你的代码对外发出了一个HTTP请求,那么只有服务的运行符合你的预期,测试程序才能按照你的预定方式执行。有时候,外部服务的临时变更有可能会导致你的测试组件出现间歇性故障。

因此,最好是在可控的环境中运行你的测试。使用mock对象替换实际请求可以允许你以可预知的方式模拟外部请求服务的中断以及成功。

有时,你很难测试到代码库的特定区域。这些区域包括except和if语句块等难以满足条件的部分。使用Python mock对象可以帮助你控制你的代码执行路径以进入这些区域,并提高你的测试代码覆盖率。

使用mock对象的另一个原因是为了更好的理解你代码中实际使用的对象。你可以发现,Python mock对象包含的数据的用处:

    1. 如果你调用了方法

    2. 你怎样调用的这个方法

    3. 你多久调用一次这个方法

了解mock对象的作用是学习使用mock对象的第一步。

现在,你将学习如何使用Python mock对象。

Python mock库

Python的mock对象库是unittest.mock。它提供了一种在测试中引入mock的简单方式。

注:Python 3.3以及之后的版本在标准库中添加了unittest.mock。如果你使用的是较早版本的Python,你需要安装官方的代码资源库。这里,你需要从PyPI安装mock:

20165d3ea747f5e84031961cb8e13fde.png

unittest.mock提供了一个Mock类,它可以模拟你代码库中的实际对象。Mock可以提供极其灵活准确的数据,它与它的子类可以满足你在测试中遇到的大多数Python mock需求。

unittest.mock库还提供了一个叫做patch()的函数,它可以用Mock对象的实例替换实际的对象。你可以将patch()用作装饰器或上下文管理器,从而控制mock对象的使用范围。一旦退出了指定范围,patch()就会使用原始对象替换并清除你代码中的mock对象。

最后,unittest.mock为mock对象中的常规问题提供了一些解决方案。

现在,你已经对什么是mock以及你将使用哪些库有了更深的理解。我们来深入探讨一下unittest.mock提供的功能和方法。

Mock对象

unittest.mock为mock对象提供了一个基类Mock。因为Mock非常灵活,它的用例实际上并没有多少限制。

首先实例化一个新的Mock实例:

f6639307dda29cb20075b0ff1b7ae90d.png

现在可以使用新建的Mock来替换你代码中的对象。你可以通过将其作为参数传递给对象或者重新定义另一个对象来完成这个操作:

71ad43f3bfb492ee899d0bcdede717d0.png

当你替换代码中的对象时,Mock必须模拟的与实际对象相像才可以。否则,Mock无法替换原始对象。

举个例子,假设你正在模拟json库并使用程序调用dumps()方法,那么你的Python mock对象就必须包含dumps()方法。

接下来,你将了解到Mock是如何解决这个问题的。

惰性成员及方法

Mock的实例必须模拟它所需要替换的对象。为了灵活的实现这种方式,Mock会在访问这些属性的时候创建它们。

d5a17915a4eeda290211f4837b25c746.png

由于Mock可以动态的创建任意属性,所以它适用于替换所有对象。

看一个之前的例子,如果你mock一个json库并调用dumps()方法,那么Python mock对象为了让自身的接口能够匹配库里的接口,会创建这个方法。

6e441b0b661a071337cc3e4254be9bd8.png

请注意这两个版本中dumps()的两个重要特征:

    1. 与真正的dumps()不同,mock出来的方法不需要任何参数。实际上,它可以接收你传递给它的任何参数。

    2. dumps()方法的返回值同样也是一个Mock对象。Mock能够以递归的方式定义其他的mock对象,以供你在更复杂的情况下使用。

2085f2d5f24c626499234d07c8a24420.png

因为每个mock方法返回的也是一个Mock对象,所以你可以通过多种方式使用mock。

Mock保持灵活的同时也提供了足够的信息。接下来,你将会学习到如何使用mock更好的理解你的代码。

断言检查

Mock实例存储了如何使用它们的信息。例如,你可以看到你是否调用了一个方法,你怎样调用的这个方法,等等。这里有两种主要的方式使用到了这些信息。

第一,你可以断言你的程序使用的对象是你预期的那样:

d98a88d89791d368840a69445d8e09fe.png

使用.assert_called()确保你调用了方法,而.assert_called_once()确保你只调用了一次这个方法。

这两个方法都有衍生的其他方法,便于你检查传递给mock方法的参数:

    1. .assert_called_with(*args, **kwarge)

    2. .assert_called_once_with(*args, **kwarge)

你必须使用与传递给实际方法相同的参数调用mock的方法,断言才会通过。

aaff067d0338f26278c9d58ca4a5783a.png

json.loads.assert_called_with('{"key": "value"}')抛出了一个AssertionError异常信息,因为你原本希望使用一个位置参数调用loads(),但是实际上你使用了一个关键字参数调用了该方法。json.loads.assert_called_with(s='{"key": "value"}')这样才是正确的。

第二,你可以查看Mock的特殊属性来了解应用程序如何使用对象:

11e05040f8a8b0a0d26a92acf763ad61.png

你可使用这些属性编写测试程序,来确保你代码中的对象按预期执行。

现在你可以创建mock对象并检查其使用的数据。接下来我们将介绍如何自定义mock方法来更多的应用于你的测试环境。

管理Mock的返回数据

使用mock的一个原因是在测试期间控制代码的行为。一种方式是指定函数的返回值。我们来举个例子说明一下它是怎么工作的。

首先,创建一个名为my_calendar.py的文件,添加一个使用了Python的datetime库的方法is_weekday(),判断今天是否为工作日。最后写一个测试,断言函数按照预期工作。

a5869b01acbf57e4c34c07ddec98cc5a.png

因为测试的是今天是否为工作日,所以结果取决于运行测试的那天:

3fc5e4594705dd35f7e34c2780eb481a.png

如果这个命令运行没有输出结果,那么断言是成功的。不幸的是,如果你在周末运行了这个命令,你将看到一个AssertionError错误。

2bfbd2a78852e3f10094090b039b8099.png

开始写测试时,确保结果的可预见性是很重要的。你可以在测试过程中使用Mock排除不确定因素。下面的例子中,你可以mock一个datetime并且通过使用.return_value为.today()选择一个你希望设置的日期。

82011c73f233f32d3cd27dffabe210a9.png

这个例子中,.today()是一个mock出来的方法。你已经通过mock中的.return_value指定一个特定的日期消除不一致性。这样,当你调用.today()时会返回你指定的日期datetime。

在第一个测试中,确定了星期二是工作日。第二次测试,确定星期六不是工作日。现在,你运行测试的时候是哪一天不重要了,因为已经可以mockdatetime并控制对象的行为。

进一步阅读:尽管这样通过Mock对象来mock一个datetime的方式是一个不错的例子,但是已经有一个非常棒的库freezegun可以mock日期datetime了。

在构建测试时,通常因为函数要比简单的单项逻辑更为复杂,你可能会遇到mock的函数返回值不够用的情况。

有时,你可能会多次调用函数以期可以返回不同的值甚至是抛出异常。你可能使用.side_effect实现这一点。

管理Mock的侧面影响

你可以通过指定mock的函数的侧面影响来控制代码的行为。.side_effect定义了当你调用mock的函数是会发生什么。

在my_calendar.py中增加一个函数来测试这是怎么工作的:

8e964dc4f23d5c6a41953c426a0c3e0d.png

get_holidays()向本地服务器发起了一个访问一组假期的请求。如果服务器响应成功,get_holidays()会返回一个字典,否则函数返回None。

你可通过设置requests.get.side_effect来测试get_holidays()是如何响应连接超时的。

对于这个例子,你只能看到my_calendar.py中的部分相关代码。可以使用unittest库来构建测试用例。

02f27e75f6d5aede5aeeff1181b56c57.png

用.assertRaises()验证在设置了侧面影响的get()函数之后get_holidays()是不是会引发异常。

运行并查看测试结果:

876bce3d843c9c58e3e62763f186a0cb.png

如果你希望更加动态的加载,可以将.side_effect设置为Mock调用是mock的函数。这个函数会分配side_effect的参数和返回值。

3fc3fb37fb83acd8d9fdbe607c60a69a.png

首先,创建.log_request(),它接收一个URL,使用print()打印输出,然后返回一个mock的响应。之后,使用.side_effect为.get()设置.log_request(),在调用get_holidays()时会调用这个方法。当你开始运行测试时,可以看到.get()将自己的参数转发给.log_request()然后接受返回值并返回:

98c046bcad1c10b1413d97004ab2311d.png

很棒,print()语句打印了正确的值。而且get_holidays()也返回了假字典。

.side_effect还可以迭代。迭代的话必须包含返回值,异常或者两者都有。每次调用mock的方法时,这个迭代会产生下一个值。比如,你可以在Timeout之后重试并返回一个成功的响应。

cc38ffe56783c9ba8aee758107e35456.png

第一次调用get_holidays(),.get()会引发一个Timeout异常。第二次则会返回一个有效的假字典。这些侧面影响会匹配传递给.side_effect的列表的顺序。

你可以直接在Mock对象上设置.return_value和.side_effect。但是Python mock对象希望能灵活的创建其属性,所以还有很好的方式配置这些以及其他设置。

配置你的Mock

你可以配置Mock来控制对象的某些行为。Mock中可配置的成员包括.side_effect,.return_value以及.name。当你创建或者使用.configure_mock()时会配置Mock。

你可以初始化是通过指定某些属性来配置Mock:

8d4c569899e3b0052f40d18efd55f7b6.png

虽然可以在Mock实例上设置 .side_effect和.return_value,但是像.name等其他属性只能通过.__init__()或者.configure_mock()来设置。如果你尝试在Mock实例上设置.name,你得到结果会不一样:

b36587e92b2ce1f04dc9b84951515824.png.name是要使用的对象的公共属性。因此,Mock不允许你以与 .side_effect或.return_value相同的方式来设置这个值。如果你访问mock.name,将会创建一个.name属性而不是配置mock对象。

你可以使用.configure_mock()配置一个已有的Mock实例:

32c55888aec8680601c18dda97f9fc26.png

通过将字典解包到.configure_mock()或Mock.__init__(),你甚至可以配置Python mock对象的属性。使用Mock配置,可以简化之前的例子:e40311fdcc36475896c144708ad794d6.png

现在,你可以创建和配置Python mock对象了。你还可以使用mock控制你的应用程序的行为。目前,你已经使用mock作为函数的参数或者在与测试中相同的模块中匹配对象。

接下来,你将学习如何将mock对象替换为其他模块中的实际对象。

patch()

unittest.mock为mock对象提供了一种强大的名为patch()的机制,它可以在给定模块中查找对象并用Mock替换该对象。

通常,使用patch()作为装饰器或者上下文管理器来提供一个mock目标对象的范围。

patch()作为装饰器

如果你要在整个测试函数的持续时间内使用mock对象,可以使用patch()作为装饰器。

将逻辑代码和测试放入单独的文件重新组织my_calendar.py文件,可以看到其工作原理:

bf5a4cd8d4c338d424dbe87119301d6e.png

这些函数现在都在自己的文件中,与测试区分开来。接下来,将在tests.py文件中重新创建测试。

到目前为止,你已经在对象各自所在的文件中打了"猴子补丁"。猴子补丁在运行时将一个对象替换为另一个对象。现在,将使用patch()替换my_calendar.py中的对象:

05564fd62ef2854a4e1113345b31775d.png

最初,你在本地范围内创建了Mock对象并对requests打了补丁。现在你需要从tests.py中访问my_calendar.p中的requests库。

对于这种情况,使用了patch()作为装饰器并传递目标对象的路径。目标路径为'my_calendar.requests',它由模块名称和对象组成。

你还为测试函数定义了一个新的参数,patch()使用这个参数将mock对象传递到测试中。这样可以根据断言修改mock或者断言。

你可以执行这个测试模块确保符合预期:

948a7605a97ebba38bf6d4bc93f1e7d0.png

技术细节:patch()会返回一个继承自Mock的MagicMock的实例。MagicMock对你很有帮助,因为它使用合理的默认值实现了大量的魔术方法,诸如.__len__(), .__str__(), 及 .__iter__()。

使用patch()作为装饰器在这个例子中可以正常运行。在某些情况下,使用patch()作为上下文管理器更具有可读性,更有效且更容易。

patch()作为上下文管理器

有时,你会希望使用patch()作为上下文管理器而不是装饰器。你更倾向于上下文管理器的原因可能包括:

    1. 你只想在测试的一部分范围内mock一个对象

    2. 你已经使用了太多的装饰器或参数,降低了测试的可读性

想要patch()作为上下文管理器,你可以使用with语句:

e14ce98b80796a5f0fdb8db0f707f6f2.png

当你退出with语句时,patch()将mock对象再替换回原始对象。

现在,你都是mock的完整的对象,但是有时候你可能仅仅希望mock对象的一部分。

为对象的属性打补丁

假设你只希望mock一个对象的方法而不是整个对象。你可以使用patch.object()来完成此操作。

比如,对象方法.test_get_holidays_timeout()实际上只是需要mock一个requests.get()并将.side_effect设置为Timeout。

56caddc9c4945ef9a0b631410af2b5be.png

这个例子中,你只mock了get()方法而不是整个requests包,其他的属性还是与原来相同。

object()采用和patch()相同的配置参数。但是,将目标本身作为第一个参数传递而不是传递目标的路径。第二个参数是你尝试mock的目标对象的属性。你还可以使用object()作为类似于patch()的上下文管理器。

进一步阅读:除了对象和属性,你还可以使用patch.dict()来将patch()作用于字典。

学习如何使用patch()对于mock其他模块中的对象非常重要。然而,有时候需要mock的目标对象的路径并不明显。

在哪里打补丁

有一个很重要的点是告知patch()去哪里查找你需要mock的对象,因为如果你选择了一个错误的目标位置,那么patch()的结果可能出乎你的预料。

假设你在my_calendar.py使用patch()mock函数is_weekday():

9dc3f69eca5bf8c85da68e977527f7a1.png

首先导入my_calendar.py,然后用Mock替换is_weekday()。很好,运行符合预期。

现在,我们稍微改变一下这个例子,并直接引用函数:

4338c8050d82e99891318a2a69d89b9c.png

注意:根据你阅读本教程的日期,你从控制台看到的输出可能是Trus或False。重要的是输出与之前的Mock不同。

请注意,即使传递给patch()的目标位置没有更改,调用is_weekday()的结果也不同,这是由于导入函数的方式不同。

from my_calendar import is_weekday意思是将实际函数引入到本地文件范围。因此,即使你稍后使用patch()操作了该函数,之后也会忽略已有的mock,因为你已经持有对未mock函数的本地引用。

使用patch()的 好的法则是查找这个对象。

在第一个例子中,mock'my_calendar.is_weekday()'是生效的,因为你是在模块中查找该函数。第二个例子中,你持有对is_weekday()的本地引用,由于是在本地范围内找到了函数,所以mock的是本地的函数:

4e3812fd9c082f2ddd540b66ec943fa2.png

现在,你已经深刻领略了patch()的强大之处。并且知道如何对对象和属性使用patch()并对为其打补丁。

接下来,你将看到mock对象的一些常规问题以及unittest.mock提供的解决方案。

英文原文:https://realpython.com/python-mock-library/

  译者:敦伟

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值