We're sunsetting PodQuest on 2025-07-28. Thank you for your support!
Export Podcast Subscriptions
cover of episode Mocking in Python with unittest.mock  -  Michael Foord

Mocking in Python with unittest.mock - Michael Foord

2025/2/7
logo of podcast Test & Code

Test & Code

AI Deep Dive AI Chapters Transcript
People
B
Brian Okken
Topics
Brian Okken: 我对 Michael Foord 的生平和贡献进行了介绍,他是一位 Python 核心开发者,对 Python 社区做出了巨大贡献,尤其是在单元测试方面。他创建了 unittest.mock 库,并对 Python 的测试工具的构建做出了重要贡献。 Michael Foord: 我目前从事 Python 培训和承包工作,之前在 Red Hat 工作过,参与 Ansible Tower 的测试自动化。我参与过英国原子能机构的项目,为核聚变反应堆设计软件。我编写了 unittest.mock 库,它源于我在 Resolver Systems 的工作经历,并受到测试驱动开发的影响。我严格遵循测试驱动开发,测试与代码比例至少为 3:1,但过度的单元测试会带来问题。单元测试应该测试行为单元,而不是实现单元;应该通过公共 API 进行测试。mock 的使用存在误解,Martin Fowler 对 mock 的定义过于严格。patch 和 mock 的结合使得测试难以测试的代码成为可能,但这可能会导致编写易于测试的代码的动力不足。patch 的强大之处在于它能够自动恢复被修改的状态。mock 对象可以伪装成任何其他对象。我更倾向于 AAA(Arrange, Act, Assert)风格的模拟,先设置,再执行,最后断言。模拟的经典用途是模拟外部依赖项,以在单元测试中返回确定性结果。unittest.mock 库最初是为了替换 Resolver Systems 代码库中大量的模拟对象而创建的,后来又增加了 API 限制功能。每次使用 patch 都是一种失败的承认,应该尽量减少 mock 和 patch 的使用。mock 应该作为最后手段,应该努力设计易于测试的系统。清晰的层次设计有助于减少对 mock 的依赖。测试策略会影响设计,应该尽量将副作用放入单独的函数中,以便更容易测试。在 Python 测试培训中,mock 只是其中一部分内容,我更推荐使用 Pytest 而不是 unittest。我通常直接使用 unittest.mock,而不是 Pytest 的 mock 插件。Pytest 的 fixture 机制虽然强大,但过度使用会造成“fixture hell”。任何框架或策略都不能阻止编写糟糕的代码。unittest 的缺点在于样板代码过多,以及缺乏 Pytest 的 fixture 机制等特性。我写了一篇关于软件开发和测试最佳实践的文章,可以在 opensource.com 上找到。

Deep Dive

Chapters
This episode is a tribute to Michael Foord, a key figure in the Python community and creator of the unittest.mock library. The interview, originally recorded in 2021, covers his career, contributions to Python testing, and his philosophy on mocking.
  • Episode is a replay of a 2021 interview with Michael Foord.
  • Michael Foord was a pivotal figure in the Python community.
  • He created the mock library (now unittest.mock).

Shownotes Transcript

本集重播了我 2021 年对 Michael Ford 的采访,主题是单元测试中包含的模拟库。Michael 是一位 Python 培训师和承包商。他专门教授 Python 和系统的端到端自动化测试。他热衷于代码的简洁性和清晰性、高效轻量级的流程以及设计良好的系统。作为 Python 核心开发者,

他编写了 Unitest 的一部分,并创建了模拟库,该库后来成为 Unitest.mock。我刚从他在 Agile abstractions 上的自我介绍中读到这些。我们在一月份失去了 Michael,我很高兴能采访他。获得这次采访也不容易。

我从 2012 年开始写博客,不久之后就看到 Michael Ford 的名字在测试方面,尤其是在 Python 和测试、软件测试方面到处出现。我在 2016 年邀请他来节目中谈论单元测试,但当时他正在研究 Go,他建议我改和 Robert Collins 谈谈,我照做了,那是第 19 集。

但我仍然想和 Michael 谈谈。我在 2018 年第一次见到他本人。我在走廊里看到他,他穿着黑色外套和黑色帽子。我想他还戴着护目镜。所以我只是假设他是从蒸汽朋克会议上来的。他可能也参加了。我不记得我是否在 2018 年和他谈过话。但我确实在 2019 年有机会和他谈话。

他写了一篇关于软件实践的文章,我想采访他关于这篇文章的内容。我们试图在 2019 年的 PyCon 上当面进行采访,但音频效果不佳。我们同意重新安排时间。直到 2021 年我们才最终完成采访。到那时,我认为最好只是和他谈谈模拟。所以这就是这次采访的重点。我很感激我最终完成了对 Michael 的采访。

欢迎来到 Test & Code。

Python 中作为 unit test.mock 提供的模拟库始于 Michael Ford。在本集中,Michael 和我讨论了模拟,当然还有测试理念、单元测试以及什么是单元,以及 TDD,甚至 Michael 的毛巾在哪里以及是什么颜色。Michael 对构建 Python 测试工具起到了重要作用,他有很多很好的建议。

关于测试的建议。我希望您喜欢这一集。

问题是,单 O 更加常见。由于某种原因,人们似乎认为它是荷兰语。不是的。它只是英语。在过去我们使用电话簿的时候,每个电话簿里总有一些。所以,你知道,不算常见,也不算罕见。每当我看到你的姓氏时,我不知道它是否拼写相同,但我想到福特·普雷费克特。但他用一个 O 还是两个 O 拼写它?他用一个 O 拼写它。英语中还有一个 Hoopy Frood。

道格拉斯·亚当斯的作品,我一直觉得与这些作品有一种亲和力,既是因为福特·普雷费克特,也是因为我是知道自己毛巾在哪里的人,你带毛巾了吗?我没有带,但我知道它在哪里。是的,在哪里?是的,它是紫色的,在楼上。它是一种超细纤维毛巾,所以你可以把它卷得很小方便旅行,很棒,完美。

所以其中一件事情,好吧,我们最终想谈谈模拟,但你现在在做什么?你有一家咨询公司之类的吗?是的,承包和培训。培训。所以大约 10 年前,我开始与 David Beasley 一起进行 Python 培训,教授他的实用 Python 和高级 Python 精通课程以及 Python。

我的日常工作,你知道,我一年只做几次,我真的很喜欢做,也很享受。这是一个不错的行业。然后我在 Red Hat 工作。我最近的职业经历是,我最后一份工作是在 Red Hat 的 Ansible Tower,这是他们用于管理 Ansible 和使用 Ansible 管理计算机基础设施的企业 Web 应用程序。我在那里的测试自动化团队工作,帮助他们构建 Ansible。

我在那里工作了一年,我觉得我真的很想自己创业。是时候这么做了。所以我做了一半的承包和一半的培训,我真的很享受。我仍然教授 David Beasley 的课程。我还教授了一些 Flask 和一些测试以及其他一切。

面向对象理论,为皇家测量局,使用 Python 的面向对象理论。他们不只是想要一个 Python 课程。他们想要一些更高级的东西,因为他们是皇家测量局。所以我用 Python 讲解了面向对象理论。那太有趣了。承包方面,我最有趣的项目是为英国原子能管理局做的。那是从去年一月到六月。那是为设计聚变反应堆的软件工作。哦,哇。

所以通常情况下,这就像,这就像梦想中的工作。他实际上是一位博士,这是他的博士项目。所以,但在某种程度上,在他与英国接受这份工作之前,他已经承诺将其开源。所以他们将把它全部开源。它是蓝图。我和他一起把它变成了一个工程项目。他有一个,嗯,在某个时候,我的意思是,他是一个非常聪明的人。我的意思是,我实际上是在从元类中删除描述符,

不,从描述符中删除元类。你知道,这根本不需要存在。但是,你必须非常聪明才能制造出这么大的混乱。

那太有趣了。是的。所以聚变反应堆设计的常规方法,学术界,他们都是孤立的。每个人都有自己的专业领域,但需要完成一系列的事情。你知道,他们想计算出磁约束、等离子体约束室。所以他们有位置,他们有整体尺寸、功率要求、磁铁的位置。他们现在正在做的非常酷的事情是增殖毯,它

基本上,聚变反应堆,你输入氢,用激光点燃它,产生足够的热量和压力来引发聚变。然后使用磁铁,你通常在环形体中塑造和约束等离子体。

然后这会产生一股中子,撞击反应器侧面内衬的大金属吸收块,将其转化为热量。从那里开始,就是非常成熟的涡轮技术。他们所做的是,他们发现如果他们将锂放入这些毯子中,中子流轰击锂时,有时会击中锂,分裂成两个氚原子。

原子分裂成氚,这是反应堆的燃料,所以你需要足够的氚,氚的生产非常昂贵,而且具有放射性或相当危险的氢同位素,一旦你有了点火,那么它在氚方面就是自给自足的,这是一个绝妙的创新,无论如何,你过去都是通过获取你的规格来设计这些东西,发送给第一个人来做第一部分设计,他需要一个月的时间

给你发送一堆数字,你再发送给下一个人。所以整个过程需要几个月。你不能真正地通过这种方式进行迭代过程来尝试一堆东西。而这个人,他的天才之处在于跨学科的方法。他基本上写了一个,呃,

一个应用程序可以执行所有这些不同的显示。它用于初步设计工作。这不是详细设计,但你现在可以用漂亮的 OpenGL 绘图进行初步设计,这些绘图非常漂亮,每个人都认为这是重点,但当然不是。它是它输出的所有数字。你可以在 40 分钟内完成。所以你可以进行迭代设计过程。所以它将彻底改变聚变反应堆的设计。这是一个参与其中的绝佳机会,非常有趣。太酷了。

嗯,你说那是开源的东西吗?嗯,我最近给他发邮件问他它是否已经开源了。他没有回复那部分。所以他们应该在现在之前就做到了。所以我认为答案是否定的,它被称为蓝图。有一些

他发布了一篇关于它的论文。是的。好吧,如果它在那里,我只是想放一个链接。是的,当它发布时,我会在 Twitter 上关注它,因为它玩起来很棒。你可以在你的客厅里设计聚变反应堆。我知道你有一个网站,agileabstractions.com。那是……

是的,那是我的专业网站。我认为我没有添加 2019 年和 2020 年以来我参与过的任何项目。所以它有点过时了。但那是我的专业网站。但如果有人想找你进行培训之类的,他们可以去那里,对吧?是的。哦,michael.python.org。好的。一个 python.org 邮箱,是的。那是……

即时的信誉。我得到了它,因为我帮助了很多网络。我是各种邮件列表、管理的网站管理员之一。嗯,是的,所以,我要求 Michael,没有人占用它,但这是一个漂亮的电子邮件地址。我为此感到非常自豪。是的。所以有一个 Python 提要的东西。我不记得它在哪里了。Python。你还记得那是什么吗?Python?再说一遍?我可以提要。你是说 Planet Python 吗?是的,Planet Python。

我只是记得这一点,因为我认为这是我第一次遇到你的名字,因为当我开始写关于 Python 的博客时,我听到有人说,你必须让你的博客在 Planet Python 上列出,否则人们不会关注它。然后我请求它,我认为你回复说,好的,它在那里。这是我负责的事情之一。那是我认为的 Python 社区的黄金时代,当时 Python……

即将随着网络革命和 Google 采用 Python 而爆炸式增长。在此之前,Python 主要只被爱好者使用,只被真正热爱这种语言的人使用,这使得一个充满热情的人们组成的美丽的社区真正渴望教你,你知道。然后

Python 突然爆炸了。所以在过去,将你的博客放在 Planet Python 上,你可以获得 1000 次博客文章浏览量。辉煌的日子。是的。现在,当我开始研究测试内容时,我再次遇到了你的名字。你有很多测试文章。但你的名字也与 unit test 中的模拟库相关联。

没错。没错。是的。我最初编写了现在称为 unit test.mock 的内容,并作为库维护了相当长一段时间。这源于我在伦敦 Resolver Systems 的第一个程序和演出。我从 2006 年开始在那里工作,他们都在做极限编程。所以都是结对编程、完全测试驱动开发、客户代表进行优先级排序、估计、跟踪、速度,

所有这些,我们严格地做了四年,这是一次令人惊叹的经历,是的,它来自于那个时期,特别是,我培养了对测试的热情,将其作为一种确保编程中产品质量的方法,而在此之前,我习惯使用的编程风格,知道事情是否有效是一个真正的挑战,你必须尝试一切,我们可以自动化很多这样的事情,是的,所以,所以是的,这就是我,我

我热爱 Python,在那段时间里,我对测试也充满了热情。那么,我想,从那时起,你仍然在所有开发过程中都包含测试吗?是的,我的意思是……

我们严格地进行了测试驱动开发。我们严格地进行了四年的极限编程。所以我们做的第一件事是编写一个功能测试,它会端到端地练习我们试图添加的功能或演示错误。然后我们开始编写单元测试,这些测试是分层的。我们的测试与代码比率至少为 3:1。而且

我们,你知道,我们在各种地方进行了过度测试。我们的测试与实现相耦合。所以我学到了很多关于测试和问题的知识。如果你过度测试,你耦合你的单元测试,你正在测试实现,所以你正在测试你的私有方法。然后当你进行重构并想要更改你的代码时,更改你调用代码的方式,你的测试将不再告诉你任何有用的信息。

它们坏了,因为即使你再次修复了你的代码,测试也不会向你显示正确的东西。它们正在测试旧代码。功能测试,你的端到端测试,如果你有良好的、合理的端到端覆盖率,即使只是一组冒烟测试,那么你可以进行重构

你可以有理由相信你没有破坏功能,因为随着重构,你的端到端测试不会改变。所以我认为端到端测试更有价值。我认为过度测试存在危险。我喜欢说单元测试是关于测试行为单元,而不是实现单元。通过公共 API 进行测试。如果你不能通过公共 API 进行测试,那么你的抽象就不正确,通常情况下。

这些事情可以帮助你避免过度测试。我不会测试脚本任务。向大型遗留项目添加测试也极具挑战性,尤其是在持续进行功能工作的情况下。这是你必须逐步和务实地努力融入的东西,因为企业,你知道,只有当企业继续运营时,你才能作为程序员获得报酬。是的。

所以你必须结合构建测试和测试来保持理智……

维护和扩展遗留项目的工作。我真的很喜欢这个。我要偷走这个。行为单元,而不是实现单元。我非常喜欢这个。因为这就是你想测试的,对吧?你想测试它是否做了正确的事情。你不想测试它是如何做的,因为理论上,至少,如果你可以改变方法,而你的测试仍然告诉你一些有用的东西,那就太好了。如果你只通过公共 API 进行测试……

只要你的公共 API 正确,你知道,通常你需要更改它。那么,你知道,你可以自由地更改内部结构,而你的测试仍然会告诉你一些有用的东西。是的。现在,当然,总有一些情况

你真的想在中间打败一些算法位。所以会有一些围绕它的测试。有一些错误你无法复制它,除非,你知道,它是一些时间问题或一些文件系统怪癖。复制它的最简单方法是将一些无效数据放入一些内部状态,因为你知道是什么触发了错误。而且,你知道,这种情况会发生。

所有概括都是错误的,包括这个。是的,好吧,我绝对必须将这个转录出来,因为这里有很多宝石。好的,模拟,你开始使用模拟,它自然地成为……的一部分,我想这取决于你做它的年份,极限编程和测试驱动开发。

但是模拟被很多人误解了,可能包括我自己。可能还有我。所以,部分原因是你的错。嗯哼,嗯哼。Alex Gayback 责怪我。好吧,我的意思是,如果人们开始研究它,他们会遇到诸如,好吧,这篇文章,我会说错的,叫做模拟不是存根。

我不记得了。啊,马丁·福勒,是的。马丁·福勒,是的。静态类型的,他定义了各种模拟类别和类型,使用模拟的方法,他将它们定义为不同的对象。与其定义它们,它们作为类别很好,但它们的定义相当严格。我认为根据他的定义,模拟是他所有类型的模拟,除了模拟。

好的,是的,我对此很好奇。所以我反对他的定义。好的,所以在 Python 中,我们使用 unit test 模拟库或围绕它的包装器。所以像 PyTest 模拟是围绕模拟对象的东西的包装器,以便你可以使用。它基本上是围绕它构建的上下文管理器,这很酷。你也可以直接将它们用作上下文管理器,这也很酷。我一开始不知道这一点。

但是是的,所以……这是一个有趣的观点。这实际上是模拟中的一项创新……

但这来自 Resolver Systems 的一个人,实际上不是我,尽管我获得了这个人的功劳,名叫 Tom,我忘了他的姓氏,哦,天哪,可怜的家伙,好人,他发现了这一点,所以补丁是一个装饰器,补丁修补了东西,所以你可以注入或模拟到任何,你知道,各种地方,Alex Gainer 责怪我,我认为你可能要说的原因,我会抢走你的雷声,我的意思是补丁使测试代码成为可能

之前实际上很难测试的代码。因此,模拟和补丁的组合使得测试非常难以测试的代码成为可能,这不会激励你编写易于测试的代码。

易于测试的代码通常是更好的代码。因此,补丁和模拟可以掩盖你正在编写糟糕代码的事实。这绝对是正确的。但创新之处在于补丁,补丁将模拟放到位,它所做的真正强大的事情是它撤消了它。它将事情恢复到之前的状态。修补东西很容易。取消修补它们很难。

Patch 中有很多逻辑知道如何做到这一点。因此,你可以将其用作上下文管理器,使用 Patch time.time as mock time。然后上下文管理器中的任何内容都会将 time.time 视为你的模拟对象,然后你可以配置返回值。

但在上下文管理器之外,time.time 将恢复为它通常的状态。因此,补丁的影响范围受上下文管理器限制,这就是上下文管理器擅长的地方,你知道,可见的影响范围。你也可以将其用作装饰器。

在装饰的函数的内部函数期间,补丁在那里,这实际上是在 Python 2.4 中最初完成的,在我们拥有上下文管理器之前,能够做到这一点,而且是 Tom 在 Resolver Systems 中意识到这一点,他意识到我们使用装饰器的方式与上下文管理器完全兼容,所以补丁可以同时做到这两点,补丁是一种有趣的野兽,实际上在上下文滑块中现在有

上下文装饰器,你可以编写也可以用作装饰器的上下文管理器,这源于在 Resolver Systems 的模拟中首先发生的事情,Python 2.5 的一些 Python 考古学,是的,所以如果人们到目前为止错过了它,你如何向甚至不知道它是什么的人描述模拟?

好的,这是,所以模拟对象是一个可以伪装成任何其他对象的物体,现在,马丁·福勒定义了关于模拟的内容以及当时常见的内容,所以他谈到了模拟和存根,我忘记了另一个,但模拟的要点是它记录你可以记录你的期望,然后重播它们,所以你

你创建一个模拟对象。你配置模拟对象并说我认为这是马丁·福勒的,以及当时的 Java 模拟库和 Python 模拟库。你创建一个模拟对象。你说,我希望你拥有这些方法,我希望调用此方法。它应该返回这个。然后我希望调用此方法。然后你做,你运行你的代码并点击重播,如果它以错误的方式使用,它就会抛出异常。

哦,好的。所以那是模拟的记录重播风格。对我来说,这把,你记录你对模拟的期望,然后你调用你的代码。那是我们关于面孔。我想做的是我想将模拟注入系统并

运行一些代码,然后我想能够断言它以正确的方式使用,因为发生在其身上的并非所有事情都可能相关,它可能只是一件特定的事情,我想断言你被调用了这个参数,我想断言模拟甚至可能只是为了返回预先准备的值而进入那里,来模拟你不想在单元测试中调用的系统函数,这是模拟的一个非常好的用途,模拟外部依赖项以返回确定性结果,以便进行测试,以避免在单元测试中进行外部调用,这是经典的,也是模拟的一个很好的用途,你可能会模拟 API 调用,模拟网络调用,模拟文件调用,模拟库中有一些特别针对文件的支持

是的,例如,我想到了一个例子,这可能不太常见,但如果我的测试,如果我有一个系统,我不知道,一个日志系统或其他什么,如果我发现一些关键的东西,它会给很多人发电子邮件。在测试期间,我不想实际给每个人发电子邮件,但我可以确保在测试期间调用了向合适的人发送电子邮件的适当调用。

使用正确的参数。是的,完全正确。这就是模拟的作用。为了完成之前的想法,所以与其说是记录重播风格,模拟是 AAA。它是什么?安排、行动、断言。你设置你的代码,你调用行动,这是安排,然后你调用你的代码,这是行动,然后你进行断言。所以 unit test.mock,它所做的是你可以配置它,以便方法将返回值或

或者方法调用将产生副作用,引发异常或调用另一个函数,或者你可以为多次调用提供一系列值。各种方法。你可以配置对象。它们可以伪装成任何对象。它们可以伪装成字典。你可以配置任何魔术方法或所有这些东西。你把它放在适当的位置,然后所有调用都会记录在上面。有一些方便的断言方法和方便的调用方法。所以

我创建它的原因有两个。首先,我们正在进行完全测试驱动的开发,我们正在创建所有这些小的存根对象,这些对象散布在我们的代码库中,带有模拟工作簿、模拟工作表、模拟单元格、模拟行、模拟列。总的来说,它加起来可能有数百行,甚至数千行代码。最初,我想,我可以替换所有……

所有这些都使用一个 Python 对象,该对象具有一个 done-to-get 属性,该属性响应每个属性查找。所以最初的实现大约有 30 行代码,它用于替换 Resolver 代码库中的所有这些模拟对象。但当时担任首席技术官的 Giles Thomas 的一个要求是,我们必须有一种限制 API 的方法。所以如果我们访问了一个不应该存在的属性,它仍然会引发属性错误。这就是模拟中所有规范内容的来源。

所以这就是它最初的来源。它也受到不希望任何现有模拟框架的愿望的驱动,所有这些都在 Python 中。它们都是这种记录替换风格,我不喜欢。而且 Python 中的 Python 测试社区特别紧密和有趣。我们在 PyCons 上获得了 Python 测试 BoF,它在很多年里都运行得很好。这是一个非常棒的社区。模拟的演变非常迅速。

与 Python 测试邮件列表中的用户以及他们的反馈以及与编写库的其他人的竞争。那时我充满了激情和活力。如果任何模拟库出现我认为好的功能,我几天后就会发布一个包含该功能的新版本。我回复了 Python 测试电子邮件列表上关于模拟的每一封电子邮件。

向他们展示如何使用我的库来做到这一点,你知道,我击败了竞争对手。好吧,这很有趣。我纯粹是出于热情。所以 Python 测试邮件列表仍然存在,但流量很少。所以很多人认为它可能已经死了,但我们仍然会,我的意思是,我仍然关注它,回复真的,如果你在那里提问,你会得到非常好的回复。太棒了。

非常好。现在,我可能后悔把它放在播客里,因为突然人们会开始使用它。开始使用 Python 测试邮件列表吧,朋友们。对。但是是的,我们有一些非常优秀、聪明的人关注它。好的,那么你的关系是什么?我认为这是有人进来并说,也许我应该在我的测试中使用模拟。

我们告诉那些以前没有使用过它的人什么?因为正如你所描述的那样,存在这种测试驱动开发模型,即,好的,有两种巨大的,经典的和模拟的,但显然我们正在谈论模拟 TDD,这意味着我们试图用周围的一切来隔离测试每个函数。对,对,对。然后的问题是你没有测试各个部分之间的连接。巨大的优势是

这样做的好处是你的测试运行得很快,但你可以在单个函数和方法级别拥有完全覆盖率,仍然不知道你的系统是否有效,因为你的组件之间的连接根本没有测试。在功能测试级别上花费相同的精力

会让你相信应用程序实际上有效。测试驱动开发的优势,以及它被称为测试驱动开发的原因,另一种说法是我经常谈论测试优先。但其思想是测试驱动设计。这就是我喜欢测试驱动开发的原因,这也是我从中获得的东西,即使我这些天并不总是严格遵守测试优先。这是思考设计的那一步。如果你先进行测试……

第一步不应该考虑“解决方案是什么?让我快速编写一些代码。”而是应该考虑“我该如何调用它?它应该如何工作?对我来说,最好的、正确的API是什么?”所以就是这样。它从一开始就融入其中。首先要思考的是,它应该是什么样子?因为否则默认情况下,你会快速编写一些代码。你认为它有效。我会添加一个测试。而你测试的是你碰巧想出的任何东西,而不是任何东西。

最好的方法。TDD 的其他方面,即尽可能简单的方案,逐步构建测试,是通过这样演进设计,只要你偿还重构的技术债务,将重构的成本纳入你的估算,你实际上就能想出更好的解决方案。

通过将你的增量方法建立在实际使用上。所以这些都是先进行测试的很好的理由。正如你所说,如果你先进行测试,如果你试图保持某种程度的隔离,你将需要一些模拟对象。但我认为问题是,

问题就变成了,我的测试理念是什么?我们提出的具体问题是,我该如何开始使用模拟对象?所以我认为我们可以更简单地回答这个问题。但我们可以说,unittest.mock 库中的两件事是 patch 装饰器、patch 上下文管理器和模拟类、模拟对象。实际上,大多数情况下,patch 可能会为你创建模拟对象。

所以首先你需要使用 patch。这经常会让人困惑。如果你想的话,我们可以稍微谈谈这个。但基本上,你说 with patch,然后是对象的地址。我将修补类上的一个方法。它是模块名.类.方法作为模拟对象。然后在上下文管理器内部,你可以配置模拟对象的值。我们可能想说 mock method.return_value = 3。

然后在我们执行代码后,我们只需说 mock object.assert_called_with 一个断言。它被调用了正确的参数。因此,使用 patch 来注入模拟对象并进行断言的基本知识非常简单。但是你应该在哪里这样做呢?一个显而易见的地方是用模拟对象替换文件访问。你可以。

如果你修补 back open,内置的 open,那么如果你有结果,你就会知道它被调用了,并且你的单元测试将节省时间。用确定性的东西修补对 time.time 的调用,用一些能返回确定性结果的东西。系统调用、网络调用、数据库查询,任何你想

返回预先确定的确定性结果,并且你可以避免真实的网络。如果你没有测试你的数据库访问,如果你对数据库访问感到满意,那么这可能在集成测试级别得到涵盖。在单元测试级别模拟它们。使你的测试更快,并且减少对底层模型的依赖。这种东西是模拟可以让你获胜的地方。是的,以及你系统的这些外部部分,实际上是正在测试的系统。我认为

人们很少谈论的一件事是测试架构通常模仿人员架构。有趣。我的意思是,假设我正在使用数据库,但我有一个其他团队正在使用的数据库层。我认为这将是……我不负责用户界面。我负责这一中间层的东西。然后完全合理……

我认为需要进行系统级测试,但作为一个团队,我将,我将喜欢保持,

馈送我的 API 并模拟我的依赖项或模拟我的依赖项。这个原则有时被表达为不要测试浏览器,这实际上只适用于 Web 应用程序开发。你测试你拥有的代码,而不是你没有拥有的代码,除非你必须这样做。对。然后另一个真正无关紧要的是你正在进行哪种风格的测试,无论你是在进行大量的测试驱动开发还是单元测试

微小的单元测试甚至功能测试,人们最终都需要模拟他们的外部内容,例如对外部服务的 API 调用。你不会喜欢我忘记他名字的那个例子,但 Harry Percival 提到的一个例子是信用卡处理。你绝对不会访问信用卡 API,除非有一个调试 API,但为什么不只是模拟它或存根它呢?

所以 Harry 提到的另一件事,我认为这是一个很酷的想法,是任何真正的第三方系统,而不是你公司内部的其他团队的系统,而是一个第三方,比如信用卡处理或其他你真正想尝试的系统

用你自己的对象或你自己的模块来快速使用他们的 API,以便你有一组有限的 API 函数或进入该服务的入口点。然后,然后模拟或存根这些函数的自然位置就是访问这些函数。所以,是的。如果你的 API 系统正在进行网络调用,那么你,

你可以通过模拟网络调用、对请求或其他任何内容的调用或对它们的客户端的调用来测试这个抽象层。因此你可以确定它有效,它正在做正确的事情。然后你有一个很好的抽象层,你可以在其中放置你的模拟对象并完全替换系统的这一部分。因此你可以分别测试这两个层,并确信它确实可以端到端工作。我知道正在测试的系统调用……

API 正确,我们已经提供了抽象层。我知道这会发出正确的 API 调用,因为我们也对此进行了测试。所以是的,我喜欢这种方法。我认为另一件很巧妙的事情,我想知道这是否是人们责备你设计不好的原因之一,那就是我实际上不必理解依赖注入就能使用模拟对象。哈哈哈

是的,这是另一个动机。在 Python 中,依赖注入通常归结为向你的函数签名添加额外的参数。我认为函数签名是你 API 的一部分,将它们弄乱作为管理依赖项的一种方式并不一定理想。我可能在这方面有所软化。我有时会这么说,我认为这是一件有用的话,那就是每次你使用 patch 时,这都是一次失败的承认。

你知道,模拟对象应该是你的最后手段。应该有可能测试你的系统和你的系统的各个部分。它应该是可测试的。

如果你必须替换实时系统中的一部分才能对其进行测试,那么你所说的就是我无法以不需要这样做的方式设计系统。因此,努力最大限度地减少模拟对象和 patch 的使用意味着我认为你会从中获得最大的价值。只有在模拟对象非常明显是最佳方法的情况下,而不是将其作为你首先使用的工具。因为有一个明确的……我的意思是,我已经编写了代码

无论它首先是执行此函数调用,然后执行该函数调用,然后执行另一个函数调用,然后返回结果,我都模拟了我的所有依赖项,我模拟了此函数调用和其他函数调用以及该函数调用,现在我正在测试我的模拟对象而不是我的代码,你知道,这太疯狂了,你知道,我在那里,这是你真正将你的测试与你的实现紧密耦合,然后你正在完全测试你的实现细节,这不是一个有用的测试,你知道,真的

所以让我们更进一步。所以假设我为了测试我的系统的信用卡处理部分,我一直在使用模拟对象或类似的东西。有什么替代方案?我该如何?我认为拥有清晰定义的层会有所帮助。所以你有一个单点。

我的意思是,我可能做的另一件事是通过配置文件来做这件事,你知道,或者有另一种机制在我在开发配置下运行时加载模拟机器的一部分。你知道,这就像,

我想能够保证当我运行我的测试时,我永远不会发送信用卡请求。所以我不希望将实时内容注入到实时系统中以确保它不会。我希望我建立的系统无法做到这一点。所以……

特别是对于信用卡或任何隐私或安全关键的内容,我会考虑使用可以为我做到这一点的机制。它可能会放置一个模拟 API,也许它会使用模拟对象,但你将有一个垫片层,你将有一些垫片机制。

好吧,这让我想到数据库内容。所以人们在测试中经常做的一件事是用内存数据库替换实时数据库或基于文件的数据库。对。完全正确。是的,是的。但是许多数据库(如 Postgres 和其他数据库)都有内存功能,因此你可以只使用实时数据库,如果需要的话,可以将其设置为内存数据库。是的。

有什么警告要给人们吗?我喜欢这样的想法,首先看看 patch,然后看看你的外部系统,就像你给出的列表一样,很好,网络、系统调用等等。

然后隔离这些最简单的方法,例如,确保这些方法不会遍布你的系统。我不会在我的每个文件中都进行请求调用,在我的层中完成,这更容易存根和测试,并且让你的调用通过它。这是……所以,在这里你的测试策略开始影响你的设计。我认为这是一个好方法。是的。

你知道,这就像将副作用放入一个单独的函数中。所以尽可能地,你的函数是纯函数,然后这些函数很容易测试。然后很容易存根、模拟具有副作用的代码位。这是相同的概念。如果文件写入发生在其自身的函数中,那么其余函数就更容易测试。

这种事情,是的。当你教人们的时候,我在你的培训网站上看到你确实教授测试培训课程。其中有多少是关于模拟的,或者这只是你教人们的一小部分?这是一部分。这有点取决于客户及其优先事项。我维护了标准库中的单元测试相当长一段时间。这是我参与测试的另一个方面。我添加了测试发现。我帮助将其分解成一个包。

我认为 Benjamin Peterson 实际上完成了这项工作,但这很有争议。所以我以维护者的身份以我的名义提交了它,并且它成功了。但即便如此,我仍然推荐将 PyTest 用于新项目,并且我将 PyTest 用于新项目。而且,你知道,我会教人们使用单元测试,而且在我通常教授的关于 Python 的课程中,通常有一节关于模拟的课程。但总的来说,当我开始一个新项目时,我转向的是 PyTest,我教的是 PyTest。所以你使用……

如果你使用 PyTest,你会使用 PyTest 模拟插件还是直接使用单元测试模拟?

我倾向于直接使用单元测试模拟。这通常是可能的,因为我将模拟作为单独的一节课来教授,你知道,因为,你知道,你可以花一两天时间来模拟,这取决于你想学习多少 API 以及你想涵盖多少不同的场景。而且,你知道,我非常喜欢 unittest.mock,但我不知道。我不确定这是否是使用我的测试的最佳实践。好的。

所以当我伸手去拿它时,我通常使用 pytest 模拟插件,但它有这个你可以调用的 mocker 对象等等,但无论如何,它是一个夹具,对,它是一个夹具,我对 pytest 的一个问题是它很容易陷入夹具地狱,我甚至在一个公司工作过,他们有

通过夹具提供内容的文化或约定,因此你将使用夹具而不是导入

因此你会查看你的代码,你的 ID 现在不再知道事情来自哪里或在哪里。他们有夹具获取夹具,返回夹具,你试图弄清楚事情发生在哪里。你正在跟踪这个夹具图,你已经达到了第八级。你就像,为什么?为什么?你知道,我的生活已经堕入地狱。所以,所以图片很棒。

对于,你知道,限制内容的范围。我喜欢这个范围,你知道,但这就像你谨慎地使用它们。你知道,就像导入一样很棒。使用导入,而不是夹具。好吧,我会在这点上不同意你。夹具也很好。我只是陷入了夹具地狱。但也要提醒一下,没有任何框架或策略可以阻止你编写非常糟糕的代码。

哦,是的,你太对了。也许是 Scala 或 Haskell。

好吧,所以你暗示的一件事,我只是想把它插入到这里,将它依赖注入到对话中,那就是使用 PyTest,你可以将夹具放在你的测试文件中或 conf test 文件中,并且你可以为你的测试结构中的每个目录创建一个 conf test 文件。哦,那是另一件事,不是吗?这就像我正在使用的这个夹具,它到底来自哪里?是的,你可以有……然后你像……一样散布在你的代码库中

你的夹具可以依赖于其父层次结构中任何其他夹具。但我建议项目中的人们在顶部有一个 conf test 文件。

我喜欢这个功能。这就像如果我在项目的根目录中放置一个 comf.py,PyTest 现在就知道我的项目的根目录是什么,并且许多其他事情都可以正常工作。这是 PyTest 的优点之一。这也有助于人们,如果他们知道我的夹具不在我正在查看的文件中,它就在这里。这有助于你的整个项目。是的,PyTest 非常灵活。但是……

当我开始查看所有这些内容时,我确实查看了单元测试,并且我使用过一些单元测试。实际上,我对许多人对单元测试的抱怨感到恼火。因为人们说的是它有太多的样板代码。通过向单元测试添加测试发现,你不需要在你的文件中执行 name=main 的操作。我认为没有很多样板代码,但我

所以,是的,我的意思是,如果你使用单元测试,你过去必须自己编写测试集合和测试运行。有一个测试结果。有测试收集器。有一整套机制。而且真的……

测试发现是将其提升到项目可使用水平的最低限度。我听到的许多批评来自……它是 JUnit 版本 5 或其他版本的端口,JUnit 发生了巨大变化,而 UnitTest 没有。所以我们有非 PEP8 风格的命名,而 PyTest 将测试作为函数,而对于 UnitTest,你继承自 test case,并且你编写测试方法,并且

你使用 assert 方法而不是 assert 语句。老实说,这就是人们通常所说的样板代码。这就像,是的,我喜欢为 PyTest 编写测试函数,但我也不介意将测试分组到类中。

你知道,所以我并不介意将测试分组到类中,我不介意 assert 方法,多年来我一直怀疑 pytest assert 魔术重写,因为如果你得到,你知道运行测试室的代码,他们神奇地重写了字节码,所以他们可以告诉所有点的中间值,所以你可以告诉你表达式中失败的变量的值,这太棒了,但这同时也意味着你

你知道,你依赖于他们已经做对了。但是,是的,几年后,我相信他们已经做对了,足以让我放弃这一点。使用裸 assert 语句对于测试来说很棒。是的。但是是的,单元测试很好。你知道,它运行良好。这是一种每个人都习惯的常见测试风格。实际上,样板代码归结为,好吧,你需要继承自 test case,并且你调用 assert 方法来进行断言。但是,你知道,不是很好。

主要的是它没有 PyTest 夹具。对,它没有夹具、参数化测试。我们在单元测试中具有子测试,这是一个很好的补充,并非每个人都熟悉。所以这是一个上下文管理器,它允许你进行一堆测试

在一个子测试中,每个子测试都可以有单独的名称,并告诉你哪个条件在哪个参数下失败了。所以是的,这在单元测试中很好,但是有很多东西。PyTest 插件非常容易编写。我为一家名为 GuRock 的公司做了一些外包工作,他们有一个名为 TestRail 的产品。我编写了一个 PyTest 插件,它为他们报告,它报告 PyTest 测试结果并在 TestRail 中记录它们。那是……

比我预期的要容易得多。这很棒,你知道,单元测试更难扩展。并且真正使用继承作为扩展机制的结果,对于框架来说,提供一些东西来进行子类化是很常见的,但是像测试结果一样,如果你想要一个输出 XML、JUnit XML 的测试结果,然后你想要另一个,其他人已经编写了另一个测试结果子类,它

按时间或其他方式对你的测试进行排序,你必须在单元测试中使用一个或另一个。你实际上无法组合扩展。所以 PyTest 使用插件点的方式来处理插件,我实际上有一个单元测试版本可以做到这一点,或者单元测试 2,但我最终没有将其合并回单元测试。它变成了 nose 2,一段时间以来很流行。但我认为那是 Jason Pellegrin。是他的名字吗?是他的项目吗?是的,我认为是的。

这是一个一人项目,不幸的是,他无法继续进行,这很可惜。但这使得单元测试的行为非常相似。但我认为 PyTest 使用插件的方式是正确的。关于继承作为框架的扩展机制的一个有趣的观点是,它缺乏灵活性。一开始更容易理解,但从长远来看,钩子函数确实使它更容易。

带有事件的钩子函数,我认为这可能是我们越来越熟悉的东西,因为我们越来越多的人正在进行异步编程。这是一种不同的机制,一种不同的模式。好吧,迈克尔,我和你一起谈论测试真是太开心了。我需要结束了,但对任何人有什么行动号召吗?是的。

行动号召。你知道,或者任何东西,放下你的,我们已经谈到了你的敏捷抽象网站。是的。给无家可归的人。但我还可以说的是,我确实写了一篇文章,它发表在 opensource.com 上,我会给你提供 URL。你也许可以把它放在节目说明中。我认为这类似于每个开发人员都希望他们不必以艰难的方式学习的 30 件事情。

这是一些关于测试和开发我多年经验的简洁智慧。关于测试,它非常宗教化和教条化,但有很多非常好的观点,我们已经讨论过其中一些,而我们还没有讨论过一些。所以我会给你发送那个 URL。它发表在 opensource.com 上,我认为这是一篇值得阅读的文章。我喜欢那篇文章。是的,它很棒。它是我对人们的一些体面实践的巨大脑力激荡。很好。非常感谢你的时间,我们将保持联系。

谢谢,布莱恩。我也非常喜欢。感谢收听。感谢所有通过购买课程来支持节目的所有人,包括 Hello PyTest(学习 PyTest 的最快新方法)和完整的 PyTest 课程(如果你想真正成为 PyTest 专家)。两者都可以在 courses.pythontest.com 上获得。在那里你也可以加入 Python 测试社区。现在就到这里。现在出去测试一些东西吧。