def preamble () -> str : return "The sum is "
呼叫模块 (mod2.py) import mod1 def summer ( x : int , y : int ) -> str : return mod1 . preamble () + f " { x + y } "
summer() 函数计算其参数的总和, 并返回一个带有前导码和总和的字符串 示例 12-3 是一个非常小的 pytest 脚本 以验证 summer() Pytest 脚本test_summer1.py import mod2 def test_summer (): assert "The sum is 11" == mod2 . summer ( 5 , 6 )
示例 12-4 成功运行它运行 Pytest 脚本$ pytest -q test_summer1.py. [100%]1 passed in 0.04s
(-q 静默地运行测试, 没有很多额外的打印细节 好的,它通过了 但是 summer() 函数从序言中得到了一些文本 功能 如果我们只想测试添加是否成功怎么办?我们可以编写一个新函数,它只是 返回两个数字的字符串化总和, 然后重写 summer() 以返回此 附加到前导码 () 字符串或者,我们可以模拟 preamble() 来消除它的效果, 如示例12-5中的多种方式所示Pytest with a mock (test_summer2.py) from unittest import mock import mod1 import mod2 def test_summer_a (): with mock . patch ( "mod1.preamble" , return_value = "" ): assert "11" == mod2 . summer ( 5 , 6 ) def test_summer_b (): with mock . patch ( "mod1.preamble" ) as mock_preamble : mock_preamble . return_value = "" assert "11" == mod2 . summer ( 5 , 6 ) @mock . patch ( "mod1.preamble" , return_value = "" ) def test_summer_c ( mock_preamble ): assert "11" == mod2 . summer ( 5 , 6 ) @mock . patch ( "mod1.preamble" ) def test_caller_d ( mock_preamble ): mock_preamble . return_value = "" assert "11" == mod2 . summer ( 5 , 6 )
这些测试表明,可以通过多种方式创建模拟 函数 test_caller_a() 使用 mock.patch() 作为 一个 Python (with 语句) 其论点是:“mod1.preamble” : 的完整字符串名称 模块 mod1 中的前导码() 函数return_value=“” 使此模拟版本返回一个空字符串函数 test_caller_b() 几乎相同, 但添加为mock_preamble以在下一行使用模拟对象函数 test_caller_c() 定义模拟 一个 Python 模拟的对象作为参数传递给 test_caller2() 函数 test_caller_d() 类似于 test_caller_b() , 在对 mock_preamble 的单独调用中设置return_value在每种情况下,要模拟的事物的字符串名称 必须与正在测试的代码中的调用方式匹配 (在本例中为 summer() ) 模拟库将此字符串名称转换为变量 这将截获对原始变量的任何引用 用这个名字 (请记住,在 Python 中,变量只是对 真实的对象因此,当运行下面的示例 12-6 时, 在所有四个 summer() 测试函数中, 当夏天(5,6)被称为变身者时 模拟前导码()被调用而不是真正的前言() 模拟版本会删除该字符串,因此测试可以 确保 summer() 返回 其两个参数的总和运行模拟的 Pytest$ pytest -q test_summer2.py.... [100%]4 passed in 0.13s
注意为简单起见,这是一个人为的案例 模拟可能相当复杂; 有关清晰示例,请参阅文章, 以及官方以获取令人痛心的细节测试替身和假货要执行该模拟,您需要知道 summer() 函数导入了 来自模块 mod1 的函数前导码 () . 这是一个结构测试, 需要了解特定的变量和模块名称有没有办法执行行为 测试不 需要这个吗?一种方法是定义: 在测试中执行我们想要的操作的单独代码 (在这种情况下,使 preamble() 返回一个空字符串) 一种方法是导入 我将首先将其应用于此示例, 在将其用于层中的单元测试之前 接下来的三个部分首先,重新定义示例 2-12 中的 如果单元测试,mod2.py 导入双精度 import os if os . get_env ( "UNIT_TEST" ): import fake_mod1 as mod1 else : import mod1 def summer ( x : int , y : int ) -> str : return mod1 . preamble () + f " { x + y } "
示例 12-8 定义了双模块双fake_mod1.py def preamble () -> str : return ""
示例 12-9 是测试测试脚本test_summer_fake.py import os os . environ [ "UNIT_TEST" ] = "true" import mod2 def test_summer_fake (): assert "11" == mod2 . summer ( 5 , 6 )
....示例 12-10 运行运行新的单元测试$ pytest -q test_summer_fake.py. [100%]1 passed in 0.04s
此导入切换方法确实需要添加检查 对于环境变量, 但避免了为函数调用编写特定的模拟 你可以成为你喜欢的法官 在接下来的几节中,我将使用导入方法, 它与包装配合得很好 我一直在定义代码层时使用总结:这些示例将 preamble() 替换为 测试脚本,或导入分身 还有其他方法可以隔离正在测试的代码, 但这些工作, 并且不像谷歌可能为您找到的其他方法那样棘手蹼此层实现站点的 API 理想情况下,每个路径函数(端点) 应该至少有一个测试 - 如果函数可能以多种方式失败,则可能更多 在 Web 层,您通常希望查看 端点存在, 使用正确的参数, 并返回正确的状态代码和数据注意这些是浅层 API 测试, 仅在 Web 层内进行测试 因此,服务层调用 (这反过来会调用数据层和数据库) 需要与任何其他呼叫一起被拦截 退出 Web 图层使用上一节的导入思路, 我将使用环境变量CRYPTID_UNIT_TEST 将包导入为服务, 而不是真正的服务. 这将阻止 Web 函数调用服务函数, 而是将它们短路到(双打)版本 然后,较低的数据层和数据库也不涉及 我们得到了我们想要的:单元测试 例 12-11 具有修改后的 文件修改后的网/生物.py import os from fastapi import APIRouter , HTTPException from model.creature import Creature if os . getenv ( "CRYPTID_UNIT_TEST" ): from fake import creature as service else : from service import creature as service from error import Missing , Duplicate router = APIRouter ( prefix = "/creature" ) @router . get ( "/" ) def get_all () -> list [ Creature ]: return service . get_all () @router . get ( "/ {name} " ) def get_one ( name ) -> Creature : try : return service . get_one ( name ) except Missing as exc : raise HTTPException ( status_code = 404 , detail = exc . msg ) @router . post ( "/" , status_code = 201 ) def create ( creature : Creature ) -> Creature : try : return service . create ( creature ) except Duplicate as exc : raise HTTPException ( status_code = 409 , detail = exc . msg ) @router . patch ( "/" ) def modify ( name : str , creature : Creature ) -> Creature : try : return service . modify ( name , creature ) except Missing as exc : raise HTTPException ( status_code = 404 , detail = exc . msg ) @router . delete ( "/ {name} " ) def delete ( name : str ) -> None : try : return service . delete ( name ) except Missing as exc : raise HTTPException ( status_code = 404 , detail = exc . msg )
示例 12-12 包含使用两个 Pytest 的测试:sample() : 一个新的生物对象fakes():“现有”生物的列表假货是从较低级别的模块获得的 通过设置环境变量CRYPTID_UNIT_TEST , 例 12-11 中的 Web 模块 导入虚假服务版本 (提供虚假数据而不是调用数据库) 而不是 真正的一个 这隔离了测试,这就是重点使用夹具对生物进行 Web 单元测试 from fastapi import HTTPException import pytest import os os . environ [ "CRYPTID_UNIT_TEST" ] = "true" from model.creature import Creature from web import creature from error import Missing , Duplicate @pytest . fixture def sample () -> Creature : return Creature ( name = "dragon" , description = "Wings! Fire! Aieee!" , location = "worldwide" ) @pytest . fixture def fakes () -> list [ Creature ]: return creature . get_all () def assert_duplicate ( exc ): assert exc . value . status_code == 404 assert "Duplicate" in exc . value . msg def assert_missing ( exc ): assert exc . value . status_code == 404 assert "Missing" in exc . value . msg def test_create ( sample ): assert creature . create ( sample ) == sample def test_create_duplicate ( fakes ): with pytest . raises ( HTTPException ) as exc : resp = creature . create ( fakes [ 0 ]) assert_duplicate ( exc ) def test_get_one ( fakes ): assert creature . get_one ( fakes [ 0 ] . name ) == fakes [ 0 ] def test_get_one_missing (): with pytest . raises ( HTTPException ) as exc : resp = creature . get_one ( "bobcat" ) assert_missing ( exc ) def test_modify ( fakes ): assert creature . modify ( fakes [ 0 ] . name , fakes [ 0 ]) == fakes [ 0 ] def test_modify_missing ( sample ): with pytest . raises ( HTTPException ) as exc : resp = creature . modify ( sample . name , sample ) assert_missing ( exc ) def test_delete ( fakes ): assert creature . delete ( fakes [ 0 ] . name ) is None def test_delete_missing ( sample ): with pytest . raises ( HTTPException ) as exc : resp = creature . delete ( "emu" ) assert_missing ( exc )
服务在某种程度上,这是重要的一层, 并且可以连接到不同的 Web 和数据层 例 12-13 与例 12-11 非常相似, 主要区别在于进口和使用 的较低级别数据模块 它也不会捕获可能出现的任何异常 在数据层, 将它们留给 Web 层处理修改的服务/生物.py import os from model.creature import Creature if os . getenv ( "CRYPTID_UNIT_TEST" ): from fake import creature as data else : from data import creature as data def get_all () -> list [ Creature ]: return data . get_all () def get_one ( name ) -> Creature : return data . get_one ( name ) def create ( creature : Creature ) -> Creature : return data . create ( creature ) def modify ( name : str , creature : Creature ) -> Creature : return data . modify ( name , creature ) def delete ( name : str ) -> None : return data . delete ( name )
示例 12-14 具有相应的单元测试测试/单元/服务/test_creature.py中的服务测试 import os os . environ [ "CRYPTID_UNIT_TEST" ] = "true" import pytest from model.creature import Creature from error import Missing , Duplicate @pytest . fixture def sample () -> Creature : return Creature ( name = "yeti" , description = "Abominable Snowman" , location = "Himalayas" ) def test_create ( sample ): resp = data . create ( sample ) assert resp == sample def test_create_duplicate ( data ): resp = data . create ( data ) assert resp == data with pytest . raises ( Duplicate ): resp = service . create ( data ) def test_get_exists ( data ): resp = data . create ( data ) assert resp == data resp = data . get_one ( data . name ) assert resp == data def test_get_missing (): with pytest . raises ( Missing ): resp = data . get_one ( "boxturtle" ) def test_modify ( data ): data . location = "Sesame Street" resp = data . modify ( data . name , data ) assert resp == data def test_modify_missing (): bob : Creature = Creature ( name = "bob" , description = "some guy" , location = "somewhere" ) with pytest . raises ( Missing ): resp = data . modify ( bob . name , bob )
数据此层更易于单独测试,因为 不用担心意外调用某些函数 在更低的层 单元测试应涵盖此层中的两个功能, 以及他们使用的特定数据库查询 到目前为止,SQLite一直是数据库的“服务器”和SQL 查询语言 但正如我在第17章中提到的, 你可以决定使用像SQLALchemy这样的包, 并使用其 SQL 表达式语言或 ORM 然后这些将需要全面的测试 到目前为止,我一直保持在最低级别 - Python的 DB-API 和 vanilla SQL 查询取消排列 Web 和服务单元测试, 这次我们不需要“假”模块来替换 现有的数据层模块 相反,设置一个不同的环境变量来获取 使用仅内存 SQLite 实例的数据层, 而不是基于文件的 这不需要对现有数据进行任何更改 模块,只是示例 12-15 中的一个设置 在导入任何数据模块进行测试数据单元测试/单元/数据/test_creature.py import os import pytest from model.creature import Creature from error import Missing , Duplicate # set this before data.init import below os . environ [ "CRYPTID_SQLITE_DB" ] = ":memory:" from data import init , creature @pytest . fixture def sample () -> Creature : return Creature ( name = "yeti" , description = "Abominable Snowman" , location = "Himalayas" ) def test_create ( sample ): resp = creature . create ( sample ) assert resp == sample def test_create_duplicate ( sample ): with pytest . raises ( Duplicate ): resp = creature . create ( sample ) def test_get_one ( sample ): resp = creature . get_one ( sample . name ) assert resp == sample def test_get_one_missing (): with pytest . raises ( Missing ): resp = creature . get_one ( "boxturtle" ) def test_modify ( sample ): creature . location = "Sesame Street" resp = creature . modify ( sample . name , sample ) assert resp == sample def test_modify_missing (): thing : Creature = Creature ( name = "snurfle" , description = "some thing" , location = "somewhere" ) with pytest . raises ( Missing ): resp = creature . modify ( thing . name , thing ) def test_delete ( sample ): resp = creature . delete ( sample . name ) assert resp is None def test_delete_missing ( sample ): with pytest . raises ( Missing ): resp = creature . delete ( sample . name )
自动化集成测试集成测试查看不同代码交互程度 但如果你寻找这样的例子, 你会得到许多不同的答案 是否应测试部分调用跟踪 如 Web →服务、Web →数据等?全面测试 A → B → C 中的每个连接 管道,您需要测试:A → BB → CA → C如果你有更多,箭头会填满箭袋 比这三个路口或者集成测试本质上应该是完整的测试, 但随着 非常最后的一块 - 磁盘上的数据存储 - 嘲笑?到目前为止,我一直在使用SQLite作为数据库, 我们可以将内存中的SQLite用作双精度(假) 对于磁盘上的 SQLite 数据库 如果您的查询标准的 SQL, 内存中的SQLite可能是一个足够的模拟 其他数据库也是如此 如果没有,则有一些模块是量身定制的 模拟特定数据库::::在Docker容器中启动了各种测试数据库, 并与 Pytest 集成最后,你可以启动一个同类的测试数据库 作为生产 环境变量可以包含细节, 很像我一直在使用的单元测试/假技巧存储库模式虽然我没有在这本书中实现它, 是一种有趣的方法 是一个简单的中间内存数据存储,就像您到目前为止在这里看到的假数据层一样 然后,这将与真实数据库的可插拔后端进行对话 它伴随着模式,该模式可确保单个中的一组操作作为一个整体提交或回滚 到目前为止,本书中的数据库查询都是原子的 对于实际的数据库工作,您可能需要 多步骤查询和某种会话 处理存储库模式也与相吻合, 你在这里其他地方看到的, 现在可能有点欣赏自动化全面测试完整的测试一起练习所有层, 尽可能接近生产使用 您已经看到的大多数测试 书已经满了 — 调用 Web 端点, 穿过服务镇到达达特维尔市中心, 并带着杂货回来 这些是黑盒测试 一切都是活的,你不在乎它是如何做到的, 只是它做到了您可以在 中全面测试每个终结点 整体 API 有两种方式::编写单独的Python测试客户端 访问服务器本书中很多例子都做到了这一点, 使用像 Httpie 这样的备用客户端,或者在使用请求的脚本中:使用此内置的 FastAPI/Starlette 对象直接访问服务器,无需公开 TCP 连接但这需要为每个终结点编写一个或多个测试 这可以成为中世纪,我们已经有几个世纪了 过去中世纪现在 较新的方法基于 这利用了FastAPI的 自动生成的文档 每次更改路径函数时,FastAPI 都会创建一个名为 的 OpenAPI 或 Web 图层中的路径修饰器 此架构详细介绍了有关每个终结点的所有内容:参数、 返回值等 这就是OpenAPI的用途:OAS定义了一个标准的、与编程语言无关的接口 REST API 的描述,它允许人类和计算机 发现和了解服务的功能,而无需 访问源代码、附加文档或检查 网络流量https://www.openapis.org/faq需要两个包::点安装假设pip install schemathesis假设是基础库, 和图式应用它 到 FastAPI 生成的 OpenAPI 3.0 架构 运行模式会读取此架构, 生成具有不同数据的大量测试 (你不需要想出并与 Pytest 配合使用为了保持简短,首先使用示例 12-16 瘦身 到底部 生物和探索者端点:裸露的 main.py
from fastapi import FastAPI from web import explorer , creature app = FastAPI () app . include_router ( explorer . router ) app . include_router ( creature . router )
Exmople 12-17运行测试运行架构测试$ schemathesis http://localhost:8000/openapi.json===================== Schemathesis test session starts =====================Schema location: http://localhost:8000/openapi.jsonBase URL: http://localhost:8000/Specification version: Open API 3.0.2Workers: 1Collected API operations: 12GET /explorer/ . [ 8%]POST /explorer/ . [ 16%]PATCH /explorer/ F [ 25%]GET /explorer . [ 33%]POST /explorer . [ 41%]GET /explorer/{name} . [ 50%]DELETE /explorer/{name} . [ 58%]GET /creature/ . [ 66%]POST /creature/ . [ 75%]PATCH /creature/ F [ 83%]GET /creature/{name} . [ 91%]DELETE /creature/{name} . [100%]
我得到了两个F,都在PATCH调用中 ( 修改() 函数) 多么令人羞愧此输出部分后跟一个标记的失败, 包含任何失败测试的详细堆栈跟踪 这些需要修复 最后一部分标记为摘要:Performed checks: not_a_server_error 717 / 727 passed FAILEDHint: You can visualize test results in Schemathesis.ioby using `--report` in your CLI command.
那太快了, 而且我不必为每个端点创建多个测试, 想象可能会破坏它们的输入 基于属性的测试读取类型和约束 来自 API 架构的输入参数, 并生成一系列值 在每个端点射击这是类型提示的另一个意想不到的好处, 乍一看似乎只是好事:→生成的文档测试 OpenAPI →架构的类型提示安全测试安全不是一回事,而是一切 你需要防御恶意, 但也反对普通的旧错误, 甚至是您无法控制的事件 让我们将扩展问题推迟到下一节,并处理 这里主要是对潜在威胁的分析第11章讨论了身份验证和授权 这些因素总是混乱且容易出错 使用聪明的方法来抵消是很诱人的 巧妙的攻击,设计总是一个挑战 易于理解和实施的保护但是现在我们知道了Schemathesis, 阅读基于属性的身份验证测试的文档 正如它大大简化了大多数 API 的测试一样, 它可以自动执行端点的大部分测试 需要身份验证(更多:授权详细信息?负载测试负载测试练习应用程序如何处理繁重的流量:接口调用数据库读取或写入内存使用情况磁盘使用情况网络延迟和带宽有些可以是模拟用户大军测试 吵着要使用您的服务; 你想在那一天到来之前做好准备 本节和 第十四章 表演 和 第 14 章(故障排除)那里有很多优秀的负载测试仪, 但在这里我将使用一种叫做的 使用Locust,你可以用普通的Python脚本定义所有的测试 它可以模拟数十万用户, 所有这些都冲击着您的站点,甚至多台服务器, 立即用点子安装蝗虫在本地安装它您可能要测试的第一件事是您的网站有多好 站起来 您的网站可以处理多少并发访问者? 这就像测试建筑物的极端天气程度一样 面对飓风/地震/暴风雪时可以承受, 或其他房屋保险事件 因此,您需要一些网站结构测试例 12-18 将蝗虫火带瞄准当前生物/探险者地点运行蝗虫负载测试$ ...----
但还有更多最近,扩展了蝗虫做事 比如测量多个 HTTP 调用的时间 要尝试一下,请使用 pip 安装蝗虫-蝙蜢 .回顾本章充实了测试的类型, 以及 Pytest 在 单元、集成和完整级别 API 测试可以通过架构自动化 它还讨论了如何公开安全性和性能 问题在他们来袭之前
(图片来源网络,侵删)
0 评论