[{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nAnthropic 官方专门写了一篇文章来解释。看完之后你会发现，这几个概念不仅不重复，反而各有分工，组合起来才能发挥完整的 AI 能力。\n先搞懂五个概念分别是什么 Skills：一本随时能翻的操作手册 Skills 是一个文件夹，里面装着指令、脚本和资源文件。Claude 会在需要时自动发现并加载它。\n你可以把 Skills 理解成给 Claude 准备的专业培训手册。比如你写一个\u0026quot;Excel 报表生成\u0026quot;Skill，里面包含公司要求的固定格式、公式模板和 Python 脚本。以后每次让 Claude 做报表，它就知道该按什么标准来。\n什么时候用 Skills？ 当你发现自己在多个对话里反复输入同样的指令时，就该把它做成 Skill 了。Skills 是可复用的、跨对话持久存在的。\nPrompts：你当场说的一句话 Prompts 就是你在对话框里输入的那段话。它是一次性的、临时的，对话结束就没了。比如你让 Claude\u0026quot;帮我总结这篇文章\u0026quot;或者\u0026quot;把这段代码改成 Python 风格\u0026quot;，都属于 Prompts。\n什么时候用 Prompts？ 当你只是临时给个指令、提供即时上下文，或者在进行一轮对话时，用 Prompts 就够了。\n什么时候用 Skills 替代 Prompts？ 如果你发现自己跨多个对话反复输入同样的指令，就该把它做成 Skill 了。比如\u0026quot;按团队代码规范检查空指针、并发安全、SQL 注入\u0026quot;、\u0026ldquo;周报按本周进展、风险问题、下周计划三段式输出\u0026quot;这类经常要重复的要求，做成 Skill 之后就不用每次都重新解释一遍，也能保证输出更稳定。\nProjects：一个项目的专属资料室 Projects 是一个带有自己知识库和对话历史的\u0026quot;项目空间\u0026rdquo;。你可以在里面上传文档、设置自定义指令，进入这个项目后的所有对话都能访问这些背景知识。\n什么时候用 Projects？ 当你需要为某个特定项目提供固定的背景资料时，比如产品规格书、市场调研报告、竞品文档等。Projects 提供的是静态的背景知识，进入项目就始终加载。\n什么时候用 Skills 替代 Projects？ Projects 给 Claude 提供特定工作领域的持久上下文——比如公司代码库、一个研究项目、一个长期客户合作。Skills 则是教 Claude 怎么做某件事。一个 Project 可以包含你产品发布的全部背景，而一个 Skill 可以教 Claude 团队的写作标准或代码审查流程。如果你发现自己跨多个 Project 复制同样的指令，那就是该创建 Skill 的信号。\nSubagents：一个专职的 AI 同事 Subagents 是独立的任务执行助手，只在 Claude Code 和 SDK 中可用。每个 Subagent 有自己的上下文窗口、自定义提示词和特定的工具权限。\n什么时候用 Subagents？ 当你需要把某个复杂任务交给一个专门的\u0026quot;AI 员工\u0026quot;独立处理时。比如你可以创建一个只读权限的代码审查 Subagent，它能看代码但不能改代码，既安全又专注。\n什么时候用 Skills 替代 Subagents？ 如果多个 Agent 或对话需要同样的专业知识——比如安全审查流程或数据分析方法——那就创建 Skill，而不是把这些知识硬编码到各个 Subagent 里。Skills 可移植、可复用，而 Subagents 是为特定工作流量身定制的。用 Skills 教任何 Agent 都能用的专长；用 Subagents 当你需要独立的、带特定工具权限和上下文隔离的任务执行。\nMCP：连接外部世界的万能插头 MCP（Model Context Protocol）是 Claude 连接外部系统的开放标准。想让 Claude 访问 Google Drive、Slack、GitHub、数据库等外部工具，就得靠 MCP。\n什么时候用 MCP？ 当你需要 Claude 访问外部数据或工具时。如果你在解释\u0026quot;如何使用某个工具\u0026quot;或\u0026quot;遵循什么流程\u0026quot;，那是 Skill 的事；但如果你需要 Claude 首先能连接到数据库或文件，那就是 MCP 的事。\n什么时候用 Skills 替代 MCP？ MCP 负责让 Claude 连接到数据；Skills 教 Claude 拿到数据后该做什么。如果你在解释\u0026quot;如何使用某个工具\u0026quot;或\u0026quot;遵循什么流程\u0026quot;——比如\u0026quot;查数据库时先按日期范围过滤\u0026quot;或\u0026quot;Excel 报表用这些特定公式格式化\u0026quot;——那是 Skill 的事。但如果你需要 Claude 首先能连接到数据库或文件，那就是 MCP 的事。两者配合使用：MCP 负责连接，Skill 负责流程知识。\n一张表看懂区别 维度 Skills Prompts Projects Subagents MCP 提供什么 流程知识 即时指令 背景知识 任务委派 工具连接 持久性 跨对话 单次对话 项目内 跨会话 持续连接 包含什么 指令+代码+资源 自然语言 文档+上下文 完整代理逻辑 工具定义 何时加载 动态按需 每轮对话 项目中始终 被调用时 始终可用 能包含代码 是 否 否 是 是 最适合 专业专长 快速请求 集中上下文 专业任务 数据访问 实际场景：什么时候该用哪个？ 场景一：统一团队的代码审查标准 你希望每次代码审查都遵循同样的规范——检查安全性、命名风格、注释完整性。如果你每次都手动写一遍要求，太累了。\n解决方案： 创建一个\u0026quot;代码审查\u0026quot;Skill，把审查标准、检查清单和自动化脚本打包进去。以后 Claude 看到代码审查任务，就会自动调用这个 Skill。\n场景二：让 Claude 连接公司数据库查数据 你需要 Claude 查询数据库中的订单信息，并且查询时必须先按日期范围过滤。\n解决方案： 用 MCP 连接数据库（让 Claude 能访问数据），再用 Skill 定义查询流程（告诉 Claude 怎么查、怎么过滤）。两者配合使用：MCP 负责连接，Skill 负责流程。\n场景三：管理一个产品开发项目的全部背景 你有一个产品开发项目，里面有产品规格书、用户研究报告、设计稿等大量资料。你希望 Claude 在讨论这个项目时始终能参考这些资料。\n解决方案： 创建一个 Project，把所有项目文档上传进去。这样每次在这个项目里对话，Claude 都能自动获取背景知识。如果还需要生成技术文档或分析用户反馈，可以搭配相应的 Skills。\n场景四：让一个专门的 AI 助手处理数据分析 你需要定期做数据分析，但不希望每次都在主对话里处理，而是希望有一个独立的\u0026quot;数据分析师\u0026quot;来处理。\n解决方案： 创建一个数据分析 Subagent，给它配置专门的提示词和数据访问权限。这个 Subagent 还可以调用 pandas-analysis Skill 来按照团队约定的方式处理数据。\n场景五：MCP + Skills 组合拳 你想让 Claude 自动从 Google Drive 读取竞品简报，用标准框架分析，然后生成一份 Excel 报告。\n解决方案：\n用 MCP 连接 Google Drive，让 Claude 能读取文件 创建一个\u0026quot;竞品分析\u0026quot;Skill，定义分析框架和输出格式 创建一个\u0026quot;Excel 报表\u0026quot;Skill，确保输出符合公司模板 Claude 会自动识别需要哪些 Skills，通过 MCP 获取数据，然后按 Skills 定义的流程完成整个任务。\n常见问题 Skills 是怎么工作的？ Skills 使用一种叫\u0026quot;渐进式加载\u0026quot;的机制来保持高效。\nClaude 不会一次性把所有 Skills 的内容都加载进来，而是分三步：\n扫描元数据：Claude 先快速浏览所有 Skills 的名称和简介（每个只占约 100 个 token），判断哪些和当前任务相关。 加载完整指令：如果某个 Skill 匹配，Claude 才会加载它的详细指令文件（SKILL.md）。 按需加载资源：如果 Skill 包含可执行代码或参考文件，只有在真正需要时才加载。 这意味着你可以安装几十个甚至上百个 Skills，平时完全不占上下文空间，需要时瞬间激活。\nSkills 和 Subagents 怎么选？ 用 Skills：当你希望任何 Claude 会话都能加载和使用的能力。Skills 像培训材料，让 Claude 在特定任务上变得更专业。 用 Subagents：当你需要独立的、自包含的 AI 助手来独立处理任务。Subagents 像有自己上下文和工具权限的专职员工。 一起用：你可以让 Subagent 调用 Skills。比如一个代码审查 Subagent 可以使用语言特定的最佳实践 Skill，把 Subagent 的独立性和 Skills 的可移植专长结合起来。 Skills 和 Prompts 怎么选？ 用 Prompts：当你给一次性指令、提供即时上下文，或者在进行对话来回时。Prompts 是反应式的、临时的。 用 Skills：当你有需要反复使用的流程或专长时。Skills 是主动式的——Claude 知道什么时候该用它——而且跨对话持久存在。 一起用：Prompts 和 Skills 天然互补。用 Skills 提供基础专长，用 Prompts 为每个具体任务提供上下文和细化要求。 Skills 和 Projects 怎么选？ 用 Projects：当你需要为某个特定项目提供固定的背景知识和上下文时。Projects 提供始终加载的静态参考资料。 用 Skills：当你需要只在相关时才激活的流程知识和可执行代码时。Skills 提供按需加载的动态专长，节省上下文窗口。 一起用：比如一个\u0026quot;产品开发\u0026quot;Project 包含产品规格和用户研究，搭配用于创建技术文档和分析用户反馈数据的 Skills。 核心区别： Projects 说的是\u0026quot;这是你需要知道的\u0026quot;，Skills 说的是\u0026quot;这是做事的方法\u0026quot;。Projects 提供你工作的知识基地，Skills 提供在任何对话、任何项目中都能用的能力。\nSubagents 能使用 Skills 吗？ 可以。在 Claude Code 和 Agent SDK 中，Subagents 可以像主 Agent 一样访问和使用 Skills。这让专职 Subagent 能够利用可移植的专长，形成强大的组合。\n总结一下 把这五个概念串起来，可以用一句话概括各自的定位：\nPrompts = 当场吩咐一句 Skills = 把做事的方法写进操作手册 Projects = 给 AI 搭一个项目档案室 Subagents = 请来专职的 AI 同事 MCP = 打通所有外部系统的总线 它们不是互相替代的关系，而是互补的积木。Prompts 解决即时沟通，Skills 解决可复用流程，Projects 解决项目背景，Subagents 解决任务隔离，MCP 解决外部连接。搞清楚各自的边界，你就能像搭乐高一样，组合出适合自己的 AI 使用方案。\n参考：https://claude.com/resources/articles/skills-explained\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/001002-ai%E5%BC%80%E5%8F%91claude-skills-%E5%92%8C-promptsprojectsmcpsubagents-%E7%9A%84%E5%8C%BA%E5%88%AB/","summary":"Anthropic 官方专门写了一篇文章来解释。看完之后你会发现，这几个概念不仅不重复，反而各有分工，组合起来才能发挥完整的 AI 能力。","title":"Claude Skills 和 Prompts、Projects、MCP、Subagents 的区别"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nAgent Skills 的发展简史 Agent Skills 的出现，其实是 AI 能力组织方式自然演进的结果。\n早期的大模型应用主要靠提示词工程，把任务规则、流程说明、输出格式全部写进 System Prompt。这种方式简单直接，但提示词越写越长，上下文窗口被大量占用，关键指令容易被淹没，而且很难在不同项目之间复用。\n随后 Function Calling 和工具调用逐渐普及，模型开始能够调用搜索、代码执行、数据库查询等外部能力。但这更多解决的是\u0026quot;能不能调用工具\u0026quot;的问题，还没有很好地回答\u0026quot;应该按什么流程、遵循什么标准、产出什么格式\u0026quot;的问题。\n2025 年 10 月，Anthropic 在 Claude Code 中首次推出 Agent Skills 能力，把可复用的任务流程封装成以 SKILL.md 为核心的文件夹结构。 同年 12 月 18 日，Anthropic 将 Agent Skills 规范作为开放标准发布，并开源官方技能库，随后 OpenAI Codex、GitHub Copilot、VS Code、Cursor 等产品陆续跟进支持。 到 2026 年，Agent Skills 已经从单一产品的功能扩展为跨平台的能力封装格式，公开可用的 Skills 数量逼近百万级，累计安装约 2.8 亿次。\n从技术演进看，Agent Skills 标志着 AI 应用开发从\u0026quot;手搓提示词\u0026quot;走向\u0026quot;标准化技能工程\u0026quot;。它把专业知识、执行流程、脚本工具和验收标准打包在一起，让 Agent 不再只是会聊天，而是能稳定完成具体工作。\n一个 Skill 长什么样 最简单的 Skill 只需要一个文件：\nmy-skill/ └── SKILL.md 完整结构可以包含这些目录：\nmy-skill/ ├── SKILL.md # 必需：元数据 + 指令 ├── scripts/ # 可选：可执行脚本 ├── references/ # 可选：参考文档 ├── assets/ # 可选：模板、资源 └── ... 核心文件 SKILL.md 的结构 SKILL.md 由两部分组成：YAML Frontmatter（元数据） 和 Markdown 正文（指令），用 --- 分隔。\nFrontmatter 元数据 官方规范只允许 6 个字段，其中 2 个必填：\n字段 必填 说明 name ✅ 唯一标识，1-64 字符，仅小写字母/数字/连字符，必须与目录名一致 description ✅ 触发核心描述，1-1024 字符，Agent 据此决定是否加载 license ❌ 许可证，如 Apache-2.0 compatibility ❌ 环境要求，最多 500 字符 metadata ❌ 自定义键值对（作者、版本等放这里） allowed-tools ❌ 允许使用的工具列表（实验性） 注意：author、version 等字段不能直接写在 frontmatter 顶层，必须放在 metadata 里，否则验证失败。\nMarkdown 正文 正文是给 Agent 的执行指令，建议控制在 500 行 / 5000 tokens 以内，超出部分拆分到 references/。 应包含：\n何时使用：帮助 Agent 判断触发时机 分步指令：用祈使句写清楚每一步做什么 输出规范：规定结果格式 示例：输入输出对，降低理解成本 动手：写第一个 Skill 我们以 \u0026ldquo;数据周报生成器\u0026rdquo; 为例。很多测试或开发人员每周都要从数据库拉数据、做图表、写报告，这个流程完全可以交给 AI 自动完成。\n第一步：创建目录\nmkdir -p .agents/skills/weekly-report 第二步：编写 SKILL.md\n--- name: weekly-report description: \u0026gt; 自动生成数据分析周报。当用户需要分析销售数据、制作图表并生成周报时使用。 支持连接数据库查询、Python 数据处理和 Markdown 报告输出。 license: MIT metadata: author: gang version: \u0026#34;1.0\u0026#34; --- # 数据周报生成技能 ## 何时使用本技能 当用户提出以下请求时触发本技能： - \u0026#34;帮我生成上周的销售周报\u0026#34; - \u0026#34;分析一下最近的数据，出个报告\u0026#34; - \u0026#34;把上周的测试结果汇总一下\u0026#34; - \u0026#34;做个数据分析周报\u0026#34; ## 执行流程 1. **数据获取**：使用 `scripts/query_db.py` 连接数据库，执行 SQL 查询，获取指定时间段的数据。 2. **数据分析与绘图**：使用 `scripts/analyze.py` 对数据进行同比/环比分析，并生成柱状图和趋势图，图片保存到 `./output/` 目录。 3. **报告生成**：根据 `assets/report_template.md` 模板，填充分析结果和图表，生成 Markdown 格式的周报。 4. **结果输出**：将生成的报告展示给用户，并告知图表存放路径。 ## 注意事项 - 数据库连接信息需在 `.env` 文件中配置，脚本会自动读取。 - 如果数据查询为空，应生成\u0026#34;本周无数据\u0026#34;的提示报告，而不是报错。 - 生成的图表统一保存在 `./output/` 目录下，文件名包含日期。 ## 输出格式 报告必须包含以下三个部分： 1. **数据概览**：关键指标汇总（表格形式） 2. **趋势分析**：同比/环比变化，配图表 3. **结论与建议**：用简短的 bullet points 列出 第三步：配套脚本（可选）\nweekly-report/ ├── SKILL.md ├── scripts/ │ ├── query_db.py # 数据库查询脚本 │ └── analyze.py # 数据分析与绘图脚本 ├── references/ │ └── db_schema.md # 数据库表结构说明 └── assets/ └── report_template.md # 报告模板 第四步：测试。在支持 Skills 的客户端（如 Claude Code、Cursor）中打开项目，输入\u0026quot;帮我生成上周的销售周报\u0026quot;，Agent 会自动识别并加载你的 Skill，按步骤执行。\n渐进式加载机制 Agent 不会一次性加载所有内容，而是分三层：\n发现层：启动时只读取每个 Skill 的 name + description（约 100 tokens） 激活层：任务匹配时加载完整 SKILL.md 正文 执行层：按需读取 scripts/、references/、assets/ 中的文件 这就是为什么 description 写得准不准，直接决定了 Skill 能不能被触发。\n编写高质量 Skill 的要点 description 要具体：包含功能、场景、触发关键词。\u0026ldquo;处理数据\u0026quot;太模糊，\u0026ldquo;分析 CSV 文件，计算统计摘要并生成图表\u0026quot;更精准。 单一职责：一个 Skill 只做一件事，复杂任务靠组合多个 Skill。 关键校验用脚本：确定性逻辑写在 scripts/ 里，比让模型用自然语言判断更可靠。 指令前置：最重要的步骤放在正文开头，必要时重复强调。 命名用动名词：如 analyzing-sales-data、generating-unit-tests，见名知意。 用 skill-creator 自动生成 Skill https://github.com/anthropics/skills/tree/main/skills/skill-creator\n如果你不想从零手写 SKILL.md，可以直接使用 Anthropic 官方提供的 skill-creator。它是一个“元技能”，也就是专门用来帮你创建、校验、测试和优化其他 Skill 的工具，源码在 GitHub 的 anthropics/skills 仓库中。\n安装方式大致有两种：\nClaude Code 中安装：先添加官方插件市场，再安装示例技能包： /plugin marketplace add anthropics/skills /plugin install example-skills@anthropic-agent-skills 手动安装：从 GitHub 仓库下载 skill-creator 目录，放到本地技能目录，例如 ~/.claude/skills/skill-creator。 安装完成后，在对话里输入：\n使用 skill-creator 帮我创建一个新的 Claude Skill。 目标是：自动生成数据分析周报，读取数据库或 CSV 数据，生成图表，并输出 Markdown 报告。 skill-creator 会追问你几个问题：技能名称、触发场景、输入输出、允许使用的工具、边界条件等。回答完后，它会自动生成标准目录结构，包括 SKILL.md、scripts/、assets/、references/ 等文件。\n它还能帮你做几件比较工程化的事：\n校验格式：检查 SKILL.md 的 frontmatter、字段、权限配置是否合规； 跑测试用例：对比“带 Skill”和“不带 Skill”的输出差异； 迭代优化：根据测试结果调整 description 和正文指令； 打包分发：把技能打包成可分享的 .skill 文件。 如果你只是想快速试手，用 skill-creator 会比手写更省事；但如果你想彻底理解 Agent Skill 的结构，还是建议先照着前面的步骤手动写一遍。\n进阶：评估你的 Skill 写完后别只靠“感觉还行”来判断。Agent Skill 的评估，核心思路其实和写单元测试很像：定义好输入和预期输出，反复跑，看它是不是稳定达标。\n一个测试用例怎么设计 每个测试用例只需要三样东西：\nPrompt：真实用户会怎么问，比如“生成上周的销售周报” Expected：好的输出应该长什么样，比如“报告必须包含数据概览、趋势分析、结论与建议三个部分” Input files（可选）：测试需要的输入文件，比如一份示例 CSV 把这些用例集中放在 evals/evals.json 里，就构成了你的测试集。\n配对评测：装上和关掉各跑一遍 最关键的一步是对比运行：同一个测试用例，装上 Skill 跑一遍，关掉 Skill 再跑一遍。\n这样你才能回答两个问题：\n这个 Skill 真的有用吗？ 如果关掉 Skill 模型也能做得一样好，没必要保留。 它带来了什么增量？ 装上之后，输出更完整了？更准确了？还是反而更慢了、更贵了？ 看什么指标 跑完一轮测试后，重点关注这几个维度：\n通过率（Pass Rate）：测试用例中有多少比例达到了预期 耗时（Latency）：装上 Skill 后响应变快还是变慢了 Token 消耗（Cost）：每次执行花了多少 token，增量是否值得 如果装上 Skill 后通过率大幅提升、耗时和成本可控，说明这个 Skill 是有价值的。反之，如果结果差不多，那就要考虑简化或移除它。\n自动化评估 如果你不想手工跑对比，官方提供的 skill-creator 内置了一套评估流水线，可以自动完成以下工作：\nGrader：逐条检查输出是否满足预期 Comparator：盲测对比“有 Skill”和“无 Skill”的输出，客观打分 Analyzer：分析胜者为什么赢，给出改进建议 Benchmark：批量跑测试集，输出通过率、耗时、token 用量等汇总指标 这些工具把评估从“凭感觉”变成了“看数据”，让你在每次修改 Skill 后都能量化验证效果。\n一句话总结，评估的本质就是：定义好“什么算成功”，然后反复跑，用数据证明你的 Skill 确实比裸模型做得更好。\n小结 Agent Skills 的核心思想就一句话：教一次，永久受益。 把你知道的工作流程、团队规范、脚本工具打包进一个文件夹，AI 就能在需要时自动加载并精准执行。从 weekly-report 开始动手试试吧。\n参考： https://agentskills.io/\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/001001-ai%E5%BC%80%E5%8F%91%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%BC%96%E5%86%99%E4%BD%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA-agent-skill/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"agent-skills-的发展简史\"\u003eAgent Skills 的发展简史\u003c/h1\u003e\n\u003cp\u003eAgent Skills 的出现，其实是 AI 能力组织方式自然演进的结果。\u003c/p\u003e\n\u003cp\u003e早期的大模型应用主要靠提示词工程，把任务规则、流程说明、输出格式全部写进 System Prompt。这种方式简单直接，但提示词越写越长，上下文窗口被大量占用，关键指令容易被淹没，而且很难在不同项目之间复用。\u003c/p\u003e\n\u003cp\u003e随后 Function Calling 和工具调用逐渐普及，模型开始能够调用搜索、代码执行、数据库查询等外部能力。但这更多解决的是\u0026quot;能不能调用工具\u0026quot;的问题，还没有很好地回答\u0026quot;应该按什么流程、遵循什么标准、产出什么格式\u0026quot;的问题。\u003c/p\u003e\n\u003cp\u003e2025 年 10 月，Anthropic 在 Claude Code 中首次推出 Agent Skills 能力，把可复用的任务流程封装成以 \u003ccode\u003eSKILL.md\u003c/code\u003e 为核心的文件夹结构。 同年 12 月 18 日，Anthropic 将 Agent Skills 规范作为开放标准发布，并开源官方技能库，随后 OpenAI Codex、GitHub Copilot、VS Code、Cursor 等产品陆续跟进支持。 到 2026 年，Agent Skills 已经从单一产品的功能扩展为跨平台的能力封装格式，公开可用的 Skills 数量逼近百万级，累计安装约 2.8 亿次。\u003c/p\u003e\n\u003cp\u003e从技术演进看，Agent Skills 标志着 AI 应用开发从\u0026quot;手搓提示词\u0026quot;走向\u0026quot;标准化技能工程\u0026quot;。它把专业知识、执行流程、脚本工具和验收标准打包在一起，让 Agent 不再只是会聊天，而是能稳定完成具体工作。\u003c/p\u003e\n\u003ch1 id=\"一个-skill-长什么样\"\u003e一个 Skill 长什么样\u003c/h1\u003e\n\u003cp\u003e最简单的 Skill 只需要一个文件：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emy-skill/\n└── SKILL.md\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e完整结构可以包含这些目录：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emy-skill/\n├── SKILL.md          # 必需：元数据 + 指令\n├── scripts/          # 可选：可执行脚本\n├── references/       # 可选：参考文档\n├── assets/           # 可选：模板、资源\n└── ...\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"核心文件-skillmd-的结构\"\u003e核心文件 SKILL.md 的结构\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003eSKILL.md\u003c/code\u003e 由两部分组成：\u003cstrong\u003eYAML Frontmatter（元数据）\u003c/strong\u003e 和 \u003cstrong\u003eMarkdown 正文（指令）\u003c/strong\u003e，用 \u003ccode\u003e---\u003c/code\u003e 分隔。\u003c/p\u003e","title":"从零开始编写你的第一个 Agent Skill"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n简介 uv是一款开源的Python包安装工具，GitHub star高达56k，以性能极快著称，具有以下特性（官方英文原文）：\n🚀 A single tool to replace pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv, and more. ⚡️ 10-100x faster than pip. 🗂️ Provides comprehensive project management, with a universal lockfile. ❇️ Runs scripts, with support for inline dependency metadata. 🐍 Installs and manages Python versions. 🛠️ Runs and installs tools published as Python packages. 🔩 Includes a pip-compatible interface for a performance boost with a familiar CLI. 🏢 Supports Cargo-style workspaces for scalable projects. 💾 Disk-space efficient, with a global cache for dependency deduplication. ⏬ Installable without Rust or Python via curl or pip. 🖥️ Supports macOS, Linux, and Windows. 在Python MCP SDK的介绍中，也出现了We recommend using uv to manage your Python projects的字样，说明uv已经是一款实用性极强的工具。\n安装 macOS and Linux\ncurl -LsSf https://astral.sh/uv/install.sh | sh Windows\npowershell -ExecutionPolicy ByPass -c \u0026#34;irm https://astral.sh/uv/install.ps1 | iex\u0026#34; 当然也可以通过pip安装：\npip install uv 使用 创建项目：\nuv init example cd example 虚拟环境：\nuv venv 执行后会提示：\nCreating virtual environment at: .venv Activate with: source .venv/bin/activate 激活：\nsource .venv/bin/activate 安装其他包，比如pandas：\nuv add pandas 第一次安装可能会慢一点，但也要比pip快很多。第二次基本上就是秒装了，而pip可能还需要多花时间加载。\nPython多版本 安装多个Python版本：\nuv python install 3.10 3.11 3.12 指定某个版本创建虚拟环境：\nuv venv --python 3.12.0 可以创建多个不同版本的虚拟环境，激活某个虚拟环境就会使用相应版本。\n如果需要临时切到某个版本，可以在当前目录指定：\nuv python pin 3.11 ## Pinned `.python-version` to ``3.11 更多用法请阅读官方文档。\n参考资料：\nhttps://docs.astral.sh/uv/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000036-%E5%BA%93%E6%AF%94pip%E5%BF%AB100%E5%80%8D%E7%9A%84python%E5%8C%85%E5%AE%89%E8%A3%85%E5%B7%A5%E5%85%B7/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"简介\"\u003e简介\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003euv\u003c/strong\u003e是一款开源的Python包安装工具，GitHub star高达\u003cstrong\u003e56k\u003c/strong\u003e，以性能极快著称，具有以下特性（官方英文原文）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e🚀 A single tool to replace \u003ccode\u003epip\u003c/code\u003e, \u003ccode\u003epip-tools\u003c/code\u003e, \u003ccode\u003epipx\u003c/code\u003e, \u003ccode\u003epoetry\u003c/code\u003e, \u003ccode\u003epyenv\u003c/code\u003e, \u003ccode\u003etwine\u003c/code\u003e, \u003ccode\u003evirtualenv\u003c/code\u003e, and more.\u003c/li\u003e\n\u003cli\u003e⚡️ \u003ca href=\"https://github.com/astral-sh/uv/blob/main/BENCHMARKS.md\"\u003e10-100x faster\u003c/a\u003e than \u003ccode\u003epip\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e🗂️ Provides \u003ca href=\"https://docs.astral.sh/uv/#projects\"\u003ecomprehensive project management\u003c/a\u003e, with a \u003ca href=\"https://docs.astral.sh/uv/concepts/projects/layout/#the-lockfile\"\u003euniversal lockfile\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003e❇️ \u003ca href=\"https://docs.astral.sh/uv/#scripts\"\u003eRuns scripts\u003c/a\u003e, with support for \u003ca href=\"https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies\"\u003einline dependency metadata\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003e🐍 \u003ca href=\"https://docs.astral.sh/uv/#python-versions\"\u003eInstalls and manages\u003c/a\u003e Python versions.\u003c/li\u003e\n\u003cli\u003e🛠️ \u003ca href=\"https://docs.astral.sh/uv/#tools\"\u003eRuns and installs\u003c/a\u003e tools published as Python packages.\u003c/li\u003e\n\u003cli\u003e🔩 Includes a \u003ca href=\"https://docs.astral.sh/uv/#the-pip-interface\"\u003epip-compatible interface\u003c/a\u003e for a performance boost with a familiar CLI.\u003c/li\u003e\n\u003cli\u003e🏢 Supports Cargo-style \u003ca href=\"https://docs.astral.sh/uv/concepts/projects/workspaces/\"\u003eworkspaces\u003c/a\u003e for scalable projects.\u003c/li\u003e\n\u003cli\u003e💾 Disk-space efficient, with a \u003ca href=\"https://docs.astral.sh/uv/concepts/cache/\"\u003eglobal cache\u003c/a\u003e for dependency deduplication.\u003c/li\u003e\n\u003cli\u003e⏬ Installable without Rust or Python via \u003ccode\u003ecurl\u003c/code\u003e or \u003ccode\u003epip\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e🖥️ Supports macOS, Linux, and Windows.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在Python MCP SDK的介绍中，也出现了We recommend using uv to manage your Python projects的字样，说明uv已经是一款实用性极强的工具。\u003c/p\u003e","title":"比pip快100倍的Python包安装工具"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nif if语句是最常见的控制流：\n\u0026gt;\u0026gt;\u0026gt; x = int(input(\u0026#34;Please enter an integer: \u0026#34;)) Please enter an integer: 42 \u0026gt;\u0026gt;\u0026gt; if x \u0026lt; 0: ... x = 0 ... print(\u0026#39;Negative changed to zero\u0026#39;) ... elif x == 0: ... print(\u0026#39;Zero\u0026#39;) ... elif x == 1: ... print(\u0026#39;Single\u0026#39;) ... else: ... print(\u0026#39;More\u0026#39;) ... More for for语句用来遍历：\n## Create a sample collection users = {\u0026#39;Hans\u0026#39;: \u0026#39;active\u0026#39;, \u0026#39;Éléonore\u0026#39;: \u0026#39;inactive\u0026#39;, \u0026#39;景太郎\u0026#39;: \u0026#39;active\u0026#39;} ## Strategy: Iterate over a copy for user, status in users.copy().items(): if status == \u0026#39;inactive\u0026#39;: del users[user] ## Strategy: Create a new collection active_users = {} for user, status in users.items(): if status == \u0026#39;active\u0026#39;: active_users[user] = status range range函数生成数字序列：\n\u0026gt;\u0026gt;\u0026gt; for i in range(5): ... print(i) ... 0 1 2 3 4 \u0026gt;\u0026gt;\u0026gt; list(range(5, 10)) [5, 6, 7, 8, 9] \u0026gt;\u0026gt;\u0026gt; list(range(0, 10, 3)) [0, 3, 6, 9] \u0026gt;\u0026gt;\u0026gt; list(range(-10, -100, -30)) [-10, -40, -70] \u0026gt;\u0026gt;\u0026gt; a = [\u0026#39;Mary\u0026#39;, \u0026#39;had\u0026#39;, \u0026#39;a\u0026#39;, \u0026#39;little\u0026#39;, \u0026#39;lamb\u0026#39;] \u0026gt;\u0026gt;\u0026gt; for i in range(len(a)): ... print(i, a[i]) ... 0 Mary 1 had 2 a 3 little 4 lamb \u0026gt;\u0026gt;\u0026gt; sum(range(4)) # 0 + 1 + 2 + 3 6 值得注意的是，range函数的返回类型并不是list类型，而是range对象\u0026lt;class 'range'\u0026gt;\n虽然它看起来像列表，但实际上它是一个更高效的内存对象，只在需要时生成序列中的数字。例如：\nrange(5) 返回 range(0, 5) type(range(5)) 返回\u0026lt;class 'range'\u0026gt; 如果需要列表，可以使用list()函数进行转换：list(range(5))会返回[0, 1, 2, 3, 4]。\nbreak和continue \u0026gt;\u0026gt;\u0026gt; for n in range(2, 10): ... for x in range(2, n): ... if n % x == 0: ... print(f\u0026#34;{n} equals {x} * {n//x}\u0026#34;) ... break ... 4 equals 2 * 2 6 equals 2 * 3 8 equals 2 * 4 9 equals 3 * 3 \u0026gt;\u0026gt;\u0026gt; for num in range(2, 10): ... if num % 2 == 0: ... print(f\u0026#34;Found an even number {num}\u0026#34;) ... continue ... print(f\u0026#34;Found an odd number {num}\u0026#34;) ... Found an even number 2 Found an odd number 3 Found an even number 4 Found an odd number 5 Found an even number 6 Found an odd number 7 Found an even number 8 Found an odd number 9 else 这里的else不是if的else，而是在for循环或者while循环中的else：\n\u0026gt;\u0026gt;\u0026gt; for n in range(2, 10): ... for x in range(2, n): ... if n % x == 0: ... print(n, \u0026#39;equals\u0026#39;, x, \u0026#39;*\u0026#39;, n//x) ... break ... else: ... # loop fell through without finding a factor ... print(n, \u0026#39;is a prime number\u0026#39;) ... 2 is a prime number 3 is a prime number 4 equals 2 * 2 5 is a prime number 6 equals 2 * 3 7 is a prime number 8 equals 2 * 4 9 equals 3 * 3 在for循环中，循环结束时，会执行else。在while循环中，循环条件变为false后，会执行else。比如执行了break就不会执行else，没有执行break就会执行else。\npass pass什么都不做，可以在写代码时临时占个位置：\nclass MyEmptyClass: pass def initlog(*args): pass # Remember to implement this! match match语句跟C、Java的switch语句有点像，跟Rust、Haskell的模型匹配有点像。\ndef http_error(status): match status: case 400: return \u0026#34;Bad request\u0026#34; case 404: return \u0026#34;Not found\u0026#34; case 418: return \u0026#34;I\u0026#39;m a teapot\u0026#34; case _: return \u0026#34;Something\u0026#39;s wrong with the internet\u0026#34; 注意最后一个case的_是通配符，能匹配任何值。\n多个值可以用或：\ncase 401 | 403 | 404: return \u0026#34;Not allowed\u0026#34; 也能匹配元组并解包：\n## point is an (x, y) tuple match point: case (0, 0): print(\u0026#34;Origin\u0026#34;) case (0, y): print(f\u0026#34;Y={y}\u0026#34;) case (x, 0): print(f\u0026#34;X={x}\u0026#34;) case (x, y): print(f\u0026#34;X={x}, Y={y}\u0026#34;) case _: raise ValueError(\u0026#34;Not a point\u0026#34;) 还能匹配类对象：\nclass Point: def __init__(self, x, y): self.x = x self.y = y def where_is(point): match point: case Point(x=0, y=0): print(\u0026#34;Origin\u0026#34;) case Point(x=0, y=y): print(f\u0026#34;Y={y}\u0026#34;) case Point(x=x, y=0): print(f\u0026#34;X={x}\u0026#34;) case Point(): print(\u0026#34;Somewhere else\u0026#34;) case _: print(\u0026#34;Not a point\u0026#34;) 可以通过设置类的__match_args__特殊属性来定义模式匹配时的位置参数顺序。例如：\nfrom dataclasses import dataclass @dataclass class Point: x: int y: int __match_args__ = (\u0026#34;x\u0026#34;, \u0026#34;y\u0026#34;) # 定义模式匹配时的参数顺序 ## 以下三种模式匹配方式是等价的： point = Point(1, 2) match point: case Point(1, var): # 使用位置参数 print(f\u0026#34;y is {var}\u0026#34;) case Point(x=1, y=var): # 使用关键字参数 print(f\u0026#34;y is {var}\u0026#34;) case Point(y=var, x=1): # 使用关键字参数 print(f\u0026#34;y is {var}\u0026#34;) case Point(1, y=var): # 混合使用 print(f\u0026#34;y is {var}\u0026#34;) 当设置了__match_args__后，模式匹配会按照指定的顺序(x, y)来解析位置参数。这在处理复杂数据结构时能提供更灵活的模式匹配方式。\ncase还能加if：\nmatch point: case Point(x, y) if x == y: print(f\u0026#34;Y=X at {x}\u0026#34;) case Point(x, y): print(f\u0026#34;Not on the diagonal\u0026#34;) 元组和列表模式可以匹配任意序列(除了迭代器和字符串)，这与解包赋值的行为一致。示例：\ndef check_sequence(seq): match seq: case [1, 2, *rest]: # 匹配任何以1,2开头的序列(除迭代器和字符串) print(f\u0026#34;匹配列表/元组，剩余元素: {rest}\u0026#34;) case (1, 2, *rest): # 与列表模式效果相同 print(f\u0026#34;匹配元组/列表，剩余元素: {rest}\u0026#34;) case _: print(\u0026#34;不匹配\u0026#34;) check_sequence([1, 2, 3, 4]) # 匹配 check_sequence((1, 2, 5, 6)) # 匹配 check_sequence(range(1, 100)) # 不匹配(迭代器) check_sequence(\u0026#34;1234\u0026#34;) # 不匹配(字符串) enum的match：\nfrom enum import Enum class Color(Enum): RED = \u0026#39;red\u0026#39; GREEN = \u0026#39;green\u0026#39; BLUE = \u0026#39;blue\u0026#39; color = Color(input(\u0026#34;Enter your choice of \u0026#39;red\u0026#39;, \u0026#39;blue\u0026#39; or \u0026#39;green\u0026#39;: \u0026#34;)) match color: case Color.RED: print(\u0026#34;I see red!\u0026#34;) case Color.GREEN: print(\u0026#34;Grass is green\u0026#34;) case Color.BLUE: print(\u0026#34;I\u0026#39;m feeling the blues :(\u0026#34;) 补充下，使用Enum枚举类相比直接定义类变量有以下主要优势：\n类型安全：Enum成员是单例的，确保不会创建重复值，而类变量可以被随意修改\n防止值冲突：Enum会自动处理重复值问题，比如：\nclass Color(Enum): RED = 1 CRIMSON = 1 # 会被视为RED的别名 迭代支持：Enum类可以直接迭代所有成员\nfor color in Color: print(color) 值验证：Enum会自动验证值的唯一性，防止意外覆盖\n功能扩展：Enum提供了额外方法如：\nColor.RED.name 获取成员名称 Color.RED.value 获取成员值 Color['RED'] 通过名称访问成员 语义明确：明确表达了这是一组有限的、预定义的常量集合\n防止实例化：Enum类不能被实例化，保证了常量使用的正确性\n函数 关于函数，已经在Python入门系列和进阶系列文章介绍了，这里摘取一些官网有意思的点。\n一、函数如果没有return语句，实际上也会返回None：\n\u0026gt;\u0026gt;\u0026gt; fib(0) \u0026gt;\u0026gt;\u0026gt; print(fib(0)) None 而对于迭代器来说，迭代器协议要求迭代器必须实现__iter__()和__next__()方法。当迭代器耗尽时，next()方法会抛出StopIteration异常，而不是返回None。这是迭代器与普通函数返回值的一个重要区别。\n比如：\ndef my_range(n): i = 0 while i \u0026lt; n: yield i i += 1 for num in my_range(3): print(num) # 输出0,1,2 gen = my_range(3) print(next(gen)) # 0 print(next(gen)) # 1 print(next(gen)) # 2 print(next(gen)) # 抛出StopIteration异常，而不是返回None 这与普通函数返回None的行为形成对比，展示了Python中不同的控制流机制。\n二、默认参数值在函数定义时只被求值一次。当默认参数是可变对象（如列表、字典或类实例）时，这个特性会导致默认参数在多次函数调用间共享状态。比如：\ndef f(a, L=[]): L.append(a) return L print(f(1)) print(f(2)) print(f(3)) 输出：\n[1] [1, 2] [1, 2, 3] 如果不想共享状态，可以这样写：\ndef f(a, L=None): if L is None: L = [] L.append(a) return L 三、函数参数可以是positional-only或keyword-only：\ndef f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): ----------- ---------- ---------- | | | | Positional or keyword | | - Keyword only -- Positional only 示例：\ndef standard_arg(arg): print(arg) def pos_only_arg(arg, /): print(arg) def kwd_only_arg(*, arg): print(arg) def combined_example(pos_only, /, standard, *, kwd_only): print(pos_only, standard, kwd_only) 参考资料：\nhttps://docs.python.org/3.13/tutorial/controlflow.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000035-python3.13%E5%AE%98%E7%BD%91%E5%AD%A6%E4%B9%A0%E4%B9%8B%E6%8E%A7%E5%88%B6%E6%B5%81/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"if\"\u003eif\u003c/h1\u003e\n\u003cp\u003eif语句是最常见的控制流：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; x = int(input(\u0026#34;Please enter an integer: \u0026#34;))\nPlease enter an integer: 42\n\u0026gt;\u0026gt;\u0026gt; if x \u0026lt; 0:\n...     x = 0\n...     print(\u0026#39;Negative changed to zero\u0026#39;)\n... elif x == 0:\n...     print(\u0026#39;Zero\u0026#39;)\n... elif x == 1:\n...     print(\u0026#39;Single\u0026#39;)\n... else:\n...     print(\u0026#39;More\u0026#39;)\n... \nMore\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"for\"\u003efor\u003c/h1\u003e\n\u003cp\u003efor语句用来遍历：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## Create a sample collection\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eusers \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Hans\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;active\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Éléonore\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;inactive\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;景太郎\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;active\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## Strategy:  Iterate over a copy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e user\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e status \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e users\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecopy\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eitems\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e status \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;inactive\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003edel\u003c/span\u003e users\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003euser\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## Strategy:  Create a new collection\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eactive_users \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e user\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e status \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e users\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eitems\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e status \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;active\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        active_users\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003euser\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e status\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"range\"\u003erange\u003c/h1\u003e\n\u003cp\u003erange函数生成数字序列：\u003c/p\u003e","title":"官网学习之控制流"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n开篇 个人而言，Python是写起来最顺手、看起来最喜欢的编程语言，也是实际工作中使用最多的编程语言。从Python入门，到完全使用Java，再回归Python，编程语言本质上是工具，与Excel、XMind无异。对于测试开发来说，要学习的东西很多，Python是可以作为基础技术深入学习的。第1次通过菜鸟教程学习Python，第2次阅读《流畅的Python》学习Python，第3次跟着官网学习Python。该系列文章参考的是Python3.13版本官方教程，期望学习者具有一定编程基础。\nPython解释器 终端使用uv安装Python3.13：\nuv python install 3.13 查看安装位置：\nwhich python3.13 进入Python解释器：\n$ python3.13 Python 3.13 (default, April 4 2023, 09:25:04) [GCC 10.2.0] on linux Type \u0026#34;help\u0026#34;, \u0026#34;copyright\u0026#34;, \u0026#34;credits\u0026#34; or \u0026#34;license\u0026#34; for more information. \u0026gt;\u0026gt;\u0026gt; 计算器 平时我们基本不会这样来写Python，但是可以当做计算器来用：\n\u0026gt;\u0026gt;\u0026gt; 2 + 2 4 \u0026gt;\u0026gt;\u0026gt; 50 - 5*6 20 \u0026gt;\u0026gt;\u0026gt; (50 - 5*6) / 4 5.0 \u0026gt;\u0026gt;\u0026gt; 8 / 5 # division always returns a floating-point number 1.6 \u0026gt;\u0026gt;\u0026gt; 17 / 3 # classic division returns a float 5.666666666666667 \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; 17 // 3 # floor division discards the fractional part 5 \u0026gt;\u0026gt;\u0026gt; 17 % 3 # the % operator returns the remainder of the division 2 \u0026gt;\u0026gt;\u0026gt; 5 * 3 + 2 # floored quotient * divisor + remainder 17 \u0026gt;\u0026gt;\u0026gt; 5 ** 2 # 5 squared 25 \u0026gt;\u0026gt;\u0026gt; 2 ** 7 # 2 to the power of 7 128 \u0026gt;\u0026gt;\u0026gt; width = 20 \u0026gt;\u0026gt;\u0026gt; height = 5 * 9 \u0026gt;\u0026gt;\u0026gt; width * height 900 \u0026gt;\u0026gt;\u0026gt; 4 * 3.75 - 1 14.0 \u0026gt;\u0026gt;\u0026gt; tax = 12.5 / 100 \u0026gt;\u0026gt;\u0026gt; price = 100.50 \u0026gt;\u0026gt;\u0026gt; price * tax 12.5625 \u0026gt;\u0026gt;\u0026gt; price + _ # In interactive mode, the last printed expression is assigned to the variable _ 113.0625 \u0026gt;\u0026gt;\u0026gt; round(_, 2) 113.06 Python也很适合用来学习算法，比如最简单的冒泡排序：\ndef bubble_sort(arr: list) -\u0026gt; list: n = len(arr) for i in range(n): for j in range(n - 1 - i): if arr[j] \u0026gt; arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] return arr 参考资料：\nhttps://docs.python.org/3.13/tutorial/index.html\nhttps://docs.python.org/3.13/tutorial/appetite.html\nhttps://docs.python.org/3.13/tutorial/interpreter.html\nhttps://docs.python.org/3.13/tutorial/introduction.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000034-python3.13%E8%B7%9F%E7%9D%80%E5%AE%98%E7%BD%91%E5%AD%A6%E4%B9%A0python%E7%B3%BB%E5%88%97/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"开篇\"\u003e开篇\u003c/h1\u003e\n\u003cp\u003e个人而言，Python是写起来最顺手、看起来最喜欢的编程语言，也是实际工作中使用最多的编程语言。从Python入门，到完全使用Java，再回归Python，编程语言本质上是工具，与Excel、XMind无异。对于测试开发来说，要学习的东西很多，Python是可以作为基础技术深入学习的。第1次通过菜鸟教程学习Python，第2次阅读《流畅的Python》学习Python，第3次跟着官网学习Python。该系列文章参考的是Python3.13版本官方教程，期望学习者具有一定编程基础。\u003c/p\u003e\n\u003ch1 id=\"python解释器\"\u003ePython解释器\u003c/h1\u003e\n\u003cp\u003e终端使用uv安装Python3.13：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003euv python install 3.13\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看安装位置：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ewhich python3.13\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e进入Python解释器：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e$ python3.13\nPython 3.13 (default, April 4 2023, 09:25:04)\n[GCC 10.2.0] on linux\nType \u0026#34;help\u0026#34;, \u0026#34;copyright\u0026#34;, \u0026#34;credits\u0026#34; or \u0026#34;license\u0026#34; for more information.\n\u0026gt;\u0026gt;\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"计算器\"\u003e计算器\u003c/h1\u003e\n\u003cp\u003e平时我们基本不会这样来写Python，但是可以当做计算器来用：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; 2 + 2\n4\n\u0026gt;\u0026gt;\u0026gt; 50 - 5*6\n20\n\u0026gt;\u0026gt;\u0026gt; (50 - 5*6) / 4\n5.0\n\u0026gt;\u0026gt;\u0026gt; 8 / 5  # division always returns a floating-point number\n1.6\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; 17 / 3  # classic division returns a float\n5.666666666666667\n\u0026gt;\u0026gt;\u0026gt;\n\u0026gt;\u0026gt;\u0026gt; 17 // 3  # floor division discards the fractional part\n5\n\u0026gt;\u0026gt;\u0026gt; 17 % 3  # the % operator returns the remainder of the division\n2\n\u0026gt;\u0026gt;\u0026gt; 5 * 3 + 2  # floored quotient * divisor + remainder\n17\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; 5 ** 2  # 5 squared\n25\n\u0026gt;\u0026gt;\u0026gt; 2 ** 7  # 2 to the power of 7\n128\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; width = 20\n\u0026gt;\u0026gt;\u0026gt; height = 5 * 9\n\u0026gt;\u0026gt;\u0026gt; width * height\n900\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; 4 * 3.75 - 1\n14.0\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; tax = 12.5 / 100\n\u0026gt;\u0026gt;\u0026gt; price = 100.50\n\u0026gt;\u0026gt;\u0026gt; price * tax\n12.5625\n\u0026gt;\u0026gt;\u0026gt; price + _  # In interactive mode, the last printed expression is assigned to the variable _\n113.0625\n\u0026gt;\u0026gt;\u0026gt; round(_, 2)\n113.06\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003ePython也很适合用来学习算法，比如最简单的冒泡排序：\u003c/p\u003e","title":"跟着官网学习Python系列"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文是Python进阶系列专题的最后一篇文章了，学习一下Python并发编程。懂得并发编程，就算是真正进阶了。\nPython多线程 Python有真正的多线程吗？我本来以为是没有的。因为CPython解释器本身就不是线程安全的，因此有全局解释器锁（GIL），一次只允许使用一个线程执行Python字节码。换句话说，Python即便是有多线程，也会受GIL限制，按顺序执行。所以我就以为Python是没有多线程的，也就是一个Python进程不能同时使用多个CPU核心。然而，Python标准库中所有执行阻塞型I/O操作的函数，在等待操作系统返回结果时，都会释放GIL，允许其他线程运行。这就意味着Python线程在I/O密集型应用中还是可以发挥作用的，比如一个Python线程等待网络响应时，阻塞型I/O函数会释放GIL，再运行一个线程。再比如time.sleep()函数也会释放GIL。\nPython多进程 但是对于CPU密集型应用来说，要想利用所有可用的CPU核心，就得使用多进程，规避GIL。\n多线程与多进程对比 什么时候用多线程？什么时候用多进程？\nI/O密集型应用使用多线程，CPU密集型应用使用多进程。\n什么是I/O密集型应用？什么是CPU密集型应用？\n简单来说，I/O密集一般涉及到网络、磁盘IO。而CPU密集指的是计算比较多。\n创建多线程可以使用concurrent.futures，创建多进程可以使用multiprocessing。\n多线程与协程对比 多线程存在着切换开销，同时为了避免写变量冲突，在控制共享资源时需要加锁，因此编写程序会比较复杂比较困难。而协程是通过单线程实现的并发，既能自由切换，也不需要锁机制，执行效率要高很多。\n多线程和协程有个共同点是只对I/O密集型应用有效，因为GIL限制。如果想处理CPU密集型应用，那么可以结合多进程一起使用，以提高CPU使用率。\nasyncio包 asyncio包比较多用来实现Python协程并发，原书在这一章节引用了很多示例，穿插了很多代码，导致我看起来有点乱，不是很清楚到底该怎么使用这个包。所以我看了一下官方文档：\nhttps://docs.python.org/3/library/asyncio.html\n从Python3.5开始引入了async和await，替代了@asyncio.coroutine和yield from语法，语义更简洁更明确了。并且从Python3.7开始引入了asyncio.run()，替代了这一串代码：\nloop = asyncio.get_event_loop() try: loop.run_until_complete(main()) finally: loop.close() 创建task的语法也发生了变化，可以用asyncio.create_task：\nasync def coro(): ... ## In Python 3.7+ task = asyncio.create_task(coro()) ... ## This works in all Python versions but is less readable task = asyncio.ensure_future(coro()) ... 对于多个并行task可以用asyncio.gather，替代asyncio.wait：\ntask_list = [] for i in range(5): task = asyncio.create_task(async_func(i)) task_list.append(task) done, pending = await asyncio.wait(task_list, timeout=None) for done_task in done: print((f\u0026#34;[{current_time()}] 得到执行结果 {done_task.result()}\u0026#34;)) task_list = [] for i in range(5): task = asyncio.create_task(func(i)) task_list.append(task) results = await asyncio.gather(*task_list) for result in results: print((f\u0026#34;[{current_time()}] 得到执行结果 {result}\u0026#34;)) 所以对于第三方包的学习，最好是看看最新的官方文档，说不定已经进行了很多优化。\n最后，完结，撒花。\n我会把《流畅的Python》这本书送给大家，感谢一直以来的关注。\n参考资料：\n《流畅的Python》 第17章使用future处理并发 第18章使用asyncio包处理并发\nhttps://blog.csdn.net/youanyyou/article/details/78990156\nhttps://zhuanlan.zhihu.com/p/104918655\nhttps://www.cnblogs.com/liugp/p/11072823.html\nhttps://www.jianshu.com/p/6872bf356af7\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000033-pythonpython%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E8%BF%9B%E9%98%B6/","summary":"本文是Python进阶系列专题的最后一篇文章了，学习一下Python并发编程。懂得并发编程，就算是真正进阶了。","title":"Python并发编程从入门到进阶"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n从概念上来说，我们都知道多进程和多线程，而协程其实是在单线程中实现多并发。从句法上看，协程与生成器类似，都是定义体中包含yield关键字的函数。区别在于协程的yield通常出现在表达式的右边：datum = yield。这一下就让初学者瞬间觉得yield关键字不香了，本来以为yield就是简简单单的暂停执行顺手返回个值，结果还能放右边？\n从生成器到协程 先看一个可能是协程最简单的使用示例：\n\u0026gt;\u0026gt;\u0026gt; def simple_coroutine(): ... print(\u0026#34;-\u0026gt; coroutine started\u0026#34;) ... x = yield ... print(\u0026#34;-\u0026gt; coroutine received:\u0026#34;, x) ... \u0026gt;\u0026gt;\u0026gt; my_coro = simple_coroutine() \u0026gt;\u0026gt;\u0026gt; my_coro \u0026lt;generator object simple_coroutine at 0x0000019A681F27B0\u0026gt; \u0026gt;\u0026gt;\u0026gt; next(my_coro) -\u0026gt; coroutine started \u0026gt;\u0026gt;\u0026gt; my_coro.send(42) -\u0026gt; coroutine received: 42 Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; StopIteration 之所以yield可以放右边，是因为协程可以接收调用方使用.send()推送的值。\nyield放在右边以后，它的右边还能再放个表达式，请看下面这个例子：\ndef simple_coro2(a): b = yield a c = yield a + b my_coro2 = simple_coro2(14) next(my_coro2) my_coro2.send(28) my_coro2.send(99) 执行过程是：\n调用next(my_coro2)，执行yield a，产出14。 调用my_coro2.send(28)，把28赋值给b，然后执行yield a + b，产出42。 调用my_coro2.send(99)，把99赋值给c，协程终止。 由此得出结论，对于b = yield a这行代码来说，= 右边的代码在赋值之前执行。\n在示例中，需要先调用next(my_coro)启动生成器，让程序在yield语句处暂停，然后才可以发送数据。这是因为协程有四种状态：\n\u0026lsquo;GEN_CREATED\u0026rsquo; 等待开始执行 \u0026lsquo;GEN_RUNNING\u0026rsquo; 解释器正在执行 \u0026lsquo;GEN_SUSPENDED\u0026rsquo; 在yield表达式处暂停 \u0026lsquo;GEN_CLOSED\u0026rsquo; 执行结束 只有在GEN_SUSPENDED状态才能发送数据，提前做的这一步叫做预激，既可以调用next(my_coro)预激，也可以调用my_coro.send(None)预激，效果一样。\n预激协程 协程必须预激才能使用，也就是send前，先调用next，让协程处于GEN_SUSPENDED状态。但是这件事经常会忘记。为了避免忘记，可以定义一个预激装饰器，比如：\nfrom functools import wraps def coroutine(func): @wraps(func) def primer(*args, **kwargs): gen = func(*args, **kwargs) next(gen) return gen return primer 但实际上Python给出了一个更优雅的方式，叫做yield from，它会自动预激协程。\n自定义预激装饰器和yield from是不兼容的。\nyield from yield from相当于其他语言中的await关键字，作用是：在生成器gen中使用yield from subgen()时，subgen会获得控制权，把产出的值传给gen的调用方，即调用方可以直接控制subgen。与此同时，gen会阻塞，等待subgen终止。\nyield from可以用来简化for循环中的yield：\nfor c in \u0026#34;AB\u0026#34;: yield c yield from \u0026#34;AB\u0026#34; yield from x表达式对x做的第一件事就是，调用iter(x)，从中获取迭代器。\n但yield from的作用远不止于此，它更重要的作用是打开双向通道。如下图所示：\n这个图信息量很大，很难理解。\n首先要理解这3个概念：调用方、委派生成器、子生成器。\n调用方\n说白了就是main函数，也就是众所周知的程序入口main函数。\n# the client code, a.k.a. the caller def main(data): # \u0026lt;8\u0026gt; results = {} for key, values in data.items(): group = grouper(results, key) # \u0026lt;9\u0026gt; next(group) # \u0026lt;10\u0026gt; for value in values: group.send(value) # \u0026lt;11\u0026gt; group.send(None) # important! \u0026lt;12\u0026gt; # print(results) # uncomment to debug report(results) 委派生成器\n就是包含了yield from语句的函数，也就是协程。\n# the delegating generator def grouper(results, key): # \u0026lt;5\u0026gt; while True: # \u0026lt;6\u0026gt; results[key] = yield from averager() # \u0026lt;7\u0026gt; 子生成器\n就是yield from语句右边跟着的子协程。\n# the subgenerator def averager(): # \u0026lt;1\u0026gt; total = 0.0 count = 0 average = None while True: term = yield # \u0026lt;2\u0026gt; if term is None: # \u0026lt;3\u0026gt; break total += term count += 1 average = total/count return Result(count, average) # \u0026lt;4\u0026gt; 这比术语看着舒服多了。\n然后是5条线：send、yield、throw、StopIteration、close。\nsend\n协程在yield from表达式处暂停时，main函数可以通过yield from表达式把数据发给yield from语句右边跟着的子协程。\nyield\nyield from语句右边跟着的子协程再把产出的值通过yield from表达式发给main函数。\nthrow\nmain函数通过group.send(None)，传入一个None值，让yield from语句右边跟着的子协程的while循环终止，这样控制权才会交回协程，才能继续执行，否则会一直暂在yield from语句暂停。\nStopIteration\nyield from语句右边跟着的生成器函数返回之后，解释器会抛出StopIteration异常。并把返回值附加到异常对象上，此时协程会恢复。\nclose\nmain函数执行完以后，会调用close()方法退出协程。\n大体流程搞清楚了，更多的技术细节就不继续研究了，有时间的话，在以后的Python原理系列中再学习吧。\nyield from经常与Python3.4标准库里的@asyncio.coroutine装饰器结合使用。\n协程用作累加器 这是协程的常见用途，代码如下：\ndef averager(): total = 0.0 count = 0 average = None while True: # \u0026lt;1\u0026gt; term = yield average # \u0026lt;2\u0026gt; total += term count += 1 average = total/count 协程实现并发 这里例子有点复杂，源码地址是：\nhttps://github.com/fluentpython/example-code/blob/master/16-coroutine/taxi_sim.py\n核心代码片段是：\n## BEGIN TAXI_PROCESS def taxi_process(ident, trips, start_time=0): # \u0026lt;1\u0026gt; \u0026#34;\u0026#34;\u0026#34;Yield to simulator issuing event at each state change\u0026#34;\u0026#34;\u0026#34; time = yield Event(start_time, ident, \u0026#39;leave garage\u0026#39;) # \u0026lt;2\u0026gt; for i in range(trips): # \u0026lt;3\u0026gt; time = yield Event(time, ident, \u0026#39;pick up passenger\u0026#39;) # \u0026lt;4\u0026gt; time = yield Event(time, ident, \u0026#39;drop off passenger\u0026#39;) # \u0026lt;5\u0026gt; yield Event(time, ident, \u0026#39;going home\u0026#39;) # \u0026lt;6\u0026gt; # end of taxi process # \u0026lt;7\u0026gt; ## END TAXI_PROCESS def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, seed=None): \u0026#34;\u0026#34;\u0026#34;Initialize random generator, build procs and run simulation\u0026#34;\u0026#34;\u0026#34; if seed is not None: random.seed(seed) # get reproducible results taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL) for i in range(num_taxis)} sim = Simulator(taxis) sim.run(end_time) 这个示例说明了如何在一个主循环中处理事件，以及如何通过发送数据驱动协程。这是asyncio包底层的基本思想。使用协程代替线程和回调，实现并发。\n参考资料：\n《流畅的Python》第16章 协程\nhttps://zhuanlan.zhihu.com/p/104918655\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000032-pythonpython%E5%8D%8F%E7%A8%8B%E7%9F%A5%E5%A4%9A%E5%B0%91/","summary":"从概念上来说，我们都知道多进程和多线程，而协程其实是\u003cstrong\u003e在单线程中实现多并发\u003c/strong\u003e。从句法上看，协程与生成器类似，都是定义体中包含yield关键字的函数。区别在于协程的yield通常出现在表达式的右边：\u003ccode\u003edatum = yield\u003c/code\u003e。这一下就让初学者瞬间觉得yield关键字不香了，本来以为yield就是简简单单的暂停执行顺手返回个值，结果还能放右边？","title":"Python协程知多少"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nwith语句会设置一个临时的上下文，交给上下文管理器对象控制，并且负责清理上下文。\n比如在打开文件时通常都会使用with语句：\nwith open(\u0026#34;a.txt\u0026#34;) as f: f.read() with块执行完后会自动关闭文件。\nPython上下文管理器对象存在的目的就是管理with语句。\n实现一个上下文管理器 上下文管理器协议包含__enter__和__exit__两个方法，所以要实现一个上下文管理器，就得实现这两个方法，比如：\nclass LookingGlass: def __enter__(self): import sys self.original_write = sys.stdout.write sys.stdout.write = self.reverse_write return \u0026#39;JABBERWOCKY\u0026#39; def reverse_write(self, text): self.original_write(text[::-1]) def __exit__(self, exc_type, exc_value, traceback): import sys sys.stdout.write = self.original_write if exc_type is ZeroDivisionError: print(\u0026#39;Please DO NOT divide by zero!\u0026#39;) return True __enter__是上下文管理器的入口，在with语句开始运行时调用。 __exit__是上下文管理器的出口，在with语句运行结束后调用。 上下文管理器对象是在执行with后面的表达式得到的：\nwith LookingGlass() as what: print(what) as子句是可选的，在as执行前就已经得到了上下文管理器对象，在as执行时实际就已经调用__enter__了。\n把生成器变为上下文管理器 使用@contextmanager装饰器能减少创建上下文管理器的样板代码量，只需要实现一个有yield语句的生成器，生成想让__enter__方法返回的值。\n示例：\nimport contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write msg = \u0026#39;\u0026#39; try: yield \u0026#39;JABBERWOCKY\u0026#39; except ZeroDivisionError: msg = \u0026#39;Please DO NOT divide by zero!\u0026#39; finally: sys.stdout.write = original_write if msg: print(msg) yield语句的作用是：\n在yield语句前面的代码在with块开始时执行，相当于__enter__。 在yield语句后面的代码在with块结束时执行，相当于__exit__。 注意这里的yield与迭代没有任何关系，这其实引出了Python另一个重要技术点，协程：执行到某一点时暂停，让客户代码运行，直到客户让协程继续做事。下篇文章将展开对Python协程的学习。\n参考资料：\n《流畅的Python》第15章 上下文管理器和else块\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000031-pythonpython%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AE%A1%E7%90%86%E5%99%A8/","summary":"with语句会设置一个临时的上下文，交给上下文管理器对象控制，并且负责清理上下文。","title":"Python上下文管理器"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPython生成器是什么 一句话解释：包含了yield关键字的函数就是生成器，它的返回值是一个生成器对象。我简单画了个示意图：\nyield相当于return。 函数遇到yield就暂停，保存当前信息，返回yield的值。 在下次执行next()时，从当前位置继续执行。 比较有意思的事情是，曾经有人建议生成器函数不应该使用def，而应该发明一个新的关键字比如gen，但是Python之父Guido并没有同意这样做。\n生成器函数的工作原理 先通过一个简单示例来说明生成器的行为：\n## 定义一个生成器 \u0026gt;\u0026gt;\u0026gt; def gen_123(): ... yield 1 ... yield 2 ... yield 3 ... ## 生成器本身是个函数 \u0026gt;\u0026gt;\u0026gt; gen_123 \u0026lt;function gen_123 at 0x0000019F60710790\u0026gt; ## 返回值是生成器对象 \u0026gt;\u0026gt;\u0026gt; gen_123() \u0026lt;generator object gen_123 at 0x0000019F606AC040\u0026gt; ## 生成器也是迭代器 \u0026gt;\u0026gt;\u0026gt; for i in gen_123(): ... print(i) ... 1 2 3 ## 验证生成器也是迭代器，定义迭代器g \u0026gt;\u0026gt;\u0026gt; g = gen_123() ## 可以通过next()获取yield生成的下一个元素 \u0026gt;\u0026gt;\u0026gt; next(g) 1 \u0026gt;\u0026gt;\u0026gt; next(g) 2 \u0026gt;\u0026gt;\u0026gt; next(g) 3 \u0026gt;\u0026gt;\u0026gt; next(g) Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; StopIteration 生成器的原理就是：\n生成器函数会创建一个生成器对象。 把生成器传给next()函数时，生成器函数会执行函数定义体中的下一个yield语句，返回产出的值，并在当前位置暂停。 函数的定义体返回时，外层的生成器对象会抛出StopIteration异常 yield关键字一般是和for循环搭配使用的，在for循环中会隐式调用next()函数。\n生成器的作用其实是解决内存的问题，比如我们都知道Python的正则表达式有一个re.findall()函数，它会把所有匹配到的元素都一次性写入内存中，假如匹配到的数据很多，就会占用大量的内存。为了解决这个问题，Python3有一个re.finditer()函数，返回的就是一个生成器，取值时才生成数据放入内存中，能节省大量内存。\n标准库中的生成器函数 实现生成器时要知道标准库中有什么可用，否则很可能会重新发明轮子。有些是内置的，有些在itertools模块中，有些functools模块中。\n用于过滤的生成器函数\n从输入的可迭代对象中产出元素的子集，而且不修改元素本身。\n用于映射的生成器函数\n在输入的单个可迭代对象中的各个元素上做计算，然后返回结果。\n合并多个可迭代对象的生成器函数\n从输入的多个可迭代对象中产出元素。\n把输入的各个元素扩展成多个输出元素的生成器函数\n从一个元素中产出多个值，扩展输入的可迭代对象。\n用于重新排列元素的生成器函数\n产出输入的可迭代对象中的全部元素，不过会以某种方式重新排列。\nyield from yield from是Python3.3新出现的句法，它的作用是把不同的生成器结合在一起使用。\n比如生成器函数需要产出另一个生成器生成的值，传统的解决办法是使用for循环：\ndef chain(*iterables): for it in iterables: for i in it: yield i s = \u0026#34;ABC\u0026#34; t = tuple(range(3)) print(list(chain(s, t))) # [\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;, 0, 1, 2] 改成yield from：\ndef chain(*iterables): for it in iterables: yield from i 完全代替了内层的for循环。\n参考资料：\n《流畅的Python》第14章 可迭代的对象、迭代器和生成器\nhttps://www.runoob.com/python3/python3-iterator-generator.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000030-python%E9%95%BF%E7%AF%87%E5%A4%A7%E8%AE%BApython%E7%94%9F%E6%88%90%E5%99%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"python生成器是什么\"\u003ePython生成器是什么\u003c/h1\u003e\n\u003cp\u003e一句话解释：\u003cstrong\u003e包含了yield关键字的函数就是生成器，它的返回值是一个生成器对象\u003c/strong\u003e。我简单画了个示意图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/844594b76a18450f830023cd07ea7bc2.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eyield相当于return。\u003c/li\u003e\n\u003cli\u003e函数遇到yield就暂停，保存当前信息，返回yield的值。\u003c/li\u003e\n\u003cli\u003e在下次执行next()时，从当前位置继续执行。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e比较有意思的事情是，曾经有人建议生成器函数不应该使用def，而应该发明一个新的关键字比如gen，但是Python之父Guido并没有同意这样做。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"生成器函数的工作原理\"\u003e生成器函数的工作原理\u003c/h1\u003e\n\u003cp\u003e先通过一个简单示例来说明生成器的行为：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 定义一个生成器\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003egen_123\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e...\u003c/span\u003e     \u003cspan style=\"color:#cf222e\"\u003eyield\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e...\u003c/span\u003e     \u003cspan style=\"color:#cf222e\"\u003eyield\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e...\u003c/span\u003e     \u003cspan style=\"color:#cf222e\"\u003eyield\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 生成器本身是个函数\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e gen_123\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003efunction gen_123 at \u003cspan style=\"color:#0550ae\"\u003e0x0000019F60710790\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 返回值是生成器对象\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e gen_123\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003egenerator \u003cspan style=\"color:#6639ba\"\u003eobject\u003c/span\u003e gen_123 at \u003cspan style=\"color:#0550ae\"\u003e0x0000019F606AC040\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 生成器也是迭代器\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e i \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e gen_123\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e...\u003c/span\u003e     \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e...\u003c/span\u003e     \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 验证生成器也是迭代器，定义迭代器g\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e g \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e gen_123\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 可以通过next()获取yield生成的下一个元素\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003enext\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eg\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003enext\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eg\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003enext\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eg\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003enext\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eg\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eTraceback \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emost recent call last\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  File \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026lt;input\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e line \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003emodule\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eStopIteration\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e生成器的原理就是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e生成器函数会创建一个生成器对象。\u003c/li\u003e\n\u003cli\u003e把生成器传给next()函数时，生成器函数会执行函数定义体中的下一个yield语句，返回产出的值，并在当前位置暂停。\u003c/li\u003e\n\u003cli\u003e函数的定义体返回时，外层的生成器对象会抛出StopIteration异常\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eyield关键字一般是和for循环搭配使用的，在for循环中会隐式调用next()函数。\u003c/p\u003e","title":"长篇大论Python生成器"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n什么是迭代？迭代是指按需一次获取一个数据。是否可以迭代，可以通过是否可以使用for循环取值来进行简单的判断。更准确的判断是使用iter()函数，这是一个Python内置函数。\n可迭代的对象 iter()函数的作用如下：\n如果对象实现了__iter__方法，那么就调用它，获取一个迭代器。比如：\ndef __iter__(self): return SentenceIterator(self.words) 如果对象没有实现__iter__但是实现了__getitem__方法，那么就创建一个迭代器，尝试从索引0开始获取元素。\n如果尝试获取元素失败，就会抛出TypeError异常。\n可迭代的对象，就是使用iter()函数判断，满足前面2点的对象。\n任何Python序列都是可以迭代的，因为它们都实现了__getitem__方法。\n迭代器 从前面iter()函数的作用可以发现，迭代器是从可迭代的对象中获取的。\n如果对象本身是可迭代的，就调用__iter__方法获取一个迭代器。 如果对象不可迭代但是实现了__getitem__方法，那么就会创建一个迭代器。 比如可以使用iter()函数把列表转换为迭代器：\n\u0026gt;\u0026gt;\u0026gt; test_list = [1, 2, 3] \u0026gt;\u0026gt;\u0026gt; print(type(test_list)) \u0026lt;class \u0026#39;list\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; test_iter = iter(test_list) \u0026gt;\u0026gt;\u0026gt; print(type(test_iter)) \u0026lt;class \u0026#39;list_iterator\u0026#39;\u0026gt; 迭代器可以使用for循环遍历：\nfor x in test_iter: print(x) 也可以使用while循环遍历：\nwhile True: try: print(next(test_iter)) except StopIteration: del test_iter break next()函数用于获取迭代器下一个元素。 没有元素了，迭代器会抛出StopIteration异常。 标准的迭代器接口有两个方法：\n__next__返回下一个元素。\n__iter__返回self，以便在应该使用可迭代对象的地方使用迭代器，比如for循环中。\ndef __iter__(self): return self 迭代器的准确定义是：迭代器是这样的对象，它实现了无参数的__next__方法，返回序列中的下一个元素；如果没有元素了，那么抛出StopIteration异常。Python中的迭代器还实现了__iter__方法，因此迭代器也是可以迭代的。\n最后，通过对比可以发现，可迭代对象的__iter__返回的是迭代器：\ndef __iter__(self): return SentenceIterator(self.words) 迭代器的__iter__返回的是self：\nclass SentenceIterator: def __iter__(self): return self 而且迭代器还需要有__next__方法。\n从这一点就能清楚看出它们的区别了。\n需要特别注意的是，可迭代的对象一定不能是自身的迭代器，也就是说，可迭代对象必须实现__iter__方法，但是不能实现__next__方法。否则会让设计模式变得混乱不堪。\n参考资料：\n《流畅的Python》第14章 可迭代的对象、迭代器和生成器\nhttps://www.runoob.com/python3/python3-iterator-generator.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000029-pythonpython%E5%8F%AF%E8%BF%AD%E4%BB%A3%E7%9A%84%E5%AF%B9%E8%B1%A1%E4%B8%8E%E8%BF%AD%E4%BB%A3%E5%99%A8%E7%9A%84%E5%AF%B9%E6%AF%94/","summary":"什么是迭代？迭代是指按需一次获取一个数据。是否可以迭代，可以通过是否可以使用for循环取值来进行简单的判断。更准确的判断是使用\u003ccode\u003eiter()\u003c/code\u003e函数，这是一个Python内置函数。","title":"Python可迭代的对象与迭代器的对比"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n我们写Python基本不需要自己创建抽象基类，而是通过鸭子类型来解决大部分问题。《流畅的Python》作者使用了15年Python，但只在项目中创建过一个抽象基类。我们更多时候是创建现有抽象基类的子类，或者使用现有的抽象基类注册。本文的意义在于，了解抽象基类的定义与使用，可以帮助我们理解抽象基类是如何实现的，为我们以后学习后端语言（比如Java、Golang）打下基础。毕竟抽象基类是编程语言通用设计。\n定义抽象基类的子类 先回顾下什么是抽象基类：Python的抽象基类是指必须让继承它的子类去实现它所要求的抽象方法的类。如下代码定义了抽象基类collections.MutableSequence的子类：\nimport collections Card = collections.namedtuple(\u0026#39;Card\u0026#39;, [\u0026#39;rank\u0026#39;, \u0026#39;suit\u0026#39;]) class FrenchDeck2(collections.MutableSequence): ranks = [str(n) for n in range(2, 11)] + list(\u0026#39;JQKA\u0026#39;) suits = \u0026#39;spades diamonds clubs hearts\u0026#39;.split() def __init__(self): self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks] def __len__(self): return len(self._cards) def __getitem__(self, position): return self._cards[position] def __setitem__(self, position, value): # \u0026lt;1\u0026gt; self._cards[position] = value def __delitem__(self, position): # \u0026lt;2\u0026gt; del self._cards[position] def insert(self, position, value): # \u0026lt;3\u0026gt; self._cards.insert(position, value) 通过抽象基类collections.MutableSequence源码：\n可以发现，它有三个抽象方法__setitem__、__delitem__、insert，所以FrenchDeck2类必须实现它们。而对于其他非抽象方法比如append、extend、pop等，则可以直接继承无需实现。\n注意，Python只会在运行时实例化FrenchDeck2类时真正检查抽象方法的实现，如果未实现会抛出TypeError异常，提示Can\u0026rsquo;t instantiate abstract class之类的。\n标准库中的抽象基类 为了知道哪些抽象基类可以使用，我们可以看看标准库。\ncollections.abc collections.abc的抽象基类如下图所示：\nIterable、Container、Sized\n这三个抽象基类是最基础的类，各个集合都继承了这三个抽象基类。\nItearble通过__iter__方法支持迭代 Container通过__contains__方法支持in运算符 Sized通过__len__方法支持len()函数 Sequence、Mapping、Set\n不可变集合类型，各自都有可变的子类。\nMappingView\n.items()、.keys()、.values()返回的对象分别是ItemsView、KeysView和ValuesView的实例。\nCallable、Hashable\n为内置函数isinstance提供支持，判断对象能不能调用或散列。\nIterator\n迭代器。\nnumbers numbers的抽象基类有以下几种：\nNumber Complex Real Rational Integral 这叫做数字塔，顶部是超类，底部是子类。比如使用isinstance(x, numbers.Integral)检查一个数是不是整数，这样代码就能接受int、bool（int的子类），再比如使用isinstance(x, numbers.Real)检查浮点数，这样代码就能接受bool、int、float、fractions.Fraction。\n定义抽象基类 **本小结可以跳过。**不过了解抽象基类的定义有助于阅读标准库和其他包中的抽象基类源码。\n抽象基类的示例代码如下：\n## BEGIN TOMBOLA_ABC import abc class Tombola(abc.ABC): # \u0026lt;1\u0026gt; @abc.abstractmethod def load(self, iterable): # \u0026lt;2\u0026gt; \u0026#34;\u0026#34;\u0026#34;Add items from an iterable.\u0026#34;\u0026#34;\u0026#34; @abc.abstractmethod def pick(self): # \u0026lt;3\u0026gt; \u0026#34;\u0026#34;\u0026#34;Remove item at random, returning it. This method should raise `LookupError` when the instance is empty. \u0026#34;\u0026#34;\u0026#34; def loaded(self): # \u0026lt;4\u0026gt; \u0026#34;\u0026#34;\u0026#34;Return `True` if there\u0026#39;s at least 1 item, `False` otherwise.\u0026#34;\u0026#34;\u0026#34; return bool(self.inspect()) # \u0026lt;5\u0026gt; def inspect(self): \u0026#34;\u0026#34;\u0026#34;Return a sorted tuple with the items currently inside.\u0026#34;\u0026#34;\u0026#34; items = [] while True: # \u0026lt;6\u0026gt; try: items.append(self.pick()) except LookupError: break self.load(items) # \u0026lt;7\u0026gt; return tuple(sorted(items)) ## END TOMBOLA_ABC 要点：\n继承abc.ABC 使用@abc.abstractmethod装饰器标记抽象方法 抽象基类也可以包含普通方法 抽象基类的子类必须覆盖抽象方法（普通方法可以不覆盖），可以使用super()函数调用抽象方法，为它添加功能，而不是从头开始实现 再看白鹅类型 白鹅类型的定义有一点难以理解，如果理解了虚拟子类，就能加快理解白鹅类型。虚拟子类并不是抽象基类的真正子类，而是注册到抽象基类上的子类，这样Python就不会做强制检查了。\n注册的方式有两种：\nregister方法\nPython3.3以前只能使用register方法，比如collections.abc模块的源码中，把内置类型tuple、str、range和memoryview注册为Sequence的虚拟子类：\nSequence.register(tuple) Sequence.register(str) Sequence.register(range) Sequence.register(memoryview) register装饰器\n把TomboList注册为Tombola的虚拟子类：\n@Tombola.register class TomboList(list): ... 白鹅类型和鸭子类型是Python的动态特性，它们的共同点是，只要长的像，Python就不会做强制检查，鸭子类型是针对普通类的子类而言的，白鹅类型是针对抽象基类的虚拟子类而言的。\n参考资料：\n《流畅的Python》第11章 接口：从协议到抽象基类\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000028-pythonpython%E6%8A%BD%E8%B1%A1%E5%9F%BA%E7%B1%BB%E7%9A%84%E5%AE%9A%E4%B9%89%E4%B8%8E%E4%BD%BF%E7%94%A8/","summary":"\u003cstrong\u003e我们写Python基本不需要自己创建抽象基类，而是通过鸭子类型来解决大部分问题。\u003c/strong\u003e《流畅的Python》作者使用了15年Python，但只在项目中创建过一个抽象基类。我们更多时候是创建现有抽象基类的子类，或者使用现有的抽象基类注册。\u003cstrong\u003e本文的意义在于\u003c/strong\u003e，了解抽象基类的定义与使用，可以帮助我们理解抽象基类是如何实现的，为我们以后学习后端语言（比如Java、Golang）打下基础。**毕竟抽象","title":"Python抽象基类的定义与使用"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPython接口与协议 Python语言是没有interface关键字的，这也是动态类型语言的特点之一。Python的接口指的是类实现或继承的公开属性，包括数据或方法。比如Sequence的正式接口如下图所示：\n这些都叫做Python接口。事实上，Python每个类都有接口，除了抽象基类。\n接口是正式的，它定义了类具有哪些属性，协议是非正式的接口，只由文档和约定定义。比如只要类实现了__getitem__方法，按照序列协议的约定，Python就会支持这个类完成访问元素、迭代和使用in运算符等一系列操作。\n猴子补丁 我们可以通过猴子补丁来进一步认识，Python中协议的约定是怎么回事。猴子补丁是这样一种技术：在运行时修改类或模块，而不改动源码。还是以序列协议来举例，比如：\n\u0026gt;\u0026gt;\u0026gt; from random import shuffle \u0026gt;\u0026gt;\u0026gt; from frenchdeck import FrenchDeck \u0026gt;\u0026gt;\u0026gt; deck = FrenchDeck() \u0026gt;\u0026gt;\u0026gt; shuffle(deck) 假设运行报错了：TypeError: 'FrenchDeck' object does not support item assignment，因为FrenchDeck类只有__getitem__方法，只实现了不可变的序列协议。为了解决这个报错，需要通过__setitem__方法实现可变的序列协议。\n代码如下：\ndef set_card(deck, position, card): deck._cards[position] = card FrenchDeck.__setitem__ = set_card shuffle(deck) 这就是一段猴子补丁技术的实现代码，没有修改FrenchDeck类的源码，而是在运行时动态修改。\n抽象基类 Python的抽象基类是指必须让继承它的子类去实现它所要求的方法的类。Python的collections.abc模块中就定义了很多抽象基类：\n虽然我们在实际编程中并不会自己编写抽象基类（一般也不建议这样做，因为可能会导致意想不到的问题），但是了解抽象基类，可以帮助我们更好理解Python面向对象的继承等概念。\n抛开枯燥的理论知识，我在书中找到了一处具有实践价值的片段：\n「在一连串if/elif/elif中使用isinstance做检查，然后根据对象的类型执行不同的操作，通常是不好的做法；此时应该使用多态，即采用一定的方式定义类，让解释器把调用分派给正确的方法，而不使用if/elif/elif块硬编码分派逻辑。」\n交流群和公司都有人问过我这个问题，写了太多的ifelse有没有更好的实现方式，我想这里已经给出了答案。\n鸭子类型和白鹅类型 Python鸭子类型是指对象的类型无关紧要，只要实现了特定的协议即可。它的好处是避免过多的isinstance，如果遵守既定协议，能增加利用现有的标准库和第三方代码的可能性。示例：\nclass Animal(object): def run(self): print(\u0026#34;The animal is running...\u0026#34;) class Dog(Animal): def run(self): print(\u0026#39;The dog is running...\u0026#39;) class Cat(Animal): def run(self): print(\u0026#39;The cat is running...\u0026#39;) def make_run(animal): animal.run() dog = Dog() cat = Cat() make_run(dog) make_run(cat) 对于 make_run() 函数来说，传入的参数并不一定需要是 Animal 类型的，只需要保证传入的对象有一个 run() 方法即可。\n白鹅类型是指只要cls是抽象基类（即cls的元类是abc.ABCMeta），就可以使用isinstance(obj, cls)。它的基本特性是，即便不继承，也有办法把一个类注册为抽象基类的虚拟子类。Python不会检查虚拟子类是否实现了抽象基类要求实现的方法，而是由我们自己保证，并捕获异常。具体会在下篇文章《Python抽象基类的定义与使用》进行介绍。\n参考资料：\n《流畅的Python》第11章 接口：从协议到抽象基类\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000027-pythonpython%E4%B8%AD%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%8D%8F%E8%AE%AE%E5%92%8C%E6%8A%BD%E8%B1%A1%E5%9F%BA%E7%B1%BB/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"python接口与协议\"\u003ePython接口与协议\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003ePython语言是没有interface关键字的\u003c/strong\u003e，这也是动态类型语言的特点之一。Python的接口指的是类实现或继承的公开属性，包括数据或方法。比如Sequence的正式接口如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/109cdc21f5b149b582b136d1e8e82e11.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这些都叫做Python接口。事实上，Python每个类都有接口，\u003cstrong\u003e除了抽象基类\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e接口是正式的\u003c/strong\u003e，它定义了类具有哪些属性，\u003cstrong\u003e协议是非正式的接口\u003c/strong\u003e，只由文档和约定定义。比如只要类实现了\u003ccode\u003e__getitem__\u003c/code\u003e方法，按照序列协议的约定，Python就会支持这个类完成访问元素、迭代和使用in运算符等一系列操作。\u003c/p\u003e\n\u003ch1 id=\"猴子补丁\"\u003e猴子补丁\u003c/h1\u003e\n\u003cp\u003e我们可以通过猴子补丁来进一步认识，Python中协议的约定是怎么回事。猴子补丁是这样一种技术：\u003cstrong\u003e在运行时修改类或模块，而不改动源码\u003c/strong\u003e。还是以序列协议来举例，比如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003erandom\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e shuffle\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003efrenchdeck\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e FrenchDeck\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e deck \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e FrenchDeck\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e shuffle\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edeck\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e假设运行报错了：\u003ccode\u003eTypeError: 'FrenchDeck' object does not support item assignment\u003c/code\u003e，因为FrenchDeck类只有\u003ccode\u003e__getitem__\u003c/code\u003e方法，只实现了\u003cstrong\u003e不可变的序列协议\u003c/strong\u003e。为了解决这个报错，需要通过\u003ccode\u003e__setitem__\u003c/code\u003e方法实现\u003cstrong\u003e可变的序列协议\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e代码如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eset_card\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edeck\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e position\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e card\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    deck\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cards\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eposition\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e card\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eFrenchDeck\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003e__setitem__\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e set_card\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eshuffle\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edeck\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这就是一段猴子补丁技术的实现代码，没有修改FrenchDeck类的源码，而是在运行时动态修改。\u003c/p\u003e\n\u003ch1 id=\"抽象基类\"\u003e抽象基类\u003c/h1\u003e\n\u003cp\u003ePython的抽象基类是指\u003cstrong\u003e必须让继承它的子类去实现它所要求的方法\u003c/strong\u003e的类。Python的collections.abc模块中就定义了很多抽象基类：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b4085d0047b24193aad3456dd116b560.png\"\u003e\u003c/p\u003e\n\u003cp\u003e虽然我们在实际编程中并不会自己编写抽象基类（一般也不建议这样做，因为可能会导致意想不到的问题），但是了解抽象基类，可以帮助我们更好理解Python面向对象的继承等概念。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e抛开枯燥的理论知识，我在书中找到了一处具有实践价值的片段：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e「在一连串if/elif/elif中使用isinstance做检查，然后根据对象的类型执行不同的操作，通常是不好的做法；此时应该使用多态，即采用一定的方式定义类，让解释器把调用分派给正确的方法，而不使用if/elif/elif块硬编码分派逻辑。」\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e交流群和公司都有人问过我这个问题，写了太多的ifelse有没有更好的实现方式，我想这里已经给出了答案。\u003c/p\u003e\n\u003ch1 id=\"鸭子类型和白鹅类型\"\u003e鸭子类型和白鹅类型\u003c/h1\u003e\n\u003cp\u003ePython鸭子类型是指对象的类型无关紧要，只要实现了特定的协议即可。它的好处是避免过多的isinstance，如果遵守既定协议，能增加利用现有的标准库和第三方代码的可能性。示例：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eAnimal\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eobject\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;The animal is running...\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eDog\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eAnimal\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;The dog is running...\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eCat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eAnimal\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;The cat is running...\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003emake_run\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eanimal\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    animal\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erun\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edog \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e Dog\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecat \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e Cat\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake_run\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edog\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake_run\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecat\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e对于 \u003ccode\u003emake_run()\u003c/code\u003e 函数来说，传入的参数并不一定需要是 \u003ccode\u003eAnimal\u003c/code\u003e 类型的，只需要保证传入的对象有一个 \u003ccode\u003erun()\u003c/code\u003e 方法即可。\u003c/p\u003e","title":"Python中的接口协议和抽象基类"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文将在上篇文章二维向量Vector2d类的基础上，定义表示多维向量的Vector类。\n第1版：兼容Vector2d类 代码如下：\nfrom array import array import reprlib import math class Vector: typecode = \u0026#39;d\u0026#39; def __init__(self, components): self._components = array(self.typecode, components) # 多维向量存数组中 def __iter__(self): return iter(self._components) # 构建迭代器 def __repr__(self): components = reprlib.repr(self._components) # 有限长度表示形式 components = components[components.find(\u0026#39;[\u0026#39;):-1] return \u0026#39;Vector({})\u0026#39;.format(components) def __str__(self): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(self._components)) def __eq__(self, other): return tuple(self) == tuple(other) def __abs__(self): return math.sqrt(sum(x * x for x in self)) def __bool__(self): return bool(abs(self)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(memv) # 因为构造函数入参是数组，所以不用再使用*拆包了 其中的reprlib.repr()函数用于生成大型结构或递归结构的安全表达形式，比如：\n\u0026gt;\u0026gt;\u0026gt; Vector([3.1, 4.2]) Vector([3.1, 4.2]) \u0026gt;\u0026gt;\u0026gt; Vector((3, 4, 5)) Vector([3.0, 4.0, 5.0]) \u0026gt;\u0026gt;\u0026gt; Vector(range(10)) Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...]) 超过6个的元素用...来表示。\n第2版：支持切片 **Python协议是非正式的接口，只在文档中定义，在代码中不定义。**比如Python的序列协议只需要__len__和__getitem__两个方法，Python的迭代协议只需要__getitem__一个方法，它们不是正式的接口，只是Python程序员默认的约定。\n切片是序列才有的操作，所以Vector类要实现序列协议，也就是__len__和__getitem__两个方法，代码如下：\ndef __len__(self): return len(self._components) def __getitem__(self, index): cls = type(self) # 获取实例所属的类 if isinstance(index, slice): # 如果index是slice切片对象 return cls(self._components[index]) # 调用构造方法，返回新的Vector实例 elif isinstance(index, numbers.Integral): # 如果index是整型 return self._components[index] # 直接返回元素 else: msg = \u0026#39;{cls.__name__} indices must be integers\u0026#39; raise TypeError(msg.format(cls=cls)) 测试一下：\n\u0026gt;\u0026gt;\u0026gt; v7 = Vector(range(7)) \u0026gt;\u0026gt;\u0026gt; v7[-1] # \u0026lt;1\u0026gt; 6.0 \u0026gt;\u0026gt;\u0026gt; v7[1:4] # \u0026lt;2\u0026gt; Vector([1.0, 2.0, 3.0]) \u0026gt;\u0026gt;\u0026gt; v7[-1:] # \u0026lt;3\u0026gt; Vector([6.0]) \u0026gt;\u0026gt;\u0026gt; v7[1,2] # \u0026lt;4\u0026gt; Traceback (most recent call last): ... TypeError: Vector indices must be integers 第3版：动态存取属性 通过实现__getattr__和__setattr__，我们可以对Vector类动态存取属性。这样就能支持v.my_property = 1.1这样的赋值。\n如果使用__setitem__方法，那么只能支持v[0] = 1.1。\n代码如下：\nshortcut_names = \u0026#39;xyzt\u0026#39; # 4个分量属性名 def __getattr__(self, name): cls = type(self) # 获取实例所属的类 if len(name) == 1: # 只有一个字母 pos = cls.shortcut_names.find(name) if 0 \u0026lt;= pos \u0026lt; len(self._components): # 落在范围内 return self._components[pos] msg = \u0026#39;{.__name__!r} object has no attribute {!r}\u0026#39; # \u0026lt;5\u0026gt; raise AttributeError(msg.format(cls, name)) def __setattr__(self, name, value): cls = type(self) if len(name) == 1: if name in cls.shortcut_names: # name是xyzt其中一个不能赋值 error = \u0026#39;readonly attribute {attr_name!r}\u0026#39; elif name.islower(): # 小写字母不能赋值，防止与xyzt混淆 error = \u0026#34;can\u0026#39;t set attributes \u0026#39;a\u0026#39; to \u0026#39;z\u0026#39; in {cls_name!r}\u0026#34; else: error = \u0026#39;\u0026#39; if error: msg = error.format(cls_name=cls.__name__, attr_name=name) raise AttributeError(msg) super().__setattr__(name, value) # 其他name可以赋值 值得说明的是，__getattr__的机制是：对my_obj.x表达式，Python会检查my_obj实例有没有名为x的属性，如果有就直接返回，不调用__getattr__方法；如果没有，到my_obj.__class__中查找，如果还没有，才调用**__getattr__方法**。\n正因如此，name是xyzt其中一个时才不能赋值，否则会出现下面的奇怪现象：\n\u0026gt;\u0026gt;\u0026gt; v = Vector([range(5)]) \u0026gt;\u0026gt;\u0026gt; v.x = 10 \u0026gt;\u0026gt;\u0026gt; v.x 10 \u0026gt;\u0026gt;\u0026gt; v Vector([0.0, 1.0, 2.0, 3.0, 4.0]) 对v.x进行了赋值，但实际未生效，因为赋值后Vector新增了一个x属性，值为10，对v.x表达式来说，直接就返回了这个值，不会走我们自定义的__getattr__方法，也就没办法拿到v[0]的值。\n第4版：散列 通过实现__hash__方法，加上现有的__eq__方法，Vector实例就变成了可散列的对象。\n代码如下：\nimport functools import operator def __eq__(self, other): return (len(self) == len(other) and all(a == b for a, b in zip(self, other))) def __hash__(self): hashes = (hash(x) for x in self) # 创建一个生成器表达式 return functools.reduce(operator.xor, hashes, 0) # 计算聚合的散列值 其中__eq__方法做了下修改，用到了归约函数all()，比tuple(self) == tuple(other)的写法，能减少处理时间和内存。\nzip()函数取名自zipper拉链，把两个序列咬合在一起。比如：\n\u0026gt;\u0026gt;\u0026gt; list(zip(range(3), \u0026#39;ABC\u0026#39;)) [(0, \u0026#39;A\u0026#39;), (1, \u0026#39;B\u0026#39;), (2, \u0026#39;C\u0026#39;)] 第5版：格式化 Vector的格式化跟Vector2d大同小异，都是定义__format__方法，只是计算方式从极坐标换成了球面坐标：\ndef angle(self, n): r = math.sqrt(sum(x * x for x in self[n:])) a = math.atan2(r, self[n-1]) if (n == len(self) - 1) and (self[-1] \u0026lt; 0): return math.pi * 2 - a else: return a def angles(self): return (self.angle(n) for n in range(1, len(self))) def __format__(self, fmt_spec=\u0026#39;\u0026#39;): if fmt_spec.endswith(\u0026#39;h\u0026#39;): # hyperspherical coordinates fmt_spec = fmt_spec[:-1] coords = itertools.chain([abs(self)], self.angles()) outer_fmt = \u0026#39;\u0026lt;{}\u0026gt;\u0026#39; else: coords = self outer_fmt = \u0026#39;({})\u0026#39; components = (format(c, fmt_spec) for c in coords) return outer_fmt.format(\u0026#39;, \u0026#39;.join(components)) 极坐标和球面坐标是啥？我也不知道，略过就好。\n小结 经过上下两篇文章的介绍，我们知道了Python风格的类是什么样子的，跟常规的面向对象设计不同的是，Python的类通过魔法方法实现了Python协议，使Python类在使用时能够享受到语法糖，不用通过get和set的方式来编写代码。\n参考资料：\n《流畅的Python》第10章 序列的修改、散列和切片\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000026-pythonpython%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%B1%BB%E4%B8%8B/","summary":"本文将在上篇文章二维向量Vector2d类的基础上，定义表示多维向量的Vector类。","title":"Python如何设计面向对象的类（下）"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPython是一门高级语言，支持面向对象设计，如何设计一个符合Python风格的面向对象的类，是一个比较复杂的问题，本文提供一个参考，表达一种思路，探究一层原理。\n目标 期望实现的类具有以下基本行为：\n__repr__ 为repr()提供支持，返回便于开发者理解的对象字符串表示形式。 __str__ 为str()提供支持，返回便于用户理解的对象字符串表示形式。 __bytes__ 为bytes()提供支持，返回对象的二进制表示形式。 __format__ 为format()和str.format()提供支持，使用特殊的格式代码显示对象的字符串表示形式。 Vector2d是一个向量类，期望它能支持以下操作：\n\u0026gt;\u0026gt;\u0026gt; v1 = Vector2d(3, 4) \u0026gt;\u0026gt;\u0026gt; print(v1.x, v1.y) # 通过属性直接访问 3.0 4.0 \u0026gt;\u0026gt;\u0026gt; x, y = v1 # 支持拆包 \u0026gt;\u0026gt;\u0026gt; x, y (3.0, 4.0) \u0026gt;\u0026gt;\u0026gt; v1 # 支持repr Vector2d(3.0, 4.0) \u0026gt;\u0026gt;\u0026gt; v1_clone = eval(repr(v1)) # 验证repr描述准确 \u0026gt;\u0026gt;\u0026gt; v1 == v1_clone # 支持==运算符 True \u0026gt;\u0026gt;\u0026gt; print(v1) # 支持str (3.0, 4.0) \u0026gt;\u0026gt;\u0026gt; octets = bytes(v1) # 支持bytes \u0026gt;\u0026gt;\u0026gt; octets b\u0026#39;d\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x08@\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x10@\u0026#39; \u0026gt;\u0026gt;\u0026gt; abs(v1) # 实现__abs__ 5.0 \u0026gt;\u0026gt;\u0026gt; bool(v1), bool(Vector2d(0, 0)) # 实现__bool__ (True, False) 基本实现 代码与解析如下：\nfrom array import array import math class Vector2d: # Vector2d实例和二进制之间转换时使用 typecode = \u0026#39;d\u0026#39; def __init__(self, x, y): # 转换为浮点数 self.x = float(x) self.y = float(y) def __iter__(self): # 生成器表达式，把Vector2d实例变成可迭代对象，这样才能拆包 return (i for i in (self.x, self.y)) def __repr__(self): class_name = type(self).__name__ # {!r}是个万能的格式符 # *self是拆包，*表示所有元素 return \u0026#39;{}({!r}, {!r})\u0026#39;.format(class_name, *self) def __str__(self): # Vector2d实例是可迭代对象，可以得到一个元组，并str return str(tuple(self)) def __bytes__(self): # 转换为二进制 return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __eq__(self, other): # 比较相等 return tuple(self) == tuple(other) def __abs__(self): # 向量的模是直角三角形的斜边长 return math.hypot(self.x, self.y) def __bool__(self): # 0.0是False，非零值是True return bool(abs(self)) @classmethod def frombytes(cls, octets): # classmethod不传self传cls typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) # 拆包后得到构造方法所需的一对参数 代码最后用到了@classmethod装饰器，它容易跟@staticmethod混淆。\n@classmethod的用法是：定义操作类，而不是操作实例的方法。常用来定义备选构造方法。\n@staticmethod其实就是个普通函数，只不过刚好放在了类的定义体里。实际定义在类中或模块中都可以。\n格式化显示 代码与解析如下：\ndef angle(self): return math.atan2(self.y, self.x) def __format__(self, fmt_spec=\u0026#39;\u0026#39;): if fmt_spec.endswith(\u0026#39;p\u0026#39;): # 以\u0026#39;p\u0026#39;结尾，使用极坐标 fmt_spec = fmt_spec[:-1] coords = (abs(self), self.angle()) # 计算极坐标(magnitude, angle) outer_fmt = \u0026#39;\u0026lt;{}, {}\u0026gt;\u0026#39; # 尖括号 else: coords = self # 不以\u0026#39;p\u0026#39;结尾，构建直角坐标(x, y) outer_fmt = \u0026#39;({}, {})\u0026#39; # 圆括号 components = (format(c, fmt_spec) for c in coords) # 使用内置format函数格式化字符串 return outer_fmt.format(*components) # 拆包后代入外层格式 它实现了以下效果：\n直角坐标：\n\u0026gt;\u0026gt;\u0026gt; format(v1) \u0026#39;(3.0, 4.0)\u0026#39; \u0026gt;\u0026gt;\u0026gt; format(v1, \u0026#39;.2f\u0026#39;) \u0026#39;(3.00, 4.00)\u0026#39; \u0026gt;\u0026gt;\u0026gt; format(v1, \u0026#39;.3e\u0026#39;) \u0026#39;(3.000e+00, 4.000e+00)\u0026#39; 极坐标：\n\u0026gt;\u0026gt;\u0026gt; format(Vector2d(1, 1), \u0026#39;p\u0026#39;) # doctest:+ELLIPSIS \u0026#39;\u0026lt;1.414213..., 0.785398...\u0026gt;\u0026#39; \u0026gt;\u0026gt;\u0026gt; format(Vector2d(1, 1), \u0026#39;.3ep\u0026#39;) \u0026#39;\u0026lt;1.414e+00, 7.854e-01\u0026gt;\u0026#39; \u0026gt;\u0026gt;\u0026gt; format(Vector2d(1, 1), \u0026#39;0.5fp\u0026#39;) \u0026#39;\u0026lt;1.41421, 0.78540\u0026gt;\u0026#39; 可散列的 实现__hash__特殊方法能让Vector2d变成可散列的，不过在这之前需要先让属性不可变，代码如下：\ndef __init__(self, x, y): # 双下划线前缀，变成私有的 self.__x = float(x) self.__y = float(y) @property # 标记为特性 def x(self): return self.__x @property def y(self): return self.__y 这样x和y就只读不可写了。\n属性名字的双下划线前缀叫做名称改写（name mangling），相当于_Vector2d__x和_Vector2d__y，能避免被子类覆盖。\n然后使用位运算符异或混合x和y的散列值：\ndef __hash__(self): return hash(self.x) ^ hash(self.y) 节省内存 Python默认会把实例属性存储在__dict__字典里，字典的底层是散列表，数据量大了以后会消耗大量内存（以空间换时间）。通过__slots__类属性，能把实例属性存储到元组里，大大节省内存空间。\n示例：\nclass Vector2d: __slots__ = (\u0026#39;__x\u0026#39;, \u0026#39;__y\u0026#39;) typecode = \u0026#39;d\u0026#39; 有几点需要注意：\n必须把所有属性都定义到__slots__元组中。 子类也必须定义__slots__。 实例如果要支持弱引用，需要把__weakref也加入__slots__。 覆盖类属性 实例覆盖 Python有个很独特的特性：类属性可用于为实例属性提供默认值。实例代码中的typecode就能直接被self.typecode拿到。但是，如果为不存在的实例属性赋值，会新建实例属性，类属性不会受到影响，self.typecode拿到的是实例属性的typecode。\n示例：\n\u0026gt;\u0026gt;\u0026gt; v1 = Vector2d(1, 2) \u0026gt;\u0026gt;\u0026gt; v1.typecode = \u0026#39;f\u0026#39; \u0026gt;\u0026gt;\u0026gt; v1.typecode \u0026#39;f\u0026#39; \u0026gt;\u0026gt;\u0026gt; Vector2d.typecode \u0026#39;d\u0026#39; 子类覆盖 类属性是公开的，所以可以直接通过Vector2d.typecode = 'f'进行修改。但是更符合Python风格的做法是定义子类：\nclass ShortVector2d(Vector2d): typecode = \u0026#39;f\u0026#39; Django基于类的视图大量使用了这个技术。\n小结 本文先介绍了如何实现特殊方法来设计一个Python风格的类，然后分别实现了格式化显示与可散列对象，使用__slots__能为类节省内存，最后讨论了类属性覆盖技术，子类覆盖是Django基于类的视图大量用到的技术。\n参考资料：\n《流畅的Python》第9章 符合Python风格的对象\nhttps://www.jianshu.com/p/7fc0a177fd1f\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000025-pythonpython%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%B1%BB%E4%B8%8A/","summary":"Python是一门高级语言，支持面向对象设计，如何设计一个符合Python风格的面向对象的类，是一个比较复杂的问题，本文提供一个参考，表达一种思路，探究一层原理。","title":"Python如何设计面向对象的类（上）"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n变量全都是引用 跟其他编程语言不同，Python的变量不是盒子，不会存储数据，它们只是引用，就像标签一样，贴在对象上面。\n比如：\n\u0026gt;\u0026gt;\u0026gt; a = [1, 2, 3] \u0026gt;\u0026gt;\u0026gt; b = a \u0026gt;\u0026gt;\u0026gt; a.append(4) \u0026gt;\u0026gt;\u0026gt; b [1, 2, 3, 4] \u0026gt;\u0026gt;\u0026gt; b is a True a变量和b变量引用的是同一个列表[1, 2, 3]。b可以叫做a的别名。\n比较来看：\n\u0026gt;\u0026gt;\u0026gt; a = [1, 2, 3] \u0026gt;\u0026gt;\u0026gt; c = [1, 2, 3] \u0026gt;\u0026gt;\u0026gt; c == a True \u0026gt;\u0026gt;\u0026gt; c is a False c引用的是另外一个列表，虽然和a引用的列表的值相等，但是它们是不同的对象。\n浅复制与深复制 浅复制是指只复制最外层容器，副本中的元素是源容器中元素的引用。如果所有元素都是不可变的，那么这样没有问题，还能节省内容。但是，如果有可变的元素，那么结果可能会出乎意料之外。构造方法或[:]做的都是浅复制。\n示例：\n\u0026gt;\u0026gt;\u0026gt; x1 = [3, [66, 55, 44], (7, 8, 9)] ## x2是x1的浅复制 \u0026gt;\u0026gt;\u0026gt; x2 = list(x1) ## 不可变元素没有影响 \u0026gt;\u0026gt;\u0026gt; x1.append(100) \u0026gt;\u0026gt;\u0026gt; x1 [3, [66, 55, 44], (7, 8, 9), 100] \u0026gt;\u0026gt;\u0026gt; x2 [3, [66, 55, 44], (7, 8, 9)] ## x1[1]是列表，可变元素会影响x2 ## 因为它们引用的是同一个对象 \u0026gt;\u0026gt;\u0026gt; x1[1].remove(55) \u0026gt;\u0026gt;\u0026gt; x1 [3, [66, 44], (7, 8, 9), 100] \u0026gt;\u0026gt;\u0026gt; x2 [3, [66, 44], (7, 8, 9)] ## x2[1]也会反过来影响x1 \u0026gt;\u0026gt;\u0026gt; x2[1] += [33, 22] \u0026gt;\u0026gt;\u0026gt; x1 [3, [66, 44, 33, 22], (7, 8, 9), 100] \u0026gt;\u0026gt;\u0026gt; x2 [3, [66, 44, 33, 22], (7, 8, 9)] ## 不可变元组也不会有影响 ## +=运算符创建了一个新元组 \u0026gt;\u0026gt;\u0026gt; x2[2] += (10, 11) \u0026gt;\u0026gt;\u0026gt; x1 [3, [66, 44, 33, 22], (7, 8, 9), 100] \u0026gt;\u0026gt;\u0026gt; x2 [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)] 深复制是指我们常规理解的复制，副本不共享内部对象的引用，是完全独立的一个副本。这可以借助copy.deepcopy来实现。\n示例：\n\u0026gt;\u0026gt;\u0026gt; a = [10, 20] \u0026gt;\u0026gt;\u0026gt; b = [a, 30] \u0026gt;\u0026gt;\u0026gt; a.append(b) \u0026gt;\u0026gt;\u0026gt; a [10, 20, [[...], 30]] \u0026gt;\u0026gt;\u0026gt; from copy import deepcopy \u0026gt;\u0026gt;\u0026gt; c = deepcopy(a) \u0026gt;\u0026gt;\u0026gt; c [10, 20, [[...], 30]] 即使是有循环引用也能正确复制。\n注意copy.copy()是浅复制，copy.deepcopy()是深复制。\n函数传参 Python唯一支持的参数传递模式是共享传参，也就是指函数的各个形式参数获得实参中各个引用的副本。因为Python的变量全都是引用。对于不可变对象来说没有问题，但是对于可变对象就不一样了。\n示例：\n\u0026gt;\u0026gt;\u0026gt; def f(a, b): ... a += b ... return a ... ## 数字不变 \u0026gt;\u0026gt;\u0026gt; x = 1 \u0026gt;\u0026gt;\u0026gt; y = 2 \u0026gt;\u0026gt;\u0026gt; f(x, y) 3 \u0026gt;\u0026gt;\u0026gt; x, y (1, 2) ## 列表变了 \u0026gt;\u0026gt;\u0026gt; a = [1, 2] \u0026gt;\u0026gt;\u0026gt; b = [3, 4] \u0026gt;\u0026gt;\u0026gt; f(a, b) [1, 2, 3, 4] \u0026gt;\u0026gt;\u0026gt; a, b ([1, 2, 3, 4], [3, 4]) ## 元组不变 \u0026gt;\u0026gt;\u0026gt; t = (10, 20) \u0026gt;\u0026gt;\u0026gt; u = (30, 40) \u0026gt;\u0026gt;\u0026gt; f(t, u) (10, 20, 30, 40) \u0026gt;\u0026gt;\u0026gt; t, u ((10, 20), (30, 40)) 由此可以得出一条警示：函数参数尽量不要使用可变参数，如果非用不可，应该考虑在函数内部进行复制。\n示例：\nclass TwilightBus: \u0026#34;\u0026#34;\u0026#34;A bus model that makes passengers vanish\u0026#34;\u0026#34;\u0026#34; def __init__(self, passengers=None): if passengers is None: self.passengers = [] else: self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name) 测试一下：\n\u0026gt;\u0026gt;\u0026gt; basketball_team = [\u0026#39;Sue\u0026#39;, \u0026#39;Tina\u0026#39;, \u0026#39;Maya\u0026#39;, \u0026#39;Diana\u0026#39;, \u0026#39;Pat\u0026#39;] \u0026gt;\u0026gt;\u0026gt; bus = TwilightBus(basketball_team) \u0026gt;\u0026gt;\u0026gt; bus.drop(\u0026#39;Tina\u0026#39;) \u0026gt;\u0026gt;\u0026gt; bus.drop(\u0026#39;Pat\u0026#39;) \u0026gt;\u0026gt;\u0026gt; basketball_team [\u0026#39;Sue\u0026#39;, \u0026#39;Maya\u0026#39;, \u0026#39;Diana\u0026#39;] TwilightBus下车的学生，竟然从basketball_team中消失了。这是因为self.passengers引用的是同一个列表对象。修改方法很简单，复制个副本：\ndef __init__(self, passengers=None): if passengers is None: self.passengers = [] else: self.passengers = list(passengers) # 使用构造函数复制副本 del和垃圾回收 del语句删除的是引用，而不是对象。但是del可能会导致对象没有引用，进而被当做垃圾回收。\n示例：\n\u0026gt;\u0026gt;\u0026gt; import weakref \u0026gt;\u0026gt;\u0026gt; s1 = {1, 2, 3} ## s2和s1引用同一个对象 \u0026gt;\u0026gt;\u0026gt; s2 = s1 \u0026gt;\u0026gt;\u0026gt; def bye(): ... print(\u0026#34;Gone\u0026#34;) ... ## 监控对象和调用回调 \u0026gt;\u0026gt;\u0026gt; ender = weakref.finalize(s1, bye) \u0026gt;\u0026gt;\u0026gt; ender.alive True ## 删除s1后还存在s2引用 \u0026gt;\u0026gt;\u0026gt; del s1 \u0026gt;\u0026gt;\u0026gt; ender.alive True ## s2重新绑定导致{1, 2, 3}引用归零 \u0026gt;\u0026gt;\u0026gt; s2 = \u0026#34;spam\u0026#34; Gone ## 对象被销毁了 \u0026gt;\u0026gt;\u0026gt; ender.alive False 在CPython中，对象的引用数量归零后，对象会被立即销毁。如果除了循环引用之外没有其他引用，两个对象都会被销毁。\n弱引用 某些情况下，可能需要保存对象的引用，但不留存对象本身。比如，有个类想要记录所有实例。这个需求可以使用弱引用实现。\n比如上面示例中的weakref.finalize(s1, bye)，finalize就持有{1, 2, 3}的弱引用，虽然有引用，但是不会影响对象被销毁。\n其他使用弱引用的方式是WeakDictionary、WeakValueDictionary、WeakSet。\n示例：\nclass Cheese: def __init__(self, kind): self.kind = kind def __repr__(self): return \u0026#39;Cheese(%r)\u0026#39; % self.kind \u0026gt;\u0026gt;\u0026gt; import weakref \u0026gt;\u0026gt;\u0026gt; stock = weakref.WeakValueDictionary() \u0026gt;\u0026gt;\u0026gt; catalog = [Cheese(\u0026#39;Red Leicester\u0026#39;), Cheese(\u0026#39;Tilsit\u0026#39;), ... Cheese(\u0026#39;Brie\u0026#39;), Cheese(\u0026#39;Parmesan\u0026#39;)] ... \u0026gt;\u0026gt;\u0026gt; for cheese in catalog: # 用作缓存 # key是cheese.kind # value是cheese的弱引用 ... stock[cheese.kind] = cheese ... \u0026gt;\u0026gt;\u0026gt; sorted(stock.keys()) [\u0026#39;Brie\u0026#39;, \u0026#39;Parmesan\u0026#39;, \u0026#39;Red Leicester\u0026#39;, \u0026#39;Tilsit\u0026#39;] ## 删除catalog引用，stock弱引用不影响垃圾回收 ## WeakValueDictionary的值引用的对象被销毁后，对应的键也会自动删除 \u0026gt;\u0026gt;\u0026gt; del catalog \u0026gt;\u0026gt;\u0026gt; sorted(stock.keys()) # 还存在一个cheese临时变量的引用 [\u0026#39;Parmesan\u0026#39;] ## 删除cheese临时变量的引用，stock就完全清空了 \u0026gt;\u0026gt;\u0026gt; del cheese \u0026gt;\u0026gt;\u0026gt; sorted(stock.keys()) [] 注意不是每个Python对象都可以作为弱引用的目标，比如基本的list和dict就不可以，但是它们的子类是可以的：\nclass MyList(list): pass a_list = MyList(range(10)) weakref_to_a_list = weakref.ref(a_list) 小结 本文首先阐述了Python变量全部都是引用的这个事实，这意味着在Python中，简单的赋值是不创建副本的。如果要创建副本，可以选择浅复制和深复制，浅复制使用构造方法、[:]或copy.copy()，深复制使用copy.deepcopy()。del删除的是引用，但是会导致对象没有引用而被当做垃圾回收。有时候需要保留引用而不保留对象（比如缓存），这叫做弱引用，weakref库提供了相应的实现。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000024-pythonpython%E5%8F%98%E9%87%8F%E5%B0%8F%E7%A7%98%E5%AF%86/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"变量全都是引用\"\u003e变量全都是引用\u003c/h1\u003e\n\u003cp\u003e跟其他编程语言不同，Python的变量不是盒子，不会存储数据，它们只是引用，就像标签一样，贴在对象上面。\u003c/p\u003e\n\u003cp\u003e比如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e b \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e a\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eappend\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e b\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e b \u003cspan style=\"color:#0550ae\"\u003eis\u003c/span\u003e a\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003ea变量和b变量引用的是同一个列表\u003ccode\u003e[1, 2, 3]\u003c/code\u003e。b可以叫做a的别名。\u003c/p\u003e\n\u003cp\u003e比较来看：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e c \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e c \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e a\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e c \u003cspan style=\"color:#0550ae\"\u003eis\u003c/span\u003e a\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eFalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003ec引用的是另外一个列表，虽然和a引用的列表的值相等，但是它们是不同的对象。\u003c/p\u003e\n\u003ch1 id=\"浅复制与深复制\"\u003e浅复制与深复制\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e浅复制\u003c/strong\u003e是指只复制最外层容器，\u003cstrong\u003e副本中的元素是源容器中元素的引用\u003c/strong\u003e。如果所有元素都是不可变的，那么这样没有问题，还能节省内容。但是，如果有可变的元素，那么结果可能会出乎意料之外。\u003cstrong\u003e构造方法或\u003ccode\u003e[:]\u003c/code\u003e做的都是浅复制。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e示例：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x1 \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e55\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## x2是x1的浅复制\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x2 \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elist\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ex1\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 不可变元素没有影响\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x1\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eappend\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e100\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x1\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e55\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e100\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e55\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## x1[1]是列表，可变元素会影响x2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 因为它们引用的是同一个对象\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x1\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eremove\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e55\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x1\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e100\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## x2[1]也会反过来影响x1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x2\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e33\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e22\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x1\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e33\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e22\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e100\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e33\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e22\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 不可变元组也不会有影响\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## +=运算符创建了一个新元组\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x2\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e10\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e11\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x1\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e33\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e22\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e100\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e x2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e66\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e44\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e33\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e22\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e10\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e11\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e深复制\u003c/strong\u003e是指我们常规理解的复制，副本不共享内部对象的引用，是\u003cstrong\u003e完全独立的一个副本\u003c/strong\u003e。这可以借助copy.deepcopy来实现。\u003c/p\u003e","title":"Python变量小秘密"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n编解码器 在字符与字节之间的转换过程称为编解码，Python自带了超过100种编解码器，比如：\nascii（英文体系） gb2312（中文体系） utf-8（全球通用） latin1 utf-16 编解码器一般有多个别名，比如utf8、utf-8、U8。\n这些编解码器可以传给open()、str.encode()、bytes.decode()等函数的encoding参数。\nUnicodeEncodeError 多数非UTF编解码器（比如cp437）只能处理Unicode字符的一小部分子集。把字符转换成字节时，如果目标编码中没有定义这个字符，那么就会抛出UnicodeEncodeError异常。\n处理方式一：使用utf8编码。\n处理方式二：添加errors参数：\n## 忽略 如b\u0026#39;So Paulo\u0026#39; city.encode(\u0026#34;cp437\u0026#34;, errors=\u0026#34;ignore\u0026#34;) ## 替换为? 如b\u0026#39;S?o Paulo\u0026#39; city.encode(\u0026#34;cp437\u0026#34;, errors=\u0026#34;replace\u0026#34;) ## 替换为XML实体 如b\u0026#39;S\u0026amp;#227;o Paulo\u0026#39; city.encode(\u0026#34;cp437\u0026#34;, errors=\u0026#34;xmlcharrefreplace\u0026#34;) UnicodeDecodeError 把字节转换为字符时，遇到无法转换的字节时会抛出UnicodeDecodeError异常。这是因为不是每个字节都包含有效的ASCII字符，也不是每个字符都是有效的UTF-8。\n处理方式也有两种，跟上面一样。\nSyntaxError Python3默认使用UTF-8编码源码。如果加载的.py模块中包含UTF-8之外的数据，而且没有声明编码，就会抛出SyntaxError异常。\n处理方式是在文件顶部添加coding注释：\n## coding: cp1252 但是这个办法并不好，最好还是找到这些报错字符，把它们转换为UTF-8。\n从网上直接复制代码到IDE中执行经常会报这个错。\n处理文本文件 Unicode三明治：\n在程序中尽量少接触二进制，把字节解码为字符，只处理字符串对象。比如在Django中，view应该输出Unicode字符串，Django会负责把响应数据编码成字节序列，而且默认使用UTF-8编码。\nPython内置的open函数就是采用了这个原则，在读取文件时会做必要的解码，以文本模式写入文件时会做必要的编码。\n文件乱码 Windows更容易遇到这个问题，因为Windows并不是统一的UTF-8编码，比如在Windows10中：\n\u0026gt;\u0026gt;\u0026gt; open(\u0026#34;cafe.txt\u0026#34;, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf8\u0026#34;).write(\u0026#34;café\u0026#34;) 4 \u0026gt;\u0026gt;\u0026gt; open(\u0026#34;cafe.txt\u0026#34;).read() \u0026#39;caf茅\u0026#39; 写入文件时指定了utf8，但是读取文件没有指定，Python就会使用系统默认编码：\n\u0026gt;\u0026gt;\u0026gt; import locale ## 打开文件用这个 ## 如果没有设置PYTHONENCODING环境变量，sys.stdout/stdin/stderr也用这个 \u0026gt;\u0026gt;\u0026gt; locale.getpreferredencoding() \u0026#39;cp936\u0026#39; cp936把最后一个字节解码成了茅而不是é。\n\u0026gt;\u0026gt;\u0026gt; import sys # 二进制数据和字符串之间转换用这个 \u0026gt;\u0026gt;\u0026gt; sys.getdefaultencoding() \u0026#39;utf-8\u0026#39; \u0026gt;\u0026gt;\u0026gt; import sys # 文件名（不是文件内容）用这个 \u0026gt;\u0026gt;\u0026gt; sys.getfilesystemencoding() \u0026#39;utf-8\u0026#39; GNU/Linux或Mac OS X不会遇到这个问题，因为多年来它们的默认编码都是UTF-8。\n解决办法是一定不能依赖系统默认编码，打开文件时始终应该明确传入encoding=参数，因为不同的设备使用的默认编码可能不同，有时隔一天也会发生变化。\n小结 本文介绍了Python的编解码器，以及可能出现的UnicodeEncodeError、UnicodeDecodeError、SyntaxError问题，然后给出了Python的open函数处理文本文件的原则，最后对Windows容易出现的文件乱码问题进行了说明。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000023-pythonpython%E7%BC%96%E8%A7%A3%E7%A0%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%96%87%E6%9C%AC%E6%96%87%E4%BB%B6%E5%A4%84%E7%90%86/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"编解码器\"\u003e编解码器\u003c/h1\u003e\n\u003cp\u003e在字符与字节之间的转换过程称为编解码，Python自带了超过100种编解码器，比如：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eascii（英文体系）\u003c/li\u003e\n\u003cli\u003egb2312（中文体系）\u003c/li\u003e\n\u003cli\u003eutf-8（全球通用）\u003c/li\u003e\n\u003cli\u003elatin1\u003c/li\u003e\n\u003cli\u003eutf-16\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e编解码器一般有多个别名，比如\u003ccode\u003eutf8\u003c/code\u003e、\u003ccode\u003eutf-8\u003c/code\u003e、\u003ccode\u003eU8\u003c/code\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这些编解码器可以传给open()、str.encode()、bytes.decode()等函数的encoding参数。\u003c/p\u003e\n\u003ch1 id=\"unicodeencodeerror\"\u003eUnicodeEncodeError\u003c/h1\u003e\n\u003cp\u003e多数非UTF编解码器（比如cp437）只能处理Unicode字符的一小部分子集。把字符转换成字节时，如果目标编码中没有定义这个字符，那么就会抛出UnicodeEncodeError异常。\u003c/p\u003e\n\u003cp\u003e处理方式一：使用utf8编码。\u003c/p\u003e\n\u003cp\u003e处理方式二：添加errors参数：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 忽略 如b\u0026#39;So Paulo\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecity\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eencode\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;cp437\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e errors\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;ignore\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 替换为? 如b\u0026#39;S?o Paulo\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecity\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eencode\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;cp437\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e errors\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;replace\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 替换为XML实体 如b\u0026#39;S\u0026amp;#227;o Paulo\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecity\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eencode\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;cp437\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e errors\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;xmlcharrefreplace\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"unicodedecodeerror\"\u003eUnicodeDecodeError\u003c/h1\u003e\n\u003cp\u003e把字节转换为字符时，遇到无法转换的字节时会抛出UnicodeDecodeError异常。这是因为不是每个字节都包含有效的ASCII字符，也不是每个字符都是有效的UTF-8。\u003c/p\u003e\n\u003cp\u003e处理方式也有两种，跟上面一样。\u003c/p\u003e\n\u003ch1 id=\"syntaxerror\"\u003eSyntaxError\u003c/h1\u003e\n\u003cp\u003ePython3默认使用UTF-8编码源码。如果加载的.py模块中包含UTF-8之外的数据，而且没有声明编码，就会抛出SyntaxError异常。\u003c/p\u003e\n\u003cp\u003e处理方式是在文件顶部添加coding注释：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## coding: cp1252\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e但是这个办法并不好，最好还是找到这些报错字符，把它们转换为UTF-8。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e从网上直接复制代码到IDE中执行经常会报这个错。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"处理文本文件\"\u003e处理文本文件\u003c/h1\u003e\n\u003cp\u003eUnicode三明治：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/735ae727680647258a94148dd465e039.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在程序中尽量少接触二进制，把字节解码为字符，只处理字符串对象。比如在Django中，view应该输出Unicode字符串，Django会负责把响应数据编码成字节序列，而且默认使用UTF-8编码。\u003c/p\u003e\n\u003cp\u003ePython内置的open函数就是采用了这个原则，在读取文件时会做必要的解码，以文本模式写入文件时会做必要的编码。\u003c/p\u003e\n\u003ch1 id=\"文件乱码\"\u003e文件乱码\u003c/h1\u003e\n\u003cp\u003eWindows更容易遇到这个问题，因为Windows并不是统一的UTF-8编码，比如在Windows10中：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eopen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;cafe.txt\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;w\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e encoding\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;utf8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewrite\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;café\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eopen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;cafe.txt\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eread\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;caf茅\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e写入文件时指定了utf8，但是读取文件没有指定，Python就会使用系统默认编码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003elocale\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 打开文件用这个\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 如果没有设置PYTHONENCODING环境变量，sys.stdout/stdin/stderr也用这个\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e locale\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003egetpreferredencoding\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;cp936\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003ecp936\u003c/code\u003e把最后一个字节解码成了\u003ccode\u003e茅\u003c/code\u003e而不是\u003ccode\u003eé\u003c/code\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003esys\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e# 二进制数据和字符串之间转换用这个\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e sys\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003egetdefaultencoding\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;utf-8\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003esys\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e# 文件名（不是文件内容）用这个\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e sys\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003egetfilesystemencoding\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;utf-8\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cp\u003eGNU/Linux或Mac OS X不会遇到这个问题，因为多年来它们的默认编码都是UTF-8。\u003c/p\u003e","title":"Python编解码问题与文本文件处理"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n字符 字符是一个信息单位，简单来讲就是一个字母、数字、标点符号、汉字等。\n字符的最佳定义是Unicode字符：\n它是一个全球化的标准，能表示世界上所有语言的字符。Unicode字符的标识（码位）是以4~6个十六进制数字表示的，并且加前缀U+。\n字节 字节是计算机信息计量单位，一个字节代表八个比特，存储的数值范围为0~255。\n字节跳动（ByteDance）互联网公司的字节就是这个字节。\n字节是机器的，字符是人类的。\n把人类字符转换为机器字节时使用的算法叫做编码，反之叫做解码。\n算法不同，字节与字符的关系也不同：\nbytes和bytearray 字节实际上是个二进制序列。不可变bytes类型和可变bytearray类型是用来存储二进制序列的，它们的示例如下：\n\u0026gt;\u0026gt;\u0026gt; cafe = bytes(\u0026#34;café\u0026#34;, encoding=\u0026#34;utf_8\u0026#34;) \u0026gt;\u0026gt;\u0026gt; cafe b\u0026#39;caf\\xc3\\xa9\u0026#39; \u0026gt;\u0026gt;\u0026gt; cafe[0] 99 \u0026gt;\u0026gt;\u0026gt; cafe[:1] b\u0026#39;c\u0026#39; \u0026gt;\u0026gt;\u0026gt; cafe_arr = bytearray(cafe) \u0026gt;\u0026gt;\u0026gt; cafe_arr bytearray(b\u0026#39;caf\\xc3\\xa9\u0026#39;) \u0026gt;\u0026gt;\u0026gt; cafe_arr[-1:] bytearray(b\u0026#39;\\xa9\u0026#39;) 特别的是cafe[0]返回了整数，cafe[:1]返回了二进制序列，这是因为s[0] == s[:1]只对str类型成立，而对于其他类型来说，s[i]返回一个元素，s[i:i+1]返回一个相同类型的序列。\n二进制序列实际上是整数序列。它们的字面量表示法包含ASCII字符（ASCII只能表示英文体系的字符），比如cafe的b'caf\\xc3\\xa9'，具体规则是：\n从空格到~的字符直接使用ASCII字符 制表符\\t、换行符\\n、回车符\\r、转义符\\\\ 其他字符用十六进制转义序列，比如\\x00空字节 构建bytes和bytearray对象的方式有以下几种：\n一个str对象和一个encoding关键字参数 一个可迭代对象，数值在0~255 一个实现了缓冲协议的对象，如bytes、bytearray、memoryview、array.array memoryview和struct memoryview允许在二进制数据结构之间共享内存，struct能从序列中提取结构化信息。\n示例如下，提取一个GIF图像的宽度和高度：\nimport struct with open(\u0026#34;filter.gif\u0026#34;, \u0026#34;rb\u0026#34;) as fp: img = memoryview(fp.read()) ## 这里不会复制字节序列，因为用的memoryview header = img[:10] print(bytes(header)) # b\u0026#39;GIF89a+\\x02\\xe6\\x00\u0026#39; ## \u0026lt;是小字节序，3s3s是两个3字节序列，HH是两个16位二进制整数 ## 类型、版本、宽度、高度 struct.unpack(\u0026#34;\u0026lt;3s3sHH\u0026#34;, header) # (b\u0026#39;GIF\u0026#39;, b\u0026#39;89a\u0026#39;, 555, 230) ## 删除引用，释放memoryview实例所占的内存 del header del img 小结 本文介绍了字符和字节的概念以及它们之间的关系，一个字符对应一个或多个字节。字符是人类的，字节是机器的，编码就是人类字符转换为机器字节，反之叫做解码。然后分别介绍了二进制序列的类型bytes和bytearray，和二进制序列的工具memoryview和struct。\n参考资料：\n《流畅的Python》\nhttp://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html\nhttps://zh.wikipedia.org/wiki/%E5%AD%97%E7%AC%A6_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)\nhttps://home.unicode.org/\nhttps://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82\nhttps://www.runoob.com/w3cnote/byte-character.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000022-pythonpython%E5%AD%97%E7%AC%A6%E4%B8%8E%E5%AD%97%E8%8A%82%E6%96%B0%E7%BC%96/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"字符\"\u003e字符\u003c/h1\u003e\n\u003cp\u003e字符是一个信息单位，简单来讲就是一个字母、数字、标点符号、汉字等。\u003c/p\u003e\n\u003cp\u003e字符的最佳定义是Unicode字符：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5d31243c097a48d2a67ca1babd4e29de.png\"\u003e\u003c/p\u003e\n\u003cp\u003e它是一个全球化的标准，能表示世界上所有语言的字符。Unicode字符的标识（码位）是以4~6个十六进制数字表示的，并且加前缀\u003ccode\u003eU+\u003c/code\u003e。\u003c/p\u003e\n\u003ch1 id=\"字节\"\u003e字节\u003c/h1\u003e\n\u003cp\u003e字节是计算机信息计量单位，一个字节代表八个比特，存储的数值范围为0~255。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e字节跳动（ByteDance）互联网公司的字节就是这个字节。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e字节是机器的，字符是人类的。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e把人类字符转换为机器字节时使用的算法叫做编码，反之叫做解码。\u003c/p\u003e\n\u003cp\u003e算法不同，字节与字符的关系也不同：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/47d654a43ce442f7bd95d463894f20c2.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"bytes和bytearray\"\u003ebytes和bytearray\u003c/h1\u003e\n\u003cp\u003e字节实际上是个二进制序列。不可变bytes类型和可变bytearray类型是用来存储二进制序列的，它们的示例如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e cafe \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ebytes\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;café\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e encoding\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;utf_8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e cafe\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003eb\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;caf\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\\xc3\\xa9\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e cafe\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e99\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e cafe\u003cspan style=\"color:#1f2328\"\u003e[:\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003eb\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;c\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e cafe_arr \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ebytearray\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecafe\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e cafe_arr\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003ebytearray\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003eb\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;caf\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\\xc3\\xa9\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e cafe_arr\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003ebytearray\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003eb\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\\xa9\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e特别的是cafe[0]返回了整数，cafe[:1]返回了二进制序列，这是因为\u003cstrong\u003es[0] == s[:1]只对str类型成立\u003c/strong\u003e，而对于其他类型来说，s[i]返回一个元素，s[i:i+1]返回一个相同类型的序列。\u003c/p\u003e\n\u003cp\u003e二进制序列实际上是整数序列。它们的字面量表示法包含ASCII字符（ASCII只能表示英文体系的字符），比如cafe的\u003ccode\u003eb'caf\\xc3\\xa9'\u003c/code\u003e，具体规则是：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从空格到\u003ccode\u003e~\u003c/code\u003e的字符直接使用ASCII字符\u003c/li\u003e\n\u003cli\u003e制表符\u003ccode\u003e\\t\u003c/code\u003e、换行符\u003ccode\u003e\\n\u003c/code\u003e、回车符\u003ccode\u003e\\r\u003c/code\u003e、转义符\u003ccode\u003e\\\\\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e其他字符用十六进制转义序列，比如\u003ccode\u003e\\x00\u003c/code\u003e空字节\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e构建bytes和bytearray对象的方式有以下几种：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e一个str对象和一个encoding关键字参数\u003c/li\u003e\n\u003cli\u003e一个可迭代对象，数值在0~255\u003c/li\u003e\n\u003cli\u003e一个实现了缓冲协议的对象，如bytes、bytearray、memoryview、array.array\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"memoryview和struct\"\u003ememoryview和struct\u003c/h1\u003e\n\u003cp\u003ememoryview允许在二进制数据结构之间共享内存，struct能从序列中提取结构化信息。\u003c/p\u003e\n\u003cp\u003e示例如下，提取一个GIF图像的宽度和高度：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003estruct\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003ewith\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eopen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;filter.gif\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;rb\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eas\u003c/span\u003e fp\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    img \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ememoryview\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efp\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eread\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 这里不会复制字节序列，因为用的memoryview\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eheader \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e img\u003cspan style=\"color:#1f2328\"\u003e[:\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e10\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003ebytes\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eheader\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# b\u0026#39;GIF89a+\\x02\\xe6\\x00\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## \u0026lt;是小字节序，3s3s是两个3字节序列，HH是两个16位二进制整数\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 类型、版本、宽度、高度\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003estruct\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eunpack\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026lt;3s3sHH\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e header\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# (b\u0026#39;GIF\u0026#39;, b\u0026#39;89a\u0026#39;, 555, 230)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 删除引用，释放memoryview实例所占的内存\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edel\u003c/span\u003e header\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edel\u003c/span\u003e img\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"小结\"\u003e小结\u003c/h1\u003e\n\u003cp\u003e本文介绍了字符和字节的概念以及它们之间的关系，一个字符对应一个或多个字节。字符是人类的，字节是机器的，编码就是人类字符转换为机器字节，反之叫做解码。然后分别介绍了二进制序列的类型bytes和bytearray，和二进制序列的工具memoryview和struct。\u003c/p\u003e","title":"Python字符与字节新编"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n一句话：前者在报进展，后者在讲思想。同样的素材，后者立意高一个身位。\n①从“内容分类”到“问题-症状-解法”的开篇 ✅ 问题驱动的思想表达：\n提出矛盾，让读者10秒抓住主线，使用图表更加直观。\n❌ 工作汇报流水账：\n一大段文字的背景描述。比如“系统存在1个系统对应多个应用\u0026hellip;\u0026hellip;的特点”。\n转换：把你真正解决的核心矛盾提到最前面。\n“系统的根本难点是『系统与应用多对多』——1个系统跨多个应用、1个应用被多个系统引用。所有工作都围绕一件事：让AI具备跨应用的整体视角。”\n②从“我做了什么”到“我用什么设计解决了什么” ✅ 问题驱动的思想表达：\n先写目标，再讲方案。因果链清晰。\n❌ 工作汇报流水账：\n单纯罗列标题，读者不知道每部分在解决什么。\n转换：每个大节开头加一句“目标”。\n“目标：让分散在多应用的知识，能被AI按『业务系统』这一个视角检索到。”\n③缺“金句”——观点要有锋芒 ✅ 问题驱动的思想表达：\n有观点、有立场。\n❌ 工作汇报流水账：\n平铺直述。\n转换：提炼、前置、加粗。每个大节结尾提炼一句“本质是什么”。\n“跨应用分析的前提不是更强的模型，而是『单一大脑、共享上下文』——不派subagent，让一个上下文同时看见多个仓库的变更。”\n④把“待优化”重新框定为“边界设计+路线图” 这是思想成熟度的关键差距。\n✅ 问题驱动的思想表达：\n把弱点讲成设计。\n❌ 工作汇报流水账：\n纯缺点罗列，读起来像自我否定。比如“测试用例生成质量不稳定，需要多轮人工调整”。\n转换：读者觉得你“没做完”还是“想清楚了”，取决于你怎么框定，而不取决于事实本身。同一件事可以写成bug，也可以写成feature。\n“用例生成后必须经过人工确认才入库”——这是刻意保留的质量闸门。读者读到的是：他清楚AI的边界在哪，这个卡点是故意留的。像在展示判断力。\n如果是无限时间和资源也无法完成的真缺口，只是现在还没做到，就老实承认，但配一条落地路径，证明你知道怎么补。\n注意不是所有缺点都能洗成设计，这招的分寸很重要。\n⑤进度数字服务论点，而非堆砌 ✅ 问题驱动的思想表达：\n数字背后有论点。\n❌ 工作汇报流水账：\n数字是孤立状态，各自漂浮，没有拼成一个论证。\n转换：给数字找论点。\n“AI发现18个bug”单纯看是个数字，但如果写成“这18个bug里面有5个是跨应用交互场景——恰好验证了『单一上下文看多仓库』的价值：传统按应用拆分的测试根本发现不了它们。”数字就活了。\n⑥单一视角→双视角（横向定位+纵向架构） ✅ 问题驱动的思想表达：\n先讲横向，再讲纵向。读者既知道“它在哪”，又知道“它是什么”。\n❌ 工作汇报流水账：\n线性表述，缺少”这些工作彼此如何咬合“的鸟瞰。\n转换： 在开篇加一张“全景图”，让读者先见森林。\n知识库、分析、测试如何串成一条AI-Natice动线，彼此的输入输出关系。\n⑦表格擅长罗列，流程图擅长表达“流动与因果” ✅ 问题驱动的思想表达：\n使用流程图表达数据流转。\n❌ 工作汇报流水账：\n只有表格罗列清单。\n转换：把过程用流程图来表示。\n采集→融合→写库、造数→提交→回执。\n最关键的一条：先想清楚“我要证明什么观点”，再用素材去支撑。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000022-%E5%B7%A5%E4%BD%9C%E6%B1%87%E6%8A%A5%E6%B5%81%E6%B0%B4%E8%B4%A6-vs-%E9%97%AE%E9%A2%98%E9%A9%B1%E5%8A%A8%E7%9A%84%E6%80%9D%E6%83%B3%E8%A1%A8%E8%BE%BE/","summary":"一句话：前者在报进展，后者在讲思想。同样的素材，后者立意高一个身位。","title":"工作汇报流水账 vs 问题驱动的思想表达"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在了解了Python函数装饰器基础知识和闭包之后，开始正式学习函数装饰器。\n典型的函数装饰器 以下示例定义了一个装饰器，输出函数的运行时间：\n函数装饰器和闭包紧密结合，入参func代表被装饰函数，通过自由变量绑定后，调用函数并返回结果。\n使用clock装饰器：\nimport time from clockdeco import clock @clock def snooze(seconds): time.sleep(seconds) @clock def factorial(n): return 1 if n \u0026lt; 2 else n*factorial(n-1) if __name__==\u0026#39;__main__\u0026#39;: print(\u0026#39;*\u0026#39; * 40, \u0026#39;Calling snooze(.123)\u0026#39;) snooze(.123) print(\u0026#39;*\u0026#39; * 40, \u0026#39;Calling factorial(6)\u0026#39;) print(\u0026#39;6! =\u0026#39;, factorial(6)) # 6!指6的阶乘 输出结果：\n这是装饰器的典型行为：把被装饰的函数换成新函数，二者接受相同的参数，而且返回被装饰的函数本该返回的值，同时还会做些额外操作。\n值得注意的是factorial()是个递归函数，从结果来看，每次递归都用到了装饰器，打印了运行时间，这是因为如下代码：\n@clock def factorial(n): return 1 if n \u0026lt; 2 else n*factorial(n-1) 等价于：\ndef factorial(n): return 1 if n \u0026lt; 2 else n*factorial(n-1) factorial = clock(factorial) factorial引用的是clock(factorial)函数的返回值，也就是装饰器内部函数clocked，每次调用factorial(n)，执行的都是clocked(n)。\n叠放装饰器 @d1 @d2 def f(): print(\u0026#34;f\u0026#34;) 等价于：\ndef f(): print(\u0026#34;f\u0026#34;) f = d1(d2(f)) 参数化装饰器 怎么让装饰器接受参数呢？答案是：创建一个装饰器工厂函数，把参数传给它，返回一个装饰器，然后再把它应用到要装饰的函数上。\n示例如下：\nregistry = set() def register(active=True): def decorate(func): print(\u0026#39;running register(active=%s)-\u0026gt;decorate(%s)\u0026#39; % (active, func)) if active: registry.add(func) else: registry.discard(func) return func return decorate @register(active=False) def f1(): print(\u0026#39;running f1()\u0026#39;) ## 注意这里的调用 @register() def f2(): print(\u0026#39;running f2()\u0026#39;) def f3(): print(\u0026#39;running f3()\u0026#39;) register是一个装饰器工厂函数，接受可选参数active默认为True，内部定义了一个装饰器decorate并返回。需要注意的是装饰器工厂函数，即使不传参数，也要加上小括号调用，比如@register()。\n再看一个示例：\nimport time DEFAULT_FMT = \u0026#39;[{elapsed:0.8f}s] {name}({args}) -\u0026gt; {result}\u0026#39; ## 装饰器工厂函数 def clock(fmt=DEFAULT_FMT): # 真正的装饰器 def decorate(func): # 包装被装饰的函数 def clocked(*_args): t0 = time.time() # _result是被装饰函数返回的真正结果 _result = func(*_args) elapsed = time.time() - t0 name = func.__name__ args = \u0026#39;, \u0026#39;.join(repr(arg) for arg in _args) result = repr(_result) # **locals()返回clocked的局部变量 print(fmt.format(**locals())) return _result return clocked return decorate if __name__ == \u0026#39;__main__\u0026#39;: @clock() def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123) 这是给典型的函数装饰器添加了参数fmt，装饰器工厂函数增加了一层嵌套，示例中一共有3个def。\n标准库中的装饰器 Python内置了三个用于装饰方法的函数：property、classmethod和staticmethod，这会在将来的文章中讲到。本文介绍functools中的三个装饰器：functools.wraps、functools.lru_cache和functools.singledispatch。\nfunctools.wraps Python函数装饰器在实现的时候，被装饰后的函数其实已经是另外一个函数了（函数名等函数属性会发生改变），为了不影响，Python的functools包中提供了一个叫wraps的装饰器来消除这样的副作用（它能保留原有函数的名称和函数属性）。\n示例，不加wraps：\ndef my_decorator(func): def wrapper(*args, **kwargs): \u0026#39;\u0026#39;\u0026#39;decorator\u0026#39;\u0026#39;\u0026#39; print(\u0026#39;Calling decorated function...\u0026#39;) return func(*args, **kwargs) return wrapper @my_decorator def example(): \u0026#34;\u0026#34;\u0026#34;Docstring\u0026#34;\u0026#34;\u0026#34; print(\u0026#39;Called example function\u0026#39;) print(example.__name__, example.__doc__) ## 输出wrapper decorator 加wraps：\nimport functools def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): \u0026#39;\u0026#39;\u0026#39;decorator\u0026#39;\u0026#39;\u0026#39; print(\u0026#39;Calling decorated function...\u0026#39;) return func(*args, **kwargs) return wrapper @my_decorator def example(): \u0026#34;\u0026#34;\u0026#34;Docstring\u0026#34;\u0026#34;\u0026#34; print(\u0026#39;Called example function\u0026#39;) print(example.__name__, example.__doc__) ## 输出example Docstring functools.lru_cache lru是Least Recently Used的缩写，它是一项优化技术，把耗时的函数的结果保存起来，避免传入相同的参数时重复计算。\n示例：\nimport functools from clockdeco import clock @functools.lru_cache() @clock def fibonacci(n): if n \u0026lt; 2: return n return fibonacci(n-2) + fibonacci(n-1) if __name__==\u0026#39;__main__\u0026#39;: print(fibonacci(6)) 优化了递归算法，执行时间会减半。\n注意，lru_cache可以使用两个可选的参数来配置，它的签名如下：\nfunctools.lru_cache(maxsize=128, typed=False) maxsize：最大存储数量，缓存满了以后，旧的结果会被扔掉。 typed：如果设为True，那么会把不同参数类型得到的结果分开保存，即把通常认为相等的浮点数和整型参数（如1和1.0）区分开。 functools.singledispatch Python3.4的新增语法，可以用来优化函数中的大量if/elif/elif。使用@singledispatch装饰的普通函数会变成泛函数：根据第一个参数的类型，以不同方式执行相同操作的一组函数。所以它叫做single dispatch，单分派。\n根据多个参数进行分派，就是多分派了。\n示例，生成HTML，显示不同类型的Python对象：\nimport html def htmlize(obj): content = html.escape(repr(obj)) return \u0026#39;\u0026lt;pre\u0026gt;{}\u0026lt;/pre\u0026gt;\u0026#39;.format(content) 因为Python不支持重载方法或函数，所以就不能使用不同的签名定义htmlize的变体，只能把htmlize变成一个分派函数，使用if/elif/elif，调用专门的函数，比如htmlize_str、htmlize_int等。时间一长htmlize会变得很大，跟各个专门函数之间的耦合也很紧密，不便于模块扩展。\n@singledispatch经过深思熟虑后加入到了标准库，来解决这类问题：\nfrom functools import singledispatch from collections import abc import numbers import html @singledispatch def htmlize(obj): # 基函数 这里不用写if/elif/elif来分派了 content = html.escape(repr(obj)) return \u0026#39;\u0026lt;pre\u0026gt;{}\u0026lt;/pre\u0026gt;\u0026#39;.format(content) @htmlize.register(str) def _(text): # 专门函数 content = html.escape(text).replace(\u0026#39;\\n\u0026#39;, \u0026#39;\u0026lt;br\u0026gt;\\n\u0026#39;) return \u0026#39;\u0026lt;p\u0026gt;{0}\u0026lt;/p\u0026gt;\u0026#39;.format(content) @htmlize.register(numbers.Integral) def _(n): # 专门函数 return \u0026#39;\u0026lt;pre\u0026gt;{0} (0x{0:x})\u0026lt;/pre\u0026gt;\u0026#39;.format(n) @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): # 专门函数 inner = \u0026#39;\u0026lt;/li\u0026gt;\\n\u0026lt;li\u0026gt;\u0026#39;.join(htmlize(item) for item in seq) return \u0026#39;\u0026lt;ul\u0026gt;\\n\u0026lt;li\u0026gt;\u0026#39; + inner + \u0026#39;\u0026lt;/li\u0026gt;\\n\u0026lt;/ul\u0026gt;\u0026#39; @singledispatch装饰了基函数。专门函数使用@\u0026lt;\u0026lt;base_function\u0026gt;\u0026gt;.register(\u0026lt;\u0026lt;type\u0026gt;\u0026gt;)装饰，它的名字不重要，命名为_，简单明了。\n这样编写代码后，Python会根据第一个参数的类型，调用相应的专门函数。\n小结 本文首先介绍了典型的函数装饰器：把被装饰的函数换成新函数，二者接受相同的参数，而且返回被装饰的函数本该返回的值，同时还会做些额外操作。接着介绍了装饰器的两个高级用法：叠放装饰器和参数化装饰器，它们都会增加函数的嵌套层级。最后介绍了3个标准库中的装饰器：保留原有函数属性的functools.wraps、缓存耗时的函数结果的functools.lru_cache和优化if/elif/elif代码的functools.singledispatch。\n参考资料：\n《流畅的Python》\nhttps://github.com/fluentpython/example-code/tree/master/07-closure-deco\nhttps://blog.csdn.net/liuzonghao88/article/details/103586634\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000021-pythonpython%E5%87%BD%E6%95%B0%E8%A3%85%E9%A5%B0%E5%99%A8%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/","summary":"在了解了Python函数装饰器基础知识和闭包之后，开始正式学习函数装饰器。","title":"Python函数装饰器高级用法"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n简介 TestNG是Test Next Generation的缩写，它的灵感来自于JUnit和NUnit，在它们基础上增加了很多很牛的功能，比如说：\n注解。 多线程，比如所有方法都在各自线程中，一个测试类一个线程等。 验证代码是否多线程安全。 灵活的测试配置。 支持数据驱动（@DataProvider）。 支持参数化。 强大的执行机制（不需要TestSuite）。 能跟各种工具结合（比如IDEA、Maven等）。 内嵌BeanShell。 提供运行时和日志的JDK函数（不需要添加dependencies）。 提供应用服务器测试依赖的方法。 一句话总结就是，TestNG强大到能覆盖所有测试类型：单元测试、功能测试、端到端测试、集成测试等等等。\n一个简单的示例如下：\npackage example1; import org.testng.annotations.*; public class SimpleTest { @BeforeClass public void setUp() { // code that will be invoked when this test is instantiated } @Test(groups = { \u0026#34;fast\u0026#34; }) public void aFastTest() { System.out.println(\u0026#34;Fast test\u0026#34;); } @Test(groups = { \u0026#34;slow\u0026#34; }) public void aSlowTest() { System.out.println(\u0026#34;Slow test\u0026#34;); } } setUp()会在测试类创建后，测试方法执行前运行。 测试方法名字可以任意的，TestNG通过@Test注解来标识。 可以把测试方法按group分组。 然后使用xml来配置运行策略：\n\u0026lt;project default=\u0026#34;test\u0026#34;\u0026gt; \u0026lt;path id=\u0026#34;cp\u0026#34;\u0026gt; \u0026lt;pathelement location=\u0026#34;lib/testng-testng-5.13.1.jar\u0026#34;/\u0026gt; \u0026lt;pathelement location=\u0026#34;build\u0026#34;/\u0026gt; \u0026lt;/path\u0026gt; \u0026lt;taskdef name=\u0026#34;testng\u0026#34; classpathref=\u0026#34;cp\u0026#34; classname=\u0026#34;org.testng.TestNGAntTask\u0026#34; /\u0026gt; \u0026lt;target name=\u0026#34;test\u0026#34;\u0026gt; \u0026lt;testng classpathref=\u0026#34;cp\u0026#34; groups=\u0026#34;fast\u0026#34;\u0026gt; \u0026lt;classfileset dir=\u0026#34;build\u0026#34; includes=\u0026#34;example1/*.class\u0026#34;/\u0026gt; \u0026lt;/testng\u0026gt; \u0026lt;/target\u0026gt; \u0026lt;/project\u0026gt; 使用ant调用（下面第2节给出了最新的运行方式）：\nc:\u0026gt; ant Buildfile: build.xml test: [testng] Fast test [testng] =============================================== [testng] Suite for Command line test [testng] Total tests run: 1, Failures: 0, Skips: 0 [testng] =============================================== BUILD SUCCESSFUL Total time: 4 seconds 最后就能看报告了：\nstart test-output\\index.html (on Windows) 如何运行TestNG 除了前面提到的ant调用，更常见的运行TestNG的方式是以下两种：\nIDEA Maven IDEA 在IDEA中运行TestNG第①种方式是点击方法或类前面的小箭头：\n第②种方式是在方法或类内部点击右键：\n第③种方式是使用testng.xml，IDEA可以右键testng.xml来运行，在文件名和文件内部点击均可：\ntestng.xml支持很多种配置，比如配置测试类：\n\u0026lt;!DOCTYPE suite SYSTEM \u0026#34;https://testng.org/testng-1.0.dtd\u0026#34; \u0026gt; \u0026lt;suite name=\u0026#34;Suite1\u0026#34; verbose=\u0026#34;1\u0026#34; \u0026gt; \u0026lt;test name=\u0026#34;Nopackage\u0026#34; \u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;NoPackageTest\u0026#34; /\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;test name=\u0026#34;Regression1\u0026#34;\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;test.sample.ParameterSample\u0026#34;/\u0026gt; \u0026lt;class name=\u0026#34;test.sample.ParameterTest\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;/suite\u0026gt; 配置测试包：\n\u0026lt;!DOCTYPE suite SYSTEM \u0026#34;https://testng.org/testng-1.0.dtd\u0026#34; \u0026gt; \u0026lt;suite name=\u0026#34;Suite1\u0026#34; verbose=\u0026#34;1\u0026#34; \u0026gt; \u0026lt;test name=\u0026#34;Regression1\u0026#34; \u0026gt; \u0026lt;packages\u0026gt; \u0026lt;package name=\u0026#34;test.sample\u0026#34; /\u0026gt; \u0026lt;/packages\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;/suite\u0026gt; 配置组和方法：\n\u0026lt;!DOCTYPE suite SYSTEM \u0026#34;https://testng.org/testng-1.0.dtd\u0026#34; \u0026gt; \u0026lt;suite name=\u0026#34;Suite1\u0026#34; verbose=\u0026#34;1\u0026#34; \u0026gt; \u0026lt;test name=\u0026#34;Regression1\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;run\u0026gt; \u0026lt;exclude name=\u0026#34;brokenTests\u0026#34; /\u0026gt; \u0026lt;include name=\u0026#34;checkinTests\u0026#34; /\u0026gt; \u0026lt;/run\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;test.IndividualMethodsTest\u0026#34;\u0026gt; \u0026lt;methods\u0026gt; \u0026lt;include name=\u0026#34;testMethod\u0026#34; /\u0026gt; \u0026lt;/methods\u0026gt; \u0026lt;/class\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;/suite\u0026gt; 默认TestNG会按xml顺序执行，可以设置 preserve-order为false变成随机顺序：\n\u0026lt;!DOCTYPE suite SYSTEM \u0026#34;https://testng.org/testng-1.0.dtd\u0026#34; \u0026gt; \u0026lt;suite name=\u0026#34;Suite1\u0026#34; verbose=\u0026#34;1\u0026#34; \u0026gt; \u0026lt;test name=\u0026#34;Regression1\u0026#34; preserve-order=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;test.Test1\u0026#34;\u0026gt; \u0026lt;methods\u0026gt; \u0026lt;include name=\u0026#34;m1\u0026#34; /\u0026gt; \u0026lt;include name=\u0026#34;m2\u0026#34; /\u0026gt; \u0026lt;/methods\u0026gt; \u0026lt;/class\u0026gt; \u0026lt;class name=\u0026#34;test.Test2\u0026#34; /\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;/suite\u0026gt; Maven 官方教程给出了命令行执行TestNG：\njava org.testng.TestNG testng1.xml [testng2.xml testng3.xml ...] 实际调用是调不通的，可以借助Maven来实现。\n先在pom.xml添加：\n\u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-surefire-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.19.1\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;suiteXmlFiles\u0026gt; \u0026lt;suiteXmlFile\u0026gt;testng.xml\u0026lt;/suiteXmlFile\u0026gt; \u0026lt;/suiteXmlFiles\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 然后执行mvn clean test命令就行啦。\n测试方法和测试类 使用了@Test注解的方法就是测试方法，包含测试方法的类就是测试类。比如：\npackage example1; import org.testng.annotations.*; public class SimpleTest { @BeforeClass public void setUp() { // code that will be invoked when this test is instantiated } @Test(groups = { \u0026#34;fast\u0026#34; }) public void aFastTest() { System.out.println(\u0026#34;Fast test\u0026#34;); } @Test(groups = { \u0026#34;slow\u0026#34; }) public void aSlowTest() { System.out.println(\u0026#34;Slow test\u0026#34;); } } @Test也能放在测试类上面，这样测试类下的所有方法都是测试方法，比如：\n@Test public class Test1 { public void test1() { } public void test2() { } } 而且可以在测试类和测试方法上同时使用@Test，比如给某个Test加入分组：\n@Test public class Test1 { public void test1() { } @Test(groups = \u0026#34;g1\u0026#34;) public void test2() { } } 有个点需要注意的是@Test注解的测试方法，默认是会忽略返回值的，除非在testng.xml配置：\n\u0026lt;suite allow-return-values=\u0026#34;true\u0026#34;\u0026gt; or \u0026lt;test allow-return-values=\u0026#34;true\u0026#34;\u0026gt; 测试组 测试组其实就是给测试方法打标记，比如冒烟测试用例和功能测试用例：\npublic class Test1 { @Test(groups = { \u0026#34;functest\u0026#34;, \u0026#34;checkintest\u0026#34; }) public void testMethod1() { } @Test(groups = {\u0026#34;functest\u0026#34;, \u0026#34;checkintest\u0026#34;} ) public void testMethod2() { } @Test(groups = { \u0026#34;functest\u0026#34; }) public void testMethod3() { } } 如果只配置functest，就会执行全部测试方法：\n\u0026lt;test name=\u0026#34;Test1\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;run\u0026gt; \u0026lt;include name=\u0026#34;functest\u0026#34;/\u0026gt; \u0026lt;/run\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;example1.Test1\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; 如果只配置checkintest，就会只执行前面2个方法：\n\u0026lt;test name=\u0026#34;Test1\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;run\u0026gt; \u0026lt;include name=\u0026#34;checkintest\u0026#34;/\u0026gt; \u0026lt;/run\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;example1.Test1\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; 除了指定完整name，也可以使用正则表达式：\n@Test public class Test1 { @Test(groups = { \u0026#34;windows.checkintest\u0026#34; }) public void testWindowsOnly() { } @Test(groups = {\u0026#34;linux.checkintest\u0026#34;} ) public void testLinuxOnly() { } @Test(groups = { \u0026#34;windows.functest\u0026#34; ) public void testWindowsToo() { } } \u0026lt;test name=\u0026#34;Test1\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;run\u0026gt; \u0026lt;include name=\u0026#34;windows.*\u0026#34;/\u0026gt; \u0026lt;/run\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;example1.Test1\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; 进一步，测试组除了include测试方法，还可以exclude：\n\u0026lt;test name=\u0026#34;Test1\u0026#34;\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;example1.Test1\u0026#34;\u0026gt; \u0026lt;methods\u0026gt; \u0026lt;include name=\u0026#34;.*enabledTestMethod.*\u0026#34;/\u0026gt; \u0026lt;exclude name=\u0026#34;.*brokenTestMethod.*\u0026#34;/\u0026gt; \u0026lt;/methods\u0026gt; \u0026lt;/class\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; 还有一个办法是使用*@Test和@Before/After*的enabled属性。\n测试组也能进行嵌套：\n\u0026lt;test name=\u0026#34;Regression1\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;define name=\u0026#34;functest\u0026#34;\u0026gt; \u0026lt;include name=\u0026#34;windows\u0026#34;/\u0026gt; \u0026lt;include name=\u0026#34;linux\u0026#34;/\u0026gt; \u0026lt;/define\u0026gt; \u0026lt;define name=\u0026#34;all\u0026#34;\u0026gt; \u0026lt;include name=\u0026#34;functest\u0026#34;/\u0026gt; \u0026lt;include name=\u0026#34;checkintest\u0026#34;/\u0026gt; \u0026lt;/define\u0026gt; \u0026lt;run\u0026gt; \u0026lt;include name=\u0026#34;all\u0026#34;/\u0026gt; \u0026lt;/run\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;test.sample.Test1\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;define\u0026gt;\u0026lt;/define\u0026gt;定义了父分组，里面\u0026lt;include\u0026gt;的就是子分组。\n同样的，嵌套分组也可以对子分组进行exclude：\n@Test(groups = {\u0026#34;checkintest\u0026#34;, \u0026#34;broken\u0026#34;} ) public void testMethod2() { } \u0026lt;test name=\u0026#34;Simple example\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;run\u0026gt; \u0026lt;include name=\u0026#34;checkintest\u0026#34;/\u0026gt; \u0026lt;exclude name=\u0026#34;broken\u0026#34;/\u0026gt; \u0026lt;/run\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;example1.Test1\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; 最后，对于分组的位置，如果测试类和测试方法都标记了分组，那么测试类的分组会作用到所有方法中，比如：\n@Test(groups = {\u0026#34;checkin-test\u0026#34;}) public class All { @Test(groups = {\u0026#34;func-test\u0026#34;) public void method1() { ...} public void method2() { ...} } method2()属于checkin-test分组，method1属于func-test和checkin-test两个分组。\nTestNG参数化 TestNG参数化有两种方式，第一种是从testng.xml读取数据，第二种是通过代码读取数据。\n从testng.xml读取数据 直接看示例：\n@Parameters({ \u0026#34;first-name\u0026#34; }) @Test public void testSingleString(String firstName) { System.out.println(\u0026#34;Invoked testString \u0026#34; + firstName); assert \u0026#34;Cedric\u0026#34;.equals(firstName); } \u0026lt;suite name=\u0026#34;My suite\u0026#34;\u0026gt; \u0026lt;parameter name=\u0026#34;first-name\u0026#34; value=\u0026#34;Cedric\u0026#34;/\u0026gt; \u0026lt;test name=\u0026#34;Simple example\u0026#34;\u0026gt; \u0026lt;-- ... --\u0026gt; @Parameters指定参数化名字。\n测试方法入参与参数化名字一一对应。\ntestng.xml中\u0026lt;parameter\u0026gt;定义参数化的值。\n在testng.xml中，\u0026lt;parameter\u0026gt;既可以定义在\u0026lt;suite\u0026gt;中也可以定义在\u0026lt;test\u0026gt;中，如果有同名的，会以\u0026lt;test\u0026gt;的覆盖\u0026lt;suite\u0026gt;。\n@Parameters既可以作用到@Test，也可以作用到 @Before/After和@Factory，比如：\n@Parameters({ \u0026#34;datasource\u0026#34;, \u0026#34;jdbcDriver\u0026#34; }) @BeforeMethod public void beforeTest(String ds, String driver) { m_dataSource = ...; // look up the value of datasource m_jdbcDriver = driver; } 也可以作用到测试类的构造方法中，但是只能最多一个构造方法，这样就能在初始化类的时候，进行参数化赋值，便于测试方法使用\n@Optional用于标识参数是否可选，比如：\n@Parameters(\u0026#34;db\u0026#34;) @Test public void testNonExistentParameter(@Optional(\u0026#34;mysql\u0026#34;) String db) { ... } 如果db这个参数取不到名字，那么就会取mysql的值。 通过代码读取数据 第一种参数化方式其实比较鸡肋，第二种方式才是TestNG参数化的灵魂，用到了**@DataProvider**，它会返回一个二维数组：\nimport org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class DPTest { @DataProvider(name = \u0026#34;test1\u0026#34;) public Object[][] createData1() { return new Object[][] { { \u0026#34;Cedric\u0026#34;, 36}, { \u0026#34;Anne\u0026#34;, 37}, }; } @Test(dataProvider = \u0026#34;test1\u0026#34;) public void verifyData1(String n1, Integer n2) { System.out.println(n1 + \u0026#34; \u0026#34; + n2); } } @DataProvider用于生产数据，name是唯一标识。 在@Test中通过dataProvider属性指定name。 测试方法的入参跟数组中元素一一对应。 默认@DataProvider和@Test是在同一个类中，如果想放在不同的类，那么需要定义为静态方法（或者无参数构造方法的类），比如：\nimport org.testng.annotations.DataProvider; public class StaticProvider { @DataProvider(name = \u0026#34;create\u0026#34;) public static Object[][] createData() { return new Object[][] { new Object[] {42} }; } } import org.testng.annotations.Test; public class DiffClazzTest { @Test(dataProvider = \u0026#34;create\u0026#34;, dataProviderClass = StaticProvider.class) public void test(Integer n) { System.out.println(n); } } createData()为static。 需要额外通过@Test的dataProviderClass属性指定@DataProvider所在的类。 @DataProvider的返回值（参数类型）除了已经提到的Object[][]，还可以是Iterator\u0026lt;Object[]\u0026gt;，它不会一次性生成所有数据，而是每调用一次生成一次，节约内存，比如：\nimport org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.util.Arrays; import java.util.Iterator; public class IterTest { @DataProvider(name = \u0026#34;test1\u0026#34;) public Iterator\u0026lt;Object[]\u0026gt; createData1() { Object[][] myObjects = new Object[][]{ {\u0026#34;Cedric\u0026#34;, 36}, {\u0026#34;Anne\u0026#34;, 37}, }; return Arrays.asList(myObjects).iterator(); } @Test(dataProvider = \u0026#34;test1\u0026#34;) public void verifyData1(String n1, Integer n2) { System.out.println(n1 + \u0026#34; \u0026#34; + n2); } } 看到这里，对@DataProvider已经有了足够的认识，它支持两种参数类型：\nObject[][] Iterator\u0026lt;Object[]\u0026gt; 假如测试方法只有一个入参，是不是只能用二维来实现：\nimport org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.util.Arrays; import java.util.Iterator; public class IterTest { @DataProvider(name = \u0026#34;test1\u0026#34;) public Iterator\u0026lt;Object[]\u0026gt; createData1() { Object[][] myObjects = new Object[][]{{\u0026#34;x\u0026#34;}, {\u0026#34;y\u0026#34;}}; return Arrays.asList(myObjects).iterator(); } @Test(dataProvider = \u0026#34;test1\u0026#34;) public void verifyData1(String n) { System.out.println(n); } } 其实不是，@DataProvider支持一维数组：\nimport org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class IterTest { @DataProvider(name = \u0026#34;test1\u0026#34;) public Object[] createData1() { Object[] myObjects = new Object[]{\u0026#34;x\u0026#34;, \u0026#34;y\u0026#34;}; return myObjects; } @Test(dataProvider = \u0026#34;test1\u0026#34;) public void verifyData1(String n) { System.out.println(n); } } 以及一维数组的迭代器：\nimport org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.util.Arrays; import java.util.Iterator; public class IterTest { @DataProvider(name = \u0026#34;test1\u0026#34;) public Iterator\u0026lt;Object\u0026gt; createData1() { Object[] myObjects = new Object[]{\u0026#34;x\u0026#34;, \u0026#34;y\u0026#34;}; return Arrays.asList(myObjects).iterator(); } @Test(dataProvider = \u0026#34;test1\u0026#34;) public void verifyData1(String n) { System.out.println(n); } } 最精彩的来了，@DataProvider支持反射，也就是反向获取测试方法的信息：\n@DataProvider(name = \u0026#34;dp\u0026#34;) public Object[][] createData(Method m) { System.out.println(m.getName()); // print test method name return new Object[][] { new Object[] { \u0026#34;Cedric\u0026#34; }}; } @Test(dataProvider = \u0026#34;dp\u0026#34;) public void test1(String s) { } @Test(dataProvider = \u0026#34;dp\u0026#34;) public void test2(String s) { } createData的入参是java.lang.reflect.Method，这样就能获取到测试方法的信息，比如这里的getName()会依次拿到test1、test2。 @DataProvider还支持并发：\n@DataProvider(parallel = true)// ... 默认是10个线程，可以在testng.xml中修改：\n\u0026lt;suite name=\u0026#34;Suite1\u0026#34; data-provider-thread-count=\u0026#34;20\u0026#34; \u0026gt;... 一个xml共享一个线程池，如果要用多个线程池，那么需要创建多个testng.xml。\n锦上添花的是，TestNG的参数化会打印在测试报告中：\n指定用例执行顺序 TestNG用例的执行顺序有两种方式来指定：注解和XML。\n注解 使用@Test的dependsOnMethods属性：\n@Test public void serverStartedOk() {} @Test(dependsOnMethods = { \u0026#34;serverStartedOk\u0026#34; }) public void method1() {} 或者dependsOnGroups属性：\n@Test(groups = { \u0026#34;init\u0026#34; }) public void serverStartedOk() {} @Test(groups = { \u0026#34;init\u0026#34; }) public void initEnvironment() {} @Test(dependsOnGroups = { \u0026#34;init.*\u0026#34; }) public void method1() {} @Before/After也能实现初始化，但是它们的结果不会出现在测试报告中。\n默认TestNG会强制校验，依赖的用例必须成功才会执行当前用例，否则当前用例会被标记为SKIP，这叫做强依赖。通过设置alwaysRun=true可以变成弱依赖，无论依赖用例执行成功与否，都会执行当前用例。\n需要特别注意的是，依赖测试方法是按照测试类来进行执行的（group by class），比如b()方法依赖的a()方法有多个实例，那么会按照以下顺序执行:\na(1) a(2) b(2) b(2) 举个实际的例子，登入和登出，如果想达到以下效果：\nsignIn(\u0026#34;us\u0026#34;) signOut(\u0026#34;us\u0026#34;) signIn(\u0026#34;uk\u0026#34;) signOut(\u0026#34;uk\u0026#34;) 那么需要在XML中进行配置：\n\u0026lt;suite name=\u0026#34;Factory\u0026#34; group-by-instances=\u0026#34;true\u0026#34;\u0026gt; or \u0026lt;test name=\u0026#34;Factory\u0026#34; group-by-instances=\u0026#34;true\u0026#34;\u0026gt; XML 在testng.xml中使用\u0026lt;dependencies\u0026gt;和depends-on来指定用例顺序：\n\u0026lt;test name=\u0026#34;My suite\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;group name=\u0026#34;c\u0026#34; depends-on=\u0026#34;a b\u0026#34; /\u0026gt; \u0026lt;group name=\u0026#34;z\u0026#34; depends-on=\u0026#34;c\u0026#34; /\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;/test\u0026gt; 动态创建测试用例 假设有这样的测试用例，在测试时需要对网页访问多次，那么在TestNG中会这样编写代码：\npublic class TestWebServer { @Test(parameters = { \u0026#34;number-of-times\u0026#34; }) public void accessPage(int numberOfTimes) { while (numberOfTimes-- \u0026gt; 0) { // access the web page } } } \u0026lt;test name=\u0026#34;T1\u0026#34;\u0026gt; \u0026lt;parameter name=\u0026#34;number-of-times\u0026#34; value=\u0026#34;10\u0026#34;/\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name= \u0026#34;TestWebServer\u0026#34; /\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;test name=\u0026#34;T2\u0026#34;\u0026gt; \u0026lt;parameter name=\u0026#34;number-of-times\u0026#34; value=\u0026#34;20\u0026#34;/\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name= \u0026#34;TestWebServer\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;test name=\u0026#34;T3\u0026#34;\u0026gt; \u0026lt;parameter name=\u0026#34;number-of-times\u0026#34; value=\u0026#34;30\u0026#34;/\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name= \u0026#34;TestWebServer\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; 由于访问次数不一，在testng.xml中定义了3个test，然后借助参数化将访问次数传给@Test测试方法。 在@Test测试方法中循环遍历numberOfTimes。 这种需求可以采用TestNG的Factory来对代码进行简化：\nimport org.testng.annotations.Factory; public class WebTestFactory { @Factory public Object[] createInstances() { Object[] result = new Object[3]; for (int i = 0; i \u0026lt;= 2; i++) { result[i] = new WebTest((i + 1) * 10); } return result; } } import org.testng.annotations.Test; public class WebTest { private int m_numberOfTimes; public WebTest(int numberOfTimes) { m_numberOfTimes = numberOfTimes; } @Test public void testServer() { for (int i = 0; i \u0026lt; m_numberOfTimes; i++) { // access the web page System.out.println(i); } } } WebTestFactory是工厂函数，返回Object[]。 WebTestFactory动态创建了多个WebTest实例。 既可以在IDEA中点击WebTestFactory的运行按钮执行测试：\n注意如果执行WebTest，会提示No tests were found。\n也可以在testng.xml中驱动：\n\u0026lt;class name=\u0026#34;WebTestFactory\u0026#34; /\u0026gt; 还可以直接在代码中驱动：\nTestNG testNG = new TestNG(); testNG.setTestClasses(WebTestFactory.class); testNG.run(); @Factory和@Test一样，都能使用dataProvider属性，比如：\n@Factory(dataProvider = \u0026#34;dp\u0026#34;) public FactoryDataProviderSampleTest(int n) { super(n); } @DataProvider static public Object[][] dp() { return new Object[][] { new Object[] { 41 }, new Object[] { 42 }, }; } 忽略部分测试用例 TestNG可以使用@Ignore注解忽略测试，比如：\nimport org.testng.annotations.Ignore; import org.testng.annotations.Test; @Ignore public class TestcaseSample { @Test public void testMethod1() { } @Test public void testMethod2() { } } 如果作用到测试类上，那么它会忽略这个类下面的所有测试方法。如果只作用到测试方法上，那么它就相当于@Test(enabled=false)。另外还能放在包上面：\n@Ignore package com.testng.master; import org.testng.annotations.Ignore; 多线程并行测试用例 可以给@Test添加属性，让用例以多线程并行执行：\n@Test(threadPoolSize = 3, invocationCount = 10, timeOut = 10000) public void testServer() { threadPoolSize指3个线程。 invocationCount指运行10次。 timeOut指阻塞等待超时。 也可以在testng.xml中设置，thread-count指定线程数，parallel设置不同的值有不同的含义：\nmethods：\n\u0026lt;suite name=\u0026#34;My suite\u0026#34; parallel=\u0026#34;methods\u0026#34; thread-count=\u0026#34;5\u0026#34;\u0026gt; 所有测试方法在不同的独立线程中执行。\ntests：\n\u0026lt;suite name=\u0026#34;My suite\u0026#34; parallel=\u0026#34;tests\u0026#34; thread-count=\u0026#34;5\u0026#34;\u0026gt; \u0026lt;test\u0026gt;标签内的测试方法会在同一个线程中执行，不同的\u0026lt;test\u0026gt;标签会在不同的独立线程中执行。\nclasses：\n\u0026lt;suite name=\u0026#34;My suite\u0026#34; parallel=\u0026#34;classes\u0026#34; thread-count=\u0026#34;5\u0026#34;\u0026gt; 同一个类中的测试方法会在同一个线程中执行，不同的类会在不同的独立线程中执行。\ninstances：\n\u0026lt;suite name=\u0026#34;My suite\u0026#34; parallel=\u0026#34;instances\u0026#34; thread-count=\u0026#34;5\u0026#34;\u0026gt; 同一个实例中的测试方法会在同一个线程中执行，不同的实例会在不同的独立线程中执行。（Factory能创建多个实例）\n重跑失败用例 TestNG在运行后会把失败的用例输出到testng-failed.xml文件中，可以直接运行这个文件来重跑失败用例。\n夸一句，这个设计真棒。\n有时候，需要让用例失败时自动重试，那么可以在代码中这样实现：\nimport org.testng.IRetryAnalyzer; import org.testng.ITestResult; public class MyRetry implements IRetryAnalyzer { private int retryCount = 0; private static final int maxRetryCount = 3; @Override public boolean retry(ITestResult result) { if (retryCount \u0026lt; maxRetryCount) { retryCount++; return true; } return false; } } import org.testng.Assert; import org.testng.annotations.Test; public class TestclassSample { @Test(retryAnalyzer = MyRetry.class) public void test2() { Assert.fail(); } } 实现IRetryAnalyzer接口的retry方法。 在@Test的retryAnalyzer中指定重试类。 TestNG编程 除了IDEA和Maven这两种运行方式，TestNG还可以直接在程序中调用运行：\nTestListenerAdapter tla = new TestListenerAdapter(); TestNG testng = new TestNG(); testng.setTestClasses(new Class[] { Run2.class }); testng.addListener(tla); testng.run(); TestListenerAdapter是默认的，可以实现org.testng.ITestListener接口自定义TestListener。 setTestClasses添加测试类。 run()运行。 还可以通过编程创建一个虚拟的testng.xml，org.testng.xml包的XmlClass, XmlTest等提供了这个能力：\nXmlSuite suite = new XmlSuite(); suite.setName(\u0026#34;TmpSuite\u0026#34;); XmlTest test = new XmlTest(suite); test.setName(\u0026#34;TmpTest\u0026#34;); List\u0026lt;XmlClass\u0026gt; classes = new ArrayList\u0026lt;XmlClass\u0026gt;(); classes.add(new XmlClass(\u0026#34;test.failures.Child\u0026#34;)); test.setXmlClasses(classes) ; 这段代码会创建一个这样的testng.xml：\n\u0026lt;suite name=\u0026#34;TmpSuite\u0026#34; \u0026gt; \u0026lt;test name=\u0026#34;TmpTest\u0026#34; \u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;test.failures.Child\u0026#34; /\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;/suite\u0026gt; XmlSuite可以通过TestNG程序调用：\nList\u0026lt;XmlSuite\u0026gt; suites = new ArrayList\u0026lt;XmlSuite\u0026gt;(); suites.add(suite); TestNG tng = new TestNG(); tng.setXmlSuites(suites); tng.run(); XML中写BeanShell 是的，TestNG的XML中可以写BeanShell，用来替代\u0026lt;include\u0026gt;和\u0026lt;exclude\u0026gt;：\n\u0026lt;test name=\u0026#34;BeanShell test\u0026#34;\u0026gt; \u0026lt;method-selectors\u0026gt; \u0026lt;method-selector\u0026gt; \u0026lt;script language=\u0026#34;beanshell\u0026#34;\u0026gt;\u0026lt;![CDATA[ groups.containsKey(\u0026#34;test1\u0026#34;) ]]\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/method-selector\u0026gt; \u0026lt;/method-selectors\u0026gt; \u0026lt;!-- ... --\u0026gt; CDATA用来防止跟XML标签语法冲突。 预置了method（当前测试方法）、testngMethod（当前测试方法的描述）、groups（当前测试方法所属分组）三个对象，可以用来做匹配。 使用\u0026lt;script\u0026gt;后，\u0026lt;include\u0026gt;和\u0026lt;exclude\u0026gt;会失效。 Listener TestNG提供了很多Listener用来自定义TestNG行为，类似于Hook那个意思：\nIAnnotationTransformer IAnnotationTransformer2 IHookable IInvokedMethodListener IMethodInterceptor IReporter ISuiteListener ITestListener ITestNGListener IAlterSuiteListener 比如用IAnnotationTransformer动态设置@Test属性：\npublic class MyTransformer implements IAnnotationTransformer { public void transform(ITest annotation, Class testClass, Constructor testConstructor, Method testMethod) { if (\u0026#34;invoke\u0026#34;.equals(testMethod.getName())) { annotation.setInvocationCount(5); } } } 如果想修改@Factory或@DataProvider，得用IAnnotationTransformer2。\n比如用IMethodInterceptor设定用例组的执行顺序：\npublic List\u0026lt;IMethodInstance\u0026gt; intercept(List\u0026lt;IMethodInstance\u0026gt; methods, ITestContext context) { List\u0026lt;IMethodInstance\u0026gt; result = new ArrayList\u0026lt;IMethodInstance\u0026gt;(); for (IMethodInstance m : methods) { Test test = m.getMethod().getConstructorOrMethod().getAnnotation(Test.class); Set\u0026lt;String\u0026gt; groups = new HashSet\u0026lt;String\u0026gt;(); for (String group : test.groups()) { groups.add(group); } //让fast分组第一个执行 if (groups.contains(\u0026#34;fast\u0026#34;)) { result.add(0, m); } else { result.add(m); } } return result; } 比如想自定义测试开始前和测试后的行为，可以先写个Listener：\npublic class BeforeAfterLog extends TestListenerAdapter { public void onTestSuccess(ITestResult result) { System.out.println(\u0026#34;onTestSuccess\u0026#34;); } public void onTestFailure(ITestResult result) { System.out.println(\u0026#34;onTestFailure\u0026#34;); } } 然后在测试类上添加：\n@Listeners(BeforeAfterLog.class) public class MyTest { } 这样就不用每个测试类都写@BeforeClass和@AfterClass了。\n依赖注入 TestNG支持在测试方法中添加特定类来进行依赖注入（就是获取TestNG相关信息）：\nITestContext XmlTest 当前\u0026lt;test\u0026gt;标签 Method 当前调用的测试方法 Object[] 当前测试方法的入参 ITestResult 当前测试结果 不同注解支持的方式如下表所示：\n示例：\npublic class NoInjectionTest { @DataProvider(name = \u0026#34;provider\u0026#34;) public Object[][] provide() throws Exception { return new Object[][] { { CC.class.getMethod(\u0026#34;f\u0026#34;) } }; } @Test(dataProvider = \u0026#34;provider\u0026#34;) public void withoutInjection(@NoInjection Method m) { Assert.assertEquals(m.getName(), \u0026#34;f\u0026#34;); } @Test(dataProvider = \u0026#34;provider\u0026#34;) public void withInjection(Method m) { Assert.assertEquals(m.getName(), \u0026#34;withInjection\u0026#34;); } } @NoInjection用于禁止依赖注入。\n断言 TestNG支持Java的assert关键字断言，示例：\n@Test public void verifyLastName() { assert \u0026#34;Beust\u0026#34;.equals(m_lastName) : \u0026#34;Expected name Beust, for\u0026#34; + m_lastName; } 也可以用JUnit断言方法：\nimport static org.testng.AssertJUnit.*; //... @Test public void verify() { assertEquals(\u0026#34;Beust\u0026#34;, m_lastName); } 日志 用Listener来实现，直接看代码：\npublic class DotTestListener extends TestListenerAdapter { private int m_count = 0; @Override public void onTestFailure(ITestResult tr) { log(\u0026#34;F\u0026#34;); } @Override public void onTestSkipped(ITestResult tr) { log(\u0026#34;S\u0026#34;); } @Override public void onTestSuccess(ITestResult tr) { log(\u0026#34;.\u0026#34;); } private void log(String string) { System.out.print(string); if (++m_count % 40 == 0) { System.out.println(\u0026#34;\u0026#34;); } } } 测试报告 TestNG自带了一个测试报告，运行完后会生成index.html，打开就是，比较丑，推荐用Allure。\n如果想获取测试报告的数据，那么可以从org.testng.IReporter接口的方法：\npublic void generateReport(List\u0026lt;ISuite\u0026gt; suites, String outputDirectory) 根据它的入参去拿。\n如果想给测试报告添加数据，那么可以使用org.testng.Reporter类：\nReporter.log(\u0026#34;M3 WAS CALLED\u0026#34;); YAML替代XML 如果你烦透了XML，那么可以试试YAML。\n比如XML：\n\u0026lt;suite name=\u0026#34;SingleSuite\u0026#34; verbose=\u0026#34;2\u0026#34; thread-count=\u0026#34;4\u0026#34;\u0026gt; \u0026lt;parameter name=\u0026#34;n\u0026#34; value=\u0026#34;42\u0026#34; /\u0026gt; \u0026lt;test name=\u0026#34;Regression2\u0026#34;\u0026gt; \u0026lt;groups\u0026gt; \u0026lt;run\u0026gt; \u0026lt;exclude name=\u0026#34;broken\u0026#34; /\u0026gt; \u0026lt;/run\u0026gt; \u0026lt;/groups\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;test.listeners.ResultEndMillisTest\u0026#34; /\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;/suite\u0026gt; 换成YAML：\nname: SingleSuite threadCount: 4 parameters: { n: 42 } tests: - name: Regression2 parameters: { count: 10 } excludedGroups: [ broken ] classes: - test.listeners.ResultEndMillisTest 舒服多了。不过TestNG本身没有引入YAML依赖包，需要自己添加：\n\u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;org.yaml\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;snakeyaml\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;1.23\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 彩蛋 看完了官方教程，感觉测试框架的功能基本上大同小异，只是技术实现上有所区别。让我惊喜的是，TestNG运行失败的用例可以自动生成一份失败用例的xml文件，直接拿来重跑就可以了。还有一直以为TestNG只能用XML，没想到也能用YAML了。最后，它给出了-Dtestng.mode.dryrun=true这个参数，猜猜它是干嘛的。有时候只想看跑起来会执行哪些测试，而不想真正执行，那么就可以用这个参数。\n参考资料：\nhttps://testng.org/doc/\nhttps://testng.org/doc/documentation-main.html\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000021-testng%E5%8D%95%E7%AF%87%E9%95%BF%E6%96%87testng%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E7%B2%BE%E9%80%9A/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"简介\"\u003e简介\u003c/h1\u003e\n\u003cp\u003eTestNG是Test Next Generation的缩写，它的灵感来自于JUnit和NUnit，在它们基础上增加了很多很牛的功能，比如说：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e注解。\u003c/li\u003e\n\u003cli\u003e多线程，比如所有方法都在各自线程中，一个测试类一个线程等。\u003c/li\u003e\n\u003cli\u003e验证代码是否多线程安全。\u003c/li\u003e\n\u003cli\u003e灵活的测试配置。\u003c/li\u003e\n\u003cli\u003e支持数据驱动（@DataProvider）。\u003c/li\u003e\n\u003cli\u003e支持参数化。\u003c/li\u003e\n\u003cli\u003e强大的执行机制（不需要TestSuite）。\u003c/li\u003e\n\u003cli\u003e能跟各种工具结合（比如IDEA、Maven等）。\u003c/li\u003e\n\u003cli\u003e内嵌BeanShell。\u003c/li\u003e\n\u003cli\u003e提供运行时和日志的JDK函数（不需要添加dependencies）。\u003c/li\u003e\n\u003cli\u003e提供应用服务器测试依赖的方法。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e一句话总结就是，\u003cstrong\u003eTestNG强大到能覆盖所有测试类型：单元测试、功能测试、端到端测试、集成测试\u003c/strong\u003e等等等。\u003c/p\u003e\n\u003cp\u003e一个简单的示例如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eexample1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.testng.annotations.*\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eSimpleTest\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@BeforeClass\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esetUp\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// code that will be invoked when this test is instantiated\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003egroups\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;fast\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eaFastTest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Fast test\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003egroups\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;slow\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eaSlowTest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Slow test\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003esetUp()会在\u003cstrong\u003e测试类创建后，测试方法执行前\u003c/strong\u003e运行。\u003c/li\u003e\n\u003cli\u003e测试方法名字可以任意的，TestNG通过@Test注解来标识。\u003c/li\u003e\n\u003cli\u003e可以把测试方法按group分组。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e然后使用xml来配置运行策略：\u003c/p\u003e","title":"单篇长文TestNG从入门到精通"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n首先，思维深度。AI写作是快，自己写作是慢。在思考时，我们需要慢下来，就像冥想一样，很多事情是需要时间，慢慢想清楚的。在网上已经充斥着大量的AI一键生成文章，写的很严谨、很漂亮，但缺乏最重要的“人情味”。现在还有人认真看完一篇AI写的技术文章么？至少我是很难看完的。反而是人写的一些思考感受，我会逐字看完，这是AI永远无法替代的与人交流价值。自己写文章的过程，就是深度思考的过程，这样写出来的文字，更有可能给人启发。\n其次，完整表达。让AI写东西，我们的角色是指导者，说一些零散的想法，让AI写成一篇工整的文章。我前段时间让AI写了一篇分享材料，昨天又自己写了一篇。两者的写作过程完全不一样。AI写作就是常说的Vibe Coding氛围编程，跟着感觉让AI写。而自己写文章，会梳理表达的顺序，分几个章节,怎么串起来，有头有尾。如果我们总是在碎言碎语，久而久之可能很难再进行逻辑清晰的完整表达。有骨架、有节奏的完整表达，是一项很重要的基础能力，自己写文章，可以不断强化这项能力。\n最后，个人印记。当整个世界都充满了AI，我是谁。我们需要找个地方记录自己。我们通过《史记》知道司马迁，我们通过《离骚》知道屈原，我们通过《呐喊》知道鲁迅。我们很多人都做不到这么伟大，但是当我们的后代看到我们写的东西时，他们会想些什么。历史沉淀的不只是表面的文字，还有文字背后的作者。一篇文章是AI写的，我们提取了文章的信息。一篇文章是人写的，我们记住了写文章的这个人。\n“写作不是生产句子，写作是在你试图把一个模糊的感觉翻译成清晰的语言的过程中，发现自己到底在想什么。大语言模型可以复制那个过程的外观，但它不能替你完成那个过程。因为价值不在产出的那个东西里，在产出过程中发生在你身上的转变里。”\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000021-%E5%9D%9A%E6%8C%81%E8%87%AA%E5%B7%B1%E5%86%99%E6%96%87%E7%AB%A0%E7%9A%84%E6%84%8F%E4%B9%89/","summary":"首先，思维深度。AI写作是快，自己写作是慢。在思考时，我们需要慢下来，就像冥想一样，很多事情是需要时间，慢慢想清楚的。在网上已经充斥着大量的AI一键生成文章，写的很严谨、很漂亮，但缺乏最重要的“人情味”。现在还有人认真看完一篇AI写的技术文章么？至少我是很难看完的。反而是人写的一些思考感受，我会逐字看完，这是AI永远无法替代的与人交流价值。自己写文章的过程，就是深度思考的过程，这样写出来的文字，更","title":"坚持自己写文章的意义"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n为什么要用JUnit5？\n我最近把接口平台不好实现的自动化，在本地写了个Java小项目来实现了，集成Allure发现IntelliJ IDEA 2024.3.2 (Community Edition)的surefire只能识别junit，现象是在mvn test时会打印：\n[INFO] --- surefire:3.2.3:test (default-test) @ auto --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider 无法通过IDEA配置把junit换成testng。同时断言时junit的错误提示也能到具体数据类型，而testng不会：\norg.opentest4j.AssertionFailedError: expected: java.lang.String@667025e1\u0026lt;3\u0026gt; but was: java.lang.Integer@23b2b751\u0026lt;3\u0026gt; Expected :3 Actual :3 怎么配置？\n第一步是要让mvn test命令执行后，能生成allure的结果数据，一堆json文件。参考官网教程：\nhttps://allurereport.org/docs/junit5/\n配置pom.xml即可。\n第二步是要把结果数据生成HTML报告并在浏览器打开。\n方式1是plugin插件，不需要手动下载安装包：\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;io.qameta.allure\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;allure-maven\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.12.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;reportVersion\u0026gt;${allure.version}\u0026lt;/reportVersion\u0026gt; \u0026lt;resultsDirectory\u0026gt;${project.build.directory}/allure-results\u0026lt;/resultsDirectory\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; 配置后，Maven会在根目录生成.allure文件夹，就是运行文件了，执行命令即可：\nmvn allure:serve 方式2是单独安装Allure，参考官网说明：\nhttps://allurereport.org/docs/install/\n安装后使用命令allure serve。\n最后分享1个小技巧，我用的IntelliJ IDEA 2024.3.2 (Community Edition)支持Markdown文件执行命令，也就是在.md文件里面输入这3行：\nIDEA会在左侧出现绿色运行小箭头，点击就可以执行。\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000020-allure+maven+junit5%E9%85%8D%E7%BD%AE%E5%AE%9E%E8%B7%B5/","summary":"\u003cstrong\u003e为什么要用JUnit5？\u003c/strong\u003e","title":"Allure+Maven+JUnit5配置实践"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n任何把函数当做一等对象的语言，它的设计者都要面对一个问题：作为一等对象的函数在某个作用域中定义，但是可能会在其他作用域中调用，如何处理自由变量？\n自由变量（free variable），未在局部作用域中绑定的变量。\n为了解决这个问题，Python之父Guido Van Rossum设计了闭包，有如神来之笔，代码美学尽显。在讨论闭包之前，有必要先了解Python中的变量作用域。\n变量作用域 先看一个全局变量和自由变量的示例：\n\u0026gt;\u0026gt;\u0026gt; b = 6 \u0026gt;\u0026gt;\u0026gt; def f1(a): ... print(a) ... print(b) ... \u0026gt;\u0026gt;\u0026gt; f1(3) 3 6 函数体外的b为全局变量，函数体内的b为自由变量。因为自由变量b绑定到了全局变量，所以在函数f1()中能正确print。\n如果稍微改一下，那么函数体内的b就会从自由变量变成局部变量：\n\u0026gt;\u0026gt;\u0026gt; b = 6 def f1(a): ... print(a) ... print(b) ... b = 9 ... \u0026gt;\u0026gt;\u0026gt; f1(3) 3 Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 3, in f1 UnboundLocalError: local variable \u0026#39;b\u0026#39; referenced before assignment 在函数f1()后面加上b = 9报错：局部变量b在赋值前进行了引用。\n这不是缺陷，而是Python设计：Python不要求声明变量，而是假定在函数定义体中赋值的变量是局部变量。\n如果想让解释器把b当做全局变量，那么需要使用global声明：\n\u0026gt;\u0026gt;\u0026gt; b = 6 \u0026gt;\u0026gt;\u0026gt; def f1(a): ... global b ... print(a) ... print(b) ... b = 9 ... \u0026gt;\u0026gt;\u0026gt; f1(3) 3 6 闭包 回到文章开头的自由变量问题，假如有个叫做avg的函数，它的作用是计算系列值的均值，用类实现：\nclass Averager(): def __init__(self): self.series = [] def __call__(self, new_value): self.series.append(new_value) total = sum(self.series) return totle / len(self.series) avg = Averager() avg(10) # 10.0 avg(11) # 10.5 avg(12) # 11.0 类实现不存在自由变量问题，因为self.series是类属性。但是函数实现，进行函数嵌套时，问题就出现了：\ndef make_averager(): series = [] def averager(new_value): # series是自由变量 series.append(new_value) total = sum(series) return totle / len(series) return averager avg = make_averager() avg(10) # 10.0 avg(11) # 10.5 avg(12) # 11.0 函数make_averager()在局部作用域中定义了series变量，它的内部函数averager()的自由变量series绑定了这个值。但是在调用avg(10)时，make_averager()函数已经return返回了，它的局部作用域也消失了。没有闭包的话，自由变量series一定会报错找不到定义。\n那么闭包是怎么做的呢？闭包是一种函数，它会保留定义时存在的自由变量的绑定，这样调用函数时，虽然定义作用域不可用了，但是仍然能使用那些绑定。\n如下图所示：\n闭包会保留自由变量series的绑定，在调用avg(10)时继续使用这个绑定，即使make_averager()函数的局部作用域已经消失。\nnonlocal 把上面示例的需求稍微优化下，只存储目前的总值和元素个数：\ndef make_averager(): count = 0 total = 0 def averager(new_value): count += 1 total += new_value return total / count return averager 运行后会报错：局部变量count在赋值前进行了引用。因为count +=1等同于count = count + 1，存在赋值，count就变成局部变量了。total也是如此。\n这里如果把count和total通过global关键字声明为全局变量，显然是不合适的，它们作用域最多只扩展到make_averager()函数内。为了解决这个问题，Python3引入了nonlocal关键字声明：\ndef make_averager(): count = 0 total = 0 def averager(new_value): nonlocal count, total count += 1 total += new_value return total / count return averager nonlocal的作用是把变量标记为自由变量，即使在函数中为变量赋值了，也仍然是自由变量。\n注意，对于列表、字典等可变类型来说，添加元素不是赋值，不会隐式创建局部变量。对于数字、字符串、元组等不可变类型以及None来说，赋值会隐式创建局部变量。示例：\ndef make_averager(): # 可变类型 count = {} def averager(new_value): print(count) # 成功 count[new_value] = new_value return count return averager 可变对象添加元素不是赋值，不会隐式创建局部变量。\ndef make_averager(): # 不可变类型 count = 1 def averager(new_value): print(count) # 报错 count = new_value return count return averager count是不可变类型，赋值会隐式创建局部变量，报错：局部变量count在赋值前进行了引用。\ndef make_averager(): # None count = None def averager(new_value): print(count) # 报错 count = new_value return count return averager count是None，赋值会隐式创建局部变量，报错：局部变量count在赋值前进行了引用。\n小结 本文先介绍了全局变量、自由变量、局部变量的概念，这是理解闭包的前提。闭包就是用来解决函数嵌套时，自由变量如何处理的问题，它会保留自由变量的绑定，即使局部作用域已经消失。对于不可变类型和None来说，赋值会隐式创建局部变量，把自由变量转换为局部变量，这可能会导致程序报错：局部变量在赋值前进行了引用。除了使用global声明为全局变量外，还可以使用nonlocal声明把局部变量强制变为自由变量，实现闭包。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000020-python%E5%85%B3%E4%BA%8Epython%E9%97%AD%E5%8C%85%E7%9A%84%E4%B8%80%E5%88%87/","summary":"任何把函数当做一等对象的语言，它的设计者都要面对一个问题：作为一等对象的函数在某个作用域中定义，但是可能会在其他作用域中调用，如何处理自由变量？","title":"关于Python闭包的一切"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n这个话题，网上很多人讨论过很多次，我也曾经聊过3次。\n《AI时代如何自我提升》这篇文章提到过，选择学什么，最重要的是看实用价值，编程语言的选择也是同样道理。\n编程语言对于测试开发来说，用来干啥？\n这个问题可以根据自身实际情况回答，AI也能给出标准答案。\n之所以写下这篇文章，是因为有了新的视角，不一定对，仅供参考。\n我们使用编程语言，并不是像开发那样，用来做功能开发的。也有做平台开发的测开，受限于个人经验，暂不讨论。对于业务测开来说，编程语言，并没有开发那么重要，也不需要开发那样精通。\n对于测试来说，最重要的是保障业务系统质量，业务系统是被测对象，只有对被测对象足够了解，才能发现更多潜在问题。选择什么编程语言，业务系统很关键。\n业务系统是什么编程语言，我们就有必要把这门语言作为我们学习的首选。比如传统行业系统大多都是Java开发的，那么就选Java。互联网行业有些新产品是Golang开发的，那么就选Golang。游戏行业是C++居多，那么就选C++。人工智能、数据分析行业就首选Python。等等等。测什么系统，选什么语言。\n但是我们并不需要精通这门语言，够用就行，毕竟我们的核心工作是测试，而不是开发。一切语言的学习都以解决实际问题为主。同时，语法并不是我们学习编程语言的重点，而是要关注整个编程语言的生态，建立系统全局观。充分了解被测对象。\n测试的部分工作，也需要编程才能完成。我们可以沿用业务系统同样的语言，也可以使用新的编程语言。综合考虑技术成熟度、上手难易程序、个人喜好等。比如很多人会选择Python做自动化，也挺好的。\n之前跟部分小伙伴聊过简历或职业发展，可能也是大环境影响吧，越来越卷，似乎不会编程都找不到测试工作了，于是各种学习编程，学不好又很焦虑。校招会更加注意理论，社招其实偏向于项目经验。测试开发要学习的东西很多，编程语言只是其中之一。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000020-%E8%80%81%E7%94%9F%E5%B8%B8%E8%B0%88%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/","summary":"这个话题，网上很多人讨论过很多次，我也曾经聊过3次。","title":"老生常谈测试开发如何选择编程语言"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nfastjson介绍 fastjson是一个Java库，用来在Java对象和JSON之间相互转换。\nfastjson的目标（官方原话）：\n我翻译一下：\n性能非常好；\n封装非常高；\n转换非常强；\n泛型也支持；\n自定义表示；\n复杂也支持；\n这6个目标，书读少的我理解起来费劲，看着这些字，视线开始模糊\u0026hellip;突然这些字动了，不断变幻，凝聚成了两个字：“牛逼”。阿里巴巴也确实做到了，fastjson(24.9k star)作为国产原创库，PK掉了jackson(7.6k star)和gson(21.3k star)，在国内应用领域坐上了头把交椅。\n安装fastsjon Maven引入：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;VERSION_CODE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 版本可以在 https://mvnrepository.com/artifact/com.alibaba/fastjson 查找：\n虽然2.0.x版本出来了，但是使用得多的还是1.2.x版本。至少得1.2.48以上，因为这以下的版本之前出过轰动全国的安全漏洞。\n入门示例 Group.java\npublic class Group { private Long id; private String name; private List\u0026lt;User\u0026gt; users = new ArrayList\u0026lt;User\u0026gt;(); public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List\u0026lt;User\u0026gt; getUsers() { return users; } public void setUsers(List\u0026lt;User\u0026gt; users) { this.users = users; } public void addUser(User user) { users.add(user); } } User.java\npublic class User { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 编码，Java对象转换为JSON：\nimport com.alibaba.fastjson.JSON; Group group = new Group(); group.setId(0L); group.setName(\u0026#34;admin\u0026#34;); User guestUser = new User(); guestUser.setId(2L); guestUser.setName(\u0026#34;guest\u0026#34;); User rootUser = new User(); rootUser.setId(3L); rootUser.setName(\u0026#34;root\u0026#34;); group.addUser(guestUser); group.addUser(rootUser); // toJSONString方法 String jsonString = JSON.toJSONString(group); System.out.println(jsonString); 输出：\n{\u0026#34;id\u0026#34;:0,\u0026#34;name\u0026#34;:\u0026#34;admin\u0026#34;,\u0026#34;users\u0026#34;:[{\u0026#34;id\u0026#34;:2,\u0026#34;name\u0026#34;:\u0026#34;guest\u0026#34;},{\u0026#34;id\u0026#34;:3,\u0026#34;name\u0026#34;:\u0026#34;root\u0026#34;}]} 解码，JSON转换为Java对象：\nString jsonString = ...; // parseObject方法 Group group = JSON.parseObject(jsonString, Group.class); fastjson封装了JSON类的toJSONString()和parseObject()方法，来实现Java对象和JSON之间相互转换。\n在JSON转换为Java对象时，传入的String可以写在单行：\nString jsonString = \u0026#34;{\\\u0026#34;id\\\u0026#34;:0,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;admin\\\u0026#34;,\\\u0026#34;users\\\u0026#34;:[{\\\u0026#34;id\\\u0026#34;:2,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;guest\\\u0026#34;},{\\\u0026#34;id\\\u0026#34;:3,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;root\\\u0026#34;}]}\u0026#34;; 也可以格式化后复制粘贴为多行：\nString jsonString = \u0026#34;{\\n\u0026#34; + \u0026#34; \\\u0026#34;id\\\u0026#34;:0,\\n\u0026#34; + \u0026#34; \\\u0026#34;name\\\u0026#34;:\\\u0026#34;admin\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;users\\\u0026#34;:[\\n\u0026#34; + \u0026#34; {\\n\u0026#34; + \u0026#34; \\\u0026#34;id\\\u0026#34;:2,\\n\u0026#34; + \u0026#34; \\\u0026#34;name\\\u0026#34;:\\\u0026#34;guest\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; {\\n\u0026#34; + \u0026#34; \\\u0026#34;id\\\u0026#34;:3,\\n\u0026#34; + \u0026#34; \\\u0026#34;name\\\u0026#34;:\\\u0026#34;root\\\u0026#34;\\n\u0026#34; + \u0026#34; }\\n\u0026#34; + \u0026#34; ]\\n\u0026#34; + \u0026#34;}\u0026#34;; 单行和多行都能够转换成功。\n字符串转JSON提取数据 除了Java对象和JSON相互转换以外，还有个对于测试来说，更为常见的使用场景：把字符串转为JSON，提取其中某些字段的数据。\n这也很简单，parseObject()方法不传第二个参数，返回的就是一个JSONObject对象：\nString jsonString = ...; // parseObject方法 JSONObject jsonObj = JSON.parseObject(jsonString); JSONObject提供了一系列不同数据类型的get()方法来提取数据：\n对号入座提取想要的数据就可以了。\n参考资料：\nfastjson源码 https://github.com/alibaba/fastjson\nfastjson详解 https://blog.csdn.net/cold___play/article/details/124525519\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000019-%E5%BA%93fastjson%E7%9F%A5%E5%A4%9A%E5%B0%91/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"fastjson介绍\"\u003efastjson介绍\u003c/h2\u003e\n\u003cp\u003efastjson是一个Java库，用来在Java对象和JSON之间相互转换。\u003c/p\u003e\n\u003cp\u003efastjson的目标（官方原话）：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0f56c32e240a4558b7f1ed65522a8453.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我翻译一下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e性能非常好；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e封装非常高；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e转换非常强；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e泛型也支持；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e自定义表示；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e复杂也支持；\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这6个目标，书读少的我理解起来费劲，看着这些字，视线开始模糊\u0026hellip;突然这些字动了，不断变幻，凝聚成了两个字：“牛逼”。阿里巴巴也确实做到了，fastjson(24.9k star)作为国产原创库，PK掉了jackson(7.6k star)和gson(21.3k star)，在国内应用领域坐上了头把交椅。\u003c/p\u003e\n\u003ch2 id=\"安装fastsjon\"\u003e安装fastsjon\u003c/h2\u003e\n\u003cp\u003eMaven引入：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ecom.alibaba\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003efastjson\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003eVERSION_CODE\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e版本可以在 \u003ca href=\"https://mvnrepository.com/artifact/com.alibaba/fastjson\"\u003ehttps://mvnrepository.com/artifact/com.alibaba/fastjson\u003c/a\u003e 查找：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8f2149bc289b4994acf0f4f413f569ca.png\"\u003e\u003c/p\u003e\n\u003cp\u003e虽然2.0.x版本出来了，但是使用得多的还是1.2.x版本。至少得1.2.48以上，因为这以下的版本之前出过轰动全国的安全漏洞。\u003c/p\u003e\n\u003ch2 id=\"入门示例\"\u003e入门示例\u003c/h2\u003e\n\u003cp\u003eGroup.java\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eGroup\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eLong\u003cspan style=\"color:#fff\"\u003e       \u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e     \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eUser\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eusers\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eArrayList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eUser\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eLong\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetId\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esetId\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eLong\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esetName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eUser\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetUsers\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eusers\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esetUsers\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eUser\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eusers\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eusers\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eusers\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eaddUser\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eUser\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003euser\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eusers\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eadd\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003euser\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eUser.java\u003c/p\u003e","title":"fastjson知多少"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPython处理接口数据 接口数据通常是JSON格式，Python处理起来并不轻松。\n方法一，json文件。额外编写代码读取文件，接口参数很多，文件变多，难维护。\n方法二，字典。JSON可以直接粘贴到代码中，Python认作字典，但语法有差异，JSON的语法是true/false/null，Python的语法是True/False/None，不兼容。\n这两种方法都有共同的缺点，需要编写表达式来定位某个节点。\n比如：body[\u0026quot;a\u0026quot;][0][\u0026quot;x\u0026quot;][\u0026quot;m\u0026quot;]\n字符串与占位符 占位符是一种很常见的文本替换技术，在任何编程语言中都有广泛应用。\nPython语法有个很特别的字符串，三引号字符串：\ntest_json = \u0026#34;\u0026#34;\u0026#34; { \u0026#34;bool\u0026#34;: true, \u0026#34;x\u0026#34;: null, \u0026#34;customerId\u0026#34;: \u0026#34;123456\u0026#34;, \u0026#34;items\u0026#34;: [ { \u0026#34;productId\u0026#34;: \u0026#34;987654\u0026#34;, \u0026#34;quantity\u0026#34;: 2, \u0026#34;price\u0026#34;: 199.99 } ], \u0026#34;shippingAddress\u0026#34;: { \u0026#34;recipientName\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;streetAddress\u0026#34;: \u0026#34;北京市朝阳区某街道123号\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;state\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;postalCode\u0026#34;: \u0026#34;100000\u0026#34;, \u0026#34;country\u0026#34;: \u0026#34;中国\u0026#34; }, \u0026#34;paymentMethod\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;CreditCard\u0026#34;, \u0026#34;cardNumber\u0026#34;: \u0026#34;4111111111111111\u0026#34;, \u0026#34;expiryDate\u0026#34;: \u0026#34;12/25\u0026#34;, \u0026#34;cvv\u0026#34;: \u0026#34;123\u0026#34; }, \u0026#34;orderNotes\u0026#34;: \u0026#34;请尽快发货，谢谢！\u0026#34; } \u0026#34;\u0026#34;\u0026#34; 基本思路是，把JSON写在三引号字符串中，这能保留原生JSON格式，就像很多测试平台的做法一样，然后再通过占位符，直接替换其中的节点值。占位符不用编写表达式定位节点，在JSON字符串标记即可。\n思路有了，怎么写呢？Python没有现成的函数，没关系，自己写一个。\ntep3函数v() tep3的新特性，函数v()正是用来解决这个问题的。\n实际效果：\n普通字符串\n## 以前 domain = \u0026#34;https://dongfanger.github.io\u0026#34; url = domain + \u0026#34;/blog\u0026#34; # 字符串拼接 url = f\u0026#34;{domain}/blog\u0026#34; # f-string ## 使用v()函数以后 v({ \u0026#34;domain\u0026#34;: \u0026#34;https://dongfanger.github.io\u0026#34; }) url = v(\u0026#34;${domain}/blog\u0026#34;) JSON字符串\n## dict不兼容，注意看True和None test_json = { \u0026#34;bool\u0026#34;: True, \u0026#34;x\u0026#34;: None, \u0026#34;customerId\u0026#34;: \u0026#34;123456\u0026#34;, \u0026#34;items\u0026#34;: [ { \u0026#34;productId\u0026#34;: \u0026#34;987654\u0026#34;, \u0026#34;quantity\u0026#34;: 2, \u0026#34;price\u0026#34;: 199.99 } ], \u0026#34;shippingAddress\u0026#34;: { \u0026#34;recipientName\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;streetAddress\u0026#34;: \u0026#34;北京市朝阳区某街道123号\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;state\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;postalCode\u0026#34;: \u0026#34;100000\u0026#34;, \u0026#34;country\u0026#34;: \u0026#34;中国\u0026#34; }, \u0026#34;paymentMethod\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;CreditCard\u0026#34;, \u0026#34;cardNumber\u0026#34;: \u0026#34;4111111111111111\u0026#34;, \u0026#34;expiryDate\u0026#34;: \u0026#34;12/25\u0026#34;, \u0026#34;cvv\u0026#34;: \u0026#34;123\u0026#34; }, \u0026#34;orderNotes\u0026#34;: \u0026#34;请尽快发货，谢谢！\u0026#34; } ## 需要表达式 test_json[\u0026#34;items\u0026#34;][\u0026#34;quantity\u0026#34;] = 3 test_json[\u0026#34;orderNotes\u0026#34;] = \u0026#34;测试开发Gang\u0026#34; Python的dict并不兼容JSON，需要把true改为True，null改为None，替换节点值也需要表达式。\n使用函数v()以后：\nv({ \u0026#34;quantity\u0026#34;: 3 \u0026#34;orderNotes\u0026#34;: \u0026#34;测试开发Gang\u0026#34; }) template = \u0026#34;\u0026#34;\u0026#34; { \u0026#34;bool\u0026#34;: true, \u0026#34;x\u0026#34;: null, \u0026#34;customerId\u0026#34;: \u0026#34;123456\u0026#34;, \u0026#34;items\u0026#34;: [ { \u0026#34;productId\u0026#34;: \u0026#34;987654\u0026#34;, \u0026#34;quantity\u0026#34;: ${quantity}, \u0026#34;price\u0026#34;: 199.99 } ], \u0026#34;shippingAddress\u0026#34;: { \u0026#34;recipientName\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;streetAddress\u0026#34;: \u0026#34;北京市朝阳区某街道123号\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;state\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;postalCode\u0026#34;: \u0026#34;100000\u0026#34;, \u0026#34;country\u0026#34;: \u0026#34;中国\u0026#34; }, \u0026#34;paymentMethod\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;CreditCard\u0026#34;, \u0026#34;cardNumber\u0026#34;: \u0026#34;4111111111111111\u0026#34;, \u0026#34;expiryDate\u0026#34;: \u0026#34;12/25\u0026#34;, \u0026#34;cvv\u0026#34;: \u0026#34;123\u0026#34; }, \u0026#34;orderNotes\u0026#34;: \u0026#34;${orderNotes}\u0026#34; } \u0026#34;\u0026#34;\u0026#34; test_json = v(template) 变量池，字符串，占位符。\n这就是tep3的函数v()。\n用法介绍 1、初始化变量池，传入dict，v({\u0026quot;a\u0026quot;: 1, \u0026quot;b\u0026quot;: 2})\n2、set变量，传入key和value，v(\u0026quot;c\u0026quot;, 3)\n3、get变量，传入key，v(\u0026quot;a\u0026quot;)\n4、替换字符串，传入占位符字符串，v(\u0026quot;My name is ${gang}, hello\u0026quot;)\n需要注意的是，JSON是写成多行字符串，函数v()的输出是字符串类型，在post请求时，request(\u0026quot;post\u0026quot;, json=json.loads(v(body)，v(body)是字符串，需要json.loads()转换为dict类型。\ntep.request也对这个地方做了校验：\ndef _check(method, url, **kwargs) -\u0026gt; bool: json_param = kwargs[\u0026#34;json\u0026#34;] if isinstance(json_param, str): logging.error(\u0026#34;request() json expect dict type, json.loads() convert str to dict\u0026#34;) return False return True 内置函数 函数v()不仅支持普通变量占位符，也支持函数占位符。\ntest_json = \u0026#34;\u0026#34;\u0026#34; \u0026#34;a\u0026#34;: ${random()}, # 随机数 \u0026#34;b\u0026#34;: ${random(9)}, # 随机数，指定位数 \u0026#34;c\u0026#34;: \u0026#34;${random(前缀,2)}\u0026#34;, # 随机数，前缀和位数 \u0026#34;d\u0026#34;: \u0026#34;${uuid()}\u0026#34; # uuid \u0026#34;e\u0026#34;: \u0026#34;${time()}\u0026#34; # 当前日期 \u0026#34;f\u0026#34;: \u0026#34;${time(%Y-%m-%d %H:%M:%S)}\u0026#34; # 当前日期，指定日期格式 \u0026#34;g\u0026#34;: \u0026#34;${timestamp()}\u0026#34; # 当前时间的时间戳 \u0026#34;h\u0026#34;: \u0026#34;${timestamp(2023-03-23 23:23:23)}\u0026#34; # 指定时间的时间戳 \u0026#34;\u0026#34;\u0026#34; 更多内置函数还在开发。欢迎加入交流群讨论添加哪些内置函数。\n自定义函数也很简单，先编写个函数，把函数调用写入变量池：\nv(\u0026quot;name\u0026quot;, self_defined_function())\n再通过${name}引用。\ntep版本 函数v()最新特性，请使用tep版本：\npip install tep==3.0.1\ntep3函数v()，支持${}占位符语法，便捷管理接口数据。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000019-python%E5%A4%84%E7%90%86%E6%8E%A5%E5%8F%A3%E6%95%B0%E6%8D%AE%E8%BF%99%E4%B8%AA%E5%87%BD%E6%95%B0%E5%A4%AA%E9%A6%99%E4%BA%86/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"python处理接口数据\"\u003ePython处理接口数据\u003c/h1\u003e\n\u003cp\u003e接口数据通常是JSON格式，Python处理起来并不轻松。\u003c/p\u003e\n\u003cp\u003e方法一，json文件。额外编写代码读取文件，接口参数很多，文件变多，难维护。\u003c/p\u003e\n\u003cp\u003e方法二，字典。JSON可以直接粘贴到代码中，Python认作字典，但语法有差异，JSON的语法是true/false/null，Python的语法是True/False/None，不兼容。\u003c/p\u003e\n\u003cp\u003e这两种方法都有共同的缺点，需要编写表达式来定位某个节点。\u003c/p\u003e\n\u003cp\u003e比如：\u003ccode\u003ebody[\u0026quot;a\u0026quot;][0][\u0026quot;x\u0026quot;][\u0026quot;m\u0026quot;]\u003c/code\u003e\u003c/p\u003e\n\u003ch1 id=\"字符串与占位符\"\u003e字符串与占位符\u003c/h1\u003e\n\u003cp\u003e占位符是一种很常见的文本替换技术，在任何编程语言中都有广泛应用。\u003c/p\u003e\n\u003cp\u003ePython语法有个很特别的字符串，三引号字符串：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etest_json \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e \u0026#34;bool\u0026#34;: true,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  \u0026#34;x\u0026#34;: null,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  \u0026#34;customerId\u0026#34;: \u0026#34;123456\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  \u0026#34;items\u0026#34;: [\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e      \u0026#34;productId\u0026#34;: \u0026#34;987654\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e      \u0026#34;quantity\u0026#34;: 2,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e      \u0026#34;price\u0026#34;: 199.99\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  ],\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  \u0026#34;shippingAddress\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;recipientName\u0026#34;: \u0026#34;张三\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;streetAddress\u0026#34;: \u0026#34;北京市朝阳区某街道123号\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;state\u0026#34;: \u0026#34;北京\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;postalCode\u0026#34;: \u0026#34;100000\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;country\u0026#34;: \u0026#34;中国\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  \u0026#34;paymentMethod\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;type\u0026#34;: \u0026#34;CreditCard\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;cardNumber\u0026#34;: \u0026#34;4111111111111111\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;expiryDate\u0026#34;: \u0026#34;12/25\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;cvv\u0026#34;: \u0026#34;123\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e  \u0026#34;orderNotes\u0026#34;: \u0026#34;请尽快发货，谢谢！\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e基本思路是，把JSON写在三引号字符串中，这能保留原生JSON格式，就像很多测试平台的做法一样，然后再通过占位符，直接替换其中的节点值。占位符不用编写表达式定位节点，在JSON字符串标记即可。\u003c/p\u003e","title":"Python处理接口数据，这个函数太香了"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n函数装饰器是Python语言最优秀的设计之一，它以非常简洁的方式增强了函数的行为，让崎岖不平之路变得平坦顺畅。\n函数装饰器是什么 函数装饰器是一个可调用对象，它的参数是另外一个函数。比如：\n@decorate def target(): print(\u0026#34;running target()\u0026#34;) 跟下面代码效果是一样的：\ndef target(): print(\u0026#34;running target()\u0026#34;) target = decorate(target) 简单实现@decorate:\ndef decorate(func): def inner(): print(\u0026#34;running inner()\u0026#34;) return inner 测试一下：\n\u0026gt;\u0026gt;\u0026gt; target() running inner() \u0026gt;\u0026gt;\u0026gt; target \u0026lt;function decorate.\u0026lt;locals\u0026gt;.inner at 0x04899D18\u0026gt; 新的target是decorate(target)返回的inner函数。\n因为装饰器只是代码优化的一种手段，不像if语句for语句那样，决定了程序流程，所以严格来说，装饰器只是语法糖。它有两个特性，一是能把被装饰的函数替换成其他函数，二是装饰器在加载模块时立即执行。\n装饰器在导入时执行 若想真正理解装饰器，需要区分导入时和运行时。函数装饰器在导入模块时立即执行，而被装饰的函数只在明确调用时运行。\n接下来通过示例对这个特性进行说明，新建registration.py模块：\nregistry = [] def register(func): # 装饰器函数也可以不定义内部函数 print(\u0026#34;running register(%s)\u0026#34; % func) registry.append(func) return func @register def f1(): print(\u0026#34;running f1()\u0026#34;) @register def f2(): print(\u0026#34;running f2()\u0026#34;) def f3(): print(\u0026#34;running f3()\u0026#34;) def main(): print(\u0026#34;running main()\u0026#34;) print(\u0026#34;registry -\u0026gt;\u0026#34;, registry) f1() f2() f3() if __name__ == \u0026#34;__main__\u0026#34;: main() 从结果能看出来：\n@register作用到f1和f2上，在导入时，在main()调用前就执行了。 f3没有装饰器，就没有在main()调用前执行@register。 在main()调用后，明确调用f1()、f2()、f3()才执行函数。 import模块能看得更明显：\n\u0026gt;\u0026gt;\u0026gt; import registration running register(\u0026lt;function f1 at 0x0189A730\u0026gt;) running register(\u0026lt;function f2 at 0x0189A6E8\u0026gt;) 装饰器在导入时就执行了。\n使用装饰器改进策略模式 在《Python设计模式知多少》文章中提到了装饰器可以更优雅的实现策略模式的最佳策略，它的实现代码如下：\npromos = [] def promotion(promo_func): promos.append(promo_func) return promo_func @promotion def fidelity(order): \u0026#34;\u0026#34;\u0026#34;5% discount for customers with 1000 or more fidelity points\u0026#34;\u0026#34;\u0026#34; return order.total() * .05 if order.customer.fidelity \u0026gt;= 1000 else 0 @promotion def bulk_item(order): \u0026#34;\u0026#34;\u0026#34;10% discount for each LineItem with 20 or more units\u0026#34;\u0026#34;\u0026#34; discount = 0 for item in order.cart: if item.quantity \u0026gt;= 20: discount += item.total() * .1 return discount @promotion def large_order(order): \u0026#34;\u0026#34;\u0026#34;7% discount for orders with 10 or more distinct items\u0026#34;\u0026#34;\u0026#34; distinct_items = {item.product for item in order.cart} if len(distinct_items) \u0026gt;= 10: return order.total() * .07 return 0 def best_promo(order): \u0026#34;\u0026#34;\u0026#34;Select best discount available \u0026#34;\u0026#34;\u0026#34; return max(promo(order) for promo in promos) 它解决了\u0026quot;如果想要添加新的促销策略，那么要定义相应函数并添加到promos列表中\u0026quot;这个缺陷，并有更多优点：\n新的促销策略，用@promotion装饰器即可添加。 促销策略函数不用以_promo结尾，可以任意命令。 促销策略可以在任意模块定义，只需要使用@promotion装饰器即可。 小结 本文首先介绍了函数装饰器是一个可调用对象，它的参数是另外一个函数。严格来说，它只是语法糖。要理解装饰器，需要区别导入时和运行时，装饰器在导入时就会执行。最后使用装饰器对策略模式的最佳策略进行了优化。为了进一步学习函数装饰器，得先明白另外一个很重要的概念：闭包。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000019-pythonpython%E5%87%BD%E6%95%B0%E8%A3%85%E9%A5%B0%E5%99%A8%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/","summary":"函数装饰器是Python语言最优秀的设计之一，它以非常简洁的方式增强了函数的行为，让崎岖不平之路变得平坦顺畅。","title":"Python函数装饰器基础知识"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n用例独立。用例相互之间没有依赖，随便拉出一条用例就能执行。\n这是我在文章《接口自动化框架设计漫谈V1.0》提到的，也是我一直坚持的接口自动化用例设计方式。\n这么做的依据是某些开源测试框架是这样设计的，某些公司测试平台也是这么设计的，自然而然就这样认为了。最近公司平台做了升级，能够支持用例复用了，让我重新思考接口自动化的用例，到底是独立还是复用。\n为什么用例独立？我个人理解，用例独立，可能有点受限于技术实现，比如YAML编写用例，或者平台编写用例，实现成本和取得收益对比来看，用例独立是一种均衡的选择。当然用例独立也有好处：隔离性、可维护性、并行执行、错误定位、数据隔离、报告清晰。\n其实从代码复用角度来看，用例复用能提高编写效率，把相对稳定的步骤组装成用例，再复用给其他用例，减少重复代码的编写与维护。\n如果使用tep测试框架，个人推荐使用fixture来实现用例复用，借助依赖注入技术，避免循环import等问题。\n举例，有1条用例case_hello，打招呼，name是变量，代码如下：\n## fixture/fixture_case_hello.py import pytest from tep import v @pytest.fixture(scope=\u0026#39;session\u0026#39;) def case_hello(): def main(): msg = \u0026#39;Hello ${name}\u0026#39; msg = v(msg) print(msg) return main case_hello定义为fixture，放在fixture目录下并以fixture_开头命名文件，tep框架会自动识别为pytest fixture。其中变量处理使用了函数v()。\n新建1条用例test_case_reuse，复用case_hello，代码如下：\n## test_case_reuse.py from tep import v, step def test(case_hello): v({ \u0026#34;name\u0026#34;: \u0026#34;Gang\u0026#34; }) case_hello() step(\u0026#39;welcome\u0026#39;, step_welcome) def step_welcome(): print(\u0026#39;Welcome to tep\u0026#39;) case_hello内部包装了1个内部函数main，返回的是函数引用，所以在用例中进行函数调用case_hello()就能实现用例复用。变量name使用函数v()定义，在用例之间共享。函数v()可以理解为内部变量池，跟JMeter的vars类似，详情可以参考文章《Python处理接口数据，这个函数太香了》。\n总结，用例尽量保持独立，如果需要复用，把重复代码封装为用例，在用例级别进行复用。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000019-%E6%8E%A5%E5%8F%A3%E8%87%AA%E5%8A%A8%E5%8C%96%E7%94%A8%E4%BE%8B%E7%8B%AC%E7%AB%8B%E8%BF%98%E6%98%AF%E7%94%A8%E4%BE%8B%E5%A4%8D%E7%94%A8/","summary":"用例独立。用例相互之间没有依赖，随便拉出一条用例就能执行。","title":"接口自动化，用例独立还是用例复用"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nTasks和Events是Locust性能测试工具的核心技术，有了它们，Locust才能称得上是一个性能工具。\nTasks 从上篇文章知道，locustfile里面必须要有一个类，继承User类，当性能测试开始后，会产生一个User类实例，这就是常说的模拟用户。这些用户会选择task执行，休眠一会，再选择新的task，不断迭代。\ntask是Python中的可调用对象，它是一项任务，对于Web系统来说，可以是登录、查询、下订单、支付等等。\n@task装饰器 @task是定义task最简单直接的方式，比如：\nfrom locust import User, task, constant class MyUser(User): wait_time = constant(1) @task def my_task(self): print(\u0026#34;User instance (%r) executing my_task\u0026#34; % self) @task有一个可选参数，用来设置task的选择权重，比如：\nfrom locust import User, task, between class MyUser(User): wait_time = between(5, 15) @task(3) def task1(self): pass @task(6) def task2(self): pass task2比task1被选择的可能性大两倍。\ntasks属性 除了@task装饰器，还可以设置User类的tasks属性来定义任务，比如：\nfrom locust import User, constant def my_task(user): pass class MyUser(User): tasks = [my_task] wait_time = constant(1) 注意，my_task()函数有一个参数，它是User类的实例。\ntasks可以是一个列表：\ntasks = [my_task1, my_task2, my_task3] Locust会使用Python中的random.choice()从里面随机选取。\ntasks也可以是一个字典：\n{my_task: 3, another_task: 1} 后面的int型键值代表的是被选择权重，这个字典等价于列表：\n[my_task, my_task, my_task, another_task] @tag装饰器 @tag用来打标记，在运行时选择哪些task执行，哪些task不执行。比如：\nclass MyUser(User): wait_time = constant(1) @tag(\u0026#39;tag1\u0026#39;) @task def task1(self): pass @tag(\u0026#39;tag1\u0026#39;, \u0026#39;tag2\u0026#39;) @task def task2(self): pass @tag(\u0026#39;tag3\u0026#39;) @task def task3(self): pass @task def task4(self): pass 如果使用--tags tag1，那么只有task1和task2会被选择。 如果使用--tags tag2 tag3，那么只有task2和task3会被选择。 如果使用--exclude-tags tag3，那么只有task1、task2和task4会被选择。 注意，exclude的优先级更高，如果某个tag既包括又排除，那么会被排除。\nEvents @task定义了性能测试的执行动作，@events作为补充，定义了测试开始前和测试结束后的处理。\n注意，每个模拟用户开始和结束的处理是使用的User类的on_start()和on_stop()方法。\ntest_start和test_stop 测试开始前和测试结束后触发。示例：\nfrom locust import events @events.test_start.add_listener def on_test_start(environment, **kwargs): print(\u0026#34;A new test is starting\u0026#34;) @events.test_stop.add_listener def on_test_stop(environment, **kwargs): print(\u0026#34;A new test is ending\u0026#34;) 分布式执行时，它们只会在master节点生效。\ninit init和test_start不同，它会在每个Locust进程开始时触发，分布式执行时，每个节点（worker进程）都会生效。\nfrom locust import events from locust.runners import MasterRunner @events.init.add_listener def on_locust_init(environment, **kwargs): if isinstance(environment.runner, MasterRunner): print(\u0026#34;I\u0026#39;m on master node\u0026#34;) else: print(\u0026#34;I\u0026#39;m on a worker or standalone node\u0026#34;) Events是一项hook技术，在学习Locust高级用法时再做进一步介绍。\nLocust项目结构 官方建议如下：\ncommon/\n__init__.py\nauth.py\nconfig.py\nlocustfile.py或者locustfiles/\napi.py\nwebsite.py\nrequirements.txt\nFastHttpUser 从上篇文章可以知道，HttpUser类比User类更常用，它的client属性是HttpSession类（requests.Session子类）的一个实例，可以使用requests发HTTP请求：\n## 使用HttpUser from locust import HttpUser,task,constant class MyUser(User): wait_time = constant(1) @task def my_task1(self): with self.client.get(\u0026#34;https://www.baidu.com/\u0026#34;, catch_response=True) as res: if res.status_code == 200: print(\u0026#34;成功\u0026#34;) else: print(\u0026#34;失败\u0026#34;) 但是requests性能是不太好的，如果要产生更高的压力，建议使用FastHttpUser，性能可以提升5到6倍：\n## 使用FastHttpUser from locust import task, constant from locust.contrib.fasthttp import FastHttpUser class MyUser(FastHttpUser): wait_time = constant(1) @task def my_task(self): with self.client.get(\u0026#34;https://www.baidu.com/\u0026#34;, catch_response=True) as response: if response.status_code == 200: print(\u0026#34;成功\u0026#34;) else: print(\u0026#34;失败\u0026#34;) 由于它们的API不一样，都有各自的适用场景，所以FastHttpUser不能完全替代HttpUser。\n小结 本文严格来说是上篇文章《locustfile中的User类和HttpUser类》的下篇，介绍了核心技术Tasks和Events，并给出了官方推荐的项目结构，最后介绍了比HttpUser性能更好的FastHttpUser，如果想要更多的并发，可以考虑使用后者。经过这两篇文章的学习，已经可以开始动手实践使用Locust进行性能测试了。如果使用locust命令启动后，无法打开网页，可以试试加上参数：locust --web-host=\u0026quot;127.0.0.1\u0026quot;。\n参考资料：\nhttps://zhuanlan.zhihu.com/p/118470760\nhttps://docs.locust.io/en/stable/writing-a-locustfile.html#tasks\nhttps://www.axihe.com/tools/locust/increase-performance.html\nhttps://blog.csdn.net/u012002125/article/details/113363768\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000018-locustlocust%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF@task%E5%92%8C@events/","summary":"Tasks和Events是Locust性能测试工具的核心技术，有了它们，Locust才能称得上是一个性能工具。","title":"Locust性能测试工具核心技术@task和@events"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n设计模式 设计模式是前辈们经过相当长的一段时间的试验和错误总结出来的最佳实践。我找到的资料列举了以下这些设计模式：工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式、适配器模式、桥接模式、过滤器模式、组合模式、装饰器模式、外观模式、享元模式、代理模式、责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、空对象模式、策略模式、模板模式、访问者模式、MVC模式、业务代表模式、组合实体模式、数据访问对象模式、前端控制器模式、拦截过滤器模式、服务定位器模式、传输对象模式，共33种。\n这些设计模式在纯面向对象编程语言中使用最多。**Python拥有一等函数，既不需要使用某些设计模式，也减少了某些设计模式样板代码。**本文将使用一等函数实现策略模式和命令模式，研究Python代码是如何简化的。\n策略模式 策略模式概述：“定义一系列算法，把它们一一封装起来，并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。”\n经典实现 示例，根据客户的属性或订单中的商品计算折扣，规则如下：\n有1000或以上积分的客户，每个订单享5%折扣。 同一订单中，单个商品的数量达到20个或以上，享10%折扣。 订单中的不同商品达到10个或以上，享7%折扣。 这很适合用策略模式来做，UML类图设计如下：\n上下文，集成算法的类，图中Order会根据不同的算法计算折扣。 策略，实现不同算法的组件的共同接口，图中Promotion是个抽象类。 具体策略，策略的具体子类，图中FidelityPromo、BulkItemPromo、LargeOrderPromo分别对应上面3条计算折扣规则。 代码实现如下：\nfrom abc import ABC, abstractmethod from collections import namedtuple Customer = namedtuple(\u0026#39;Customer\u0026#39;, \u0026#39;name fidelity\u0026#39;) class LineItem: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price def total(self): return self.price * self.quantity class Order: # the Context def __init__(self, customer, cart, promotion=None): self.customer = customer self.cart = list(cart) self.promotion = promotion def total(self): if not hasattr(self, \u0026#39;__total\u0026#39;): self.__total = sum(item.total() for item in self.cart) return self.__total def due(self): if self.promotion is None: discount = 0 else: discount = self.promotion.discount(self) return self.total() - discount def __repr__(self): fmt = \u0026#39;\u0026lt;Order total: {:.2f} due: {:.2f}\u0026gt;\u0026#39; return fmt.format(self.total(), self.due()) class Promotion(ABC): # the Strategy: an Abstract Base Class @abstractmethod def discount(self, order): \u0026#34;\u0026#34;\u0026#34;Return discount as a positive dollar amount\u0026#34;\u0026#34;\u0026#34; class FidelityPromo(Promotion): # first Concrete Strategy \u0026#34;\u0026#34;\u0026#34;5% discount for customers with 1000 or more fidelity points\u0026#34;\u0026#34;\u0026#34; def discount(self, order): return order.total() * .05 if order.customer.fidelity \u0026gt;= 1000 else 0 class BulkItemPromo(Promotion): # second Concrete Strategy \u0026#34;\u0026#34;\u0026#34;10% discount for each LineItem with 20 or more units\u0026#34;\u0026#34;\u0026#34; def discount(self, order): discount = 0 for item in order.cart: if item.quantity \u0026gt;= 20: discount += item.total() * .1 return discount class LargeOrderPromo(Promotion): # third Concrete Strategy \u0026#34;\u0026#34;\u0026#34;7% discount for orders with 10 or more distinct items\u0026#34;\u0026#34;\u0026#34; def discount(self, order): distinct_items = {item.product for item in order.cart} if len(distinct_items) \u0026gt;= 10: return order.total() * .07 return 0 实现策略模式的关键代码是Promotion类，它是一个抽象基类，通过继承abc.ABC来定义。\n测试下这段代码：\n\u0026gt;\u0026gt;\u0026gt; joe = Customer(\u0026#39;John Doe\u0026#39;, 0) # 顾客joe积分0 \u0026gt;\u0026gt;\u0026gt; ann = Customer(\u0026#39;Ann Smith\u0026#39;, 1100) # 顾客ann积分1100 ## 测试第一条折扣规则 \u0026gt;\u0026gt;\u0026gt; cart = [LineItem(\u0026#39;banana\u0026#39;, 4, .5), # 3类商品 ... LineItem(\u0026#39;apple\u0026#39;, 10, 1.5), ... LineItem(\u0026#39;watermellon\u0026#39;, 5, 5.0)] \u0026gt;\u0026gt;\u0026gt; Order(joe, cart, FidelityPromo()) \u0026lt;Order total: 42.00 due: 42.00\u0026gt; \u0026gt;\u0026gt;\u0026gt; Order(ann, cart, FidelityPromo()) # 积分折扣 \u0026lt;Order total: 42.00 due: 39.90\u0026gt; ## 测试第二条折扣规则 \u0026gt;\u0026gt;\u0026gt; banana_cart = [LineItem(\u0026#39;banana\u0026#39;, 30, .5), # 商品数量超过20 ... LineItem(\u0026#39;apple\u0026#39;, 10, 1.5)] \u0026gt;\u0026gt;\u0026gt; Order(joe, banana_cart, BulkItemPromo()) # 数量折扣 \u0026lt;Order total: 30.00 due: 28.50\u0026gt; ## 测试第三条折扣规则 \u0026gt;\u0026gt;\u0026gt; long_order = [LineItem(str(item_code), 1, 1.0) # 10类不同商品 ... for item_code in range(10)] \u0026gt;\u0026gt;\u0026gt; Order(joe, long_order, LargeOrderPromo()) # 种类折扣 \u0026lt;Order total: 10.00 due: 9.30\u0026gt; \u0026gt;\u0026gt;\u0026gt; Order(joe, cart, LargeOrderPromo()) \u0026lt;Order total: 42.00 due: 42.00\u0026gt; 函数实现 现在开始使用Python函数改写代码。观察上文代码可以发现每个具体策略是一个类，类里面只有一个方法：discount()，并且没有属性。**看起来就像是普通的函数。**改造如下：\n最关键的是，**删除了抽象类。**测试一下，函数拿来即用的美妙体验：\n\u0026gt;\u0026gt;\u0026gt; joe = Customer(\u0026#39;John Doe\u0026#39;, 0) \u0026gt;\u0026gt;\u0026gt; ann = Customer(\u0026#39;Ann Smith\u0026#39;, 1100) \u0026gt;\u0026gt;\u0026gt; cart = [LineItem(\u0026#39;banana\u0026#39;, 4, .5), ... LineItem(\u0026#39;apple\u0026#39;, 10, 1.5), ... LineItem(\u0026#39;watermellon\u0026#39;, 5, 5.0)] \u0026gt;\u0026gt;\u0026gt; Order(joe, cart, fidelity_promo) # 直接传函数名 \u0026lt;Order total: 42.00 due: 42.00\u0026gt; \u0026gt;\u0026gt;\u0026gt; Order(ann, cart, fidelity_promo) \u0026lt;Order total: 42.00 due: 39.90\u0026gt; \u0026gt;\u0026gt;\u0026gt; banana_cart = [LineItem(\u0026#39;banana\u0026#39;, 30, .5), ... LineItem(\u0026#39;apple\u0026#39;, 10, 1.5)] \u0026gt;\u0026gt;\u0026gt; Order(joe, banana_cart, bulk_item_promo) # 直接传函数名 \u0026lt;Order total: 30.00 due: 28.50\u0026gt; \u0026gt;\u0026gt;\u0026gt; long_order = [LineItem(str(item_code), 1, 1.0) ... for item_code in range(10)] \u0026gt;\u0026gt;\u0026gt; Order(joe, long_order, large_order_promo) # 直接传函数名 \u0026lt;Order total: 10.00 due: 9.30\u0026gt; \u0026gt;\u0026gt;\u0026gt; Order(joe, cart, large_order_promo) \u0026lt;Order total: 42.00 due: 42.00\u0026gt; 函数的意义体现在：\n可以得出结论：普通函数比只有一个方法的类使用起来更简单。\n选择最佳策略 继续看另外一个问题，从具体策略中选择最佳策略，本文示例就是要选择优惠最多的折扣，代码实现如下：\npromos = [fidelity_promo, bulk_item_promo, large_order_promo] def best_promo(order): \u0026#34;\u0026#34;\u0026#34;Select best discount available \u0026#34;\u0026#34;\u0026#34; return max(promo(order) for promo in promos) promos列表包含了三个具体策略。best_promo()函数先使用生成器表达式计算每个策略的折扣，再使用max()函数返回最大折扣。\n测试一下：\n\u0026gt;\u0026gt;\u0026gt; Order(joe, long_order, best_promo) \u0026lt;Order total: 10.00 due: 9.30\u0026gt; \u0026gt;\u0026gt;\u0026gt; Order(joe, banana_cart, best_promo) \u0026lt;Order total: 30.00 due: 28.50\u0026gt; \u0026gt;\u0026gt;\u0026gt; Order(ann, cart, best_promo) \u0026lt;Order total: 42.00 due: 39.90\u0026gt; 没有问题。但是存在一个隐藏缺陷：如果想要添加新的促销策略，那么要定义相应函数并添加到promos列表中。\n添加新策略 接下来针对这个缺陷进行优化。\n方法一\n借助globals()函数自动找到其他可用的*_promo函数：\npromos = [globals()[name] for name in globals() if name.endswith(\u0026#39;_promo\u0026#39;) and name != \u0026#39;best_promo\u0026#39;] def best_promo(order): \u0026#34;\u0026#34;\u0026#34;Select best discount available \u0026#34;\u0026#34;\u0026#34; return max(promo(order) for promo in promos) globals()返回一个字典，表示当前的全局符号表。这个符号表始终针对当前模块。对函数或方法来说，是指定义它们的模块，而不是调用它们的模块。\n方法二\n通过函数内省自动查找promotions模块中的所有函数作为策略函数（要求promotions模块中只能包含策略函数，不能包含其他函数）：\npromos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)] def best_promo(order): \u0026#34;\u0026#34;\u0026#34;Select best discount available \u0026#34;\u0026#34;\u0026#34; return max(promo(order) for promo in promos) inspect.getmembers()的第一个参数是目标模块（promotions模块），第二个参数是判断条件（只查找模块中的函数）。\n方法三\n装饰器，这个方法更优雅，在下篇文章讲到装饰器时，再给出代码实现。\n命令模式 命令模式的目的是解耦调用操作的对象（调用者）和提供实现的对象（接收者）。\n示例，菜单驱动文本编辑器，调用者是菜单，接收者是被编辑的文档。\nUML类图设计如下：\n命令模式的做法是在调用者和接收者之间放一个Command对象，让它实现只有一个execute()方法的接口，调用接收者中的方法执行具体命令。**这样调用者Menu不需要了解接收者Document的接口。**并且可以添加Command子类扩展多个不同的接收者。\n使用一等函数对命令模式的优化思路：不为调用者提供一个Command对象，而是给它一个函数，调用者不用调command.execute()，直接调command()即可。这和策略模式是类似的，把实现单方法接口的类的实例替换成可调用对象。\n注意，图中的MacroCommand是宏命令，可能保存一系列命令，它的execute()方法会在各个命令上调用相同的方法，在使用一等函数函数时，可以实现成定义了__call__方法的类：\nclass MacroCommand: \u0026#34;一个执行一组命令的命令\u0026#34; def __init__(self, commands): self.commands = list(commands) def __call__(self): for command in self.commands: command() 毕竟，__call__使得每个Python可调用对象都实现了单方法接口。\n小结 本文简单列举了33种设计模式，从两个经典的设计模式，策略模式和命令模式入手，介绍设计模式在Python中是如何实现的，借助函数是一等对象的这一特性，大大简化了代码。在此基础上，还能更Pythonic一点，那就是用函数装饰器和闭包。\n参考资料：\n《流畅的Python》\nhttps://www.runoob.com/design-pattern/design-pattern-tutorial.html\nhttps://blog.csdn.net/xldmx/article/details/112337759\nhttps://github.com/fluentpython/example-code/tree/master/06-dp-1class-func\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000018-pythonpython%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E7%9F%A5%E5%A4%9A%E5%B0%91/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"设计模式\"\u003e设计模式\u003c/h1\u003e\n\u003cp\u003e设计模式是前辈们经过相当长的一段时间的试验和错误总结出来的最佳实践。我找到的资料列举了以下这些设计模式：工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式、适配器模式、桥接模式、过滤器模式、组合模式、装饰器模式、外观模式、享元模式、代理模式、责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、空对象模式、策略模式、模板模式、访问者模式、MVC模式、业务代表模式、组合实体模式、数据访问对象模式、前端控制器模式、拦截过滤器模式、服务定位器模式、传输对象模式，\u003cstrong\u003e共33种\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e这些设计模式在纯面向对象编程语言中使用最多。**Python拥有一等函数，既不需要使用某些设计模式，也减少了某些设计模式样板代码。**本文将使用一等函数实现策略模式和命令模式，研究Python代码是如何简化的。\u003c/p\u003e\n\u003ch1 id=\"策略模式\"\u003e策略模式\u003c/h1\u003e\n\u003cp\u003e策略模式概述：“定义一系列算法，把它们一一封装起来，并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。”\u003c/p\u003e\n\u003ch2 id=\"经典实现\"\u003e经典实现\u003c/h2\u003e\n\u003cp\u003e示例，根据客户的属性或订单中的商品计算折扣，规则如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e有1000或以上积分的客户，每个订单享5%折扣。\u003c/li\u003e\n\u003cli\u003e同一订单中，单个商品的数量达到20个或以上，享10%折扣。\u003c/li\u003e\n\u003cli\u003e订单中的不同商品达到10个或以上，享7%折扣。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这很适合用策略模式来做，UML类图设计如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/46c053dd12c841acb5de50859780f673.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e上下文，集成算法的类，图中Order会根据不同的算法计算折扣。\u003c/li\u003e\n\u003cli\u003e策略，实现不同算法的组件的共同接口，图中Promotion是个抽象类。\u003c/li\u003e\n\u003cli\u003e具体策略，策略的具体子类，图中FidelityPromo、BulkItemPromo、LargeOrderPromo分别对应上面3条计算折扣规则。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e代码实现如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eabc\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e ABC\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e abstractmethod\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003ecollections\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e namedtuple\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCustomer \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e namedtuple\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Customer\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;name fidelity\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eLineItem\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__init__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e product\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e quantity\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e price\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eproduct \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e product\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003equantity \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e quantity\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eprice \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e price\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etotal\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eprice \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003equantity\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eOrder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# the Context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__init__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e customer\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e cart\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e promotion\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecustomer \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e customer\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecart \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elist\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecart\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epromotion \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e promotion\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etotal\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehasattr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;__total\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e__total \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003esum\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eitem\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etotal\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e item \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecart\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e__total\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003edue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epromotion \u003cspan style=\"color:#0550ae\"\u003eis\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            discount \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            discount \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epromotion\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ediscount\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etotal\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e discount\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__repr__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        fmt \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;\u0026lt;Order total: \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{:.2f}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e due: \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{:.2f}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e fmt\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eformat\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etotal\u003cspan style=\"color:#1f2328\"\u003e(),\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edue\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003ePromotion\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eABC\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# the Strategy: an Abstract Base Class\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e@abstractmethod\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ediscount\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e order\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;Return discount as a positive dollar amount\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eFidelityPromo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ePromotion\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# first Concrete Strategy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;5\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e% d\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003eiscount for customers with 1000 or more fidelity points\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ediscount\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e order\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e order\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etotal\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e.05\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e order\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecustomer\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003efidelity \u003cspan style=\"color:#0550ae\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1000\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eBulkItemPromo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ePromotion\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# second Concrete Strategy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;10\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e% d\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003eiscount for each LineItem with 20 or more units\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ediscount\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e order\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        discount \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e item \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e order\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecart\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e item\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003equantity \u003cspan style=\"color:#0550ae\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e20\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                discount \u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e item\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etotal\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e.1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e discount\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eLargeOrderPromo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ePromotion\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# third Concrete Strategy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;7\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e% d\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003eiscount for orders with 10 or more distinct items\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ediscount\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e order\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        distinct_items \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003eitem\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eproduct \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e item \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e order\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecart\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edistinct_items\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e10\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e order\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etotal\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e.07\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e实现策略模式的关键代码是Promotion类，它是一个抽象基类，通过继承abc.ABC来定义。\u003c/strong\u003e\u003c/p\u003e","title":"Python设计模式知多少"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n第二次阅读Java官方教程，有些不一样的理解。内容比较散乱，个人意识流笔记。\n学习开始时间：2023年12月1日\n学习结束时间：2024年4月3日\nRunning Your First Java Application Java works in a way that differs from C or C++.\n这里是想说Java有JVM吧\ncompiler：Some languages do not have a compiler; Java does. Every compiler is specific to a language.\n知道Java代码需要编译成.class文件就行了。.class文件是可执行的。\n编译和执行，需要软件支持，这个软件叫做，JDK。\n从Java11开始，单个文件，可以直接执行.java文件，JDK内部自己编译\nA Java class must be saved in a file that has the same name as your class with the extension .java. This is mandatory and is in fact very convenient because you do not need to open a file to know what class is written in it.\nAs of now, downloading \u0026ldquo;Java\u0026rdquo; means downloading the Java Development Kit, also known as the JDK. The JDK contains many tools and among them are the ones you will be using to compile and run a Java application. It is officially distributed by the OpenJDK project and by Oracle.\nJDK有2个主流版本，OpenJDK和OracleJDK\nThe JRE stands for Java Runtime Environment. It is a subset of the JDK that is not distributed by the OpenJDK or Oracle anymore. It only contained the tools needed to run a Java application. You cannot compile your code with the tools provided in the JRE.\n不用手动安装JRE！JRE是JDK子集，不能编译，只能运行Java程序。JRE包括Java虚拟机（JVM）、类库等。从Java 11版本开始，OpenJDK和Oracle只提供JDK的发行版，不再单独分发JRE。如果你需要JRE来运行Java程序，你需要先安装JDK，然后从JDK中提取JRE。现在只需要安装JDK就可以。\nhttps://jdk.java.net/\n环境变量，JAVA_HOME、PATH（bin），这样就能使用java、javac等命令了。\njavac 文件名.java\njava 类名（不能加上.class，否则报错：Could not find or load main class HelloWorldApp.class）\n编译文件，运行类。\nCLASSPATH，找不到类时要设置。the directories and archives that are searched to locate class files.\nStaying Aware of New Features Java 8, released in 2014, brought Lambda expressions to the Java platform as well as the Stream API, Optional class, and a host of other great features. This is one of the reasons why Java 8 became the most popular version in Java\u0026rsquo;s history. Even today, it remains one of the most widely used versions of Java.\n毕业那年Java8才出来\nJava8到Java9花了3年，从Java9开始每6个月1个版本，到2023年都出到JDK21了。\nGetting to Know the Language In the Java programming language, each class is allowed to have one direct superclass, and each superclass has the potential for an unlimited number of subclasses.\nIn its most common form, an interface is a group of related methods with empty bodies.\n接口没有方法体\n类必须实现接口中所有方法，缺一不可。\nThe Java Platform API Specification contains the complete listing for all packages, interfaces, classes, fields, and methods supplied by the Java SE platform.\n类变量在Java中通过static来定义，static还是挺形象，静态不变的，相反那些动态的是实例变量，不同实例不一样。\nThe Java programming language is statically-typed, which means that all variables must first be declared before they can be used.\nThe compiler will assign a reasonable default value for fields; for local variables, a default value is never assigned.最好不要依赖变量默认值，而是编码时显示指出默认值，不管是field还是variable。\n从JDK10开始可以使用var关键字来定义变量，省略指定类型，编译器能自动识别：\nThere are restrictions on the use of the var type identifier.\nYou can only use it for local variables declared in methods, constructors and initializer blocks.\nvar cannot be used for fields, not for method or constructor parameters.\nThe compiler must be able to choose a type when the variable is declared. Since null has no type, the variable must have an initializer.\n也就是说，var只能用在局部变量，并且必须初始化。\nWhen using the instanceof operator, keep in mind that null is not an instance of anything.\nWe recommend using the enhanced for statement instead of the general form whenever possible.\n按照标签break：\nclass BreakWithLabelDemo { public static void main(String[] args) { int[][] arrayOfInts = { {32, 87, 3, 589}, {12, 1076, 2000, 8}, {622, 127, 77, 955} }; int searchfor = 12; int i; int j = 0; boolean foundIt = false; search: for (i = 0; i \u0026lt; arrayOfInts.length; i++) { for (j = 0; j \u0026lt; arrayOfInts[i].length; j++) { if (arrayOfInts[i][j] == searchfor) { foundIt = true; break search; } } } if (foundIt) { System.out.println(\u0026#34;Found \u0026#34; + searchfor + \u0026#34; at \u0026#34; + i + \u0026#34;, \u0026#34; + j); } else { System.out.println(searchfor + \u0026#34; not in the array\u0026#34;); } } } break search会中断for循环，执行search标签后面的语句，也就是后面的if (foundIt)语句。\n也可以按标签continue：\nclass ContinueWithLabelDemo { public static void main(String[] args) { String searchMe = \u0026#34;Look for a substring in me\u0026#34;; String substring = \u0026#34;sub\u0026#34;; boolean foundIt = false; int max = searchMe.length() - substring.length(); test: for (int i = 0; i \u0026lt;= max; i++) { int n = substring.length(); int j = i; int k = 0; while (n-- != 0) { if (searchMe.charAt(j++) != substring.charAt(k++)) { continue test; } } foundIt = true; break test; } System.out.println(foundIt ? \u0026#34;Found it\u0026#34; : \u0026#34;Didn\u0026#39;t find it\u0026#34;); } } continue test会跳过for循环，而不是while循环。\n按标签break/continue可以在多层嵌套中使用，以中断/跳过外部循环，不然就得加flag变量，在外层循环加if语句。\nswitch语句中的yield：\nclass Test { enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } public String calculate(Day d) { return switch (d) { case SATURDAY, SUNDAY -\u0026gt; \u0026#34;week-end\u0026#34;; default -\u0026gt; { int remainingWorkDays = 5 - d.ordinal(); yield remainingWorkDays; } }; } } switch语句的类型有限制：\nYou must choose the type of your selector variable among the following types:\nbyte, short, char, and int primitive data types\nCharacter, Byte, Short, and Integer wrapper types\nenumerated types\nthe String type.\nIt is worth noting that the following primitive types cannot be used for the type of your selector variable: boolean, long, float, and double.\n注意这里的noting是note，不是nothing\nThe selector variable of a switch statement can be an object, so this object can be null. You should protect your code from null selector variables, because in this case the switch statement will throw a NullPointerException.\npublic String convertToLabel(int quarter) { String quarterLabel = switch (quarter) { case 0 -\u0026gt; { System.out.println(\u0026#34;Q1 - Winter\u0026#34;); yield \u0026#34;Q1 - Winter\u0026#34;; }; default -\u0026gt; \u0026#34;Unknown quarter\u0026#34;; }; } return quarterLabel; } switch里面的yield本质上是return，只是为了防止混淆，表示只退出switch表达式（Java14新语法），而不是退出整个方法，所以发明了yield。\nfor循环：\nfor(int i = 0; i \u0026lt; 5; i++) { System.out.println(i); } 改造为functional style：\nimport java.util.stream.IntStream; ... IntStream.range(0, 5) .forEach(System.out::println); IntStream.rangeClosed(0, 5)等价于for(int i = 0; i \u0026lt;= 5; i++)\nIntStream.iterate(0, i -\u0026gt; i \u0026lt; 15, i -\u0026gt; i + 3)等价于for(int i = 0; i \u0026lt; 15; i = i + 3)\nfor(int i = 0;; i = i + 3) { if(i \u0026gt; 20) { break; } System.out.println(i); } IntStream.iterate(0, i -\u0026gt; i + 3) .takeWhile(i -\u0026gt; i \u0026lt;= 20) .forEach(System.out::println); List\u0026lt;String\u0026gt; names = List.of(\u0026#34;Jack\u0026#34;, \u0026#34;Paula\u0026#34;, \u0026#34;Kate\u0026#34;, \u0026#34;Peter\u0026#34;); for(String name: names) { System.out.println(name); } List\u0026lt;String\u0026gt; names = List.of(\u0026#34;Jack\u0026#34;, \u0026#34;Paula\u0026#34;, \u0026#34;Kate\u0026#34;, \u0026#34;Peter\u0026#34;); names.forEach(name -\u0026gt; System.out.println(name)); The forEach() method is available both on a Collection\u0026lt;T\u0026gt; and on a Stream\u0026lt;T\u0026gt;. Functions like filter(), which we\u0026rsquo;ll use soon, are available only on a Stream\u0026lt;T\u0026gt; and not on the Collection. This is by design in order to provide efficiency when multiple intermediate operations may precede the terminal operation like forEach(), findFirst(), etc.\nList\u0026lt;String\u0026gt; names = List.of(\u0026#34;Jack\u0026#34;, \u0026#34;Paula\u0026#34;, \u0026#34;Kate\u0026#34;, \u0026#34;Peter\u0026#34;); for(String name: names) { if(name.length() == 4) { System.out.println(name); } } List\u0026lt;String\u0026gt; names = List.of(\u0026#34;Jack\u0026#34;, \u0026#34;Paula\u0026#34;, \u0026#34;Kate\u0026#34;, \u0026#34;Peter\u0026#34;); names.stream() .filter(name -\u0026gt; name.length() == 4) .forEach(name -\u0026gt; System.out.println(name)); The compiler does not consider return type when differentiating methods, so you cannot declare two methods with the same signature even if they have a different return type.\n方法重载不能根据返回类型来做，必须参数个数或者参数类型不一样。\n在Java中，默认的访问修饰符是包范围（package-private），也被称为默认访问修饰符。这意味着，如果没有指定访问修饰符，类、接口、方法和变量将只能在同一包中被访问。\n包范围不包括子目录。默认情况下，只有在同一包中的类才能访问包范围的成员。子目录中的类不被认为是同一包的一部分，因此无法访问包范围的成员。\nParameters形参、Arguments实参，注意不同叫法。\nSimply declaring a reference variable does not create an object. For that, you need to use the new operator, as described in the next section. You must assign an object to originOne before you use it in your code. Otherwise, you will get a compiler error.\nNote: You also can use interface names as return types. In this case, the object returned must implement the specified interface.\n构造函数的位置：If present, the invocation of another constructor must be the first line in the constructor.\nprotected = package-private + subclass\nNote: You can also refer to static fields with an object reference like myBike.numberOfBicycles but this is discouraged because it does not make it clear that they are class variables.\nNote: You can also refer to static methods with an object reference like instanceName.methodName(args) but this is discouraged because it does not make it clear that they are class methods.\nA common use for static methods is to access static fields. 为什么要用静态方法（类方法）？为了访问静态变量（类变量）。\nNote: If a primitive type or a string is defined as a constant and the value is known at compile time, the compiler replaces the constant name everywhere in the code with its value. This is called a compile-time constant. If the value of the constant in the outside world changes (for example, if it is legislated that pi actually should be 3.975), you will need to recompile any classes that use this constant to get the current value.\n通过static final定义的基本类型和String的常量会在编译时就在代码中替换。\nA class can have any number of static initialization blocks, and they can appear anywhere in the class body. The runtime system guarantees that static initialization blocks are called in the order that they appear in the source code.\n嵌套类的实例化：this.new EvenIterator();\nNested Classes（Inner Classes）是定义在类中\nLocal Classes是定义在方法中（ you can define a local class in a method body, a for loop, or an if clause）\nAnonymous Classes是没有类名的Local Classes（Use them if you need to use a local class only once）\n由此可见，Java的类也不是必须单独定义在文件中，也是可以在一个文件中定义多个类的。但是嵌套类是有限制的。\nYou cannot declare an interface inside a block; interfaces are inherently static.\n注意Expressions, Statements, and Blocks的区别\n接口是可以定义在类中的，但是不能定义在方法中。\nEnums are classes where all instances are known to the compiler.\nOften, the wrapping is done by the compiler—if you use a primitive where an object is expected, the compiler boxes the primitive in its wrapper class for you. Similarly, if you use a number object when a primitive is expected, the compiler unboxes the object for you.\nConstructors are not members, so they are not inherited by subclasses, but the constructor of the superclass can be invoked from the subclass.\nMethods in an interface (see the Interfaces section) that are not declared as default or static are implicitly abstract, so the abstract modifier is not used with interface methods. (It can be used, but it is unnecessary.)\n接口默认是abstract。接口里面的字段只能是静态常量（public static final）。\nWhich should you use, abstract classes or interfaces?\nConsider using abstract classes if any of these statements apply to your situation:\nYou want to share code among several closely related classes. You expect that classes that extend your abstract class have many common methods or fields, or require access modifiers other than public (such as protected and private). You want to declare non-static or non-final fields. This enables you to define methods that can access and modify the state of the object to which they belong. Consider using interfaces if any of these statements apply to your situation:\nYou expect that unrelated classes would implement your interface. For example, the interfaces Comparable and Cloneable are implemented by many unrelated classes. You want to specify the behavior of a particular data type, but not concerned about who implements its behavior. You want to take advantage of multiple inheritance of type. 抽象类实现接口部分方法，子类实现剩余方法：\nabstract class X implements Y { // implements all but one method of Y } class XX extends X { // implements the remaining method in Y } If you define a reference variable whose type is an interface, any object you assign to it must be an instance of a class that implements the interface.\nBox\u0026lt;Integer\u0026gt; is not a subtype of Box\u0026lt;Number\u0026gt; even though Integer is a subtype of Number.\nList\u0026lt;A\u0026gt; is a subtype of List\u0026lt;?\u0026gt;\nIt\u0026rsquo;s important to note that List\u0026lt;Object\u0026gt; and List\u0026lt;?\u0026gt; are not the same. You can insert an Object, or any subtype of Object, into a List\u0026lt;Object\u0026gt;. But you can only insert null into a List\u0026lt;?\u0026gt;.\n不能直接创建泛型的实例：\npublic static \u0026lt;E\u0026gt; void append(List\u0026lt;E\u0026gt; list) { E elem = new E(); // compile-time error list.add(elem); } 可以通过反射来创建：\npublic static \u0026lt;E\u0026gt; void append(List\u0026lt;E\u0026gt; list, Class\u0026lt;E\u0026gt; cls) throws Exception { E elem = cls.newInstance(); // OK list.add(elem); } List\u0026lt;String\u0026gt; ls = new ArrayList\u0026lt;\u0026gt;(); append(ls, String.class); Lambda Expression必须是functional，a functional interface is an interface that has only one abstract method\nLambda Expression只能使用值，也就是不可变。\nVariable used in lambda expression should be final or effectively final\nThe JDK 8 also introduces a new package: java.util.function with functional interfaces for you to use in your application.\nList\u0026lt;String\u0026gt; immutableStrings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;); List\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(immutableStrings); Predicate\u0026lt;String\u0026gt; isOddLength = s -\u0026gt; s.length() % 2 == 0; strings.removeIf(isOddLength); System.out.println(\u0026#34;strings = \u0026#34; + strings); 从这个例子看出，Lambda表达式的返回值是“函数接口实现”，Supplier\u0026lt;T\u0026gt; Consumer\u0026lt;T\u0026gt; Predicate\u0026lt;T\u0026gt; Function\u0026lt;T, R\u0026gt;，然后再传入Stream/Collection。\nThe java.util.function package is now central in Java, because all the lambda expressions you are going to use in the Collections Framework or the Stream API implement one of the interfaces from that package.\nThe Four Categories of Functional Interfaces:\nthe suppliers: do not take any argument, return something\nthe consumers: take an argument, do not return anything\nthe predicates: take an argument, return a boolean\nthe functions: take an argument, return something\nLambda表达式可以简写，叫做方法引用，可以理解为“匿名函数”，比如：\n// Static Method References // RefType::staticMethod IntBinaryOperator max = (a, b) -\u0026gt; Integer.max(a, b); IntBinaryOperator max = Integer::max; // Unbound Method References // RefType:instanceMethod Function\u0026lt;String, Integer\u0026gt; toLength = s -\u0026gt; s.length(); Function\u0026lt;String, Integer\u0026gt; toLength = String::length; // Bound Method References // expr:instanceMethod Consumer\u0026lt;String\u0026gt; printer = s -\u0026gt; System.out.println(s); Consumer\u0026lt;String\u0026gt; printer = System.out::println; // Constructor Method References // ClassName::new Supplier\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; newListOfStrings = () -\u0026gt; new ArrayList\u0026lt;\u0026gt;(); Supplier\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; newListOfStrings = ArrayList::new; Comparator\u0026lt;String\u0026gt; byLengthThenAlphabetically = Comparator.comparing(String::length) .thenComparing(naturalOrder()); List\u0026lt;String\u0026gt; strings = Arrays.asList(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;); strings.sort(byLengthThenAlphabetically); System.out.println(strings); List\u0026lt;String\u0026gt; strings = Arrays.asList(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;); strings.sort(byLengthThenAlphabetically.reversed()); System.out.println(strings); List\u0026lt;String\u0026gt; strings = Arrays.asList(\u0026#34;one\u0026#34;, null, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, null, null, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;); Comparator\u0026lt;String\u0026gt; naturalNullsLast = Comparator.nullsLast(naturalOrder()); strings.sort(naturalNullsLast); System.out.println(strings); Annotations have a number of uses, among them:\nInformation for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings. Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth. Runtime processing — Some annotations are available to be examined at runtime. 使用@interface自定义注解。\nDefinition: A package is a grouping of related types providing access protection and name space management. Note that types refers to classes, interfaces, enumerations, and annotation types. Enumerations and annotation types are special kinds of classes and interfaces, respectively, so types are often referred to in this section simply as classes and interfaces.\nFor convenience, the Java compiler automatically imports two entire packages for each source file:\nthe java.lang package and the current package (the package for the current file). Java无需手动导入当前包的其他类，因为Java默认已经导入了。\nYou might have to set your CLASSPATH so that the compiler and the JVM can find the .class files for your types.\nChecked exceptions are subject to the Catch or Specify Requirement. All exceptions are checked exceptions, except for those indicated by Error, RuntimeException, and their subclasses.\nchecked exceptions是指需要校验的异常，要返给用户提示。\nThe runtime system always executes the statements within the finally block regardless of what happens within the try block. 即使有异常，也会执行finally\nImportant: The finally block is a key tool for preventing resource leaks. When closing a file or otherwise recovering resources, place the code in a finally block to ensure that resource is always recovered.（还可以用try-with语句）\nstatic String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } } you can throw only objects that inherit from the java.lang.Throwable class.\n只有checked exception才需要在方法小括号后面加上throws\nMastering the API There are two main categories of interfaces in the Collections Framework: collections and maps.\nList有序，Set不可重复。\nWhat is the difference between sorting and ordering? A List keeps its elements in the order they have been added, and a SortedSet keeps them sorted.\naddAll()，This is an important point to understand: getting a true value does not mean that all the elements of the other collection have been added; it means that at least one has been added.\nArrayList and LinkedList\nIs one of these implementation better than the other? If you are not sure which one to choose, then your best choice is probably ArrayList.\nWhat was true for linked lists when computing was invented in the 60\u0026rsquo;s does not hold anymore, and the capacity of linked lists to outperform arrays on insertion and deletion operations is greatly diminished by modern hardware, CPU caches, and pointer chasing. Iterating over the elements of an ArrayList is much faster that over the elements of a LinkedList, mainly due to pointer chasing and CPU cache misses.\nThere are still cases where a linked list is faster than an array. A doubly-linked list can access its first and last element faster than an ArrayList can. This is the main use case that makes LinkedList better than ArrayList. So if your application needs a Last In, First Out (LIFO, covered later in this tutorial) stack, or a First In, First Out (FIFO, also covered later) waiting queue, then choosing a linked list is probably your best choice.\nOn the other hand, if you plan to iterate through the elements of your list, or to access them randomly by their index, then the ArrayList is probably your best bet.\nheadSet(toElement) and tailSet(fromElement) returns you subsets containing the elements lower than toElement or greater than fromElement subSet(fromElement, toElement) gives you a subset of the element between fromElement and toElement. The three subsets that these methods return are views on the main set. No copy is made, meaning that any change you make to these subsets will be reflected in the set, and the other way round.\nThe Collections Framework has a class called Arrays with about 200 methods to handle arrays.\nThe Collections Framework comes with another factory class: Collections, with a set of method to manipulate collections and their content. There are about 70 methods in this class.\nThe Collections Framework gives you two interfaces to model queues and stacks:\nthe Queue interface models a queue; the Deque interface models a double ended queue (thus the name). You can push, pop, poll and peek elements on both the tail and the head of a Deque, making it both a queue and a stack. Although Vector is not deprecated, its usage is discouraged. So is the usage of the Stack class. If you do not need the thread safety, then you can safely replace its usage with Deque and ArrayDeque. If what you need is a thread-safe stack, then you should explore the implementations of the BlockingQueue interface.\nHashSet其实是个HashMap。a hashset stores your object in a hashmap. Your objects are the keys of this hashmap, and the value is just a placeholder, an object with no significance.\nThe important point to remember here is that if you mutate your objects after you have added them to a set, you may come across weird bugs in your application, that will be hard to fix. You should never mutate an object once it has been added to a Set!\nIn a nutshell, the Stream API is about providing an implementation of the well known map-filter-reduce algorithm to the JDK.\nThe first step of the map-filter-reduce algorithm is the mapping step. A mapping consists of the transforming the objects or the values that you are processing. A mapping is a one-to-one transformation: if you map a list of 10 objects, you will get a list of 10 transformed objects.\nA mapping changes the types of objects; it does not change their number.\nA mapping is modeled by the Function functional interface.\nA filtering changes the number of objects; it does not change their type.\nA filtering is modeled by the Predicate functional interface.\nThe reduction step allows you to build complex structures with your data, including lists, sets, maps of any kind, or even structures that you can build yourself.\nFor obvious performances reasons, creating a map() method that would return a Collection on the Collection interface is not the right way to go. You would end up creating unnecessary intermediate structures with a high overhead on both the memory and the CPU.\nThis is the reason why the map() and filter() methods have not been added to the Collection interface. Instead, they have been created on the Stream interface.\nThe Stream interface avoids creating intermediate structures to store mapped or filtered objects.\n流的map是一对一映射，也就是转变类型，不改类型就不用map。如果要输出为Map类型 ，要使用collect。\nThe first, legacy one, is the File class. This class is mentioned here, with a word of warning: you should not use it anymore in your code, unless you have very good reasons to do so. You should favor the use of the Path interface, also covered in this section. Along with the factory methods from the Files, it gives you more features than the File class, and better performances, especially to access larger files and directories.\njava.security包内置了安全实现，比如生成密钥、数字签名等。\nJava reflection allows an object to look in the mirror and discover what fields, methods, and constructors it has. We can read and write fields, invoke methods, and even create new objects by calling the constructors.\nOrganizing your Application The Java APIs are organized into methods, classes, packages, and - at the highest level - modules. A module has a number of essential pieces of information attached to it:\na name a list of dependencies on other modules a public API (with everything else being module internal and inaccessible) a list of services it uses and provides Getting to know the JVM Manual memory management does have some advantages. Developers have precise control over when an object is removed. Manual memory management might also be more efficient as no background process is running, consuming system resources and monitoring memory usage. Though this advantage is less significant as modern automated memory systems have improved in performance.\nHowever, manual memory management comes with some significant downsides. With manual memory management, developers must consciously know how an application uses memory. This can be time-consuming and difficult, requiring developers to add control code for the safe allocation and deallocation of objects from memory. This control code could be distracting as it obscures the business meaningful work the code is attempting to accomplish. A developer might also fail to properly handle error conditions, resulting in objects not being deleted, leading to a memory leak.\nMost garbage collectors in Java are generational garbage collectors. Generational garbage collectors are designed to take advantage of the weak generational hypothesis, which posits that most objects are short-lived. Consequently, generational garbage collectors divide the heap into young and old generations. On allocation, objects start in a young generation, objects in young generations are frequently checked if they are no longer reachable. If an object survives enough garbage collection cycles, it\u0026rsquo;ll be copied to an old generation, which is less frequently checked.\n参考资料：\nhttps://dev.java/learn/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000018-%E5%AE%98%E6%96%B9%E4%BA%8C%E5%88%B7java%E5%AE%98%E6%96%B9%E6%95%99%E7%A8%8B/","summary":"第二次阅读Java官方教程，有些不一样的理解。内容比较散乱，个人意识流笔记。","title":"二刷Java官方教程"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n回顾 tep是基于pytest开发的接口测试框架，上个版本从小工具升级为了关键字驱动框架，通过pystats发现最近半年仍然有4000+人次在安装使用tep。时隔1年后，我对tep进行了一次大版本改造，抛弃tep2的Java代码风格，重新回归到tep1的纯粹Python风格。\n纯粹Python 所谓纯粹Python，体现在两个方面，一个方面是tep源码进行了重写，更加Pythonic，自动化用例写起来也更加简单易懂；另一方面是抛弃了依赖Java的Allure和依赖C++的loguru，分别使用Python技术栈的pytest-html和logging替代。\ntep3版本 简介 tep是Try Easy Pytest的首字母缩写，帮你轻松上手pytest。\n框架特点：\n基于pytest封装，成熟、稳定且扩展性强。 框架完全由Python构建，没有混杂其他语言。 原生Python语法，学习Python，零成本使用框架。 HAR包转换pytest用例。 安装 支持Python3.8以上版本\n创建虚拟环境：python -m venv venv\n激活虚拟环境，Windows用户：activate.bat Mac用户：source venv/bin/activate\n安装tep：pip install tep\n验证安装成功：tep -V\n目录结构 data file fixture report scripts tests .gitignore conftest.py pytest.ini run.py tep new demo就能快速创建项目名称为demo的项目脚手架。\n用例示范 简单的请求：\nfrom tep import request def test(): response = request(\u0026#34;get\u0026#34;, url=\u0026#34;http://httpbin.org/status/200\u0026#34;) assert response.status_code == 200 复杂的场景：\nfrom tep import request from tep import v from data.global_data import GlobalData class Data: v({ \u0026#34;domain\u0026#34;: GlobalData.domain, \u0026#34;skuNum\u0026#34;: 2, \u0026#34;payAmount\u0026#34;: 0.2 }) body_add_cart = \u0026#39;{\u0026#34;skuId\u0026#34;:\u0026#34;${skuId}\u0026#34;,\u0026#34;skuNum\u0026#34;:${skuNum}}\u0026#39; body_order = \u0026#39;{\u0026#34;skuId\u0026#34;:\u0026#34;${skuId}\u0026#34;,\u0026#34;price\u0026#34;:${skuPrice},\u0026#34;skuNum\u0026#34;:${skuNum},\u0026#34;totalPrice\u0026#34;:${totalPrice}}\u0026#39; body_pay = \u0026#39;{\u0026#34;orderId\u0026#34;:\u0026#34;${orderId}\u0026#34;,\u0026#34;payAmount\u0026#34;:${payAmount}}\u0026#39; def test(): url = v(\u0026#34;${domain}/searchSku?skuName=book\u0026#34;) response = request(\u0026#34;get\u0026#34;, url=url, headers=GlobalData.headers) assert response.status_code \u0026lt; 400 v(\u0026#34;skuId\u0026#34;, response.jsonpath(\u0026#34;$.skuId\u0026#34;)[0]) v(\u0026#34;skuPrice\u0026#34;, response.jsonpath(\u0026#34;$.price\u0026#34;)[0]) url = v(\u0026#34;${domain}/addCart\u0026#34;) body = v(Data.body_add_cart) response = request(\u0026#34;post\u0026#34;, url=url, headers=GlobalData.headers, json=body) assert response.status_code \u0026lt; 400 v(\u0026#34;skuNum\u0026#34;, response.jsonpath(\u0026#34;$.skuNum\u0026#34;)[0]) v(\u0026#34;totalPrice\u0026#34;, response.jsonpath(\u0026#34;$.totalPrice\u0026#34;)[0]) url = v(\u0026#34;${domain}/order\u0026#34;) body = v(Data.body_order) response = request(\u0026#34;post\u0026#34;, url=url, headers=GlobalData.headers, json=body) assert response.status_code \u0026lt; 400 v(\u0026#34;orderId\u0026#34;, response.jsonpath(\u0026#34;$.orderId\u0026#34;)[0]) url = v(\u0026#34;${domain}/pay\u0026#34;) body = v(Data.body_pay) response = request(\u0026#34;post\u0026#34;, url=url, headers=GlobalData.headers, json=body) assert response.status_code \u0026lt; 400 assert response.jsonpath(\u0026#34;$.success\u0026#34;)[0] == \u0026#34;true\u0026#34; 用例和数据 数据怎么存放，这里给出了一个参考方案，在用例内定义class Data，然后通过类变量存放数据。不同的项目，不同的场景，不同的参数，可以考虑不一样的存放方式。\ntep函数 不同于以往的fixture封装，tep内部没有再定义fixture，而是对外暴露函数：\n__all__ = [ \u0026#34;request\u0026#34;, \u0026#34;v\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;har2case\u0026#34; ] 在使用时import即可，更容易理解。\n函数v() 这是我认为tep3的最大亮点。函数v()根据不同的入参，有不同的含义：\n传入字典类型：初始化变量池\n传入字符串：解析变量\n传入2个参数：设置变量\n变量值支持占位符写法${}，参考JMeter或HttpRunner。\n通过函数v()来管理变量，用例代码清爽了不少。\nfixture fixture在创建脚手架时，自动生成了3个：\nfixture_login\nfixture_login_xdist\nfixture_mysql\n主要用于生成数据和执行setup/teardown，更多用法可以深入学习pytest fixture。\nhar2case 抓取har包后，可以快速转换为自动化用例：\nimport os from tep import har2case from tep.config import Config def test(): har2case({ \u0026#34;har\u0026#34;: os.path.join(Config().FILE_DIR, \u0026#34;har\u0026#34;, \u0026#34;flow.har\u0026#34;), \u0026#34;dir\u0026#34;: os.path.join(Config().TESTS_DIR, \u0026#34;generated\u0026#34;), \u0026#34;overwrite\u0026#34;: 1 }) 同时支持批量替换：\nimport os from tep import har2case from tep.config import Config def test(): har2case({ \u0026#34;har\u0026#34;: os.path.join(Config().FILE_DIR, \u0026#34;har\u0026#34;, \u0026#34;flow.har\u0026#34;), \u0026#34;dir\u0026#34;: os.path.join(Config().TESTS_DIR, \u0026#34;generated\u0026#34;), \u0026#34;overwrite\u0026#34;: 1, \u0026#34;var\u0026#34;: {\u0026#34;domain\u0026#34;: \u0026#34;http://127.0.0.1:5000\u0026#34;}, \u0026#34;url\u0026#34;: {\u0026#34;http://127.0.0.1:5000\u0026#34;: \u0026#34;${domain}\u0026#34;}, \u0026#34;headers\u0026#34;: {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;Cookie\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} }) 测试报告 run.py入口执行，设置report后即可生成：\nfrom tep import run if __name__ == \u0026#39;__main__\u0026#39;: run({ \u0026#34;path\u0026#34;: [\u0026#34;test_flow.py\u0026#34;], # Relative path to tests, file or directory \u0026#34;report\u0026#34;: 1 # Generate html report, 0: False, 1: True }) 更多内容请安装tep体验或查看源码阅读。\n源码 tep3 https://github.com/dongfanger/tep\ntep2 https://github.com/dongfanger/tep/tree/2.2.4\ntep1 https://github.com/dongfanger/tep/tree/1.0.0\n欢迎大家讨论，对于接口测试框架，你有什么想法呢。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000018-%E5%B9%B4%E5%BA%A6%E5%A4%A7%E7%89%88%E6%9C%ACtep3%E5%9B%9E%E5%BD%92%E7%BA%AF%E7%B2%B9python%E6%8E%A5%E5%8F%A3%E6%B5%8B%E8%AF%95%E6%A1%86%E6%9E%B6/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"回顾\"\u003e回顾\u003c/h1\u003e\n\u003cp\u003etep是基于pytest开发的接口测试框架，上个版本从小工具升级为了关键字驱动框架，通过pystats发现最近半年仍然有4000+人次在安装使用tep。时隔1年后，我对tep进行了一次大版本改造，抛弃tep2的Java代码风格，重新回归到tep1的纯粹Python风格。\u003c/p\u003e\n\u003ch1 id=\"纯粹python\"\u003e纯粹Python\u003c/h1\u003e\n\u003cp\u003e所谓纯粹Python，体现在两个方面，一个方面是tep源码进行了重写，更加Pythonic，自动化用例写起来也更加简单易懂；另一方面是抛弃了依赖Java的Allure和依赖C++的loguru，分别使用Python技术栈的pytest-html和logging替代。\u003c/p\u003e\n\u003ch1 id=\"tep3版本\"\u003etep3版本\u003c/h1\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003etep是Try Easy Pytest的首字母缩写，帮你轻松上手pytest。\u003c/p\u003e\n\u003cp\u003e框架特点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e基于pytest封装，成熟、稳定且扩展性强。\u003c/li\u003e\n\u003cli\u003e框架完全由Python构建，没有混杂其他语言。\u003c/li\u003e\n\u003cli\u003e原生Python语法，学习Python，零成本使用框架。\u003c/li\u003e\n\u003cli\u003eHAR包转换pytest用例。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003cp\u003e支持Python3.8以上版本\u003c/p\u003e\n\u003cp\u003e创建虚拟环境：\u003ccode\u003epython -m venv venv\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e激活虚拟环境，Windows用户：\u003ccode\u003eactivate.bat\u003c/code\u003e Mac用户：\u003ccode\u003esource venv/bin/activate\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e安装tep：\u003ccode\u003epip install tep\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e验证安装成功：\u003ccode\u003etep -V\u003c/code\u003e\u003c/p\u003e\n\u003ch2 id=\"目录结构\"\u003e目录结构\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edata\nfile\nfixture\nreport\nscripts\ntests\n.gitignore\nconftest.py\npytest.ini\nrun.py\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003ccode\u003etep new demo\u003c/code\u003e就能快速创建项目名称为\u003ccode\u003edemo\u003c/code\u003e的项目脚手架。\u003c/p\u003e\n\u003ch2 id=\"用例示范\"\u003e用例示范\u003c/h2\u003e\n\u003cp\u003e简单的请求：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etep\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e request\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://httpbin.org/status/200\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e200\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e复杂的场景：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etep\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e request\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etep\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e v\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003edata.global_data\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e GlobalData\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eData\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v\u003cspan style=\"color:#1f2328\"\u003e({\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e GlobalData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edomain\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;payAmount\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0.2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body_add_cart \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;{\u0026#34;skuId\u0026#34;:\u0026#34;$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{skuId}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;,\u0026#34;skuNum\u0026#34;:$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{skuNum}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e}\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body_order \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;{\u0026#34;skuId\u0026#34;:\u0026#34;$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{skuId}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;,\u0026#34;price\u0026#34;:$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{skuPrice}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e,\u0026#34;skuNum\u0026#34;:$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{skuNum}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e,\u0026#34;totalPrice\u0026#34;:$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{totalPrice}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e}\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body_pay \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;{\u0026#34;orderId\u0026#34;:\u0026#34;$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{orderId}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;,\u0026#34;payAmount\u0026#34;:$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{payAmount}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e}\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{domain}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e/searchSku?skuName=book\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eGlobalData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.price\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{domain}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e/addCart\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ebody_add_cart\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eGlobalData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{domain}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e/order\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ebody_order\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eGlobalData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;orderId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.orderId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{domain}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e/pay\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e v\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ebody_pay\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eGlobalData\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.success\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;true\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"用例和数据\"\u003e用例和数据\u003c/h1\u003e\n\u003cp\u003e数据怎么存放，这里给出了一个参考方案，在用例内定义\u003ccode\u003eclass Data\u003c/code\u003e，然后通过类变量存放数据。不同的项目，不同的场景，不同的参数，可以考虑不一样的存放方式。\u003c/p\u003e","title":"年度大版本，tep3回归纯粹Python接口测试框架"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n系统架构是软件系统的结构化解决方案，它定义了：系统组件及其相互关系，组件与环境的关系，指导设计和演进的原则，技术标准和约束条件，质量属性实现方案，系统各层面的抽象描述，关键设计决策文档，系统演进路线图。\n通常我们可以从研发那拿到一张“系统架构图”，描绘了有哪些上下游，系统内部组成，各应用之间的交互，接口调用，消息，数据存储，以及中间件等。\n那怎么讲系统架构呢？怎么才能显得专业呢？\n我们注意看系统架构的定义：系统架构是软件系统的结构化解决方案，也就是说，系统架构是一种解决方案。\n第一步，背景。系统架构要解决什么问题，为什么要采用这套架构，架构有没有演进过程，现在这套架构有什么优势。\n第二步，讲图。从上到下，从左到右，不需要讲的过于细节，而是从整体上把业务流程、系统交互、数据流，结合架构图讲清楚。\n第三步，举例。这一步是展现出你懂技术的关键。怎么保证高可用的，大数据量怎么处理，如何防止超开发票，分布式事务问题，幂等，加锁等等。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000018-%E6%80%8E%E4%B9%88%E7%BB%99%E5%88%AB%E4%BA%BA%E8%AE%B2%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/","summary":"系统架构是软件系统的结构化解决方案，它定义了：系统组件及其相互关系，组件与环境的关系，指导设计和演进的原则，技术标准和约束条件，质量属性实现方案，系统各层面的抽象描述，关键设计决策文档，系统演进路线图。","title":"怎么给别人讲系统架构"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n如果你想自我提升却找不到合适方法，或者学习没有什么效果，希望这篇文章能给你带来些许启发。\n我们从小到大一直都在学习，读书时代我们是怎么学习的呢？教材+老师+练习，是一套由专业的教育工作者研究出来的体系，在教导和监督下学习。\n可是到工作以后，情况就完全不一样了。一是时间问题，没有足够时间进行系统学习。二是惰性问题，没有集体氛围很难静下心来。三是资源问题，没有系统指引让你按部就班持续提升。一部分人选择花钱报培训班，一部分人选择网上找资源，或者干脆买几本书看看。可最终发现看了又忘，学了没用，时间花了钱花了，竹篮打水一场空，陷入想提升又不知道该怎么办的焦虑状态。\n随着AI实用性越来越强，这种状况有所改善。我们可以在工作中，及时向AI提问找答案，解决工作问题，增长知识。\n那工作之外呢？该怎么提升？这是本篇文章想讨论的内容，我的结论是：结果驱动学习。\n这个问题困扰我快一年，认知颠覆，逐渐有所感悟。可能是我个人存在很大的思维惯性，工作以后仍然会采用学校的学习方式。要想改变，就要把这个想法彻底扭转。\n工作的本质是什么？对于测试开发来说，我们每天的工作是接需求，测试，上线。我们创造的价值是提升产品和服务的质量。\n那价值又是什么呢？比如很火的PopMart，Labubu，这么多人买，可能有些人觉得没什么意思，但是据调查购买人群主要集中在17岁到35岁的一线和准一线女性，对这个群体来说，是有价值的。\n供需关系，有需求，就有价值。\n想想我们的工作，有些事情可能对我们来说没什么意思，甚至觉得没有什么意义，但是它很可能是我们没有看到的需求，可以是某个任务，也可以是某种情绪。\n到这里，第一个新的认知是，值得我们学习的东西，不是书本上的知识，而是公司环境下各种需求所要求的技能。\n我最近在看Python官方教程，有一天我意识到不对劲，赶紧关掉网页，重新思考。Python教程里面很多知识我工作并不会用，遇到实际问题也可以让AI给我写代码，我也不会靠Python找开发工作，就算面试考Python我也只需要针对性记忆就行。没有必要把时间花到教程上。\n那该学些什么？一是系统架构知识，梳理当前负责系统架构，扩展技术视野。二是业务质量问题，比如秒杀超卖问题、资损防控核对等。三是情商，稳定的情绪，沟通的艺术等。还有很多，换个思路后，学习方向已经完全变了。\n到这里，第二个新的认知是，搞清楚我们想要的结果什么，只做有用的学习，而不是宽泛的学习。\n专业、熟练，是我们作为行业领域工作者，需要追求的目标。AI时代，知识获取已经变得很廉价，实用技能才是自我提升的方向。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000017-ai%E6%97%B6%E4%BB%A3%E5%A6%82%E4%BD%95%E8%87%AA%E6%88%91%E6%8F%90%E5%8D%87/","summary":"如果你想自我提升却找不到合适方法，或者学习没有什么效果，希望这篇文章能给你带来些许启发。","title":"AI时代如何自我提升"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nProcessing Data in Memory The Stream API is probably the second most important feature added to Java SE 8, after the lambda expressions. In a nutshell, the Stream API is about providing an implementation of the well known map-filter-reduce algorithm to the JDK.\nmap-filter-reduce：\nList\u0026lt;Sale\u0026gt; sales = ...; // this is the list of all the sales int amountSoldInMarch = 0; for (Sale sale: sales) { if (sale.getDate().getMonth() == Month.MARCH) { amountSoldInMarch += sale.getAmount(); } } System.out.println(\u0026#34;Amount sold in March: \u0026#34; + amountSoldInMarch); map：通过get取值，将部分字段映射到新数据（select字段）\nfilter：根据if判断过滤部分数据（where条件）\nreduce：聚合，求和（聚合函数）\n简而言之，相当于写一段SQL：\nselect sum(amount) from Sales where extract(month from date) = 3; 看看是如何从原始代码转换为Stream API的：\nList\u0026lt;City\u0026gt; cities = ...; int sum = 0; for (City city: cities) { int population = city.getPopulation(); if (population \u0026gt; 100_000) { sum += population; } } System.out.println(\u0026#34;Sum = \u0026#34; + sum); 假设Collection有这几个方法：\nint sum = cities.map(city -\u0026gt; city.getPopulation()) .filter(population -\u0026gt; population \u0026gt; 100_000) .sum(); 为什么Collection不提供这些方法呢？拆分为每一步：\nCollection\u0026lt;Integer\u0026gt; populations = cities.map(city -\u0026gt; city.getPopulation()); Collection\u0026lt;Integer\u0026gt; filteredPopulations = populations.filter(population -\u0026gt; population \u0026gt; 100_000); int sum = filteredPopulations.sum(); 假如有1000个city，那么中间数据也是Collection，就会产生很多冗余的中间数据。而for循环却不存在这个问题，因为它不会存储中间数据。虽然Collection提供方法能让代码看起来更好理解，但却会导致大量的冗余数据。所以不得不设计一套Stream API来支持map-filter-reduce。\nStream\u0026lt;City\u0026gt; streamOfCities = cities.stream(); Stream\u0026lt;Integer\u0026gt; populations = streamOfCities.map(city -\u0026gt; city.getPopulation()); Stream\u0026lt;Integer\u0026gt; filteredPopulations = populations.filter(population -\u0026gt; population \u0026gt; 100_000); int sum = filteredPopulations.sum(); // in fact this code does not compile; we\u0026#39;ll fix it later The streams created in this code, streamOfCities, populations and filteredPopulations must all be empty objects.\nIt leads to a very important property of streams: A stream is an object that does not store any data.\nUsing streams is about creating pipelines of operations. A pipeline is made of a series of method calls on a stream. Each call produces another stream. Then at some point, a last call produces a result.\nAdding Intermediate Operations collect()\nStream本身不会存储数据，通过collect存储为List：\nList\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;); Function\u0026lt;String, Integer\u0026gt; toLength = String::length; Stream\u0026lt;Integer\u0026gt; ints = strings.stream() .map(toLength); List\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;); List\u0026lt;Integer\u0026gt; lengths = strings.stream() .map(String::length) .collect(Collectors.toList()); System.out.println(\u0026#34;lengths = \u0026#34; + lengths); lengths = [3, 3, 5, 4] 一些方法\ndistinct() sorted()\nskip()\nlimit()\ncontact()\n连接流\nList\u0026lt;Integer\u0026gt; list0 = List.of(1, 2, 3); List\u0026lt;Integer\u0026gt; list1 = List.of(4, 5, 6); List\u0026lt;Integer\u0026gt; list2 = List.of(7, 8, 9); // 1st pattern: concat List\u0026lt;Integer\u0026gt; concat = Stream.concat(list0.stream(), list1.stream()) .collect(Collectors.toList()); // 2nd pattern: flatMap List\u0026lt;Integer\u0026gt; flatMap = Stream.of(list0.stream(), list1.stream(), list2.stream()) .flatMap(Function.identity()) .collect(Collectors.toList()); System.out.println(\u0026#34;concat = \u0026#34; + concat); System.out.println(\u0026#34;flatMap = \u0026#34; + flatMap); concat = [1, 2, 3, 4, 5, 6] flatMap = [1, 2, 3, 4, 5, 6, 7, 8, 9] 连接流，推荐使用flatMap()\nWith the flatmap pattern, you just create a single stream to hold all your streams and do the flatmap. The overhead is much lower.\nconcat produces a SIZED stream, whereas flatmap does not.\nCreating Streams 前面我们看到Collection的stream()方法可以创建流，此外还有很多其他方式创建流：\na vararg argument; a supplier; a unary operator, that generates the next element from the previous one; a builder; the characters of a string; the lines of a text file; the elements created by splitting a string of characters with a regular expressions; a random variable, that can create a stream of random numbers. Iterator\u0026lt;String\u0026gt; iterator = ...; long estimateSize = 10L; int characteristics = 0; Spliterator\u0026lt;String\u0026gt; spliterator = Spliterators.spliterator(strings.iterator(), estimateSize, characteristics); boolean parallel = false; Stream\u0026lt;String\u0026gt; stream = StreamSupport.stream(spliterator, parallel); 空流：\nStream\u0026lt;String\u0026gt; empty = Stream.empty(); List\u0026lt;String\u0026gt; strings = empty.collect(Collectors.toList()); System.out.println(\u0026#34;strings = \u0026#34; + strings); Creating a Stream from a Vararg or an Array\nStream\u0026lt;Integer\u0026gt; intStream = Stream.of(1, 2, 3); List\u0026lt;Integer\u0026gt; ints = intStream.collect(Collectors.toList()); System.out.println(\u0026#34;ints = \u0026#34; + ints); String[] stringArray = {\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;}; Stream\u0026lt;String\u0026gt; stringStream = Arrays.stream(stringArray); List\u0026lt;String\u0026gt; strings = stringStream.collect(Collectors.toList()); System.out.println(\u0026#34;strings = \u0026#34; + strings); Creating a Stream from a Supplier\nStream\u0026lt;String\u0026gt; generated = Stream.generate(() -\u0026gt; \u0026#34;+\u0026#34;); List\u0026lt;String\u0026gt; strings = generated .limit(10L) .collect(Collectors.toList()); System.out.println(\u0026#34;strings = \u0026#34; + strings); Creating a Stream from a UnaryOperator and a Seed\nStream\u0026lt;String\u0026gt; iterated = Stream.iterate(\u0026#34;+\u0026#34;, s -\u0026gt; s + \u0026#34;+\u0026#34;); iterated.limit(5L).forEach(System.out::println); Creating a Stream from a Range of Numbers\nString[] letters = {\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;D\u0026#34;}; List\u0026lt;String\u0026gt; listLetters = IntStream.range(0, 10) .mapToObj(index -\u0026gt; letters[index % letters.length]) .collect(Collectors.toList()); System.out.println(\u0026#34;listLetters = \u0026#34; + listLetters); Creating a Stream of Random Numbers\nRandom random = new Random(314L); List\u0026lt;Integer\u0026gt; randomInts = random.ints(10, 1, 5) .boxed() .collect(Collectors.toList()); System.out.println(\u0026#34;randomInts = \u0026#34; + randomInts); Creating a Stream from the Characters of a String\nJava SE 10\nString sentence = \u0026#34;Hello Duke\u0026#34;; List\u0026lt;String\u0026gt; letters = sentence.chars() .mapToObj(codePoint -\u0026gt; (char)codePoint) .map(Object::toString) .collect(Collectors.toList()); System.out.println(\u0026#34;letters = \u0026#34; + letters); Creating a Stream from the Lines of a Text File\nPath log = Path.of(\u0026#34;/tmp/debug.log\u0026#34;); // adjust to fit your installation try (Stream\u0026lt;String\u0026gt; lines = Files.lines(log)) { long warnings = lines.filter(line -\u0026gt; line.contains(\u0026#34;WARNING\u0026#34;)) .count(); System.out.println(\u0026#34;Number of warnings = \u0026#34; + warnings); } catch (IOException e) { // do something with the exception } Creating a Stream from a Regular Expression\nString sentence = \u0026#34;For there is good news yet to hear and fine things to be seen\u0026#34;; Pattern pattern = Pattern.compile(\u0026#34; \u0026#34;); Stream\u0026lt;String\u0026gt; stream = pattern.splitAsStream(sentence); List\u0026lt;String\u0026gt; words = stream.collect(Collectors.toList()); System.out.println(\u0026#34;words = \u0026#34; + words); Creating a Stream with the Builder Pattern\nStream.Builder\u0026lt;String\u0026gt; builder = Stream.\u0026lt;String\u0026gt;builder(); builder.add(\u0026#34;one\u0026#34;) .add(\u0026#34;two\u0026#34;) .add(\u0026#34;three\u0026#34;) .add(\u0026#34;four\u0026#34;); Stream\u0026lt;String\u0026gt; stream = builder.build(); List\u0026lt;String\u0026gt; list = stream.collect(Collectors.toList()); System.out.println(\u0026#34;list = \u0026#34; + list); Creating a Stream on an HTTP Source\n// The URI of the file URI uri = URI.create(\u0026#34;https://www.gutenberg.org/files/98/98-0.txt\u0026#34;); // The code to open create an HTTP request HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder(uri).build(); // The sending of the request HttpResponse\u0026lt;Stream\u0026lt;String\u0026gt;\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.ofLines()); List\u0026lt;String\u0026gt; lines; try (Stream\u0026lt;String\u0026gt; stream = response.body()) { lines = stream .dropWhile(line -\u0026gt; !line.equals(\u0026#34;A TALE OF TWO CITIES\u0026#34;)) .takeWhile(line -\u0026gt; !line.equals(\u0026#34;*** END OF THE PROJECT GUTENBERG EBOOK A TALE OF TWO CITIES ***\u0026#34;)) .collect(Collectors.toList()); } System.out.println(\u0026#34;# lines = \u0026#34; + lines.size()); Reducing a Stream Compute a reduction by just providing a binary operator that operates on only two elements. This is how the reduce() method works in the Stream API.\nStream\u0026lt;Integer\u0026gt; ints = Stream.of(0, 0, 0, 0); int sum = ints.reduce(10, (a, b) -\u0026gt; a + b); System.out.println(\u0026#34;sum = \u0026#34; + sum); Adding a Terminal Operation In fact, you should use this reduce() method as a last resort, only if you have no other solution.\n要想reduce stream，还有其他更多方法，比如count()、sum()等。\ncount()\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;); long count = strings.stream() .filter(s -\u0026gt; s.length() == 3) .count(); System.out.println(\u0026#34;count = \u0026#34; + count); forEach()\nStream\u0026lt;String\u0026gt; strings = Stream.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;); strings.filter(s -\u0026gt; s.length() == 3) .map(String::toUpperCase) .forEach(System.out::println); collect()\nStream\u0026lt;String\u0026gt; strings = Stream.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;); List\u0026lt;String\u0026gt; result = strings.filter(s -\u0026gt; s.length() == 3) .map(String::toUpperCase) .collect(Collectors.toList()); max() min()\nStream\u0026lt;String\u0026gt; strings = Stream.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;); String longest = strings.max(Comparator.comparing(String::length)) .orElseThrow(); System.out.println(\u0026#34;longest = \u0026#34; + longest); findFirst() findAny()\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;); String first = strings.stream() // .unordered() // .parallel() .filter(s -\u0026gt; s.length() == 3) .findFirst() .orElseThrow(); System.out.println(\u0026#34;first = \u0026#34; + first); allMatch() anyMatch() noneMatch()\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;); boolean noBlank = strings.stream() .allMatch(Predicate.not(String::isBlank)); boolean oneGT3 = strings.stream() .anyMatch(s -\u0026gt; s.length() == 3); boolean allLT10 = strings.stream() .noneMatch(s -\u0026gt; s.length() \u0026gt; 10); System.out.println(\u0026#34;noBlank = \u0026#34; + noBlank); System.out.println(\u0026#34;oneGT3 = \u0026#34; + oneGT3); System.out.println(\u0026#34;allLT10 = \u0026#34; + allLT10); Finding the Characteristics ORDERED The order in which the elements of the stream are processed matters. DISTINCT There are no doubles in the elements processed by that stream. NONNULL There are no null elements in that stream. SORTED The elements of that stream are sorted. SIZED The number of elements this stream processes is known. SUBSIZED Splitting this stream produces two SIZED streams. Collection\u0026lt;String\u0026gt; stringCollection = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;); Stream\u0026lt;String\u0026gt; strings = stringCollection.stream().sorted(); Stream\u0026lt;String\u0026gt; filteredStrings = strings.filtered(s -\u0026gt; s.length() \u0026lt; 5); Stream\u0026lt;Integer\u0026gt; lengths = filteredStrings.map(String::length); Collection\u0026lt;String\u0026gt; stringCollection = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;); Stream\u0026lt;String\u0026gt; strings = stringCollection.stream().distinct(); Stream\u0026lt;String\u0026gt; filteredStrings = strings.filtered(s -\u0026gt; s.length() \u0026lt; 5); Stream\u0026lt;Integer\u0026gt; lengths = filteredStrings.map(String::length); Using a Collector List\u0026lt;Integer\u0026gt; numbers = IntStream.range(0, 10) .boxed() .collect(Collectors.toList()); System.out.println(\u0026#34;numbers = \u0026#34; + numbers); Set\u0026lt;Integer\u0026gt; evenNumbers = IntStream.range(0, 10) .map(number -\u0026gt; number / 2) .boxed() .collect(Collectors.toSet()); System.out.println(\u0026#34;evenNumbers = \u0026#34; + evenNumbers); LinkedList\u0026lt;Integer\u0026gt; linkedList = IntStream.range(0, 10) .boxed() .collect(Collectors.toCollection(LinkedList::new)); System.out.println(\u0026#34;linked listS = \u0026#34; + linkedList); couting\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;); long count = strings.stream().count(); long countWithACollector = strings.stream().collect(Collectors.counting()); System.out.println(\u0026#34;count = \u0026#34; + count); System.out.println(\u0026#34;countWithACollector = \u0026#34; + countWithACollector); joining\nString joined = IntStream.range(0, 10) .boxed() .map(Object::toString) .collect(Collectors.joining(\u0026#34;, \u0026#34;)); System.out.println(\u0026#34;joined = \u0026#34; + joined); partitioningBy\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;, \u0026#34;eleven\u0026#34;, \u0026#34;twelve\u0026#34;); Map\u0026lt;Boolean, List\u0026lt;String\u0026gt;\u0026gt; map = strings.stream() .collect(Collectors.partitioningBy(s -\u0026gt; s.length() \u0026gt; 4)); map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); groupingBy\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;, \u0026#34;eleven\u0026#34;, \u0026#34;twelve\u0026#34;); Map\u0026lt;Integer, List\u0026lt;String\u0026gt;\u0026gt; map = strings.stream() .collect(Collectors.groupingBy(String::length)); map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); groupingBy + counting\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;, \u0026#34;eleven\u0026#34;, \u0026#34;twelve\u0026#34;); Map\u0026lt;Integer, Long\u0026gt; map = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting())); map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); 3 :: 4 4 :: 3 5 :: 3 6 :: 2 groupingBy + joining\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;, \u0026#34;eleven\u0026#34;, \u0026#34;twelve\u0026#34;); Map\u0026lt;Integer, String\u0026gt; map = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.joining(\u0026#34;, \u0026#34;))); map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); 3 :: one, two, six, ten 4 :: four, five, nine 5 :: three, seven, eight 6 :: eleven, twelve toMap\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, \u0026#34;ten\u0026#34;, \u0026#34;eleven\u0026#34;, \u0026#34;twelve\u0026#34;); Map\u0026lt;Integer, String\u0026gt; map = strings.stream() .collect( Collectors.toMap( element -\u0026gt; element.length(), element -\u0026gt; element, (element1, element2) -\u0026gt; element1 + \u0026#34;, \u0026#34; + element2)); map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); 3 :: one, two, six, ten 4 :: four, five, nine 5 :: three, seven, eight 6 :: eleven, twelve element -\u0026gt; element.length() is the key mapper. element -\u0026gt; element is the value mapper. (element1, element2) -\u0026gt; element1 + \u0026quot;, \u0026quot; + element2) is the merge function, called with the two elements that have generated the same key. Parallelizing Streams int parallelSum = IntStream.range(0, 10) .parallel() .sum(); 参考资料：\nThe Stream API https://dev.java/learn/api/streams/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000017-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B014%E6%B5%81/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"processing-data-in-memory\"\u003eProcessing Data in Memory\u003c/h2\u003e\n\u003cp\u003eThe Stream API is probably the second most important feature added to Java SE 8, after the lambda expressions. In a nutshell, the Stream API is about providing an implementation of the well known map-filter-reduce algorithm to the JDK.\u003c/p\u003e\n\u003cp\u003emap-filter-reduce：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eSale\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esales\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e...;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// this is the list of all the sales\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eamountSoldInMarch\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e0\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eSale\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esale\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esales\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esale\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetDate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetMonth\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMonth\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eMARCH\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eamountSoldInMarch\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esale\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetAmount\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Amount sold in March: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eamountSoldInMarch\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003emap：通过get取值，将部分字段映射到新数据（select字段）\u003c/p\u003e","title":"Java官方笔记14流"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nlocustfile是什么？ locustfile是Locust性能测试工具的用户脚本，描述了单个用户的行为。\nlocustfile是个普通的Python模块，如果写作locustfile.py，那么路径切换到文件所在目录，直接执行命令就能运行：\n$ locust 如果换个名字，那么只能通过-f参数指定文件名运行：\n$ locust -f locust_files/my_locust_file.py 与一般Python模块不同的是：locustfile必须至少定义一个类，且继承自User类。\nUser类 User类表示性能测试的模拟用户，Locust会在运行时创建User类的实例。\nwait_time属性 设置等待时间，默认值不等待，立即执行。\nLocust支持4种方式设置wait_time属性。\n为了便于理解实际意义，我把源码贴在了下面。\nconstant函数，常量，任务执行完毕等待X秒开始下一任务。\ndef constant(wait_time): \u0026#34;\u0026#34;\u0026#34; Returns a function that just returns the number specified by the wait_time argument Example:: class MyUser(User): wait_time = constant(3) \u0026#34;\u0026#34;\u0026#34; return lambda instance: wait_time between函数，区间随机值，任务执行完毕等待X-Y秒（中间随机取值）开始下一任务。\ndef between(min_wait, max_wait): \u0026#34;\u0026#34;\u0026#34; Returns a function that will return a random number between min_wait and max_wait. Example:: class MyUser(User): # wait between 3.0 and 10.5 seconds after each task wait_time = between(3.0, 10.5) \u0026#34;\u0026#34;\u0026#34; return lambda instance: min_wait + random.random() * (max_wait - min_wait) constant_pacing函数，自适应，若任务耗时超过该时间，则任务结束后立即执行下一任务；若任务耗时不超过该时间，则等待达到该时间后执行下一任务。\ndef constant_pacing(wait_time): \u0026#34;\u0026#34;\u0026#34; Returns a function that will track the run time of the tasks, and for each time it\u0026#39;s called it will return a wait time that will try to make the total time between task execution equal to the time specified by the wait_time argument. In the following example the task will always be executed once every second, no matter the task execution time:: class MyUser(User): wait_time = constant_pacing(1) @task def my_task(self): time.sleep(random.random()) If a task execution exceeds the specified wait_time, the wait will be 0 before starting the next task. \u0026#34;\u0026#34;\u0026#34; def wait_time_func(self): if not hasattr(self, \u0026#34;_cp_last_run\u0026#34;): self._cp_last_wait_time = wait_time self._cp_last_run = time() return wait_time else: run_time = time() - self._cp_last_run - self._cp_last_wait_time self._cp_last_wait_time = max(0, wait_time - run_time) self._cp_last_run = time() return self._cp_last_wait_time return wait_time_func 自定义wait_time方法，比如每次等待时间1秒2秒3秒递增：\nclass MyUser(User): last_wait_time = 0 def wait_time(self): self.last_wait_time += 1 return self.last_wait_time ... weight属性 设置创建类实例的权重，默认每个类创建相同数量的实例。\nlocustfile中可以有多个继承了User类的类。\n命令行可以指定运行哪些类：\n$ locust -f locust_file.py WebUser MobileUser 通过weight属性可以让类更大概率创建实例，比如：\nclass WebUser(User): weight = 3 ... class MobileUser(User): weight = 1 ... WebUser类比MobileUser类多三倍概率创建实例。\nhost属性 设置URL前缀。\n**一般是在Locust的Web UI或者命令行，通过--host指定URL前缀。**如果没有通过--host指定，并且类中设置了host属性，那么类的host属性才会生效。\nenvironment属性 对用户运行环境的引用。\n比如在task方法中通过environment属性终止运行：\nself.environment.runner.quit() 注意，单机会终止所有运行，分布式只会终止单个worker节点。\non_start和on_stop方法 测试前初始化和测试后清理。\nHttpUser类 开篇文章的示例脚本，没有继承User类，而是继承了它的子类HttpUser：\n它比User类更常用，因为它添加了一个client属性，用来发送HTTP请求。\nclient属性/HttpSession HttpUser类的client属性是HttpSession类的一个实例：\nHttpSession是requests.Session的子类，requests就是常用来做接口测试的那个requests库：\nHttpSession没有对requests.Session做什么改动，主要是传递请求结果给Locust，比如success/fail，response time，response length，name。\n示例：\nresponse = self.client.post(\u0026#34;/login\u0026#34;, {\u0026#34;username\u0026#34;:\u0026#34;testuser\u0026#34;, \u0026#34;password\u0026#34;:\u0026#34;secret\u0026#34;}) print(\u0026#34;Response status code:\u0026#34;, response.status_code) print(\u0026#34;Response text:\u0026#34;, response.text) response = self.client.get(\u0026#34;/my-profile\u0026#34;) 由于requests.Session会暂存cookie，所以示例中登录/login请求后可以继续请求/my-profile。\n断言响应结果 可以使用with语句和catch_response参数对响应结果进行断言：\nwith self.client.get(\u0026#34;/\u0026#34;, catch_response=True) as response: if response.text == \u0026#34;Success\u0026#34;: response.success() elif response.text != \u0026#34;Success\u0026#34;: response.failure(\u0026#34;Got wrong response\u0026#34;) elif response.elapsed.total_seconds() \u0026gt; 0.5: response.failure(\u0026#34;Request took too long\u0026#34;) 或者直接抛出异常：\nfrom locust.exception import RescheduleTask ... with self.client.get(\u0026#34;/does_not_exist/\u0026#34;, catch_response=True) as response: if response.status_code == 404: raise RescheduleTask() name参数 name参数用于把不同api按同一分组进行统计，比如：\nfor i in range(10): self.client.get(\u0026#34;/blog?id=%i\u0026#34; % i, name=\u0026#34;/blog?id=[id]\u0026#34;) 会按/blog/?id=[id]统计1条数据，而不是分成10条数据。\nHTTP代理 Locust默认设置了requests.Session的trust_env为False，不查找代理，以提高运行性能。如果需要可以设置locust_instance.client.trust_env为True。\n示例代码 请求REST API并断言：\nfrom json import JSONDecodeError ... with self.client.post(\u0026#34;/\u0026#34;, json={\u0026#34;foo\u0026#34;: 42, \u0026#34;bar\u0026#34;: None}, catch_response=True) as response: try: if response.json()[\u0026#34;greeting\u0026#34;] != \u0026#34;hello\u0026#34;: response.failure(\u0026#34;Did not get expected value in greeting\u0026#34;) except JSONDecodeError: response.failure(\u0026#34;Response could not be decoded as JSON\u0026#34;) except KeyError: response.failure(\u0026#34;Response did not contain expected key \u0026#39;greeting\u0026#39;\u0026#34;) 小结 locustfile是个普通Python模块，必须继承User类或其子类HttpUser等。本文对User类和HttpUser类的属性和方法进行了介绍，使用它们可以编写性能测试的用户脚本。locustfile还有另外一个重要组成元素，@task。\n参考资料：\nhttps://docs.locust.io/en/stable/writing-a-locustfile.html\nhttps://blog.csdn.net/Orangesir/article/details/114914969\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000017-locustlocustfile%E4%B8%AD%E7%9A%84user%E7%B1%BB%E5%92%8Chttpuser%E7%B1%BB/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"locustfile是什么\"\u003elocustfile是什么？\u003c/h1\u003e\n\u003cp\u003elocustfile是Locust性能测试工具的用户脚本，描述了单个用户的行为。\u003c/p\u003e\n\u003cp\u003elocustfile是个普通的Python模块，如果写作\u003ccode\u003elocustfile.py\u003c/code\u003e，那么路径切换到文件所在目录，直接执行命令就能运行：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ locust\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e如果换个名字，那么只能通过\u003ccode\u003e-f\u003c/code\u003e参数指定文件名运行：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ locust -f locust_files/my_locust_file.py\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e与一般Python模块不同的是：\u003cstrong\u003elocustfile必须至少定义一个类，且继承自User类。\u003c/strong\u003e\u003c/p\u003e\n\u003ch1 id=\"user类\"\u003eUser类\u003c/h1\u003e\n\u003cp\u003eUser类表示性能测试的模拟用户，\u003cstrong\u003eLocust会在运行时创建User类的实例\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"wait_time属性\"\u003ewait_time属性\u003c/h2\u003e\n\u003cp\u003e设置等待时间，默认值不等待，立即执行。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLocust支持4种方式设置wait_time属性\u003c/strong\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为了便于理解实际意义，我把源码贴在了下面。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003econstant\u003c/code\u003e函数，常量，任务执行完毕等待X秒开始下一任务。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003econstant\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ewait_time\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    Returns a function that just returns the number specified by the wait_time argument\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    Example::\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e        class MyUser(User):\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e            wait_time = constant(3)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003elambda\u003c/span\u003e instance\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e wait_time\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003ebetween\u003c/code\u003e函数，区间随机值，任务执行完毕等待X-Y秒（中间随机取值）开始下一任务。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ebetween\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emin_wait\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e max_wait\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    Returns a function that will return a random number between min_wait and max_wait.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    Example::\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e        class MyUser(User):\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e            # wait between 3.0 and 10.5 seconds after each task\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e            wait_time = between(3.0, 10.5)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003elambda\u003c/span\u003e instance\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e min_wait \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e random\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erandom\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emax_wait \u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e min_wait\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003econstant_pacing\u003c/code\u003e函数，自适应，若任务耗时超过该时间，则任务结束后立即执行下一任务；若任务耗时不超过该时间，则等待达到该时间后执行下一任务。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003econstant_pacing\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ewait_time\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    Returns a function that will track the run time of the tasks, and for each time it\u0026#39;s\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    called it will return a wait time that will try to make the total time between task\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    execution equal to the time specified by the wait_time argument.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    In the following example the task will always be executed once every second, no matter\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    the task execution time::\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e        class MyUser(User):\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e            wait_time = constant_pacing(1)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e            @task\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e            def my_task(self):\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                time.sleep(random.random())\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    If a task execution exceeds the specified wait_time, the wait will be 0 before starting\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    the next task.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ewait_time_func\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehasattr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;_cp_last_run\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cp_last_wait_time \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e wait_time\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cp_last_run \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e time\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e wait_time\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            run_time \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e time\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cp_last_run \u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cp_last_wait_time\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cp_last_wait_time \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003emax\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e wait_time \u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e run_time\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cp_last_run \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e time\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cp_last_wait_time\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e wait_time_func\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e自定义\u003ccode\u003ewait_time\u003c/code\u003e方法，比如每次等待时间1秒2秒3秒递增：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyUser\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eUser\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    last_wait_time \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ewait_time\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elast_wait_time \u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elast_wait_time\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"locustfile中的User类和HttpUser类"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n四种参数 Python函数func定义如下：\ndef func(first, *args, second=\u0026#34;Hello World\u0026#34;, **kwargs): print(first) print(args) print(second) print(kwargs) func(\u0026#34;dongfanger\u0026#34;, \u0026#34;san\u0026#34;, py=\u0026#34;good\u0026#34;) 运行后会输出：\ndongfanger (\u0026#39;san\u0026#39;,) Hello World {\u0026#39;py\u0026#39;: \u0026#39;good\u0026#39;} 它有四种参数：\nfirst是定位参数，positional parameter，不可省略。 *args是可变参数，arguments，存入元组。 second是默认值参数，default argument values，可以省略。 **args是关键字参数，keyword arguments，存入字典。 func函数的调用方式有以下这些：\n①传入单个定位参数。\nfunc(\u0026#34;dongfanger\u0026#34;) dongfanger () Hello World {} ②第一个参数后的任意个参数会被*args捕获，存入一个元组。\nfunc(\u0026#34;dongfanger\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;) dongfanger (\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;) Hello World {} ③没有明确指定名称的关键字参数会被**kwargs捕获，存入一个字典。\nfunc(\u0026#34;dongfanger\u0026#34;, j=\u0026#34;1\u0026#34;, k=\u0026#34;2\u0026#34;) dongfanger () Hello World {\u0026#39;j\u0026#39;: \u0026#39;1\u0026#39;, \u0026#39;k\u0026#39;: \u0026#39;2\u0026#39;} ④second只能作为关键字参数传入。\nfunc(\u0026#34;dongfanger\u0026#34;, second=\u0026#34;cool\u0026#34;) dongfanger () cool {} ⑤定位函数也能作为关键字参数传入。\nfunc(first=\u0026#34;san\u0026#34;) san () Hello World {} ⑥字典前加上**，其所有元素作为单个参数传入，同名键会绑定到对应具名参数上，余下的被**args捕获。\nmy_dict = {\u0026#34;first\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;location\u0026#34;: \u0026#34;cd\u0026#34;, \u0026#34;second\u0026#34;: \u0026#34;cool\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;secret\u0026#34;} func(**my_dict) dongfanger () cool {\u0026#39;location\u0026#39;: \u0026#39;cd\u0026#39;, \u0026#39;age\u0026#39;: \u0026#39;secret\u0026#39;} 除了这四种参数，还有一种Python3新增加的仅限关键字参数。\n仅限关键字参数 仅限关键字参数（keyword-only argument）是Python3的新特性，func函数的second参数就是仅限关键字参数，“仅限”的意思是说，只能通过关键字参数指定，它一定不会捕获未命名的定位参数。\n假如把参数位置调整一下定义another_func函数：\ndef another_func(first, another_second=\u0026#34;Hello World\u0026#34;, *args, **kwargs): print(first) print(another_second) print(args) print(kwargs) another_func(\u0026#34;dongfanger\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;) 输出会变成：\ndongfanger a # 注意这里 (\u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;) {} another_second不是仅限关键字参数，而只是默认值参数，因为它捕获到了定位参数。\n由此得知，定义仅限关键字参数，必须把它放到*args参数后面，就像func函数一样，反例是another_func函数。\n还有第二个方法定义仅限关键字参数，在签名中放一个*：\n\u0026gt;\u0026gt;\u0026gt; def f(a, *, b): # b是仅限关键字参数 ... return a, b ... \u0026gt;\u0026gt;\u0026gt; f(1, b=2) # 只能传关键字参数 (1, 2) \u0026gt;\u0026gt;\u0026gt; f(1, 2) # 不能传定位参数 Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; TypeError: f() takes 1 positional argument but 2 were given \u0026gt;\u0026gt;\u0026gt; f(1, 2, 3) # 不能传定位参数 Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; TypeError: f() takes 1 positional argument but 3 were given 仅限关键字参数不一定要有默认值，就像b一样，强制必须传入实参。\n内省中的函数参数 函数内省的意思是说，当你拿到一个“函数对象”的时候，你可以继续知道，它的名字，参数定义等信息。这些信息可以通过函数对象的属性（一些双下划线的魔法方法）得到。\n对于func函数：\ndef func(first, *args, second=\u0026#34;Hello World\u0026#34;, **kwargs): print(first) print(second) print(args) print(kwargs) 和another_func函数：\ndef another_func(first, another_second=\u0026#34;Hello World\u0026#34;, *args, **kwargs): print(first) print(another_second) print(args) print(kwargs) __defaults__属性\n元组，保存着定位参数和关键字参数的默认值。\nprint(func.__defaults__) # None print(another_func.__defaults__) # (\u0026#39;Hello World\u0026#39;,) __kwdefaults__属性\n字典，保存仅限关键字参数。\nprint(func.__kwdefaults__) # {\u0026#39;second\u0026#39;: \u0026#39;Hello World\u0026#39;} print(another_func.__kwdefaults__) # None __code__属性\ncode对象引用，code对象自身有很多属性，其中包括参数名称。\nprint(func.__code__.co_varnames) # (\u0026#39;first\u0026#39;, \u0026#39;second\u0026#39;, \u0026#39;args\u0026#39;, \u0026#39;kwargs\u0026#39;) print(another_func.__code__.co_varnames) # (\u0026#39;first\u0026#39;, \u0026#39;another_second\u0026#39;, \u0026#39;args\u0026#39;, \u0026#39;kwargs\u0026#39;) 另外还可以使用inspect库的signature方法来查看内省中的函数参数：\nfrom inspect import signature print(signature(func)) ## (first, *args, second=\u0026#39;Hello World\u0026#39;, **kwargs) 框架和IDE等工具可以使用这些信息验证代码。\n函数注解 如果刷过力扣算法题，那么对函数注解就不会陌生。比如：\ndef clip(text:str, max_len:\u0026#39;int \u0026gt; 0\u0026#39;=80) -\u0026gt; str: pass 参数:后面是注解表达式，可以用来注解参数类型和约束。如果参数有默认值，注解放在参数名和=号之间。\n可以在函数末尾的)和:之间添加-\u0026gt;和注解表达式，来对返回值添加注解。\n注解表达式可以是任何类型，最常用的类型是类（如str或int）和字符串（如'int \u0026gt; 0'）。\n函数注解只是个注解，Python对注解所做的唯一的事情是，把它们存入函数的__annotations__属性中：\nprint(clip.__annotations__) #{\u0026#39;text\u0026#39;: \u0026lt;class \u0026#39;str\u0026#39;\u0026gt;, \u0026#39;max_len\u0026#39;: \u0026#39;int \u0026gt; 0\u0026#39;, \u0026#39;return\u0026#39;: \u0026lt;class \u0026#39;str\u0026#39;\u0026gt;} Python不做检查，不做强制，不做验证，什么操作都不做！注解只是元数据，可以供框架和IDE等工具使用。\n小结 本文介绍了Python函数的四种参数：定位参数、可变参数、默认值参数、关键字参数，和第五种Python3新特性参数：仅限关键字参数。拿到一个函数对象后，可以通过函数属性（一些双下划线的魔法方法）查看内省中的参数信息。函数注解是一种元数据，存在__annotations__属性中，备注函数的参数和返回值的类型，它只是个注解，Python不会做任何强制检查。\n参考资料：\n《流畅的Python》\nhttps://blog.csdn.net/cadi2011/article/details/86641811\nhttps://segmentfault.com/q/1010000012595419\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000017-pythonpython%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0%E5%92%8C%E6%B3%A8%E8%A7%A3%E6%98%AF%E4%BB%80%E4%B9%88/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"四种参数\"\u003e四种参数\u003c/h1\u003e\n\u003cp\u003ePython函数func定义如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003efunc\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efirst\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003eargs\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e second\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Hello World\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e**\u003c/span\u003ekwargs\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efirst\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eargs\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esecond\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ekwargs\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efunc\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dongfanger\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;san\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e py\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;good\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e运行后会输出：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edongfanger\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;san\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eHello World\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;py\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;good\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e它有四种参数：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003efirst是定位参数，positional parameter，不可省略。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e*args\u003c/code\u003e是可变参数，arguments，存入元组。\u003c/li\u003e\n\u003cli\u003esecond是默认值参数，default argument values，可以省略。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e**args\u003c/code\u003e是关键字参数，keyword  arguments，存入字典。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003efunc函数的调用方式有以下这些：\u003c/p\u003e\n\u003cp\u003e①传入单个定位参数。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efunc\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dongfanger\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edongfanger\n()\nHello World\n{}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e②第一个参数后的任意个参数会被\u003ccode\u003e*args\u003c/code\u003e捕获，存入一个元组。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efunc\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dongfanger\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;a\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;b\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;c\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edongfanger\n(\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;)\nHello World\n{}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e③没有明确指定名称的关键字参数会被\u003ccode\u003e**kwargs\u003c/code\u003e捕获，存入一个字典。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efunc\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dongfanger\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e j\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e k\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;2\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edongfanger\n()\nHello World\n{\u0026#39;j\u0026#39;: \u0026#39;1\u0026#39;, \u0026#39;k\u0026#39;: \u0026#39;2\u0026#39;}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e④second只能作为关键字参数传入。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efunc\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dongfanger\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e second\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;cool\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edongfanger\n()\ncool\n{}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e⑤定位函数也能作为关键字参数传入。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efunc\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efirst\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;san\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esan\n()\nHello World\n{}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e⑥字典前加上\u003ccode\u003e**\u003c/code\u003e，其所有元素作为单个参数传入，同名键会绑定到对应具名参数上，余下的被\u003ccode\u003e**args\u003c/code\u003e捕获。\u003c/p\u003e","title":"Python函数参数和注解是什么"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n正式版教程： https://dongfanger.gitee.io/blog/tep.html\n正式版源码： https://github.com/dongfanger/tep\ntep简介 tep是Try Easy Pytest的首字母缩写，是一款基于pytest测试框架的测试工具，集成了各种实用的第三方包和优秀的自动化测试设计思想，帮你快速实现自动化项目落地。\n快速入门 安装 pip install tep Mac用户建议创建虚拟环境并激活：\npython3 -m venv venv source venv/bin/activate 验证安装成功：\ntep -V 新建项目 tep startproject demo 带上-venv参数，可创建单个项目的Python虚拟环境，并在该项目的虚拟环境中安装tep：\ntep startproject demo -venv 启动FastAPI示例应用 运行utils/fastapi_mock.py脚本。\n执行示例用例 执行examples/tests/test_login_to_pay_flow.py用例。\n查看日志 ============================= test session starts ============================== collecting ... collected 1 item test_login_to_pay_flow.py::test ============================== 1 passed in 0.14s =============================== Process finished with exit code 0 2022-12-27 15:19:37.223 | INFO | fixtures.fixture_login:produce_expensive_data:15 - ----------------开始登录---------------- 2022-12-27 15:19:37.234 | INFO | utils.http_client:request:37 - Request URL: http://127.0.0.1:5000/login Request Method: post Request Headers: {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;} Request Payload: {\u0026#34;json\u0026#34;: {\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;}} Status Code: 200 Response: {\u0026#34;token\u0026#34;:\u0026#34;de2e3ffu29\u0026#34;} Elapsed: 0.003s 2022-12-27 15:19:37.234 | INFO | fixtures.fixture_login:produce_expensive_data:23 - ----------------登录成功---------------- PASSED [100%]2022-12-27 15:19:37.235 | INFO | utils.step:__init__:12 - ----------------搜索商品---------------- 2022-12-27 15:19:37.250 | INFO | utils.http_client:request:37 - Request URL: http://127.0.0.1:5000/searchSku Request Method: get Request Headers: {\u0026#34;token\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} Request Payload: {\u0026#34;params\u0026#34;: {\u0026#34;skuName\u0026#34;: \u0026#34;\\u7535\\u5b50\\u4e66\u0026#34;}} Status Code: 200 Response: {\u0026#34;skuId\u0026#34;:\u0026#34;222\u0026#34;,\u0026#34;price\u0026#34;:\u0026#34;2.3\u0026#34;} Elapsed: 0.001s 2022-12-27 15:19:37.250 | INFO | utils.step:__init__:12 - ----------------添加购物车---------------- 2022-12-27 15:19:37.254 | INFO | utils.http_client:request:37 - Request URL: http://127.0.0.1:5000/addCart Request Method: post Request Headers: {\u0026#34;token\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} Request Payload: {\u0026#34;json\u0026#34;: {\u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;skuNum\u0026#34;: 2}} Status Code: 200 Response: {\u0026#34;skuId\u0026#34;:\u0026#34;222\u0026#34;,\u0026#34;price\u0026#34;:\u0026#34;2.3\u0026#34;,\u0026#34;skuNum\u0026#34;:\u0026#34;3\u0026#34;,\u0026#34;totalPrice\u0026#34;:\u0026#34;6.9\u0026#34;} Elapsed: 0.001s 2022-12-27 15:19:37.254 | INFO | utils.step:__init__:12 - ----------------下单---------------- 2022-12-27 15:19:37.257 | INFO | utils.http_client:request:37 - Request URL: http://127.0.0.1:5000/order Request Method: post Request Headers: {\u0026#34;token\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} Request Payload: {\u0026#34;json\u0026#34;: {\u0026#34;orderId\u0026#34;: 222, \u0026#34;payAmount\u0026#34;: \u0026#34;0.2\u0026#34;, \u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;2.3\u0026#34;, \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34;, \u0026#34;totalPrice\u0026#34;: \u0026#34;6.9\u0026#34;}} Status Code: 200 Response: {\u0026#34;orderId\u0026#34;:\u0026#34;333\u0026#34;} Elapsed: 0.001s 2022-12-27 15:19:37.257 | INFO | utils.step:__init__:12 - ----------------支付---------------- 2022-12-27 15:19:37.259 | INFO | utils.http_client:request:37 - Request URL: http://127.0.0.1:5000/pay Request Method: post Request Headers: {\u0026#34;token\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} Request Payload: {\u0026#34;json\u0026#34;: {\u0026#34;skuId\u0026#34;: 123, \u0026#34;price\u0026#34;: 0.1, \u0026#34;skuNum\u0026#34;: 2, \u0026#34;totalPrice\u0026#34;: 0.2, \u0026#34;orderId\u0026#34;: \u0026#34;333\u0026#34;}} Status Code: 200 Response: {\u0026#34;success\u0026#34;:\u0026#34;true\u0026#34;} Elapsed: 0.001s 能在本地跑起来看到日志且没有报错，恭喜您，上手成功！\n目录结构说明 examples：示例代码，可无顾虑全删；\nfixtures：Pytest fixture，自动导入；\nresources：环境变量、全局变量；\ntests：测试用例；\nutils：工具包；\nconftest.py：Pytest挂载；\npytest.ini：Pytest配置；\nreports：测试报告，默认不显示，生成报告后会出现；\n用例组织形式 推荐MVC分层设计和数据代码分离。\n小提示：tep老版本的极速写法，即接口、数据、代码都放在一个文件的一个函数的写法，仍然适合于新手或追求效率时使用。\n用例集 在tests目录下将测试用例按功能模块分成多个用例集：\ntests user teacher student 测试用例 必须遵循用例解耦原则，每条用例都是单独可运行的。用例由2个文件组成，一个文件存放纯粹的yaml数据，一个文件存放逻辑代码：\ntest_login_to_pay_flow.yaml test_login_to_pay_flow.py 测试数据 存放在yaml文件中，第一层为说明文字，第二层为请求json：\n\u0026#34;查询SKU\u0026#34;: { \u0026#34;skuName\u0026#34;: \u0026#34;电子书\u0026#34; } \u0026#34;添加购物车\u0026#34;: { \u0026#34;skuId\u0026#34;: 123, \u0026#34;skuNum\u0026#34;: 2 } \u0026#34;下单\u0026#34;: { \u0026#34;orderId\u0026#34;: 222, \u0026#34;payAmount\u0026#34;: \u0026#34;0.2\u0026#34; } \u0026#34;支付\u0026#34;: { \u0026#34;skuId\u0026#34;: 123, \u0026#34;price\u0026#34;: 0.1, \u0026#34;skuNum\u0026#34;: 2, \u0026#34;totalPrice\u0026#34;: 0.2 } 测试标题 测试标题采用了@allure.title(\u0026quot;\u0026quot;)：\n@allure.title(\u0026#34;从登录到下单支付\u0026#34;) def test(login, env_vars, case_vars): 测试步骤 一条测试用例由多个测试步骤组成：\n@allure.title(\u0026#34;从登录到下单支付\u0026#34;) def test(login, env_vars, case_vars): case_vars.put(\u0026#34;token\u0026#34;, login[\u0026#34;token\u0026#34;]) cache = TepCache(env_vars=env_vars, case_vars=case_vars) Step(\u0026#34;搜索商品\u0026#34;, step_search_sku, cache) Step(\u0026#34;添加购物车\u0026#34;, step_add_cart, cache) Step(\u0026#34;下单\u0026#34;, step_order, cache) Step(\u0026#34;支付\u0026#34;, step_pay, cache) Step第一个参数为步骤描述，第二个参数为步骤实现函数，第三个参数为cache缓存。\n步骤实现函数定义在用例文件中：\ndef step_add_cart(cache: TepCache): url = cache.env_vars[\u0026#34;domain\u0026#34;] + \u0026#34;/addCart\u0026#34; headers = {\u0026#34;token\u0026#34;: cache.case_vars.get(\u0026#34;token\u0026#34;)} body = data(\u0026#34;添加购物车\u0026#34;) body[\u0026#34;skuId\u0026#34;] = cache.case_vars.get(\u0026#34;skuId\u0026#34;) response = request(\u0026#34;post\u0026#34;, url=url, headers=headers, json=body) assert response.status_code \u0026lt; 400 cache.case_vars.put(\u0026#34;skuNum\u0026#34;, response.jsonpath(\u0026#34;$.skuNum\u0026#34;)) cache.case_vars.put(\u0026#34;totalPrice\u0026#34;, response.jsonpath(\u0026#34;$.totalPrice\u0026#34;)) 从上往下依次为url、headers、body、参数化、请求调用、断言、数据提取。\n小技巧：打开PyCharm的Show Members，就能快速定位到某个step函数，编辑测试步骤。\n变量 环境变量：在resources/env_vars下预填变量，在resources/tep.yaml中激活某个环境，在代码中引入env_vars fixture读取变量值：\ndef test(env_vars): logger.info(env_vars[\u0026#34;domain\u0026#34;]) 全局变量：在resources/global_vars.yaml预填变量，在代码中引入global_vars fixture读取变量值：\ndef test(global_vars): print(global_vars[\u0026#34;desc\u0026#34;]) 用例变量：在用例中引入case_vars fixture，在步骤函数间通过cache传递：\n@allure.title(\u0026#34;从登录到下单支付\u0026#34;) def test(login, env_vars, case_vars): case_vars.put(\u0026#34;token\u0026#34;, login[\u0026#34;token\u0026#34;]) cache = TepCache(env_vars=env_vars, case_vars=case_vars) Step(\u0026#34;搜索商品\u0026#34;, step_search_sku, cache) Step(\u0026#34;添加购物车\u0026#34;, step_add_cart, cache) Step(\u0026#34;下单\u0026#34;, step_order, cache) Step(\u0026#34;支付\u0026#34;, step_pay, cache) def step_search_sku(cache: TepCache): url = cache.env_vars[\u0026#34;domain\u0026#34;] + \u0026#34;/searchSku\u0026#34; headers = {\u0026#34;token\u0026#34;: cache.case_vars.get(\u0026#34;token\u0026#34;)} body = data(\u0026#34;查询SKU\u0026#34;) response = request(\u0026#34;get\u0026#34;, url=url, headers=headers, params=body) assert response.status_code \u0026lt; 400 cache.case_vars.put(\u0026#34;skuId\u0026#34;, response.jsonpath(\u0026#34;$.skuId\u0026#34;)) cache.case_vars.put(\u0026#34;skuPrice\u0026#34;, response.jsonpath(\u0026#34;$.price\u0026#34;)) def step_add_cart(cache: TepCache): url = cache.env_vars[\u0026#34;domain\u0026#34;] + \u0026#34;/addCart\u0026#34; headers = {\u0026#34;token\u0026#34;: cache.case_vars.get(\u0026#34;token\u0026#34;)} body = data(\u0026#34;添加购物车\u0026#34;) body[\u0026#34;skuId\u0026#34;] = cache.case_vars.get(\u0026#34;skuId\u0026#34;) response = request(\u0026#34;post\u0026#34;, url=url, headers=headers, json=body) assert response.status_code \u0026lt; 400 cache.case_vars.put(\u0026#34;skuNum\u0026#34;, response.jsonpath(\u0026#34;$.skuNum\u0026#34;)) cache.case_vars.put(\u0026#34;totalPrice\u0026#34;, response.jsonpath(\u0026#34;$.totalPrice\u0026#34;)) 接口关联 如上所述，通过case_vars和cache实现了步骤函数里面的接口关联，上一个接口的响应，提取后存入cache，下一个接口的入参，从cache取值。\n数据提取 utils/http_client.py封装了requests.Response，添加了jsonpath方法，支持简单取值：\nresponse.jsonpath(\u0026#34;$.skuNum\u0026#34;) 断言 采用Python原生的assert断言。16种常用断言如下：\nimport allure @allure.title(\u0026#34;等于\u0026#34;) def test_assert_equal(): assert 1 == 1 @allure.title(\u0026#34;不等于\u0026#34;) def test_assert_not_equal(): assert 1 != 2 @allure.title(\u0026#34;大于\u0026#34;) def test_assert_greater_than(): assert 2 \u0026gt; 1 @allure.title(\u0026#34;小于\u0026#34;) def test_assert_less_than(): assert 1 \u0026lt; 2 @allure.title(\u0026#34;大于等于\u0026#34;) def test_assert_less_or_equals(): assert 2 \u0026gt;= 1 assert 2 \u0026gt;= 2 @allure.title(\u0026#34;小于等于\u0026#34;) def test_assert_greater_or_equals(): assert 1 \u0026lt;= 2 assert 1 \u0026lt;= 1 @allure.title(\u0026#34;长度相等\u0026#34;) def test_assert_length_equal(): assert len(\u0026#34;abc\u0026#34;) == len(\u0026#34;123\u0026#34;) @allure.title(\u0026#34;长度大于\u0026#34;) def test_assert_length_greater_than(): assert len(\u0026#34;hello\u0026#34;) \u0026gt; len(\u0026#34;123\u0026#34;) @allure.title(\u0026#34;长度小于\u0026#34;) def test_assert_length_less_than(): assert len(\u0026#34;hi\u0026#34;) \u0026lt; len(\u0026#34;123\u0026#34;) @allure.title(\u0026#34;长度大于等于\u0026#34;) def test_assert_length_greater_or_equals(): assert len(\u0026#34;hello\u0026#34;) \u0026gt;= len(\u0026#34;123\u0026#34;) assert len(\u0026#34;123\u0026#34;) \u0026gt;= len(\u0026#34;123\u0026#34;) @allure.title(\u0026#34;长度小于等于\u0026#34;) def test_assert_length_less_or_equals(): assert len(\u0026#34;123\u0026#34;) \u0026lt;= len(\u0026#34;hello\u0026#34;) assert len(\u0026#34;123\u0026#34;) \u0026lt;= len(\u0026#34;123\u0026#34;) @allure.title(\u0026#34;字符串相等\u0026#34;) def test_assert_string_equals(): assert \u0026#34;dongfanger\u0026#34; == \u0026#34;dongfanger\u0026#34; @allure.title(\u0026#34;以...开头\u0026#34;) def test_assert_startswith(): assert \u0026#34;dongfanger\u0026#34;.startswith(\u0026#34;don\u0026#34;) @allure.title(\u0026#34;以...结尾\u0026#34;) def test_assert_startswith(): assert \u0026#34;dongfanger\u0026#34;.endswith(\u0026#34;er\u0026#34;) @allure.title(\u0026#34;正则匹配\u0026#34;) def test_assert_regex_match(): import re assert re.findall(r\u0026#34;don.*er\u0026#34;, \u0026#34;dongfanger\u0026#34;) @allure.title(\u0026#34;包含\u0026#34;) def test_assert_contains(): assert \u0026#34;fang\u0026#34; in \u0026#34;dongfanger\u0026#34; assert 2 in [2, 3] assert \u0026#34;x\u0026#34; in {\u0026#34;x\u0026#34;: \u0026#34;y\u0026#34;}.keys() @allure.title(\u0026#34;类型匹配\u0026#34;) def test_assert_type_match(): assert isinstance(1, int) assert isinstance(0.2, float) assert isinstance(True, bool) assert isinstance(3e+26j, complex) assert isinstance(\u0026#34;hi\u0026#34;, str) assert isinstance([1, 2], list) assert isinstance((1, 2), tuple) assert isinstance({\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;}, set) assert isinstance({\u0026#34;x\u0026#34;: 1}, dict) 测试报告 allure下载地址： https://github.com/allure-framework/allure2/releases\n解压后将bin目录添加到系统环境变量Path。\n在pytest命令行添加参数--tep-reports就能一键生成Allure测试报告，并且会把请求入参和响应出参，记录在测试报告中。\npytest --tep-reports 若想在资源管理器中打开，需要执行命令allure open 报告所在文件夹名才能正常打开。\n用例执行 串行 使用pytest命令即可。\n并行 使用pytest -n auto，由pytest-xdist提供支持。\n特色功能 fixtures自动导入 不是必须在conftest.py里面定义fixture。只要在fixtures目录下，创建以fixture_开头的文件，fixture都会自动加载到pytest中，方便管理维护。\n全局执行一次登录 预置了fixtures/fixture_login.py登录接口，且全局仅执行一次，解决token复用问题：\nimport pytest from loguru import logger from utils.http_client import request @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login(tep_context_manager, env_vars): \u0026#34;\u0026#34;\u0026#34; tep_context_manager是为了兼容pytest-xdist分布式执行的上下文管理器 该login只会在整个运行期间执行一次 \u0026#34;\u0026#34;\u0026#34; def produce_expensive_data(variable): logger.info(\u0026#34;----------------开始登录----------------\u0026#34;) response = request( \u0026#34;post\u0026#34;, url=variable[\u0026#34;domain\u0026#34;] + \u0026#34;/login\u0026#34;, headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;} ) assert response.status_code \u0026lt; 400 logger.info(\u0026#34;----------------登录成功----------------\u0026#34;) return response.json() return tep_context_manager(produce_expensive_data, env_vars) 即便在xdist分布式场景下，也只会执行一次登录。\n工具包 cache.py，提供缓存。\nclass TepCache: \u0026#34;\u0026#34;\u0026#34; 提供缓存服务，包括全局变量、环境变量、用例变量 \u0026#34;\u0026#34;\u0026#34; def __init__(self, global_vars=None, env_vars=None, case_vars=None): self.global_vars = global_vars self.env_vars = env_vars self.case_vars = case_vars dao.py，目前支持访问MySQL，需要安装pymysql、sqlalchemy\b和pandas。\n#!/usr/bin/python ## encoding=utf-8 \u0026#34;\u0026#34;\u0026#34; @Author : dongfanger @Date : 9/2/2020 11:32 AM @Desc : 访问数据库 \u0026#34;\u0026#34;\u0026#34; from loguru import logger try: from sqlalchemy import create_engine from texttable import Texttable except ModuleNotFoundError: pass def mysql_engine(host, port, user, password, db): try: engine = create_engine(f\u0026#34;mysql+pymysql://{user}:{password}@{host}:{port}/{db}\u0026#34;) except NameError: return \u0026#34;\u0026#34; return engine def print_db_table(data_frame): \u0026#34;\u0026#34;\u0026#34;以表格形式打印数据表\u0026#34;\u0026#34;\u0026#34; tb = Texttable() tb.header(data_frame.columns.array) tb.set_max_width(0) # text * cols tb.set_cols_dtype([\u0026#39;t\u0026#39;] * data_frame.shape[1]) tb.add_rows(data_frame.to_numpy(), header=False) logger.info(tb.draw()) fastapi_mock.py，示例应用。\n#!/usr/bin/python ## encoding=utf-8 import uvicorn from fastapi import FastAPI, Request app = FastAPI() @app.post(\u0026#34;/login\u0026#34;) async def login(req: Request): body = await req.json() if body[\u0026#34;username\u0026#34;] == \u0026#34;dongfanger\u0026#34; and body[\u0026#34;password\u0026#34;] == \u0026#34;123456\u0026#34;: return {\u0026#34;token\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} return \u0026#34;\u0026#34; @app.get(\u0026#34;/searchSku\u0026#34;) def search_sku(req: Request): if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and req.query_params.get(\u0026#34;skuName\u0026#34;) == \u0026#34;电子书\u0026#34;: return {\u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;2.3\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/addCart\u0026#34;) async def add_cart(req: Request): body = await req.json() if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;skuId\u0026#34;] == \u0026#34;222\u0026#34;: return {\u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;2.3\u0026#34;, \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34;, \u0026#34;totalPrice\u0026#34;: \u0026#34;6.9\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/order\u0026#34;) async def order(req: Request): body = await req.json() if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;skuId\u0026#34;] == \u0026#34;222\u0026#34;: return {\u0026#34;orderId\u0026#34;: \u0026#34;333\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/pay\u0026#34;) async def pay(req: Request): body = await req.json() if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;orderId\u0026#34;] == \u0026#34;333\u0026#34;: return {\u0026#34;success\u0026#34;: \u0026#34;true\u0026#34;} return \u0026#34;\u0026#34; if __name__ == \u0026#39;__main__\u0026#39;: uvicorn.run(\u0026#34;fastapi_mock:app\u0026#34;, host=\u0026#34;127.0.0.1\u0026#34;, port=5000) func.py，常用函数，比如pairwise自动生成用例等。\n#!/usr/bin/python ## encoding=utf-8 \u0026#34;\u0026#34;\u0026#34; @Author : dongfanger @Date : 7/24/2020 5:41 PM @Desc : tep函数库 \u0026#34;\u0026#34;\u0026#34; import copy import inspect import itertools import json import os import time from sys import stdout import yaml from loguru import logger from utils.project import Project def current_time(): \u0026#34;\u0026#34;\u0026#34; 当前时间，年-月-日 时-分-秒 :return: \u0026#34;\u0026#34;\u0026#34; return time.strftime(\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;, time.localtime(time.time())) def current_date(): \u0026#34;\u0026#34;\u0026#34; 当前日期 年-月-日 :return: \u0026#34;\u0026#34;\u0026#34; return time.strftime(\u0026#34;%Y-%m-%d\u0026#34;, time.localtime(time.time())) def print_progress_bar(i): \u0026#34;\u0026#34;\u0026#34; 进度条 \u0026#34;\u0026#34;\u0026#34; c = int(i / 10) progress = \u0026#39;\\r %2d%% [%s%s]\u0026#39; a = \u0026#39;■\u0026#39; * c b = \u0026#39;□\u0026#39; * (10 - c) msg = progress % (i, a, b) stdout.write(msg) stdout.flush() def case_pairwise(option): \u0026#34;\u0026#34;\u0026#34; pairwise算法 \u0026#34;\u0026#34;\u0026#34; cp = [] # 笛卡尔积 s = [] # 两两拆分 for x in eval(\u0026#39;itertools.product\u0026#39; + str(tuple(option))): cp.append(x) s.append([i for i in itertools.combinations(x, 2)]) logger.info(\u0026#39;笛卡尔积:%s\u0026#39; % len(cp)) del_row = [] print_progress_bar(0) s2 = copy.deepcopy(s) for i in range(len(s)): # 对每行用例进行匹配 if (i % 100) == 0 or i == len(s) - 1: print_progress_bar(int(100 * i / (len(s) - 1))) t = 0 for j in range(len(s[i])): # 对每行用例的两两拆分进行判断，是否出现在其他行 flag = False for i2 in [x for x in range(len(s2)) if s2[x] != s[i]]: # 找同一列 if s[i][j] == s2[i2][j]: t = t + 1 flag = True break if not flag: # 同一列没找到，不用找剩余列了 break if t == len(s[i]): del_row.append(i) s2.remove(s[i]) res = [cp[i] for i in range(len(cp)) if i not in del_row] logger.info(\u0026#39;过滤后:%s\u0026#39; % len(res)) return res def load_yaml(path: str) -\u0026gt; dict: with open(path, encoding=\u0026#34;utf8\u0026#34;) as f: return yaml.load(f.read(), Loader=yaml.FullLoader) def jwt_headers(token): \u0026#34;\u0026#34;\u0026#34; jwt请求头 \u0026#34;\u0026#34;\u0026#34; return {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;authorization\u0026#34;: f\u0026#34;Bearer {token}\u0026#34;} def data(first_node: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34; 读用例同名的yaml文件 取首节点的值 \u0026#34;\u0026#34;\u0026#34; caller = inspect.stack()[1] case_path = os.path.dirname(caller.filename) basename = os.path.basename(caller.filename) data_path_yml = os.path.join(case_path, basename.rstrip(\u0026#34;.py\u0026#34;) + \u0026#34;.yml\u0026#34;) data_path_yaml = os.path.join(case_path, basename.rstrip(\u0026#34;.py\u0026#34;) + \u0026#34;.yaml\u0026#34;) node_value = {} if not os.path.exists(data_path_yml) and not os.path.exists(data_path_yaml): logger.error(\u0026#34;数据文件不存在\u0026#34;) return node_value data_path = data_path_yml if os.path.exists(data_path_yml) else data_path_yaml try: return load_yaml(data_path)[first_node] except KeyError: logger.error(f\u0026#34;数据文件{data_path}不存在首节点{first_node}\u0026#34;) http_client.py，requests库的猴子补丁，可自定义。\n#!/usr/bin/python ## encoding=utf-8 import decimal import json import time import allure import jsonpath import requests import urllib3 from loguru import logger from requests import Response urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def request(method, url, **kwargs): template = \u0026#34;\u0026#34;\u0026#34;\\n Request URL: {} Request Method: {} Request Headers: {} Request Payload: {} Status Code: {} Response: {} Elapsed: {} \u0026#34;\u0026#34;\u0026#34; start = time.process_time() response = requests.request(method, url, **kwargs) # requests.request原生用法 end = time.process_time() elapsed = str(decimal.Decimal(\u0026#34;%.3f\u0026#34; % float(end - start))) + \u0026#34;s\u0026#34; headers = kwargs.get(\u0026#34;headers\u0026#34;, {}) kwargs.pop(\u0026#34;headers\u0026#34;) payload = kwargs log = template.format(url, method, json.dumps(headers), json.dumps(payload), response.status_code, response.text, elapsed) logger.info(log) allure.attach(log, f\u0026#39;request \u0026amp; response\u0026#39;, allure.attachment_type.TEXT) return TepResponse(response) class TepResponse(Response): \u0026#34;\u0026#34;\u0026#34; 二次封装requests.Response，添加额外方法 \u0026#34;\u0026#34;\u0026#34; def __init__(self, response): super().__init__() for k, v in response.__dict__.items(): self.__dict__[k] = v def jsonpath(self, expr): \u0026#34;\u0026#34;\u0026#34; 此处强制取第一个值，便于简单取值 如果复杂取值，建议直接jsonpath原生用法 \u0026#34;\u0026#34;\u0026#34; return jsonpath.jsonpath(self.json(), expr)[0] mitm.py，流量录制，做的不是很好，将就看看。\n#!/usr/bin/python ## encoding=utf-8 ## mitmproxy录制流量自动生成用例 import os import time from mitmproxy import ctx project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) tests_dir = os.path.join(project_dir, \u0026#34;tests\u0026#34;) ## tests/mitm mitm_dir = os.path.join(tests_dir, \u0026#34;mitm\u0026#34;) if not os.path.exists(mitm_dir): os.mkdir(mitm_dir) ## 当前时间作为文件名 filename = f\u0026#39;test_{time.strftime(\u0026#34;%Y%m%d_%H%M%S\u0026#34;, time.localtime())}.py\u0026#39; case_file = os.path.join(mitm_dir, filename) ## 生成用例文件 template = \u0026#34;\u0026#34;\u0026#34;import allure from utils.http_client import request @allure.title(\u0026#34;\u0026#34;) def test(env_vars): \u0026#34;\u0026#34;\u0026#34; if not os.path.exists(case_file): with open(case_file, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf8\u0026#34;) as fw: fw.write(template) class Record: def __init__(self, domains): self.domains = domains def response(self, flow): if self.match(flow.request.url): # method method = flow.request.method.lower() ctx.log.error(method) # url url = flow.request.url ctx.log.error(url) # headers headers = dict(flow.request.headers) ctx.log.error(headers) # body body = flow.request.text or {} ctx.log.error(body) with open(case_file, \u0026#34;a\u0026#34;, encoding=\u0026#34;utf8\u0026#34;) as fa: fa.write(self.step(method, url, headers, body)) def match(self, url): if not self.domains: ctx.log.error(\u0026#34;必须配置过滤域名\u0026#34;) exit(-1) for domain in self.domains: if domain in url: return True return False def step(self, method, url, headers, body): if method == \u0026#34;get\u0026#34;: body_grammar = f\u0026#34;params={body}\u0026#34; else: body_grammar = f\u0026#34;json={body}\u0026#34; return f\u0026#34;\u0026#34;\u0026#34; # 描述 # 数据 # 请求 response = request( \u0026#34;{method}\u0026#34;, url=\u0026#34;{url}\u0026#34;, headers={headers}, {body_grammar} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 \u0026#34;\u0026#34;\u0026#34; ## ==================================配置开始================================== addons = [ Record( # 过滤域名 [ \u0026#34;http://www.httpbin.org\u0026#34;, \u0026#34;http://127.0.0.1:5000\u0026#34; ], ) ] ## ==================================配置结束================================== \u0026#34;\u0026#34;\u0026#34; ==================================命令说明开始================================== ## 正向代理（需要手动打开代理） mitmdump -s mitm.py ## 反向代理 mitmdump -s mitm.py --mode reverse:http://127.0.0.1:5000 --listen-host 127.0.0.1 --listen-port 8000 ==================================命令说明结束================================== \u0026#34;\u0026#34;\u0026#34; project.py，项目基本信息，比如根目录路径。\nimport os class Project: root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) data_dir = os.path.join(root_dir, \u0026#34;data\u0026#34;) step.py，测试步骤的泛化调用。\nfrom loguru import logger from utils.cache import TepCache class Step: \u0026#34;\u0026#34;\u0026#34; 测试步骤，泛化调用 \u0026#34;\u0026#34;\u0026#34; def __init__(self, name: str, action, cache: TepCache): logger.info(\u0026#34;----------------\u0026#34; + name + \u0026#34;----------------\u0026#34;) action(cache) 关于tep的更多技术细节，请在源码中一探究竟吧。也可以添加微信cekaigang，随时与我联系。\n结束与开始 tep小工具发布了1.0.0正式版，我也将不再对其进行维护。对Pytest做接口测试自动化的探索远没有结束，我将从teprunner2.0测试平台开始，继续研究Pytest框架的自动化落地实践，那些对测试技术的热情，终将使我们再次相遇。\n源码Gitee：\nhttps://gitee.com/dongfanger/teprunner-backend\nhttps://gitee.com/dongfanger/teprunner-frontend\n源码GitHub：\nhttps://github.com/dongfanger/teprunner-backend\nhttps://github.com/dongfanger/teprunner-frontend\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000017-tep1.0.0%E6%95%99%E7%A8%8B/","summary":"正式版教程： \u003ca href=\"https://dongfanger.gitee.io/blog/tep.html\"\u003ehttps://dongfanger.gitee.io/blog/tep.html\u003c/a\u003e","title":"tep1.0.0教程"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nStoring Data The Collections Framework is the most widely used API of the JDK.\n集合不是数据类型，它是JDK的API，可以用来存储数据等，相当于数据结构。\nthe Collections Framework is a set of interfaces that models different way of storing data in different types of containers. Then the Framework provides at least one implementation for each interface.\nThere are two main categories of interfaces in the Collections Framework: collections and maps.（所以我猜想Python也是基于这样的考虑，设计了List和Dict，并作为Python基本数据类型，实际编码中要用到的也主要是这两类）\nThat makes two main categories, Collection and Map, a subcategory, Queue, and a side category, Iterator.\na collection is an object，but an array is not an object in Java.\nCollection Hierarchy The Iterable Interface is the super interface of the Collection interface, and thus of all the interfaces of this hierarchy. An object that implements Iterable is an object that you can iterate over.\nthe Collection interface also models different ways of accessing its elements:\nyou can iterate over the elements of a collection, through the use of an iterator; you can create a stream on these elements, that can be parallel. List是有序列表：\nThe difference between a List of elements and a Collection of elements, is that a List remembers in what order its elements have been added. If you iterate over the elements of a list, the first element you will get is the first that has been added.\nYou do not have this guarantee with a plain Collection nor for a Set.\nList跟Collection的区别是，增加了index。\nSet跟Collection的区别是，不允许重复。\nSet是无序的，SortedSet是排序过的，SortedSet的排序是指从小到大排列，跟List的有序不一样，List有序是指的先添加排前面，不一定是最小的。\nStoring Elements Collection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); System.out.println(\u0026#34;strings = \u0026#34; + strings); strings.remove(\u0026#34;one\u0026#34;); System.out.println(\u0026#34;strings = \u0026#34; + strings); containsAll(): defines the inclusion\nCollection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); strings.add(\u0026#34;three\u0026#34;); Collection\u0026lt;String\u0026gt; first = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); Collection\u0026lt;String\u0026gt; second = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;four\u0026#34;); System.out.println(\u0026#34;Is first contained in strings? \u0026#34; + strings.containsAll(first)); System.out.println(\u0026#34;Is second contained in strings? \u0026#34; + strings.containsAll(second)); addAll(): defines the union 并集\nGetting a true value does not mean that all the elements of the other collection have been added; it means that at least one has been added.\nCollection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); strings.add(\u0026#34;three\u0026#34;); Collection\u0026lt;String\u0026gt; first = new ArrayList\u0026lt;\u0026gt;(); first.add(\u0026#34;one\u0026#34;); first.add(\u0026#34;four\u0026#34;); boolean hasChanged = strings.addAll(first); System.out.println(\u0026#34;Has strings changed? \u0026#34; + hasChanged); System.out.println(\u0026#34;strings = \u0026#34; + strings); removeAll(): defines the complement\nCollection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); strings.add(\u0026#34;three\u0026#34;); Collection\u0026lt;String\u0026gt; toBeRemoved = new ArrayList\u0026lt;\u0026gt;(); toBeRemoved.add(\u0026#34;one\u0026#34;); toBeRemoved.add(\u0026#34;four\u0026#34;); boolean hasChanged = strings.removeAll(toBeRemoved); System.out.println(\u0026#34;Has strings changed? \u0026#34; + hasChanged); System.out.println(\u0026#34;strings = \u0026#34; + strings); retainAll(): defines the intersection 交集\nCollection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); strings.add(\u0026#34;three\u0026#34;); Collection\u0026lt;String\u0026gt; toBeRetained = new ArrayList\u0026lt;\u0026gt;(); toBeRetained.add(\u0026#34;one\u0026#34;); toBeRetained.add(\u0026#34;four\u0026#34;); boolean hasChanged = strings.retainAll(toBeRetained); System.out.println(\u0026#34;Has strings changed? \u0026#34; + hasChanged); System.out.println(\u0026#34;strings = \u0026#34; + strings); 注意上面的并集和交集，Collection本来就是集合，所以能够求并集和交集是理所当然的。\nisEmpty()、clear()\nCollection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); if (!strings.isEmpty()) { System.out.println(\u0026#34;Indeed strings is not empty!\u0026#34;); } System.out.println(\u0026#34;The number of elements in strings is \u0026#34; + strings.size()); Collection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); System.out.println(\u0026#34;The number of elements in strings is \u0026#34; + strings.size()); strings.clear(); System.out.println(\u0026#34;After clearing it, this number is now \u0026#34; + strings.size()); size()，Collection相当于容器，用size。而array和String，相当于序列，用length。\ntoArray\n将Collection转为array：\n①无入参：\nCollection\u0026lt;String\u0026gt; strings = ...; // suppose you have 15 elements in that collection String[] tabString1 = strings.toArray(new String[] {}); // you can pass an empty array String[] tabString2 = strings.toArray(new String[15]); // or an array of the right size ②传参\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;); String[] largerTab = {\u0026#34;three\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;I\u0026#34;, \u0026#34;was\u0026#34;, \u0026#34;there\u0026#34;}; System.out.println(\u0026#34;largerTab = \u0026#34; + Arrays.toString(largerTab)); String[] result = strings.toArray(largerTab); System.out.println(\u0026#34;result = \u0026#34; + Arrays.toString(result)); System.out.println(\u0026#34;Same arrays? \u0026#34; + (result == largerTab)); Collection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;); String[] zeroLengthTab = {}; String[] result = strings.toArray(zeroLengthTab); System.out.println(\u0026#34;zeroLengthTab = \u0026#34; + Arrays.toString(zeroLengthTab)); System.out.println(\u0026#34;result = \u0026#34; + Arrays.toString(result)); ③简写\nCollection\u0026lt;String\u0026gt; strings = ...; String[] tabString3 = strings.toArray(String[]::new); Predicate + removeIf 实现有条件的删除，比如删除null和empty的元素：\nPredicate\u0026lt;String\u0026gt; isNull = Objects::isNull; Predicate\u0026lt;String\u0026gt; isEmpty = String::isEmpty; Predicate\u0026lt;String\u0026gt; isNullOrEmpty = isNull.or(isEmpty); Collection\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(); strings.add(null); strings.add(\u0026#34;\u0026#34;); strings.add(\u0026#34;one\u0026#34;); strings.add(\u0026#34;two\u0026#34;); strings.add(\u0026#34;\u0026#34;); strings.add(\u0026#34;three\u0026#34;); strings.add(null); System.out.println(\u0026#34;strings = \u0026#34; + strings); strings.removeIf(isNullOrEmpty); System.out.println(\u0026#34;filtered strings = \u0026#34; + strings); Iterating for-each\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;); for (String element: strings) { System.out.println(string); } Iterator\nCollection\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;); for (Iterator\u0026lt;String\u0026gt; iterator = strings.iterator(); iterator.hasNext();) { String element = iterator.next(); if (element.length() == 3) { System.out.println(element); } } List the List interface has 2: ArrayList and LinkedList. As you may guess, the first one is built on an internal array, and the second on a doubly-linked list.\nIterating over the elements of an ArrayList is much faster that over the elements of a LinkedList. There are still cases where a linked list is faster than an array. A doubly-linked list can access its first and last element faster than an ArrayList can. This is the main use case that makes LinkedList better than ArrayList. So if your application needs a Last In, First Out (LIFO, covered later in this tutorial) stack, or a First In, First Out (FIFO, also covered later) waiting queue, then choosing a linked list is probably your best choice.\n注意，链表在插入和删除的速度优势已经不在，因为现代硬件、CPU缓存和指针追踪已经很强大。\nindex\nadd(index, element): inserts the given object at the index, adjusting the index if there are remaining elements get(index): returns the object at the given index set(index, element): replaces the element at the given index with the new element remove(index): removes the element at the given index, adjusting the index of the remaining elements. The methods indexOf(element) and lastIndexOf(element) return the index of the given element in the list, or -1 if the element is not found.\nsubList：\nList\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(List.of(\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;, \u0026#34;5\u0026#34;)); System.out.println(strings); strings.subList(2, 5).clear(); System.out.println(strings); addAll(int index, Collection collection)\nListIterator\nThe ListIterator interface extends the regular Iterator that you already know. It adds several methods to it.\nhasPrevious() and previous(): to iterate in the descending order rather than the ascending order nextIndex() and previousIndex(): to get the index of the element that will be returned by the next next() call, or the next previous() call set(element): to update the last element returned by next() or previous(). If neither of these methods have been called on this iterator then an IllegalStateException is raised. List\u0026lt;String\u0026gt; numbers = Arrays.asList(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;); for (ListIterator\u0026lt;String\u0026gt; iterator = numbers.listIterator(); iterator.hasNext();) { String nextElement = iterator.next(); if (Objects.equals(nextElement, \u0026#34;two\u0026#34;)) { iterator.set(\u0026#34;2\u0026#34;); } } System.out.println(\u0026#34;numbers = \u0026#34; + numbers); Set The Set interface does not bring any new method to the Collection interface. The Collections Framework gives you one plain implementation of the Set interface: HashSet. Internally, a HashSet wraps an instance of HashMap.\nList\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;); Set\u0026lt;String\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); set.addAll(strings); set.forEach(System.out::println); The SortedSet interface adds new methods to Set.\nfirst() and last() returns the lowest and the largest elements of the set headSet(toElement) and tailSet(fromElement) returns you subsets containing the elements lower than toElement or greater than fromElement subSet(fromElement, toElement) gives you a subset of the element between fromElement and toElement. SortedSet\u0026lt;String\u0026gt; strings = new TreeSet\u0026lt;\u0026gt;(Set.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;f\u0026#34;)); SortedSet\u0026lt;String\u0026gt; subSet = strings.subSet(\u0026#34;aa\u0026#34;, \u0026#34;d\u0026#34;); System.out.println(\u0026#34;sub set = \u0026#34; + subSet); 注意，subSet仅仅相当于视图。No copy is made, meaning that any change you make to these subsets will be reflected in the set, and the other way round.\nNavigableSet\nSome methods are overloaded by NavigableSet. headSet(), headSet(), and headSet() may take a further boolean arguments to specify whether the limits (toElement or fromElement) are to be included in the resulting subset. Other methods have been added.\nceiling(element), and floor(element) return the greatest element lesser or equal than, or the lowest element greater or equal than the provided element. If there is no such element then null is returned floor(element), and higher(element) return the greater element lesser than, or the lowest element greater than the provided element. If there is no such element then null is returned. pollFirst(), and pollLast() return and removes the lowest or the greatest element of the set. Furthermore, NavigableSet also allows you to iterate over its elements in descending order. There are two ways to do this.\nYou can call descendingIterator(): it gives you a regular Iterator that traverses the set in the descending order. You can also call descendingSet(). What you get in return is another NavigableSet that is a view on this set and that makes you think you have the same set, sorted in the reversed order. NavigableSet\u0026lt;String\u0026gt; sortedStrings = new TreeSet\u0026lt;\u0026gt;(Set.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;f\u0026#34;)); System.out.println(\u0026#34;sorted strings = \u0026#34; + sortedStrings); NavigableSet\u0026lt;String\u0026gt; reversedStrings = sortedStrings.descendingSet(); System.out.println(\u0026#34;reversed strings = \u0026#34; + reversedStrings); Factory Methods Java SE 9\nList\u0026lt;String\u0026gt; stringList = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;); Set\u0026lt;String\u0026gt; stringSet = Set.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;); Java SE 10\nCollection\u0026lt;String\u0026gt; strings = Arrays.asList(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;); List\u0026lt;String\u0026gt; list = List.copyOf(strings); Set\u0026lt;String\u0026gt; set = Set.copyOf(strings); Arrays\nThe Collections Framework has a class called Arrays with about 200 methods to handle arrays. Most of them are implementing various algorithms on arrays, like sorting, merging, searching.\nCollections\nThe Collections Framework comes with another factory class: Collections, with a set of method to manipulate collections and their content.\nFinding a Sublist in a List\nTwo methods locate a given sublist in a bigger list: indexOfSublist(List\u0026lt;?\u0026gt; source, List\u0026lt;?\u0026gt; target): returns the first index of the first element of the target list in the source list, or -1 if it does not exist; lastIndexOfSublist(List\u0026lt;?\u0026gt; source, List\u0026lt;?\u0026gt; target): return the last of these indexes. Changing the Order of the Elements of a List\nsort() sorts the list in place. This method may take a comparator as an argument. As usual, if no comparator is provided, then the elements of the list must be comparable. If a comparator is provided, then it will be used to compare the elements. Starting with Java SE 8, you should favor the sort() method from the Listinterface. shuffle() randomly shuffles the elements of the provided list. You can provide yout instance of Random if you need a random shuffling that you can repeat. rotate() rotates the elements of the list. After a rotation the element at index 0 will be found at index 1 and so on. The last elements will be moved to the first place of the list. You can combine subList() and rotate() to remove an element at a given index and to insert it in another place in the list. reverse(): reverse the order of the elements of the list. swap(): swaps two elements from the list. This method can take a list as an argument, as well as a plain array. Stacks and Queues Stacks are also called LIFO stacks, where LIFO stands for Last In, First Out. Queues are known as FIFO: First In First Out. These structures are very simple and gives you three main operations. push(element): adds an element to the queue, or the stack pop(): removes an element from the stack, that is, the youngest element added poll(): removes an element from the queue, that is, the oldest element added peek(): allows you to see the element you will get with a pop() or a poll(), but without removing it from the queue of the stack. Queues and Stacks\nthe Queue interface models a queue; the Deque interface models a double ended queue (thus the name). You can push, pop, poll and peek elements on both the tail and the head of a Deque, making it both a queue and a stack. Collection没有Stack接口，栈是通过Deque来定义的。\nImplementing Queue and Deque\nArrayDeque: which implements both. This implementation is backed by an array. The capacity of this class automatically grows as elements are added. So this implementation always accepts new elements. LinkedList: which also implements both. This implementation is backed by a linked list, making the access to its first and last element very efficient. A LinkedList will always accept new elements. PriorityQueue: that only implements Queue. This queue is backed by an array that keeps its elements sorted by their natural order or by an order specified by a Comparator. The head of this queue is always the least element of the queue with respect to the specified ordering. The capacity of this class automatically grows as elements are added. Maps implementations：\nHashMap\nLinkedHashMap is a HashMap with an internal structure to keep the key-value pairs ordered. Iterating on the keys or the key-value pairs will follow the order in which you have added your key-value pairs.\n这里注意，HashMap是无序的，LinkedHashMap是有序的。\nIdentityHashMap is a specialized Map that you should only be used in very precise cases. This implementation is not meant to be generally used in application. Instead of using equals() and hashCode() to compare the key objects, this implementation only compares the references to these keys, with an equality operator (==). Use it with caution, only if you are sure this is what you need.\nJava SE 9\nMap\u0026lt;Integer, String\u0026gt; map = Map.of( 1, \u0026#34;one\u0026#34;, 2, \u0026#34;two\u0026#34;, 3, \u0026#34;three\u0026#34; ); The Map defines a member interface: Map.Entry to model a key-value pair. This interface defines three methods to access the key and the values:\ngetKey(): to read the key; getValue() and setValue(value): to read and update the value bound to that key. putIfAbsent()，如果是null，会替换为默认值：\nfor (String key : map.keySet()) { map.putIfAbsent(key, -1); } 如果value是null，可能会报错，比如：\nMap\u0026lt;String, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;one\u0026#34;, 1); map.put(\u0026#34;two\u0026#34;, null); map.put(\u0026#34;three\u0026#34;, 3); map.put(\u0026#34;four\u0026#34;, null); map.put(\u0026#34;five\u0026#34;, 5); for (int value : map.values()) { // 这里是int System.out.println(\u0026#34;value = \u0026#34; + value); // Integer拆包为int时会报NPE } getOrDefault()，如果没有key，返回默认值：\nMap\u0026lt;Integer, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(1, \u0026#34;one\u0026#34;); map.put(2, \u0026#34;two\u0026#34;); map.put(3, \u0026#34;three\u0026#34;); List\u0026lt;String\u0026gt; values = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; 5; i++) { values.add(map.getOrDefault(key,\u0026#34;UNDEFINED\u0026#34;)); } System.out.println(\u0026#34;values = \u0026#34; + values); 流式写法：\nList\u0026lt;String\u0026gt; values = IntStream.range(0, 5) .mapToObj(key -\u0026gt; map.getOrDefault(key, \u0026#34;UNDEFINED\u0026#34;)) .collect(Collectors.toList()); System.out.println(\u0026#34;values = \u0026#34; + values); remove(key)，remove后返回value，可能为null。\nremove(key, value)，remove时先判断value存在才移除，返回boolean，true if the key/value pair was removed from the map。\ncontainsKey(key) and containsValue(value) Both methods return true if the map contains the given key or value.\nputAll(otherMap) If some keys are present in both maps, then the values of otherMap will erase those of this map.（并集）\nkeySet(): returns an instance of Set, containing the keys defined in the map entrySet(): returns an instance of Set\u0026lt;Map.Entry\u0026gt;, containing the key/value pairs contained in the map values(): returns an instance of Collection, containing the values present in the map. 遍历推荐使用以下方式：\nfor (Map.Entry\u0026lt;Integer, String\u0026gt; entry : map.entrySet()) { System.out.println(\u0026#34;entry = \u0026#34; + entry); } Lambda Expressions\nMap\u0026lt;Integer, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(1, \u0026#34;one\u0026#34;); map.put(2, \u0026#34;two\u0026#34;); map.put(3, \u0026#34;three\u0026#34;); map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); Map\u0026lt;Integer, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(1, \u0026#34;one\u0026#34;); map.put(2, \u0026#34;two\u0026#34;); map.put(3, \u0026#34;three\u0026#34;); map.replaceAll((key, value) -\u0026gt; value.toUpperCase()); map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); compute\nThe put() methods return the previous value, whereas the compute() methods return the new value.\nList\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;); Map\u0026lt;Integer, List\u0026lt;String\u0026gt;\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); for (String word: strings) { int length = word.length(); if (!map.containsKey(length)) { map.put(length, new ArrayList\u0026lt;\u0026gt;()); } map.get(length).add(word); } map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); 使用putIfAbsent优化：\nfor (String word: strings) { int length = word.length(); map.putIfAbsent(length, new ArrayList\u0026lt;\u0026gt;()); map.get(length).add(word); } 使用computeIfAbsent优化：\nfor (String word: strings) { int length = word.length(); map.computeIfAbsent(length, key -\u0026gt; new ArrayList\u0026lt;\u0026gt;()) .add(word); } merge\nList\u0026lt;String\u0026gt; strings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;); Map\u0026lt;Integer, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); for (String word: strings) { int length = word.length(); map.merge(length, word, (existingValue, newWord) -\u0026gt; existingValue + \u0026#34;, \u0026#34; + newWord); } map.forEach((key, value) -\u0026gt; System.out.println(key + \u0026#34; :: \u0026#34; + value)); SortedMap and NavigableMap\nSortedMap\u0026lt;Integer, String\u0026gt; map = new TreeMap\u0026lt;\u0026gt;(); map.put(1, \u0026#34;one\u0026#34;); map.put(2, \u0026#34;two\u0026#34;); map.put(3, \u0026#34;three\u0026#34;); map.put(5, \u0026#34;five\u0026#34;); map.put(6, \u0026#34;six\u0026#34;); SortedMap\u0026lt;Integer, String\u0026gt; headMap = map.headMap(3); headMap.put(0, \u0026#34;zero\u0026#34;); // this line is ok headMap.put(4, \u0026#34;four\u0026#34;); // this line throws an IllegalArgumentException Here is the code of the add(element) of the HashSet class:\nprivate transient HashMap\u0026lt;E,Object\u0026gt; map; private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; } What you can see is that in fact, a hashset stores your object in a hashmap (the transient keyword is not relevant). Your objects are the keys of this hashmap, and the value is just a placeholder, an object with no significance.\nhashset 是用hashmap来存的，所以最好不要更新hashset的值（也就是hashmap的key），否则会有意想不到的Bug。\n参考资料：\nThe Collections Framework https://dev.java/learn/api/collections-framework/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000016-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B013%E9%9B%86%E5%90%88/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"storing-data\"\u003eStoring Data\u003c/h2\u003e\n\u003cp\u003eThe Collections Framework is the most widely used API of the JDK.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e集合不是数据类型，它是JDK的API，可以用来存储数据等，相当于数据结构。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003ethe Collections Framework is a set of interfaces that models different way of storing data in different types of containers. Then the Framework provides at least one implementation for each interface.\u003c/p\u003e\n\u003cp\u003eThere are two main categories of interfaces in the Collections Framework: collections and maps.（所以我猜想Python也是基于这样的考虑，设计了List和Dict，并作为Python基本数据类型，实际编码中要用到的也主要是这两类）\u003c/p\u003e\n\u003cp\u003eThat makes two main categories, \u003ca href=\"https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/Collection.html\"\u003e\u003ccode\u003eCollection\u003c/code\u003e\u003c/a\u003e and \u003ca href=\"https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/Map.html\"\u003e\u003ccode\u003eMap\u003c/code\u003e\u003c/a\u003e, a subcategory, \u003ca href=\"https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/Queue.html\"\u003e\u003ccode\u003eQueue\u003c/code\u003e\u003c/a\u003e, and a side category, \u003ca href=\"https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/Iterator.html\"\u003e\u003ccode\u003eIterator\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e","title":"Java官方笔记13集合"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nLocust是一款Python技术栈的开源的性能测试工具。Locust直译为蝗虫，寓意着它能产生蝗虫般成千上万的并发用户：\nLocust并不小众，从它Github的Star数量就可见一斑：\n截止文章写作时，一共15951Star。\nLocust生态良好，它已在多家外企（包括世界500强）投入使用：\n如此看来，Locust是非常值得学习和掌握的一款工具。\nPython的魔力在于化繁为简，基于Python的Locust也能给仍然困惑于性能测试的我们带来启发。\nLocust特点 以纯Python方式编写用户脚本，提供极大自由度。 用户脚本可以串行方式编写，Locust会通过轻量级进程/协程产生并发，无需自己做并发编程。 并发量大，借助于gevent库，Locust能产生成千上万并发请求。 开销小，Locust用户运行时开销很小。 良好的Web UI对性能结果实时监测。 能测任何系统任何协议，只需要写个client即可。 开放REST API，尽情发挥。 安装Locust 需要Python版本3.6及以上。\n执行pip命令：\n$ pip install locust 验证安装成功：\n$ locust -V 安装时会一并安装依赖库：\nInstalling collected packages: Werkzeug, pywin32, zope.event, greenlet, gevent, geventhttpclient, itsdangerous, flask, Flask-BasicAuth, ConfigArgParse, pyzmq, psutil, locust\n能看出来flask为Locust提供了Web功能。\n快速上手 使用Locust一般按照以下步骤进行：\n编写Python用户脚本。 使用locust命令执行性能测试。 （可选）通过Web界面监测结果。 示例代码如下，新建locustfile.py文件：\nimport time from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(1, 2.5) @task def hello_world(self): self.client.get(\u0026#34;/hello\u0026#34;) self.client.get(\u0026#34;/world\u0026#34;) @task(3) def view_items(self): for item_id in range(10): self.client.get(f\u0026#34;/item?id={item_id}\u0026#34;, name=\u0026#34;/item\u0026#34;) time.sleep(1) def on_start(self): self.client.post(\u0026#34;/login\u0026#34;, json={\u0026#34;username\u0026#34;:\u0026#34;foo\u0026#34;, \u0026#34;password\u0026#34;:\u0026#34;bar\u0026#34;}) 路径切换到locustfile.py文件所在目录，执行命令：\n$ locust 也可以通过-f指定某个目录文件：\n$ locust -f locust_files/my_locust_file.py 运行后，打开http://127.0.0.1:8089看到Web界面：\n填写信息后，就能开始压测了。Web界面提供了结果统计数据：\n和性能指标走势图：\n脚本解析 示例脚本解析如下：\n## Locust用户脚本就是Python模块 import time from locust import HttpUser, task, between ## 类继承自HttpUser class QuickstartUser(HttpUser): # 每个模拟用户等待1~2.5秒 wait_time = between(1, 2.5) # 被@task装饰的才会并发执行 @task def hello_world(self): # client属性是HttpSession实例，用来发送HTTP请求 self.client.get(\u0026#34;/hello\u0026#34;) self.client.get(\u0026#34;/world\u0026#34;) # 每个类只会有一个task被选中执行 # 3代表weight权重 # 权重越大越容易被选中执行 # view_items比hello_wolrd多3倍概率被选中执行 @task(3) def view_items(self): for item_id in range(10): # name参数作用是把统计结果按同一名称进行分组 # 这里防止URL参数不同会产生10个不同记录不便于观察 # 把10个汇总成1个\u0026#34;/item\u0026#34;记录 self.client.get(f\u0026#34;/item?id={item_id}\u0026#34;, name=\u0026#34;/item\u0026#34;) time.sleep(1) # 每个模拟用户开始运行时都会执行 def on_start(self): self.client.post(\u0026#34;/login\u0026#34;, json={\u0026#34;username\u0026#34;:\u0026#34;foo\u0026#34;, \u0026#34;password\u0026#34;:\u0026#34;bar\u0026#34;}) 小结 本文先了解了Locust的背景和生态，它是值得学习的，对于Python技术栈来说更加如此。接着介绍了使用pip命令安装Locust，其中发现顺带安装了flask，Locust的Web功能是flask提供的。然后给出了一段示例代码，按照步骤上手Locust。最后对示例代码进行了解析，浅尝辄止。locustfile实际上该怎么写呢？\n参考资料：\nhttps://locust.io/\nhttps://docs.locust.io/en/stable/\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000016-locustpython%E6%8A%80%E6%9C%AF%E6%A0%88%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7locust%E5%85%A5%E9%97%A8/","summary":"Locust是一款Python技术栈的开源的性能测试工具。Locust直译为蝗虫，寓意着它能产生蝗虫般成千上万的并发用户：","title":"Python技术栈性能测试工具Locust入门"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n引子 数据写在代码里，追求快速编写用例，是我设计tep的一个特点，这在个人编写时是一种非常良好的体验。但相比于HttpRunner、JMeter等来说，总觉得还差点意思。思考良久，总结为三个字：工程化。工程化是我近一年在学习Java并参与了2个测试平台模块开发，和写了几个小工具后，感受到的一种编程思想。而其中最明显的就是Spring的MVC分层设计。为了让tep更工程化，后续版本将以MVC模块编写用例为准，同时会兼容之前的脚本式快速编写。\n示例 目录结构 测试用例都放在一个文件夹下：\ntest_case：用例主程序；\nsteps：测试步骤；\ndata：纯粹的json；\n测试用例 test_case.py是测试用例，包含的只有测试步骤：\nimport allure from examples.tests.LoginToPayFlow.steps.step_add_cart import step_add_cart from examples.tests.LoginToPayFlow.steps.step_order import step_order from examples.tests.LoginToPayFlow.steps.step_pay import step_pay from examples.tests.LoginToPayFlow.steps.step_search_sku import step_search_sku from utils.cache import TepCache from utils.step import Step \u0026#34;\u0026#34;\u0026#34; 测试登录到下单流程，需要先运行utils/fastapi_mock.py \u0026#34;\u0026#34;\u0026#34; @allure.title(\u0026#34;从登录到下单支付\u0026#34;) def test(login, env_vars, case_vars): case_vars.put(\u0026#34;token\u0026#34;, login[\u0026#34;token\u0026#34;]) cache = TepCache(env_vars=env_vars, case_vars=case_vars) Step(\u0026#34;搜索商品\u0026#34;, step_search_sku, cache) Step(\u0026#34;添加购物车\u0026#34;, step_add_cart, cache) Step(\u0026#34;下单\u0026#34;, step_order, cache) Step(\u0026#34;支付\u0026#34;, step_pay, cache) TepCache是“缓存”，包括global_vars、env_vars、case_vars三个级别的变量池。\nStep是一个泛化调用类，作用是打日志，调用第二个参数对应的步骤函数。\n测试步骤 from utils.cache import TepCache from utils.func import data from utils.http_client import request def step_search_sku(cache: TepCache): url = cache.env_vars[\u0026#34;domain\u0026#34;] + \u0026#34;/searchSku\u0026#34; headers = {\u0026#34;token\u0026#34;: cache.case_vars.get(\u0026#34;token\u0026#34;)} body = data(\u0026#34;查询SKU.json\u0026#34;) response = request(\u0026#34;get\u0026#34;, url=url, headers=headers, params=body) assert response.status_code \u0026lt; 400 cache.case_vars.put(\u0026#34;skuId\u0026#34;, response.jsonpath(\u0026#34;$.skuId\u0026#34;)) cache.case_vars.put(\u0026#34;skuPrice\u0026#34;, response.jsonpath(\u0026#34;$.price\u0026#34;)) from utils.cache import TepCache from utils.func import data from utils.http_client import request def step_add_cart(cache: TepCache): url = cache.env_vars[\u0026#34;domain\u0026#34;] + \u0026#34;/addCart\u0026#34; headers = {\u0026#34;token\u0026#34;: cache.case_vars.get(\u0026#34;token\u0026#34;)} body = data(\u0026#34;添加购物车.json\u0026#34;) body[\u0026#34;skuId\u0026#34;] = cache.case_vars.get(\u0026#34;skuId\u0026#34;) response = request(\u0026#34;post\u0026#34;, url=url, headers=headers, json=body) assert response.status_code \u0026lt; 400 cache.case_vars.put(\u0026#34;skuNum\u0026#34;, response.jsonpath(\u0026#34;$.skuNum\u0026#34;)) cache.case_vars.put(\u0026#34;totalPrice\u0026#34;, response.jsonpath(\u0026#34;$.totalPrice\u0026#34;)) 步骤函数以step开头，尤其注意的是cache借助Python Typing提示，可以在编写下面代码时，获得PyCharm语法提示，所以一定不要忘了加上: TepCache：\n步骤函数里面由基本信息（url、headers、body），数据初始化，请求，断言，数据提取几个部分组织，从上往下顺序编写。\n测试数据 数据代码分离，在MVC分层设计中这点就特别重要，在data目录下存放的不做任何参数化的纯粹json：\n参数化都放在步骤函数里面来写。数据代码分离的好处是，比如现在写的用例是买3件商品，假如你想改成买10件，只改json的数据就可以了，不需要改动任何代码。你可能会想，把这个数字放在代码里，不也是只改个值吗？确实如此，但这不符合MVC分层设计了。\n以SpringMVC作为参照：\ntestcase.py相当于controller，steps相当于service，data相当于pojo，各层只做自己的事，多写点代码，换来的是可读性强、维护性高、层次分明的“工程化资产”。\n日志输出 原理 Step泛化调用：\nfrom loguru import logger from utils.cache import TepCache class Step: \u0026#34;\u0026#34;\u0026#34; 测试步骤，泛化调用 \u0026#34;\u0026#34;\u0026#34; def __init__(self, name: str, action, cache: TepCache): logger.info(\u0026#34;----------------\u0026#34; + name + \u0026#34;----------------\u0026#34;) action(cache) TepCache缓存：\nclass TepCache: \u0026#34;\u0026#34;\u0026#34; 提供缓存服务，包括全局变量、环境变量、用例变量 \u0026#34;\u0026#34;\u0026#34; def __init__(self, global_vars=None, env_vars=None, case_vars=None): self.global_vars = global_vars self.env_vars = env_vars self.case_vars = case_vars fixture实现的变量池：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def global_vars(): \u0026#34;\u0026#34;\u0026#34; 全局变量，读取resources/global_vars.yaml，返回字典 \u0026#34;\u0026#34;\u0026#34; with open(os.path.join(Config.project_root_dir, \u0026#34;resources\u0026#34;, \u0026#34;global_vars.yaml\u0026#34;)) as f: return yaml.load(f.read(), Loader=yaml.FullLoader) @pytest.fixture(scope=\u0026#34;session\u0026#34;) def env_vars(): \u0026#34;\u0026#34;\u0026#34; 环境变量，读取resources/env_vars下的变量模板，返回字典 \u0026#34;\u0026#34;\u0026#34; env_active = tep_config()[\u0026#39;env\u0026#39;][\u0026#34;active\u0026#34;] env_filename = f\u0026#34;env_vars_{env_active}.yaml\u0026#34; with open(os.path.join(Config.project_root_dir, \u0026#34;resources\u0026#34;, \u0026#34;env_vars\u0026#34;, env_filename)) as f: return yaml.load(f.read(), Loader=yaml.FullLoader) @pytest.fixture(scope=\u0026#34;session\u0026#34;) def case_vars(): \u0026#34;\u0026#34;\u0026#34; 测试用例的动态变量，1条测试用例1个实例，彼此隔离 \u0026#34;\u0026#34;\u0026#34; class CaseVars: def __init__(self): self.dict_in_memory = {} def put(self, key, value): self.dict_in_memory[key] = value def get(self, key): value = \u0026#34;\u0026#34; try: value = self.dict_in_memory[key] except KeyError: logger.error(f\u0026#34;获取用例变量的key不存在，返回空串: {key}\u0026#34;) return value return CaseVars() 登录fixture：\nimport pytest from loguru import logger from utils.http_client import request @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login(tep_context_manager, env_vars): \u0026#34;\u0026#34;\u0026#34; tep_context_manager是为了兼容pytest-xdist分布式执行的上下文管理器 该login只会在整个运行期间执行一次 \u0026#34;\u0026#34;\u0026#34; def produce_expensive_data(variable): logger.info(\u0026#34;----------------开始登录----------------\u0026#34;) response = request( \u0026#34;post\u0026#34;, url=variable[\u0026#34;domain\u0026#34;] + \u0026#34;/login\u0026#34;, headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;} ) assert response.status_code \u0026lt; 400 logger.info(\u0026#34;----------------登录成功----------------\u0026#34;) return response.json() return tep_context_manager(produce_expensive_data, env_vars) TepResponse支持response.jsonpath写法：\ndef request(method, url, **kwargs): template = \u0026#34;\u0026#34;\u0026#34;\\n Request URL: {} Request Method: {} Request Headers: {} Request Payload: {} Status Code: {} Response: {} Elapsed: {} \u0026#34;\u0026#34;\u0026#34; start = time.process_time() response = requests.request(method, url, **kwargs) # requests.request原生用法 end = time.process_time() elapsed = str(decimal.Decimal(\u0026#34;%.3f\u0026#34; % float(end - start))) + \u0026#34;s\u0026#34; headers = kwargs.get(\u0026#34;headers\u0026#34;, {}) kwargs.pop(\u0026#34;headers\u0026#34;) payload = kwargs log = template.format(url, method, json.dumps(headers), json.dumps(payload), response.status_code, response.text, elapsed) logger.info(log) allure.attach(log, f\u0026#39;request \u0026amp; response\u0026#39;, allure.attachment_type.TEXT) return TepResponse(response) class TepResponse(Response): \u0026#34;\u0026#34;\u0026#34; 二次封装requests.Response，添加额外方法 \u0026#34;\u0026#34;\u0026#34; def __init__(self, response): super().__init__() for k, v in response.__dict__.items(): self.__dict__[k] = v def jsonpath(self, expr): \u0026#34;\u0026#34;\u0026#34; 此处强制取第一个值，便于简单取值 如果复杂取值，建议直接jsonpath原生用法 \u0026#34;\u0026#34;\u0026#34; return jsonpath.jsonpath(self.json(), expr)[0] 读取数据文件：\ndef data(relative_path: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34; 与steps同层级的data目录+传入的相对路径 \u0026#34;\u0026#34;\u0026#34; caller = inspect.stack()[1] steps_path = os.path.dirname(caller.filename) data_path = os.path.join(os.path.dirname(steps_path), \u0026#34;data\u0026#34;, relative_path) if os.path.exists(data_path): with open(data_path, encoding=\u0026#34;utf8\u0026#34;) as f: return json.load(f) logger.error(\u0026#34;数据文件不存在\u0026#34;) return {} 体验 本次设计的编写方法，跟我公司的测试平台的体验很类似，因为习惯了平台操作，用这种方式写代码竟然出奇的习惯，基本上没有卡点或特别绕的感觉，在PyCharm中也能体验到测试平台的顺畅感。大家也可以试一下。\n第一步，添加用例：\n第二步，添加步骤：\nStep这一行，从左到右顺序录入，步骤名称，步骤函数，cache，特别顺手。\n第三步，添加步骤函数，直接复制这里的函数名，到steps包下面新建文件：\n然后输入函数定义：\n这里一定要记得输入TepCache的Typing提示，以获得PyCharm语法提示：\n接着顺序输入url、headers、body：\n第四步，在data目录下新建数据文件：\n第五步，回到步骤函数，做参数化、请求、断言、数据提取等：\n第六步，再回到测试用例，导入步骤函数：\n其他步骤以此类推。tep后续将以MVC分层设计编写方式为主，老用例仍然会兼容，可以不修改，新用例可以在tep正式发布后，尝试下。\ntep-template加了几个新库可能需要安装下：\npip install jsonpath pip install filelock pip install pytest-xdist 目前代码已经上传到预览版，欢迎加我或进群交流。\n参考资料：\ntep预览版 https://gitee.com/dongfanger/tep-template\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000016-%E5%BD%93pytest%E9%81%87%E4%B8%8Amvc%E5%88%86%E5%B1%82%E8%AE%BE%E8%AE%A1%E8%87%AA%E5%8A%A8%E5%8C%96%E7%94%A8%E4%BE%8B%E5%B0%B1%E8%AF%A5%E8%BF%99%E4%B9%88%E5%86%99/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"引子\"\u003e引子\u003c/h1\u003e\n\u003cp\u003e数据写在代码里，追求快速编写用例，是我设计tep的一个特点，这在个人编写时是一种非常良好的体验。但相比于HttpRunner、JMeter等来说，总觉得还差点意思。思考良久，总结为三个字：\u003cstrong\u003e工程化\u003c/strong\u003e。工程化是我近一年在学习Java并参与了2个测试平台模块开发，和写了几个小工具后，感受到的一种编程思想。而其中最明显的就是Spring的MVC分层设计。为了让tep更工程化，后续版本将以MVC模块编写用例为准，同时会兼容之前的脚本式快速编写。\u003c/p\u003e\n\u003ch1 id=\"示例\"\u003e示例\u003c/h1\u003e\n\u003ch2 id=\"目录结构\"\u003e目录结构\u003c/h2\u003e\n\u003cp\u003e测试用例都放在一个文件夹下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5487a3073adf417c82931654f233f508.png\"\u003e\u003c/p\u003e\n\u003cp\u003etest_case：用例主程序；\u003c/p\u003e\n\u003cp\u003esteps：测试步骤；\u003c/p\u003e\n\u003cp\u003edata：纯粹的json；\u003c/p\u003e\n\u003ch2 id=\"测试用例\"\u003e测试用例\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003etest_case.py\u003c/code\u003e是测试用例，包含的只有测试步骤：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eallure\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eexamples.tests.LoginToPayFlow.steps.step_add_cart\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e step_add_cart\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eexamples.tests.LoginToPayFlow.steps.step_order\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e step_order\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eexamples.tests.LoginToPayFlow.steps.step_pay\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e step_pay\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eexamples.tests.LoginToPayFlow.steps.step_search_sku\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e step_search_sku\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.cache\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e TepCache\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.step\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e Step\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e测试登录到下单流程，需要先运行utils/fastapi_mock.py\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@allure.title\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;从登录到下单支付\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elogin\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e env_vars\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e case_vars\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    case_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eput\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e login\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    cache \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e TepCache\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eenv_vars\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eenv_vars\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e case_vars\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ecase_vars\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Step\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;搜索商品\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e step_search_sku\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e cache\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Step\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;添加购物车\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e step_add_cart\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e cache\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Step\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;下单\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e step_order\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e cache\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Step\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;支付\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e step_pay\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e cache\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTepCache是“缓存”，包括global_vars、env_vars、case_vars三个级别的变量池。\u003c/p\u003e\n\u003cp\u003eStep是一个泛化调用类，作用是打日志，调用第二个参数对应的步骤函数。\u003c/p\u003e\n\u003ch2 id=\"测试步骤\"\u003e测试步骤\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.cache\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e TepCache\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.func\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e data\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.http_client\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e request\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003estep_search_sku\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecache\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e TepCache\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eenv_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/searchSku\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    headers \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecase_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e data\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;查询SKU.json\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e params\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecase_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eput\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecase_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eput\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.price\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-py\" data-lang=\"py\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.cache\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e TepCache\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.func\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e data\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eutils.http_client\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e request\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003estep_add_cart\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecache\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e TepCache\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eenv_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/addCart\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    headers \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecase_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e data\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;添加购物车.json\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecase_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecase_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eput\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    cache\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecase_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eput\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e步骤函数以step开头，尤其注意的是cache借助Python Typing提示，可以在编写下面代码时，获得PyCharm语法提示，所以一定不要忘了加上\u003ccode\u003e: TepCache\u003c/code\u003e：\u003c/p\u003e","title":"当Pytest遇上MVC分层设计自动化用例就该这么写"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文来聊一下缺陷逃逸率，上线后发现缺陷数 / 有效缺陷总数，也就是漏测分析。\n分析漏测，可以发现测试过程中的问题，优化测试策略，提高测试团队的能力，提高测试覆盖率，更加全面的验证需求。\n什么样的问题算漏测？\n只要测试能够发现的问题就算漏测。\n举2个例子，第1个例子是回归测试，研发改动代码影响了老功能，不管测试是否知道，就算漏测，为什么没有做全量回归。第2个例子是测试物料，数据不好造，没有做验证，出了问题也是算漏测，为什么不协调上下游。\n我们可能会觉得不公平，从测试人员的情感来说，是有一点难以接受。但是从老板的利益来看，是非常合理的。测试存在的价值，就是尽可能多的发现问题。测试能够发现的问题，不管什么原因，最终结果没有发现，以结果为导向，就需要让测试做出改进和提升。\n缺陷逃逸率反映了测试团队的工作结果，如果缺陷逃逸率很低，线上问题却不断发生，很可能是计算出现了偏差，要让更多的问题算做测试漏测。\n现在的测试，就是这样被卷起来的。我们接受漏测的同时，也要防止产品和研发甩锅给测试，测试不是保姆，有些问题跟测试没有关系。\n出问题很正常，哪个系统没有问题呢，禁止低级漏测，防止严重问题，尽量发现更多问题，持续改进测试方法。\n我最近发现缺陷的数据是21/71，71是总缺陷数，21是提测前发现缺陷数，其中设计评审4个，代码评审17个，分析每行代码改动，是我个人总结出来的有效测试方法。\n更多通用的、体系的测试方法，待时间沉淀后，再进行分享。\n欢迎大家讨论提高质量的测试方法。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000016-%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E8%B4%A8%E9%87%8F%E7%9A%84%E6%80%9D%E8%80%83v2.0/","summary":"本文来聊一下\u003cstrong\u003e缺陷逃逸率\u003c/strong\u003e，上线后发现缺陷数 / 有效缺陷总数，也就是漏测分析。","title":"关于版本质量的思考V2.0"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n写Python越多，写函数越多，于是乎有人觉得Python是函数式语言，其实不然，Python只是从函数式语言中借鉴了一些好的想法而已。\n函数是对象 Python中万物皆对象，函数也不例外，这意味着函数有以下特点：\n在运行时创建 能赋值给变量或数据结构中的元素 能作为参数传给函数 能作为函数的返回结果 用代码把函数类型打出来看到本质：\n\u0026gt;\u0026gt;\u0026gt; def hello(): ... print(\u0026#34;dongfanger\u0026#34;) ... \u0026gt;\u0026gt;\u0026gt; type(hello) \u0026lt;class \u0026#39;function\u0026#39;\u0026gt; hello函数是function类的实例。再看一个例子，把函数赋值：\n\u0026gt;\u0026gt;\u0026gt; def hello(): ... print(\u0026#34;dongfanger\u0026#34;) ... \u0026gt;\u0026gt;\u0026gt; a = hello \u0026gt;\u0026gt;\u0026gt; a() dongfanger Python函数就是个普普通通的对象！\n高阶函数 接受函数为参数或者把函数作为结果返回的函数，叫做高阶函数，比如map、filter、reduce，但是！作者并不建议使用这3个高阶函数，因为在Python3中有更好的替代品，而且更易于阅读。\n列表推导替代map、filter 比如，1-10，计算对每个数字的平方：\n## 使用map result = map(lambda x: x ** 2, range(1, 11)) ## 使用列表推导 result = [x ** 2 for x in range(1, 11)] 过滤奇数：\n## 用filter result = map(lambda x: x ** 2, filter(lambda x: x % 2, range(1, 11))) ## 使用列表推导 result = [x ** 2 for x in range(1, 11) if x % 2] 真香。\nsum替代reduce 计算0~99之和：\n## 使用reduce \u0026gt;\u0026gt;\u0026gt; from functools import reduce \u0026gt;\u0026gt;\u0026gt; from operator import add \u0026gt;\u0026gt;\u0026gt; reduce(add, range(100)) 4950 ## 使用sum \u0026gt;\u0026gt;\u0026gt; sum(range(100)) 4950 sum叫做归约函数，内置归约函数还有all(iterable)，如果每个元素都是真值返回True。any(iterable)，只要有元素是真值，就返回True。\n匿名函数 在Python中使用lambda表达式来创建匿名函数，示例：\nfruits = [\u0026#34;strawberry\u0026#34;, \u0026#34;fig\u0026#34;, \u0026#34;apple\u0026#34;, \u0026#34;cherry\u0026#34;, \u0026#34;raspberry\u0026#34;, \u0026#34;banana\u0026#34;] sorted(fruits, key=lambda word: word[::-1]) [\u0026#39;banana\u0026#39;, \u0026#39;apple\u0026#39;, \u0026#39;fig\u0026#39;, \u0026#39;raspberry\u0026#39;, \u0026#39;strawberry\u0026#39;, \u0026#39;cherry\u0026#39;] 它只是个表达式，不能使用while和try等语句。除了作为参数传给高阶函数之外，Python很少使用匿名函数，它的可读性并不好，如果表达式不是特别简单，那么建议不要使用lambda表达式，比如：\nimport re s = \u0026#34;192.0.0.1?!289.0.0.1!0.0.0.0!192.163.10.20?192.0.0.1\u0026#34; ips = re.split(r\u0026#34;\\?!|!|\\?\u0026#34;, s) def last_one(i): return i.split(\u0026#39;.\u0026#39;)[-1] ips.sort(key=last_one) print(ips) 通过def定义有名字的函数，可读性更佳。\n可调用对象 Python函数是对象，是可调用对象，可以使用内置的callable()函数来判断对象能否调用，比如：\n\u0026gt;\u0026gt;\u0026gt; abs, str, 13 (\u0026lt;built-in function abs\u0026gt;, \u0026lt;class \u0026#39;str\u0026#39;\u0026gt;, 13) \u0026gt;\u0026gt;\u0026gt; [callable(obj) for obj in (abs, str, 13)] [True, True, False] Python共有7种可调用对象：\n用户自定义函数\ndef语句或lambda表达式创建。\n内置函数\nC语言（CPython）实现的函数，如len或time.strftime。\n内置方法\nC语言（CPython）实现的方法，如dict.get。\n方法\n类的定义体中定义的函数。\n类\n类是可调用的，调用类时会调用类的__new__方法创建一个实例，然后调用__init__方法初始化实例，最后返回实例给调用方。\nPython没有new运算符，调用类相当于调用函数。\n类的实例\n如果类定义了__call__方法，那么它的实例可以作为函数调用。比如：\nclass Test: def __init__(self): self.name = \u0026#34;dongfanger\u0026#34; def __call__(self, *args, **kwargs): return self.name test = Test() print(test()) # dongfanger 不定义__call__方法，运行test()会报错TypeError: 'Test' object is not callable。\n生成器函数\nyield关键字定义的函数或方法。\n生成器函数的返回值是生成器对象。\n函数内省 函数对象有很多属性：\n\u0026gt;\u0026gt;\u0026gt; def func(): ... pass ... \u0026gt;\u0026gt;\u0026gt; dir(func) [\u0026#39;__annotations__\u0026#39;, \u0026#39;__call__\u0026#39;, \u0026#39;__class__\u0026#39;, \u0026#39;__closure__\u0026#39;, \u0026#39;__code__\u0026#39;, \u0026#39;__defaults__\u0026#39;, \u0026#39;__delattr__\u0026#39;, \u0026#39;__dict__\u0026#39;, \u0026#39;__dir__\u0026#39;, \u0026#39;__doc__\u0026#39;, \u0026#39;__eq__\u0026#39;, \u0026#39;__format__\u0026#39;, \u0026#39;__ge__\u0026#39;, \u0026#39;__get__\u0026#39;, \u0026#39;__getattribute__\u0026#39;, \u0026#39;__globals__\u0026#39;, \u0026#39;__gt__\u0026#39;, \u0026#39;__hash__\u0026#39;, \u0026#39;__init__\u0026#39;, \u0026#39;__init_subclass__\u0026#39;, \u0026#39;__kwdefaults__\u0026#39;, \u0026#39;__le__\u0026#39;, \u0026#39;__lt__\u0026#39;, \u0026#39;__module__\u0026#39;, \u0026#39;__name__\u0026#39;, \u0026#39;__ne__\u0026#39;, \u0026#39;__new__\u0026#39;, \u0026#39;__qualname__\u0026#39;, \u0026#39;__reduce__\u0026#39;, \u0026#39;__reduce_ex__\u0026#39;, \u0026#39;__repr__\u0026#39;, \u0026#39;__setattr__\u0026#39;, \u0026#39;__sizeof__\u0026#39;, \u0026#39;__str__\u0026#39;, \u0026#39;__subclasshook__\u0026#39;] Django源码修改函数属性示例：\ndef upper_case_name(obj): return (\u0026#34;%s %s\u0026#34; % (obj.first_name, obj.last_name)).upper() ## 修改函数属性 upper_case_name.short_description = \u0026#39;Customer name\u0026#39; 通过计算类实例和函数的差集，可以得出函数特有的属性：\n\u0026gt;\u0026gt;\u0026gt; class C: pass ... \u0026gt;\u0026gt;\u0026gt; obj = C() \u0026gt;\u0026gt;\u0026gt; def func(): pass ... \u0026gt;\u0026gt;\u0026gt; sorted(set(dir(func)) - set(dir(obj))) [\u0026#39;__annotations__\u0026#39;, \u0026#39;__call__\u0026#39;, \u0026#39;__closure__\u0026#39;, \u0026#39;__code__\u0026#39;, \u0026#39;__defaults__\u0026#39;, \u0026#39;__get__\u0026#39;, \u0026#39;__globals__\u0026#39;, \u0026#39;__kwdefaults__\u0026#39;, \u0026#39;__name__\u0026#39;, \u0026#39;__qualname__\u0026#39;] 其中的__defaults__、__code__、__annotations__属性，IDE和框架使用它们提取关于函数签名的信息。\n小结 本文通过示例看到了函数是对象的事实，高阶函数map、filter和reduce在现在已经被列表推导、生成器表达式、和sum、all、any等内置的归约函数替代。labmda表达式创建的匿名函数只在参数中且逻辑简单时使用。函数是可调用对象，实现了__call__的类也是可调用的，可以使用callable()函数来判断。最后列出了函数属性，它们记录了函数各个方面的信息。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000016-python%E5%8E%9F%E6%9D%A5python%E5%87%BD%E6%95%B0%E5%8F%AA%E6%98%AF%E4%B8%AA%E5%AF%B9%E8%B1%A1/","summary":"写Python越多，写函数越多，于是乎有人觉得Python是函数式语言，其实不然，Python只是从函数式语言中借鉴了一些好的想法而已。","title":"原来Python函数只是个对象"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nException Definition: An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program\u0026rsquo;s instructions.\nthe checked exception\n比如，java.io.FileNotFoundException\nthe error\n比如，java.io.IOError\nthe runtime exception\n比如，NullPointerException\nerror和runtime exception又叫做unchecked exception\nCatching and Handling Exceptions try { code } catch and finally blocks . . . try { } catch (ExceptionType name) { } catch (ExceptionType name) { } 使用|catch多个：\ncatch (IOException|SQLException ex) { logger.log(ex); throw ex; } finally\nThe finally block always executes when the try block exits.\nit allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break.\nfinally { if (out != null) { System.out.println(\u0026#34;Closing PrintWriter\u0026#34;); out.close(); } else { System.out.println(\u0026#34;PrintWriter not open\u0026#34;); } } finally一定会被执行，除非在try或catch的时候JVM退出了。\nThe Try-with-resources Statement\n为了确保资源被回收，可以使用try-with-resources，类似于Python的with语句：\nstatic String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } } Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.\nOkHttp的示例代码就用到了try-with-resources语句：\nOkHttpClient client = new OkHttpClient(); String run(String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } try-with-resources语句也可以跟catch和finally：\nIn a try-with-resources statement, any catch or finally block is run after the resources declared have been closed.\npublic static void viewTable(Connection con) throws SQLException { String query = \u0026#34;select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES\u0026#34;; try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String coffeeName = rs.getString(\u0026#34;COF_NAME\u0026#34;); int supplierID = rs.getInt(\u0026#34;SUP_ID\u0026#34;); float price = rs.getFloat(\u0026#34;PRICE\u0026#34;); int sales = rs.getInt(\u0026#34;SALES\u0026#34;); int total = rs.getInt(\u0026#34;TOTAL\u0026#34;); System.out.println(coffeeName + \u0026#34;, \u0026#34; + supplierID + \u0026#34;, \u0026#34; + price + \u0026#34;, \u0026#34; + sales + \u0026#34;, \u0026#34; + total); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } } 语法的区别在于，try-with-resources语句的try后面跟的是小括号()，而捕获异常语句的try后面跟的是大括号{}。\ntry-with-resources语句，小括号里面多个语句以;分隔，但是结尾没有分号：\ntry(resource1; resource2 ) { statement; // 隐式释放资源 } 对比Python with语句来看：\nwith resource: statement 捕获异常语句：\ntry { statement; } 对于try-with-resources语句，该如何捕获异常呢？\n比如：\ntry (Response response = client.newCall(request).execute()) { JSONObject resJson = (JSONObject) JSON.parse(Objects.requireNonNull(response.body()).string()); String result = resJson.getJSONObject(\u0026#34;data\u0026#34;).getString(\u0026#34;result\u0026#34;); return JSON.parseObject(result); } 可以这样写，加个try把整个都包起来：\ntry { try (Response response = client.newCall(request).execute()) { JSONObject resJson = (JSONObject) JSON.parse(Objects.requireNonNull(response.body()).string()); String result = resJson.getJSONObject(\u0026#34;data\u0026#34;).getString(\u0026#34;result\u0026#34;); return JSON.parseObject(result); } } catch (IOException e) { System.out.println(e.getMessage()); } 但更优雅的方式，是直接跟上catch：\ntry (Response response = client.newCall(request).execute()) { JSONObject resJson = (JSONObject) JSON.parse(Objects.requireNonNull(response.body()).string()); String result = resJson.getJSONObject(\u0026#34;data\u0026#34;).getString(\u0026#34;result\u0026#34;); result = result.replace(\u0026#34;\\n\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\\t\u0026#34;, \u0026#34;\u0026#34;); return JSON.parseObject(result); } catch (IOException e) { System.out.println(e.getMessage()); } try-with-resources语句的try能两用，既with-resources，又catch-exception。\nSuppressed Exceptions\nIf an exception is thrown from the try block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed.\nIf try and finally both throw exceptions, then throws the exception thrown from the finally block; the exception thrown from the try block is suppressed.\nThrowing Exceptions 使用throws关键字抛异常：\npublic void writeList() throws IOException { 而在方法内部，则使用throw关键字，注意没有s：\npublic Object pop() { // EmptyStackException是unchecked，所以这里不用throws Object obj; if (size == 0) { throw new EmptyStackException(); } obj = objectAt(size - 1); setObjectAt(size - 1, null); size--; return obj; } You can throw only objects that inherit from the java.lang.Throwable class.\nNote that the declaration of the pop() method does not contain a throws clause. EmptyStackException is not a checked exception, so pop is not required to state that it might occur.\nThe Throwable hierarchy\nChained Exceptions\n把低级别的异常，抛到高级别的异常，进行处理：\ntry { } catch (IOException e) { throw new SampleException(\u0026#34;Other IOException\u0026#34;, e); } getStackTrace()\ncatch (Exception cause) { StackTraceElement elements[] = cause.getStackTrace(); for (int i = 0, n = elements.length; i \u0026lt; n; i++) { System.err.println(elements[i].getFileName() + \u0026#34;:\u0026#34; + elements[i].getLineNumber() + \u0026#34;\u0026gt;\u0026gt; \u0026#34; + elements[i].getMethodName() + \u0026#34;()\u0026#34;); } } Logging\ntry { Handler handler = new FileHandler(\u0026#34;OutFile.log\u0026#34;); Logger.getLogger(\u0026#34;\u0026#34;).addHandler(handler); } catch (IOException e) { Logger logger = Logger.getLogger(\u0026#34;package.name\u0026#34;); StackTraceElement elements[] = e.getStackTrace(); for (int i = 0, n = elements.length; i \u0026lt; n; i++) { logger.log(Level.WARNING, elements[i].getMethodName()); } } 总结 Most applications you write will throw objects that are instances of Exception. Instances of Error are normally used for serious, hard errors in the system, such as those that prevent the JVM from running.\nthe Java programming language does not require methods to catch or to specify unchecked exceptions (RuntimeException, Error, and their subclasses).\nHere\u0026rsquo;s the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.\n异常的好处\n1、逻辑代码与异常处理代码解耦：\nerrorCodeType readFile { initialize errorCode = 0; open the file; if (theFileIsOpen) { determine the length of the file; if (gotTheFileLength) { allocate that much memory; if (gotEnoughMemory) { read the file into memory; if (readFailed) { errorCode = -1; } } else { errorCode = -2; } } else { errorCode = -3; } close the file; if (theFileDidntClose \u0026amp;\u0026amp; errorCode == 0) { errorCode = -4; } else { errorCode = errorCode and -4; } } else { errorCode = -5; } return errorCode; } readFile { try { open the file; determine its size; allocate that much memory; read the file into memory; close the file; } catch (fileOpenFailed) { doSomething; } catch (sizeDeterminationFailed) { doSomething; } catch (memoryAllocationFailed) { doSomething; } catch (readFailed) { doSomething; } catch (fileCloseFailed) { doSomething; } } 2、根据调用链抛异常：\nmethod1 { errorCodeType error; error = call method2; if (error) doErrorProcessing; else proceed; } errorCodeType method2 { errorCodeType error; error = call method3; if (error) return error; else proceed; } errorCodeType method3 { errorCodeType error; error = call readFile; if (error) return error; else proceed; } method1 { try { call method2; } catch (exception e) { doErrorProcessing; } } method2 throws exception { call method3; } method3 throws exception { call readFile; } 3、将异常分类：\ncatch (FileNotFoundException e) { ... } catch (IOException e) { ... } // A (too) general exception handler catch (Exception e) { ... } 参考资料：\nExceptions https://dev.java/learn/exceptions/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000015-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B012%E5%BC%82%E5%B8%B8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"exception\"\u003eException\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003eDefinition\u003c/em\u003e: An \u003cem\u003eexception\u003c/em\u003e is an event, which occurs during the execution of a program, that disrupts the normal flow of the program\u0026rsquo;s instructions.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ethe checked exception\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e比如，java.io.FileNotFoundException\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ethe error\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e比如，java.io.IOError\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ethe runtime exception\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e比如，NullPointerException\u003c/p\u003e\n\u003cp\u003eerror和runtime exception又叫做unchecked exception\u003c/p\u003e\n\u003ch2 id=\"catching-and-handling-exceptions\"\u003eCatching and Handling Exceptions\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003ecode\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003ecatch\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eand\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinally\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eblocks\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ecatch\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eExceptionType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ecatch\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eExceptionType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e使用\u003ccode\u003e|\u003c/code\u003ecatch多个：\u003c/p\u003e","title":"Java官方笔记12异常"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\ntep近期更新频率较快，一方面是作者在积极投入到tep工具开发中；另一方面是我们聚集了20位小伙伴，一起合力打造EasyPytest测试平台，teprunner的FastAPI升级版本，依托于tep，帮你高效管理pytest测试用例。陆续也会有平台开发日志发布，欢迎持续关注。\ntep0.11.1版本升级内容：\n1、pytest-xdist分布式执行用例，合并Allure报告；\n2、global_vars全局变量配置；\n分布式执行用例 借助于pytest-xdist，在命令行执行时添加参数-n auto：\npytest -n auto pytest-xdist会自动根据本地机器硬件配置，设置最优并发，并分发用例，分布式执行。\n测试时先运行main.py，然后在case100目录下执行pytest -n auto --tep-reports：\n第一次串行，第二次xdist并行：\n执行时间从50s一下降到5s，性能提升还是非常的明显。\n合并Allure报告 pytest-xdist分布式执行，只要把allure源文件，也就是那一堆json文件，存到同一个目录下，报告的数据就是一体的，不需要单独合并。但是有个问题，tep封装了--tep-reports 命令行参数一键生成Allure报告，背后的逻辑是在pytest_sessionfinish hook函数里面实现的，分布式执行时，每个xdist的node都是一个单独的session，多个node就会生成多份报告：\nc\n10个node生成了11份报告，其中1份master节点生成的。pytest-xdist的原理如图所示：\nmaster节点不运行任何测试，只是通过一小部分消息与节点通信。子节点执行后会通过workeroutput把数据回传给master节点。所以只需要通过是否有workeroutput属性来判断master节点：\ndef _is_master(config): \u0026#34;\u0026#34;\u0026#34; pytest-xdist分布式执行时，判断是主节点master还是子节点 主节点没有workerinput属性 \u0026#34;\u0026#34;\u0026#34; return not hasattr(config, \u0026#39;workerinput\u0026#39;) 然后只在主节点的pytest_sessionfinish，生成1次报告，就能避免生成多份报告。这样在xdist分布式执行模式下，--tep-reports也只会生成1份合并后的包含所有测试用例的Allure HTML报告。\n完整实现代码：\nimport time import shutil import allure_commons from allure_commons.logger import AllureFileLogger from allure_pytest.listener import AllureListener from allure_pytest.plugin import cleanup_factory reports_path = os.path.join(Project.root_dir, \u0026#34;reports\u0026#34;) allure_source_dir_name = \u0026#34;.allure.source.temp\u0026#34; allure_source_path = os.path.join(reports_path, allure_source_dir_name) def _tep_reports(config): \u0026#34;\u0026#34;\u0026#34; --tep-reports命令行参数不能和allure命令行参数同时使用，否则可能出错 \u0026#34;\u0026#34;\u0026#34; if config.getoption(\u0026#34;--tep-reports\u0026#34;) and not config.getoption(\u0026#34;allure_report_dir\u0026#34;): return True return False def _is_master(config): \u0026#34;\u0026#34;\u0026#34; pytest-xdist分布式执行时，判断是主节点master还是子节点 主节点没有workerinput属性 \u0026#34;\u0026#34;\u0026#34; return not hasattr(config, \u0026#39;workerinput\u0026#39;) def pytest_addoption(parser): \u0026#34;\u0026#34;\u0026#34; allure测试报告 命令行参数 \u0026#34;\u0026#34;\u0026#34; parser.addoption( \u0026#34;--tep-reports\u0026#34;, action=\u0026#34;store_const\u0026#34;, const=True, help=\u0026#34;Create tep allure HTML reports.\u0026#34; ) def pytest_configure(config): \u0026#34;\u0026#34;\u0026#34; 这段代码源自：https://github.com/allure-framework/allure-python/blob/master/allure-pytest/src/plugin.py 目的是生成allure源文件，用于生成HTML报告 \u0026#34;\u0026#34;\u0026#34; if _tep_reports(config): if os.path.exists(allure_source_path): shutil.rmtree(allure_source_path) test_listener = AllureListener(config) config.pluginmanager.register(test_listener) allure_commons.plugin_manager.register(test_listener) config.add_cleanup(cleanup_factory(test_listener)) clean = config.option.clean_alluredir file_logger = AllureFileLogger(allure_source_path, clean) # allure_source allure_commons.plugin_manager.register(file_logger) config.add_cleanup(cleanup_factory(file_logger)) def pytest_sessionfinish(session): \u0026#34;\u0026#34;\u0026#34; 测试运行结束后生成allure报告 \u0026#34;\u0026#34;\u0026#34; if _tep_reports(session.config): if _is_master(session.config): # 只在master节点才生成报告 # 最近一份报告的历史数据，填充allure趋势图 if os.path.exists(reports_path): his_reports = os.listdir(reports_path) if allure_source_dir_name in his_reports: his_reports.remove(allure_source_dir_name) if his_reports: latest_report_history = os.path.join(reports_path, his_reports[-1], \u0026#34;history\u0026#34;) shutil.copytree(latest_report_history, os.path.join(allure_source_path, \u0026#34;history\u0026#34;)) current_time = time.strftime(\u0026#34;%Y-%m-%d-%H-%M-%S\u0026#34;, time.localtime(time.time())) html_report_name = os.path.join(reports_path, \u0026#34;report-\u0026#34; + current_time) os.system(f\u0026#34;allure generate {allure_source_path} -o {html_report_name} --clean\u0026#34;) shutil.rmtree(allure_source_path) global_vars全局变量 在使用环境变量模版时，有些变量需要在多个模版重复设置，tep新增了global_vars全局变量fixture，可以将重复设置的变量，定义在resources/global_vars.yaml：\n然后引用global_vars fixture取值即可：\ndef test(global_vars): print(global_vars[\u0026#34;desc\u0026#34;]) 实现代码：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def global_vars(): \u0026#34;\u0026#34;\u0026#34;全局变量，读取resources/global_vars.yaml，返回字典\u0026#34;\u0026#34;\u0026#34; class Clazz(TepVars): def dict_(self): with open(os.path.join(Config.project_root_dir, \u0026#34;resources\u0026#34;, \u0026#34;global_vars.yaml\u0026#34;)) as f: return yaml.load(f.read(), Loader=yaml.FullLoader) return Clazz().dict_() 参考资料：\nhttps://github.com/pytest-dev/pytest-xdist\n官方文档 https://pytest-xdist.readthedocs.io/en/latest/\n如何从pytest-xdist节点获取数据 https://www.cnblogs.com/se7enjean/p/15924317.html\n博客介绍 https://blog.csdn.net/wangmcn/article/details/121080902\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000015-tep%E6%94%AF%E6%8C%81pytest-xdist%E5%88%86%E5%B8%83%E5%BC%8F%E6%89%A7%E8%A1%8C%E7%94%A8%E4%BE%8B%E5%8F%8A%E5%90%88%E5%B9%B6allure%E6%8A%A5%E5%91%8A/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003etep近期更新频率较快，一方面是作者在积极投入到tep工具开发中；另一方面是我们聚集了20位小伙伴，一起合力打造\u003cstrong\u003eEasyPytest测试平台\u003c/strong\u003e，teprunner的FastAPI升级版本，依托于tep，\u003cstrong\u003e帮你高效管理pytest测试用例\u003c/strong\u003e。陆续也会有平台开发日志发布，欢迎持续关注。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003etep0.11.1版本升级内容：\u003c/p\u003e\n\u003cp\u003e1、pytest-xdist分布式执行用例，合并Allure报告；\u003c/p\u003e\n\u003cp\u003e2、global_vars全局变量配置；\u003c/p\u003e\n\u003ch1 id=\"分布式执行用例\"\u003e分布式执行用例\u003c/h1\u003e\n\u003cp\u003e借助于pytest-xdist，在命令行执行时添加参数\u003ccode\u003e-n auto\u003c/code\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epytest \u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003en auto\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003epytest-xdist会自动根据本地机器硬件配置，设置最优并发，并分发用例，分布式执行。\u003c/p\u003e\n\u003cp\u003e测试时先运行main.py，然后在case100目录下执行\u003ccode\u003epytest -n auto --tep-reports\u003c/code\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1076643eec9e418685bdef10b49e53a5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e第一次串行，第二次xdist并行：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/55a2c725e31e47ac8b2abebd886a3abe.png\"\u003e\u003c/p\u003e\n\u003cp\u003e执行时间从50s一下降到5s，性能提升还是非常的明显。\u003c/p\u003e\n\u003ch1 id=\"合并allure报告\"\u003e合并Allure报告\u003c/h1\u003e\n\u003cp\u003epytest-xdist分布式执行，只要把allure源文件，也就是那一堆json文件，存到同一个目录下，报告的数据就是一体的，不需要单独合并。但是有个问题，tep封装了\u003ccode\u003e--tep-reports\u003c/code\u003e 命令行参数一键生成Allure报告，背后的逻辑是在pytest_sessionfinish hook函数里面实现的，分布式执行时，每个xdist的node都是一个单独的session，多个node就会生成多份报告：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8c44989d22914ca68894d80d1aa07d4c.png\"\u003ec\u003c/p\u003e\n\u003cp\u003e10个node生成了11份报告，其中1份master节点生成的。pytest-xdist的原理如图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/03de9a932bac4e3c8bb9d9cdc55cf0a3.png\"\u003e\u003c/p\u003e\n\u003cp\u003emaster节点不运行任何测试，只是通过一小部分消息与节点通信。子节点执行后会通过workeroutput把数据回传给master节点。所以只需要通过是否有workeroutput属性来判断master节点：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e_is_master\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    pytest-xdist分布式执行时，判断是主节点master还是子节点\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    主节点没有workerinput属性\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehasattr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;workerinput\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e然后只在主节点的pytest_sessionfinish，生成1次报告，就能避免生成多份报告。这样在xdist分布式执行模式下，\u003ccode\u003e--tep-reports\u003c/code\u003e也只会生成1份合并后的包含所有测试用例的Allure HTML报告。\u003c/p\u003e\n\u003cp\u003e完整实现代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etime\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eshutil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eallure_commons\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eallure_commons.logger\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e AllureFileLogger\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eallure_pytest.listener\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e AllureListener\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eallure_pytest.plugin\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e cleanup_factory\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ereports_path \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eProject\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eroot_dir\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;reports\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eallure_source_dir_name \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;.allure.source.temp\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eallure_source_path \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ereports_path\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e allure_source_dir_name\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e_tep_reports\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    --tep-reports命令行参数不能和allure命令行参数同时使用，否则可能出错\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e config\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003egetoption\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;--tep-reports\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003eand\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e config\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003egetoption\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;allure_report_dir\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eFalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e_is_master\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    pytest-xdist分布式执行时，判断是主节点master还是子节点\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    主节点没有workerinput属性\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehasattr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;workerinput\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003epytest_addoption\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eparser\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    allure测试报告 命令行参数\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    parser\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eaddoption\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;--tep-reports\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        action\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;store_const\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        const\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eTrue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        help\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Create tep allure HTML reports.\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003epytest_configure\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    这段代码源自：https://github.com/allure-framework/allure-python/blob/master/allure-pytest/src/plugin.py\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    目的是生成allure源文件，用于生成HTML报告\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e _tep_reports\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eexists\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eallure_source_path\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            shutil\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ermtree\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eallure_source_path\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        test_listener \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e AllureListener\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        config\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epluginmanager\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eregister\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003etest_listener\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        allure_commons\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eplugin_manager\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eregister\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003etest_listener\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        config\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eadd_cleanup\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecleanup_factory\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003etest_listener\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        clean \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e config\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eoption\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eclean_alluredir\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        file_logger \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e AllureFileLogger\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eallure_source_path\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e clean\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# allure_source\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        allure_commons\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eplugin_manager\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eregister\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efile_logger\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        config\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eadd_cleanup\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecleanup_factory\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efile_logger\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003epytest_sessionfinish\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esession\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    测试运行结束后生成allure报告\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e _tep_reports\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esession\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e _is_master\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esession\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# 只在master节点才生成报告\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# 最近一份报告的历史数据，填充allure趋势图\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eexists\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ereports_path\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                his_reports \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elistdir\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ereports_path\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e allure_source_dir_name \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e his_reports\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    his_reports\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eremove\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eallure_source_dir_name\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e his_reports\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    latest_report_history \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ereports_path\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e his_reports\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;history\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    shutil\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ecopytree\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elatest_report_history\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eallure_source_path\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;history\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            current_time \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e time\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estrftime\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%Y-%m-\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e%d\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e-%H-%M-%S\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e time\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elocaltime\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003etime\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etime\u003cspan style=\"color:#1f2328\"\u003e()))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            html_report_name \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ereports_path\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;report-\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e current_time\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003esystem\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003ef\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;allure generate \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003eallure_source_path\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e -o \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003ehtml_report_name\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e  --clean\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            shutil\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ermtree\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eallure_source_path\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"global_vars全局变量\"\u003eglobal_vars全局变量\u003c/h1\u003e\n\u003cp\u003e在使用环境变量模版时，有些变量需要在多个模版重复设置，tep新增了global_vars全局变量fixture，可以将重复设置的变量，定义在\u003ccode\u003eresources/global_vars.yaml\u003c/code\u003e：\u003c/p\u003e","title":"tep支持pytest-xdist分布式执行用例及合并Allure报告"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n集合 集合的字面量是{1}、{1, 2}，和字典有点像，不同的是集合只有值没有键。\nset构造方法 set构造方法如下：\nmy_set = {1, 2, 3} my_set = set([1, 2, 3]) 前者比后者更快更易读，因为对于前者，Python会利用一个专门的叫做BUILD_SET的字节码来创建集合。而对于后者，Python必须先从set这个名字来查询构造方法，然后新建一个列表，最后再把这个列表传入到构造方法里。\n{}是空字典，空集合必须用set()。\n集合也有集合推导：\n\u0026gt;\u0026gt;\u0026gt; my_set = {x for x in range(1, 4)} \u0026gt;\u0026gt;\u0026gt; my_set {1, 2, 3} \u0026gt;\u0026gt;\u0026gt; type(my_set) \u0026lt;class \u0026#39;set\u0026#39;\u0026gt; 集合还有个不可变类型叫做frozenset，如同元组之于列表，它的构造函数如下：\n\u0026gt;\u0026gt;\u0026gt; frozenset(range(10)) frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 只有这一种方法，没有字面量。\n集合的操作 集合的本质是许多唯一对象的聚集，它最常见的操作是用来去重：\n\u0026gt;\u0026gt;\u0026gt; my_list = [1, 2, 2, 3] \u0026gt;\u0026gt;\u0026gt; set(my_list) {1, 2, 3} \u0026gt;\u0026gt;\u0026gt; list(set(my_list)) [1, 2, 3] 除此之外，集合还能进行数学运算，比如求交集、并集、差集等。示例，求needles的元素在haystack里出现的次数，暴力解法：\nfound = 0 for n in needles: if n in haystack: found += 1 集合解法：\nfound = len(needles \u0026amp; haystack) 即使不是set也能先转换为set：\nfound = len(set(needles) \u0026amp; set(haystack)) ## 另一种写法 found = len(set(needles).intersection(haystack)) 集合支持的操作如下：\n散列表揭秘 这一部分的内容略微有点硬，请注意提前喝点水！\n从上篇的简介可以知道，散列表就是一张表，它通过计算一个关于键值的函数，将所需查询的数据映射到表中一个位置来访问记录。散列表其实是一个稀疏数组（总是有空白元素的数组称为稀疏数组），散列表里的单元叫作表元，在dict的散列表中，每个键值对占用一个表元，每个表元有两个部分，一个是对键的引用，另一个是对值的引用，因为所有表元的大小一致，所以可以通过偏移量来读取某个表元。\n为什么要用稀疏数组？举个例子，身份证号411697199702076425，如果把它作为键存储到数组中，虽然能用O(1)时间就找到，但是需要开辟一个999999999999999999大的空间。假如只有1的空间，就只能把最后一位作为键存储到数组中，多个身份证号的键就容易冲突，得多看n位才能找到，要用O(n)时间。空间既不能太大，也不能太小，需要结合时间，在两者之间产生一个平衡，即空间和时间的平衡，所以要用稀疏数组！\nPython会设法保证大概还有三分之一的表元是空的，用空间换时间，提高散列表查询效率。如果剩余空间不足，原有的散列表会被复制到一个更大的空间里面。\n散列表的键值，又称为散列值，Python中可以用hash()方法来计算所有内置类型对象的散列值。\n自定义类型实际上调用的是自定义的__hash__。\n如果两个对象在比较的时候是相等的，那么它们的散列值必须相等，否则散列表就不能正常运行了：\n\u0026gt;\u0026gt;\u0026gt; a = 1 \u0026gt;\u0026gt;\u0026gt; b = 1 \u0026gt;\u0026gt;\u0026gt; a == b True \u0026gt;\u0026gt;\u0026gt; hash(a) 1 \u0026gt;\u0026gt;\u0026gt; hash(b) 1 \u0026gt;\u0026gt;\u0026gt; a = \u0026#34;x\u0026#34; \u0026gt;\u0026gt;\u0026gt; b = \u0026#34;x\u0026#34; \u0026gt;\u0026gt;\u0026gt; a == b True \u0026gt;\u0026gt;\u0026gt; hash(a) 706802421 \u0026gt;\u0026gt;\u0026gt; hash(b) 706802421 越是相似但不相等的对象，它们的散列值的差别越大：\n\u0026gt;\u0026gt;\u0026gt; a = 1.0001 \u0026gt;\u0026gt;\u0026gt; b = 1.0002 \u0026gt;\u0026gt;\u0026gt; hash(a) 783616733 \u0026gt;\u0026gt;\u0026gt; hash(b) 1567233465 这是因为散列值是散列表索引，它们必须在索引空间尽量分散开来。\n我的理解是，散列值是要被尽量打散的，1.0001和1.0002相差0.0001，这个0.0001被打散后的值导致它们的散列值相差很大。\n了解了基本概念后，该看下散列表算法了（不用害怕，只有思路，没有代码）：\n为了获取my_dict[search_key]背后的值：\n调用hash(search_key)计算search_key的散列值。 把最低几位数字当做偏移量，在散列表里查找表元。 如果表元为空，返回KeyError。 如果表元有值，表元里会有一对found_key:found_value。 检验search_key == found_key，相等就返回found_key。 不相等的情况称为散列冲突！为了解决冲突，算法会在散列值中另外再取几位，处理一下，把新得到的数字当做索引来寻找表元。 实际上散列冲突发生概率非常小，散列表查询效率非常高！\n添加新元素和更新现有键值的操作几乎一样，区别在于添加新元素时发现空表元，会放入一个新元素；更新现有键值时，会把原表里的值替换成新值。\n另外，添加新元素时，Python会根据剩余空间大小决定是否要重新分配内容为它扩容。\n散列表与dict dict的键必须是可散列的：\n支持hash()函数，通过__hash__()得到的散列值是不变的。 支持通过__eq__()来判断是否相等。 若a == b为真，则hash(a) == hash(b)也为真。 所有由用户自定义的对象默认都是可散列的，因为它们的散列值由id()来获取（符合第1条），而且它们都是不相等的（符合第2条和第3条）。\ndict的实现是典型的空间换时间：键查询很快，在内存上的开销巨大！\ndict键的次序取决于添加顺序，当往dict添加新键时，如果发生了散列冲突，新键可能会被放到另一个位置，键的位置不一样，次序也就不一样了。比如：\nmy_list = [(\u0026#34;a\u0026#34;, 1), (\u0026#34;b\u0026#34;, 2)] my_dict1 = dict(my_list) my_dict2 = dict(reversed(my_list)) print(my_dict1) # {\u0026#39;a\u0026#39;: 1, \u0026#39;b\u0026#39;: 2} print(my_dict2) # {\u0026#39;b\u0026#39;: 2, \u0026#39;a\u0026#39;: 1} print((my_dict1 == my_dict2)) # True 但是它们是相等的，因为它们所包含的数据是一样的。\n值得注意的是，往字典里添加新键可能会改变已有键的顺序！当空间不足，Python会为字典扩容，新建一个更大的散列表，并把字典已有的元素添加进去，这个过程中可能会发生散列冲突，导致新散列表中键的次序变化。\n由此可知，不要对字典同时进行迭代和修改，循环很可能会跳过一些键，甚至是跳过那些字典中已经有的键。最好分成两步来做，首先对字典进行迭代，得出需要添加的内容，把这些内容放在一个新字典里；在迭代结束后再对原有字典进行更新。\n散列表与set 集合的散列表里存放的只有元素的引用（就像在字典里只存放键而没有相应的值）。上一节讨论的散列表与dict的内容，对集合来说几乎都是适用的。\n在set加入Python以前，原书作者他们是把字典加上无意义的值当作集合来用的。\n小结 本文为下篇，介绍了集合，重点揭秘了散列表的原理，正是由于散列表的支撑，dict和set的查询效率非常高，代价是空间换时间，内容占用也比较大，当数据量很大时，不适合用dict和set，而应该考虑用元组或由具名元组构成的列表。散列表也给dict和set带来了限制，比如dict键的次序取决于添加顺序，往字典里添加新键可能会改变已有键的顺序等。\n参考资料：\n《流畅的Python》\nhttps://www.jianshu.com/p/4e64fce04a38\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000015-python%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90python%E5%AD%97%E5%85%B8%E5%92%8C%E9%9B%86%E5%90%88%E4%B8%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"集合\"\u003e集合\u003c/h1\u003e\n\u003cp\u003e集合的字面量是{1}、{1, 2}，和字典有点像，不同的是集合只有值没有键。\u003c/p\u003e\n\u003ch2 id=\"set构造方法\"\u003eset构造方法\u003c/h2\u003e\n\u003cp\u003eset构造方法如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emy_set \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emy_set \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e([\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e前者比后者更快更易读，因为对于前者，Python会利用一个专门的叫做BUILD_SET的字节码来创建集合。而对于后者，Python必须先从set这个名字来查询构造方法，然后新建一个列表，最后再把这个列表传入到构造方法里。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003e{}\u003c/code\u003e是空字典，空集合必须用\u003ccode\u003eset()\u003c/code\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e集合也有集合推导：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e my_set \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003ex \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e x \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erange\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e my_set\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etype\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emy_set\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e集合还有个不可变类型叫做frozenset，如同元组之于列表，它的构造函数如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003efrozenset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erange\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e10\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003efrozenset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e({\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e5\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e6\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e9\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e只有这一种方法，没有字面量。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"集合的操作\"\u003e集合的操作\u003c/h2\u003e\n\u003cp\u003e集合的本质是许多唯一对象的聚集，它最常见的操作是用来去重：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e my_list \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emy_list\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elist\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emy_list\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e除此之外，集合还能进行数学运算，比如求交集、并集、差集等。示例，求needles的元素在haystack里出现的次数，暴力解法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efound \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e n \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e needles\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e n \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e haystack\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        found \u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e集合解法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efound \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eneedles \u003cspan style=\"color:#0550ae\"\u003e\u0026amp;\u003c/span\u003e haystack\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e即使不是set也能先转换为set：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efound \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eneedles\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e\u0026amp;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ehaystack\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 另一种写法\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efound \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eneedles\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eintersection\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ehaystack\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e集合支持的操作如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9710a5b6aa2c42d5b944dd8701f6fa5b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0084371d56594e63b3c8fd85af07b83f.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"散列表揭秘\"\u003e散列表揭秘\u003c/h1\u003e\n\u003cp\u003e这一部分的内容略微有点硬，请注意提前喝点水！\u003c/p\u003e\n\u003cp\u003e从上篇的简介可以知道，散列表就是一张表，它通过计算一个关于键值的函数，将所需查询的数据映射到表中一个位置来访问记录。散列表其实是一个稀疏数组（总是有空白元素的数组称为稀疏数组），散列表里的单元叫作表元，在dict的散列表中，每个键值对占用一个表元，每个表元有两个部分，一个是对键的引用，另一个是对值的引用，因为所有表元的大小一致，所以可以通过偏移量来读取某个表元。\u003c/p\u003e\n\u003cp\u003e为什么要用稀疏数组？举个例子，身份证号411697199702076425，如果把它作为键存储到数组中，虽然能用O(1)时间就找到，但是需要开辟一个999999999999999999大的空间。假如只有1的空间，就只能把最后一位作为键存储到数组中，多个身份证号的键就容易冲突，得多看n位才能找到，要用O(n)时间。空间既不能太大，也不能太小，需要结合时间，在两者之间产生一个平衡，即空间和时间的平衡，所以要用稀疏数组！\u003c/p\u003e\n\u003cp\u003ePython会设法保证大概还有三分之一的表元是空的，用空间换时间，提高散列表查询效率。如果剩余空间不足，原有的散列表会被复制到一个更大的空间里面。\u003c/p\u003e\n\u003cp\u003e散列表的键值，又称为散列值，Python中可以用hash()方法来计算所有内置类型对象的散列值。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e自定义类型实际上调用的是自定义的\u003ccode\u003e__hash__\u003c/code\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e如果两个对象在比较的时候是相等的，那么它们的散列值必须相等，否则散列表就不能正常运行了：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e b \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e b\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehash\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ea\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehash\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eb\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;x\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e b \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;x\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e b\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehash\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ea\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e706802421\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehash\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eb\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e706802421\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e越是相似但不相等的对象，它们的散列值的差别越大：\u003c/p\u003e","title":"深度剖析Python字典和集合（下）"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n什么是TP99？ TP是Top Percentile的首字母缩写，TP99是指“前百分之九十九”，用来表示满足百分之九十九的网络请求所需的最低耗时。\n该怎么理解？举个简单例子，下图是100次请求的响应时间走势图：\n第23次请求的响应时间为600ms，其他请求的响应全部为1ms，这个走势图也就是常说的“尖刺”。我们来计算一下：\n①平均响应时间\n平均响应时间 = 所有请求响应时间之和 / 请求次数，等于6.99ms\n②TP99\nTP99 = 把响应时间从小到大排列，取第99%那个位置的值，等于1ms\n为什么TP99更需要关注？ 平均响应时间受极端值的影响较大，因为出现1个尖刺，平均响应时间几乎是TP99的7倍！\nTP99更能反映大多数请求的情况，通过TP99为1ms可以知道，应用系统对于大多数请求，都能在1ms内响应，整体性能达标。而对于尖刺，我们可以通过走势图的MAX来发现，单独进行分析。\n在JMeter的Aggregate Report中，有3个字段：90% Line、95% Line、99%Line\n其实就是指TP90、TP95、TP99。此外，在实际工作中，我们还会关注TP999，三个九，也就是99.9%。关注TP多少，取决于我们对性能的要求怎么样，我们希望应用系统满足多少请求。如果对性能要求不高，关注TP90就够了，允许10%请求出现波动。如果对性能要求很高，则需要关注TP999，99.9%请求都能够快速响应。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000015-%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95%E6%8C%87%E6%A0%87tp99%E6%AF%94%E5%B9%B3%E5%9D%87%E5%93%8D%E5%BA%94%E6%97%B6%E9%97%B4%E6%9B%B4%E9%9C%80%E8%A6%81%E5%85%B3%E6%B3%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"什么是tp99\"\u003e什么是TP99？\u003c/h1\u003e\n\u003cp\u003eTP是Top Percentile的首字母缩写，TP99是指“前百分之九十九”，用来表示满足百分之九十九的网络请求所需的最低耗时。\u003c/p\u003e\n\u003cp\u003e该怎么理解？举个简单例子，下图是100次请求的响应时间走势图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e47a6fabaec14c8ebef5d085f0fa0399.png\"\u003e\u003c/p\u003e\n\u003cp\u003e第23次请求的响应时间为600ms，其他请求的响应全部为1ms，这个走势图也就是常说的“尖刺”。我们来计算一下：\u003c/p\u003e\n\u003cp\u003e①平均响应时间\u003c/p\u003e\n\u003cp\u003e平均响应时间 = 所有请求响应时间之和 / 请求次数，等于6.99ms\u003c/p\u003e\n\u003cp\u003e②TP99\u003c/p\u003e\n\u003cp\u003eTP99 = 把响应时间从小到大排列，取第99%那个位置的值，等于1ms\u003c/p\u003e\n\u003ch1 id=\"为什么tp99更需要关注\"\u003e为什么TP99更需要关注？\u003c/h1\u003e\n\u003cp\u003e平均响应时间受极端值的影响较大，因为出现1个尖刺，平均响应时间几乎是TP99的7倍！\u003c/p\u003e\n\u003cp\u003eTP99更能反映大多数请求的情况，通过TP99为1ms可以知道，应用系统对于大多数请求，都能在1ms内响应，整体性能达标。而对于尖刺，我们可以通过走势图的MAX来发现，单独进行分析。\u003c/p\u003e\n\u003cp\u003e在JMeter的Aggregate Report中，有3个字段：90% Line、95% Line、99%Line\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/de01b3cdbe37490f8dff7a9d105d28b5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e其实就是指TP90、TP95、TP99。此外，在实际工作中，我们还会关注TP999，三个九，也就是99.9%。关注TP多少，取决于我们对性能的要求怎么样，我们希望应用系统满足多少请求。如果对性能要求不高，关注TP90就够了，允许10%请求出现波动。如果对性能要求很高，则需要关注TP999，99.9%请求都能够快速响应。\u003c/p\u003e","title":"性能测试指标TP99，比平均响应时间更需要关注"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n一、引言 技术学习有个特点，看完就忘，再看再忘，还看还忘。艾宾浩斯遗忘曲线，解释了这一现象，学习后的20分钟内，记忆的遗忘速度最快。1个小时后，遗忘速度开始减慢。1天后，遗忘速度显著降低。一段时间后，记忆会变得相对稳定。\n为了能够记忆技术知识点，我们可以尝试理解技术逻辑，定时复习，主动回忆。通常的做法是将原文摘抄下来，回顾时再读一遍。缺点在于，技术逻辑与文学不同，每次都要烧脑，无法唤醒记忆。\n二、什么是知识卡片 知识卡片是我在《高效学习法》学到的笔记技巧。简单来说，知识卡片就是把知识“画”在一张卡片上，重点是“画”。\n（图片来源于《高效学习法》）\n三、为什么需要知识卡片 知识呈现形式，影响着阅读理解难度，图形\u0026gt;表格\u0026gt;文字。技术逻辑是一个思考过程，想通了以后，在我们脑海中会浮现出一副图像，可能是流程图、数据流、树状图等。如果每次复习都能看到这幅图像，那么复习效率将会显著提升。很有必要，将这幅图像刻画出来。\n四、怎么制作知识卡片 我们可以找个本子，用笔进行绘画，但是相对于纸质笔记，我更推荐使用电子笔记，便于存储，多设备阅读和分享。\n知识卡片，可以使用常见的办公软件来制作，PowerPoint，俗称PPT。\n先做好知识卡片分类，将同一类知识卡片放在同一个文件里面，创建多张幻灯片。\n画图时随意发挥，只要能把技术逻辑描绘出来，自己能看懂，方便理解即可。\n五、我的知识卡片分享 ①流量录制回放\n②消息队列\n③ Java\n六、总结 知识卡片，是一种视觉化的笔记方法，便于理解和记忆。学而不思则罔，思而不学则殆。知识卡片特别适合技术学习，温故而知新。感兴趣的小伙伴，动手画起来吧。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000015-%E7%9F%A5%E8%AF%86%E5%8D%A1%E7%89%87%E5%9C%A8%E6%8A%80%E6%9C%AF%E5%AD%A6%E4%B9%A0%E4%B8%AD%E7%9A%84%E8%BF%90%E7%94%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"一引言\"\u003e一、引言\u003c/h1\u003e\n\u003cp\u003e技术学习有个特点，看完就忘，再看再忘，还看还忘。艾宾浩斯遗忘曲线，解释了这一现象，学习后的20分钟内，记忆的遗忘速度最快。1个小时后，遗忘速度开始减慢。1天后，遗忘速度显著降低。一段时间后，记忆会变得相对稳定。\u003c/p\u003e\n\u003cp\u003e为了能够记忆技术知识点，我们可以尝试理解技术逻辑，定时复习，主动回忆。通常的做法是将原文摘抄下来，回顾时再读一遍。缺点在于，技术逻辑与文学不同，每次都要烧脑，无法唤醒记忆。\u003c/p\u003e\n\u003ch1 id=\"二什么是知识卡片\"\u003e二、什么是知识卡片\u003c/h1\u003e\n\u003cp\u003e知识卡片是我在《高效学习法》学到的笔记技巧。简单来说，知识卡片就是把知识“画”在一张卡片上，重点是“画”。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/257e90a750c448428bbc7fead855a2ef.png\"\u003e\u003c/p\u003e\n\u003cp\u003e（图片来源于《高效学习法》）\u003c/p\u003e\n\u003ch1 id=\"三为什么需要知识卡片\"\u003e三、为什么需要知识卡片\u003c/h1\u003e\n\u003cp\u003e知识呈现形式，影响着阅读理解难度，图形\u0026gt;表格\u0026gt;文字。技术逻辑是一个思考过程，想通了以后，在我们脑海中会浮现出一副图像，可能是流程图、数据流、树状图等。如果每次复习都能看到这幅图像，那么复习效率将会显著提升。很有必要，将这幅图像刻画出来。\u003c/p\u003e\n\u003ch1 id=\"四怎么制作知识卡片\"\u003e四、怎么制作知识卡片\u003c/h1\u003e\n\u003cp\u003e我们可以找个本子，用笔进行绘画，但是相对于纸质笔记，我更推荐使用电子笔记，便于存储，多设备阅读和分享。\u003c/p\u003e\n\u003cp\u003e知识卡片，可以使用常见的办公软件来制作，PowerPoint，俗称PPT。\u003c/p\u003e\n\u003cp\u003e先做好知识卡片分类，将同一类知识卡片放在同一个文件里面，创建多张幻灯片。\u003c/p\u003e\n\u003cp\u003e画图时随意发挥，只要能把技术逻辑描绘出来，自己能看懂，方便理解即可。\u003c/p\u003e\n\u003ch1 id=\"五我的知识卡片分享\"\u003e五、我的知识卡片分享\u003c/h1\u003e\n\u003cp\u003e①流量录制回放\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/7760998c1aae4e6f8c414232ce7a61b4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e②消息队列\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ae0de150616c4c3aa6b96e70be2a8621.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fce86266b6fc49889a9b235eb642071a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e③ Java\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/94cd94a72f144736a60616c3e8130edd.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1ddc992469684281a41bc271ee62aa7a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/151fa1ad3b834dc795b480b64bdfe065.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"六总结\"\u003e六、总结\u003c/h1\u003e\n\u003cp\u003e知识卡片，是一种视觉化的笔记方法，便于理解和记忆。学而不思则罔，思而不学则殆。知识卡片特别适合技术学习，温故而知新。感兴趣的小伙伴，动手画起来吧。\u003c/p\u003e","title":"知识卡片，在技术学习中的运用"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n10.1 AI安全风险的来源和分类 系统风险：AI模型软件的供应链风险、暴露面风险以及算力劫持风险。\n网络风险：面向公网的入侵攻击，以及内网的隔离风险。\n身份风险：对非人类身份的管控，越权访问，身份冒充等。\n数据风险：Agent模型训练时的数据投毒，以及输入/输出阶段的敏感信息泄露等。\n模型风险：Agent模型输入输出内容的恶意诱导、提示词攻击等风险。\n应用风险：当AI在线上提供服务时，会面临Web入侵、DDoS攻击导致服务不可用等风险。\n10.2 保护应用安全 10.2.1 背景和挑战 攻击者不再需要直接寻找暴露在公网服务中的漏洞，他们只需操纵相关的AI Agent，就能让这些AI Agent代为攻击内外部的弱电服务。\n服务器端请求伪造（SSRF）是引入AI Agent后最突出的威胁。攻击者仅需通过自然语言提示，即可诱导受信任的Agent向内部网络发起恶意请求，窃取云环境凭证或扫描内网资产。Agent也可能被攻击者诱导产生包含SQL注入或XSS等恶意载荷，攻击与之交互的后端服务。\n10.3.2 新的攻击面 构造恶意的语言诱导，让AI Agent变为一个无节制的资源消耗工具，使其无法正常为其他用户提供服务。构造复杂度炸弹式的指令，诱使AI Agent陷入生成海量文本的陷阱，生成数万乃至数十万的Token，导致其自身的计算成本飙升。\n攻击者也可以攻击AI Agent调用的外部API，高频或海量触发复杂或昂贵的API调用。攻击者还能将恶意指令隐藏在AI Agent将要处理的网页、表格、PDF等外部数据中，当AI Agent处理这些被污染的数据时，隐藏的指令就会被执行。\n10.3.3 防护思路 沿用并强化传统的应用安全最佳实践，同时实施严格的资源治理策略，针对工作流劫持类攻击，则需要设计更加完善的上下文隔离机制。\n10.3.4 解决建议 一方面，对在AI场景下输入输出做安全检测的AI安全护栏方案，以及相关AI应用开发平台和网关对AI安全护栏的集成。另一方面，由于AI Agent进一步打破了传统的攻击面和网络边界，要更加关注纵深防御、动态检测、运行时安全等多种安全措施的组合。\n例如当AI Agent未经过WAF或防火墙直接在内网发起恶意请求时，可以通过RASP等运行时检测方案或VPC防火墙来做防护，当AI Agent在用各种方式泄露敏感数据时，可以通过API安全、NDR等流量安全方案，从敏感数据流转的角度去及时发现风险。\n10.3 保护模型安全 10.3.1 背景与挑战 输入层威胁：包括对抗样本攻击（如通过微调图像/音频诱导多模态Agent误判）、提示词注入的变种攻击（如上下文分割攻击、语义混淆攻击），以及通过恶意文件（如PDF隐写术）触发供应链漏洞。\n推理层威胁：涵盖模型越狱导致的伦理失控、RAG知识库的定向爬取引发的数据资产泄露，以及函数调用劫持（如篡改API参数执行未授权操作）。\n输出层威胁：设计生成式钓鱼内容（如伪造银行通知）、模型幻觉在医疗/金融场景的致命误导，以及通过隐写术在AIGC内容中植入隐蔽指令。\n10.3.2 防护机制 内容合规审核，提示词攻击防御，敏感信息防护，恶意文件检测，恶意URL拦截，提示词反爬机制，模型越狱检测，模型幻觉抑制，数字水印标识。\n10.3.3 模型安全的未来 自定义检测Agent可满足不同行业客户的特定业务风险识别需求。它引入了专用算法模型，并支持多行业多场景灵活配置，从而实现从通用到专属的安全检测能力升级。\n10.4 保护数据安全 10.4.1 背景和挑战 10.4.2 防护框架 打造“平台可靠、链路可信、数据可控、自主可选、操作可审、责任可追”的数据安全保障体系。\n10.4.3 构建全数据全生命周期的安全保障 1、数据收集\n（1）数据来源：用户上传类数据、模型文件和推理数据、运行日志类数据。\n（2）数据分类：以阿里云数据安全中心为例，从数据价值、敏感性、数据合规和业务需求等多角度将数据分为4个安全级别：S1、S2、S3、S4。\n（3）数据脱敏\n（4）数据去毒：对自有的存储资源中多种违规内容（例如涉黄、涉政、暴力、违法等）进行实时监控、检测和拦截。\n2、数据传输\n（1）面向数据传输提供私有网络加密\n（2）提示词推理加密\n（3）应用协议加密\n3、数据存储\n（1）存储隔离\n（2）存储加密\n4、数据访问\n5、数据处理\n6、数据删除\n10.5 保护身份安全 10.5.1 背景和挑战 AI身份的管理不同于当前人类身份管理，AI Agent行为链复杂不可测，动态权限需求使得传统的静态权限模型难以适用，黑客可通过窃取API密钥、访问令牌等敏感凭据，可轻易绕过传统安全防护，实施数据窃取、资源劫持等恶意行为。\n10.5.2 身份安全闭环 1、身份权限治理整体架构\n2、事前：NHI检测与风险识别（NHI 非人类身份）\n3、事中：动态权限管理机制\n4、事后：自动化审计与清理\n10.5.3 通过密钥管理实现凭据秘钥托管与轮转 统一密钥管理与加密服务，自动化密钥轮转机制、细粒度密钥访问控制。\n10.5.4 通过M2M能力解决NHI凭据的托管 M2M，Machine to Machine。\n集中化的NHI凭据管理，动态授权与最小权原则，自动化凭据轮转与生命周期管理。\n10.6 保护系统和网络安全 10.6.1 背景和挑战 任何基础设施层面的疏漏都可能引发模型盗用、数据泄露或服务滥用等严重风险。\n10.6.2 基础设施的统一安全态势管理 1、AI资产的自动发现与盘点：自动化识别，集中化视图。\n2、多维度风险评估：开源组件漏洞检测，公网暴露面分析，配置风险检查，敏感信息扫描。\n10.6.3 计算层安全加固 1、主机安全基础防护\n2、容器化环境安全防护\n3、镜像供应链安全：扫描与合规性，签名与验签。\n10.6.4 网络隔离与访问控制 1、互联网边界防护：智能访问控制、主动威胁防御、全链路可视化。\n2、内网微隔离防护：阻断攻击横向渗透路径，零信任网络实践。\n通过安全组实现节点级防护（点），VPC边界防火墙拦截跨区流量威胁（线），互联网防火墙把控公网入口（面），形成覆盖全网络层级的防护网络。\n个人学习总结 《AI白皮书》是阿里出的，内容除了AI通识，更多是阿里相关产品的推广。共10章，学习过程漫长且枯燥。通过全篇学习，可以建立新的认知，AI是一整个生态系统，就像现在的软件生态一样。从研发到商业，从调优到评估，从监控到安全，AI改变着工作和生活的方式，我们正在经历一个全新的时代。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005010-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E5%AE%89%E5%85%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"101-ai安全风险的来源和分类\"\u003e10.1 AI安全风险的来源和分类\u003c/h1\u003e\n\u003cp\u003e系统风险：AI模型软件的供应链风险、暴露面风险以及算力劫持风险。\u003c/p\u003e\n\u003cp\u003e网络风险：面向公网的入侵攻击，以及内网的隔离风险。\u003c/p\u003e\n\u003cp\u003e身份风险：对非人类身份的管控，越权访问，身份冒充等。\u003c/p\u003e\n\u003cp\u003e数据风险：Agent模型训练时的数据投毒，以及输入/输出阶段的敏感信息泄露等。\u003c/p\u003e\n\u003cp\u003e模型风险：Agent模型输入输出内容的恶意诱导、提示词攻击等风险。\u003c/p\u003e\n\u003cp\u003e应用风险：当AI在线上提供服务时，会面临Web入侵、DDoS攻击导致服务不可用等风险。\u003c/p\u003e\n\u003ch1 id=\"102-保护应用安全\"\u003e10.2 保护应用安全\u003c/h1\u003e\n\u003ch2 id=\"1021-背景和挑战\"\u003e10.2.1 背景和挑战\u003c/h2\u003e\n\u003cp\u003e攻击者不再需要直接寻找暴露在公网服务中的漏洞，他们只需操纵相关的AI Agent，就能让这些AI Agent代为攻击内外部的弱电服务。\u003c/p\u003e\n\u003cp\u003e服务器端请求伪造（SSRF）是引入AI Agent后最突出的威胁。攻击者仅需通过自然语言提示，即可诱导受信任的Agent向内部网络发起恶意请求，窃取云环境凭证或扫描内网资产。Agent也可能被攻击者诱导产生包含SQL注入或XSS等恶意载荷，攻击与之交互的后端服务。\u003c/p\u003e\n\u003ch2 id=\"1032-新的攻击面\"\u003e10.3.2 新的攻击面\u003c/h2\u003e\n\u003cp\u003e构造恶意的语言诱导，让AI Agent变为一个无节制的资源消耗工具，使其无法正常为其他用户提供服务。构造复杂度炸弹式的指令，诱使AI Agent陷入生成海量文本的陷阱，生成数万乃至数十万的Token，导致其自身的计算成本飙升。\u003c/p\u003e\n\u003cp\u003e攻击者也可以攻击AI Agent调用的外部API，高频或海量触发复杂或昂贵的API调用。攻击者还能将恶意指令隐藏在AI Agent将要处理的网页、表格、PDF等外部数据中，当AI Agent处理这些被污染的数据时，隐藏的指令就会被执行。\u003c/p\u003e\n\u003ch2 id=\"1033-防护思路\"\u003e10.3.3 防护思路\u003c/h2\u003e\n\u003cp\u003e沿用并强化传统的应用安全最佳实践，同时实施严格的资源治理策略，针对工作流劫持类攻击，则需要设计更加完善的上下文隔离机制。\u003c/p\u003e\n\u003ch2 id=\"1034-解决建议\"\u003e10.3.4 解决建议\u003c/h2\u003e\n\u003cp\u003e一方面，对在AI场景下输入输出做安全检测的AI安全护栏方案，以及相关AI应用开发平台和网关对AI安全护栏的集成。另一方面，由于AI Agent进一步打破了传统的攻击面和网络边界，要更加关注纵深防御、动态检测、运行时安全等多种安全措施的组合。\u003c/p\u003e\n\u003cp\u003e例如当AI Agent未经过WAF或防火墙直接在内网发起恶意请求时，可以通过RASP等运行时检测方案或VPC防火墙来做防护，当AI Agent在用各种方式泄露敏感数据时，可以通过API安全、NDR等流量安全方案，从敏感数据流转的角度去及时发现风险。\u003c/p\u003e\n\u003ch1 id=\"103-保护模型安全\"\u003e10.3 保护模型安全\u003c/h1\u003e\n\u003ch2 id=\"1031-背景与挑战\"\u003e10.3.1 背景与挑战\u003c/h2\u003e\n\u003cp\u003e输入层威胁：包括对抗样本攻击（如通过微调图像/音频诱导多模态Agent误判）、提示词注入的变种攻击（如上下文分割攻击、语义混淆攻击），以及通过恶意文件（如PDF隐写术）触发供应链漏洞。\u003c/p\u003e\n\u003cp\u003e推理层威胁：涵盖模型越狱导致的伦理失控、RAG知识库的定向爬取引发的数据资产泄露，以及函数调用劫持（如篡改API参数执行未授权操作）。\u003c/p\u003e\n\u003cp\u003e输出层威胁：设计生成式钓鱼内容（如伪造银行通知）、模型幻觉在医疗/金融场景的致命误导，以及通过隐写术在AIGC内容中植入隐蔽指令。\u003c/p\u003e\n\u003ch2 id=\"1032-防护机制\"\u003e10.3.2 防护机制\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6b145b6b587a4bcc8b791d1d1d52953c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e内容合规审核，提示词攻击防御，敏感信息防护，恶意文件检测，恶意URL拦截，提示词反爬机制，模型越狱检测，模型幻觉抑制，数字水印标识。\u003c/p\u003e\n\u003ch2 id=\"1033-模型安全的未来\"\u003e10.3.3 模型安全的未来\u003c/h2\u003e\n\u003cp\u003e自定义检测Agent可满足不同行业客户的特定业务风险识别需求。它引入了专用算法模型，并支持多行业多场景灵活配置，从而实现从通用到专属的安全检测能力升级。\u003c/p\u003e\n\u003ch1 id=\"104-保护数据安全\"\u003e10.4 保护数据安全\u003c/h1\u003e\n\u003ch2 id=\"1041-背景和挑战\"\u003e10.4.1 背景和挑战\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0022e3b5d360450f8b8a926d8ef0eed9.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ebbacece3f68464ca62efd1bb3937874.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"1042-防护框架\"\u003e10.4.2 防护框架\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/62a7622d856343faa4f25711eecbbb3a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e打造“平台可靠、链路可信、数据可控、自主可选、操作可审、责任可追”的数据安全保障体系。\u003c/p\u003e\n\u003ch2 id=\"1043-构建全数据全生命周期的安全保障\"\u003e10.4.3 构建全数据全生命周期的安全保障\u003c/h2\u003e\n\u003cp\u003e1、数据收集\u003c/p\u003e\n\u003cp\u003e（1）数据来源：用户上传类数据、模型文件和推理数据、运行日志类数据。\u003c/p\u003e\n\u003cp\u003e（2）数据分类：以阿里云数据安全中心为例，从数据价值、敏感性、数据合规和业务需求等多角度将数据分为4个安全级别：S1、S2、S3、S4。\u003c/p\u003e\n\u003cp\u003e（3）数据脱敏\u003c/p\u003e\n\u003cp\u003e（4）数据去毒：对自有的存储资源中多种违规内容（例如涉黄、涉政、暴力、违法等）进行实时监控、检测和拦截。\u003c/p\u003e\n\u003cp\u003e2、数据传输\u003c/p\u003e\n\u003cp\u003e（1）面向数据传输提供私有网络加密\u003c/p\u003e","title":"AI安全"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nMyBatis是什么 MyBatis是Java的持久层框架，GitHub的star数高达15.8k，是Java技术栈中最热门的ORM框架之一。它支持自定义SQL、存储过程以及高级映射，可以通过XML或注解来配置和映射原始类型、接口和Java POJOs为数据库中的记录。\nPOJOs，Plain Old Java Objects，纯的传统意义的Java对象，最基本的Java Bean只有属性加上属性的get和set方法。\n安装 Maven pom.xml：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;x.x.x\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 版本查询：https://mvnrepository.com/artifact/org.mybatis/mybatis\nSqlSessionFactory SqlSessionFactory的实例是每个MyBatis应用的核心，通过SqlSessionFactoryBuilder创建，能基于XML配置，也能使用Configuration类。\n基于XML配置：\nString resource = \u0026#34;org/mybatis/example/mybatis-config.xml\u0026#34;; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 实际项目中不会手动设置XML文件路径，而是直接读取classpath去找XML。\n使用Configuration类：\nDataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment(\u0026#34;development\u0026#34;, transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(BlogMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); 由于Java注解的一些限制以及某些MyBatis映射的复杂性，MyBatis会优先选择XML配置。在上面的示例中，MyBatis会根据classpath和BlogMapper.class去找BlogMapper.xml。\nSqlSession SqlSessionFactory能创建SqlSession实例，SqlSession提供了在数据库执行SQL的所有方法。比如：\ntry (SqlSession session = sqlSessionFactory.openSession()) { BlogMapper mapper = session.getMapper(BlogMapper.class); Blog blog = mapper.selectBlog(101); } 背后的SQL语句 隐藏在mapper.selectBlog()方法背后的SQL语句是配置在XML文件里面的。\n一般XML的配置信息如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;environment id=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;${driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${password}\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;org/mybatis/example/BlogMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; environment配置了数据源和连接池。\nmapper包含了SQL语句：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;org.mybatis.example.BlogMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;selectBlog\u0026#34; resultType=\u0026#34;Blog\u0026#34;\u0026gt; select * from Blog where id = #{id} \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; mapper.selectBlog()方法映射到了XML里面的id selectBlog。\n值得注意的是，对于简单SQL来说，可以直接使用Java注解：\npackage org.mybatis.example; public interface BlogMapper { @Select(\u0026#34;SELECT * FROM blog WHERE id = #{id}\u0026#34;) Blog selectBlog(int id); } 但是对于复杂SQL还是写XML更方便。\n作用域与生命周期 从前面几个小节我们知道了SqlSessionFactoryBuilder→SqlSessionFactory→SqlSession→Mapper Instances的实例对象产生链路，接下来看看它们各自的作用域与生命周期：\nSqlSessionFactoryBuilder\n作用域是method，用完就释放，避免XML解析资源占用。\nSqlSessionFactory\n作用域是application，只要创建就一直存在，可以通过单例模式来实现。\nSqlSession\n作用域是request或method，每个线程独一份，每次收到HTTP请求，打开一个SqlSession，返回响应后，就关闭它。以下代码能确保每次关闭：\ntry (SqlSession session = sqlSessionFactory.openSession()) { // do work } Mapper Instances\n它是由SqlSession创建的，作用域类似，不过更建议放到method，用完就释放，比如：\ntry (SqlSession session = sqlSessionFactory.openSession()) { BlogMapper mapper = session.getMapper(BlogMapper.class); // do work } 小结 本文首先介绍了MyBatis是什么，然后通过SqlSessionFactoryBuilder→SqlSessionFactory→SqlSession→Mapper Instances链路阐述了MyBatis是如何从数据库查询SQL映射到代码里面的，最后给出了这几个类实例的作用域的使用建议。\n参考资料：\nhttps://mybatis.org/mybatis-3/getting-started.html\nhttps://www.jianshu.com/p/b934b0d72602\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000014-mybatisjava%E6%8C%81%E4%B9%85%E5%B1%82%E6%A1%86%E6%9E%B6mybatis%E5%85%A5%E9%97%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"mybatis是什么\"\u003eMyBatis是什么\u003c/h1\u003e\n\u003cp\u003eMyBatis是Java的持久层框架，GitHub的star数高达15.8k，是Java技术栈中最热门的ORM框架之一。它支持自定义SQL、存储过程以及高级映射，可以通过XML或注解来配置和映射原始类型、接口和Java POJOs为数据库中的记录。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003ePOJOs，Plain Old Java Objects，纯的传统意义的Java对象，最基本的Java Bean只有属性加上属性的get和set方法。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/852aa90922e745658e2b415623bc2566.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"安装\"\u003e安装\u003c/h1\u003e\n\u003cp\u003eMaven pom.xml：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.mybatis\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003emybatis\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003ex.x.x\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e版本查询：https://mvnrepository.com/artifact/org.mybatis/mybatis\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"sqlsessionfactory\"\u003eSqlSessionFactory\u003c/h1\u003e\n\u003cp\u003eSqlSessionFactory的实例是每个MyBatis应用的核心，通过SqlSessionFactoryBuilder创建，能基于XML配置，也能使用Configuration类。\u003c/p\u003e\n\u003cp\u003e基于XML配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresource\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;org/mybatis/example/mybatis-config.xml\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInputStream\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003einputStream\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eResources\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetResourceAsStream\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresource\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSqlSessionFactory\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esqlSessionFactory\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eSqlSessionFactoryBuilder\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003einputStream\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e实际项目中不会手动设置XML文件路径，而是直接读取classpath去找XML。\u003c/p\u003e\n\u003cp\u003e使用Configuration类：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDataSource\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edataSource\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eBlogDataSourceFactory\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetBlogDataSource\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eTransactionFactory\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etransactionFactory\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eJdbcTransactionFactory\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eEnvironment\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eenvironment\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eEnvironment\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;development\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etransactionFactory\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edataSource\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eConfiguration\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econfiguration\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eConfiguration\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eenvironment\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003econfiguration\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eaddMapper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eBlogMapper\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eclass\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSqlSessionFactory\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esqlSessionFactory\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eSqlSessionFactoryBuilder\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfiguration\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e由于Java注解的一些限制以及某些MyBatis映射的复杂性，MyBatis会优先选择XML配置。在上面的示例中，MyBatis会根据classpath和BlogMapper.class去找BlogMapper.xml。\u003c/p\u003e","title":"Java持久层框架Mybatis入门"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPackages Definition: A package is a grouping of related types providing access protection and name space management. Note that types refers to classes, interfaces, enumerations, and annotation types. Enumerations and annotation types are special kinds of classes and interfaces, respectively, so types are often referred to in this section simply as classes and interfaces.\n创建 //in the Draggable.java file package graphics; public interface Draggable { . . . } //in the Graphic.java file package graphics; public abstract class Graphic { . . . } //in the Circle.java file package graphics; public class Circle extends Graphic implements Draggable { . . . } //in the Rectangle.java file package graphics; public class Rectangle extends Graphic implements Draggable { . . . } //in the Point.java file package graphics; public class Point extends Graphic implements Draggable { . . . } //in the Line.java file package graphics; public class Line extends Graphic implements Draggable { . . . } 文件顶部第一行，每个文件只能有1个package关键字。\n命名 Companies use their reversed Internet domain name to begin their package names—for example, com.example.mypackage for a package named mypackage created by a programmer at example.com.\nName collisions that occur within a single company need to be handled by convention within that company, perhaps by including the region or the project name after the company name (for example, com.example.region.mypackage).\nPackages in the Java language itself begin with java. or javax.\n如果按此规则命名出现不合法，使用下划线：\nhyphenated-name.example.org，org.example.hyphenated_name\nexample.int，int_.example\n123name.example.com，com.example._123name\n导包 To use a public package member from outside its package, you must do one of the following:\nRefer to the member by its fully qualified name\ngraphics.Rectangle myRect = new graphics.Rectangle(); Import the package member\nimport graphics.Rectangle; Rectangle myRectangle = new Rectangle(); Import the member\u0026rsquo;s entire package\nimport graphics.*; Circle myCircle = new Circle(); Rectangle myRectangle = new Rectangle(); Java会默认导2个包：\nthe java.lang package and the current package (the package for the current file). Java导父不导子：\nImporting java.awt.* imports all of the types in the java.awt package, but it does not import java.awt.color, java.awt.font, or any other java.awt.xxxx packages. If you plan to use the classes and other types in java.awt.color as well as those in java.awt, you must import both packages with all their files:\nimport java.awt.*; import java.awt.color.*; 命名冲突：如果导的包，类名相同，那么必须加上完整包路径进行区分\ngraphics.Rectangle rect; static import：import the static members\nimport static java.lang.Math.PI; 参考资料：\nPackages https://dev.java/learn/packages/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000014-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B011%E5%8C%85/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"packages\"\u003ePackages\u003c/h2\u003e\n\u003cp\u003eDefinition: A package is a grouping of related types providing access protection and name space management. Note that types refers to \u003cstrong\u003eclasses, interfaces, enumerations, and annotation types\u003c/strong\u003e. Enumerations and annotation types are special kinds of classes and interfaces, respectively, so types are often referred to in this section simply as classes and interfaces.\u003c/p\u003e\n\u003ch2 id=\"创建\"\u003e创建\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//in the Draggable.java file\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003egraphics\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eDraggable\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//in the Graphic.java file\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003egraphics\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eabstract\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eGraphic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//in the Circle.java file\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003egraphics\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eCircle\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eGraphic\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eDraggable\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//in the Rectangle.java file\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003egraphics\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eRectangle\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eGraphic\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eDraggable\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//in the Point.java file\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003egraphics\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003ePoint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eGraphic\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eDraggable\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//in the Line.java file\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003egraphics\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eLine\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eGraphic\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eDraggable\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e文件顶部第一行，每个文件只能有1个\u003ccode\u003epackage\u003c/code\u003e关键字。\u003c/p\u003e","title":"Java官方笔记11包"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\ntep此次更新，旨在从“工具”升级为“框架”，为此做了大量的代码整洁工作，重新设计了部分功能，项目脚手架也焕然一新。\n功能展示 conftest.py 脚手架生成的conftest.py只有一行代码：\nfixture自动加载等操作都隐藏到了tep的科技与狠活里面。\n环境变量模板 使用yaml维护环境变量：\n你可以env_vars目录下维护env_vars_开头的任意命名的yaml模板，内容自定义：\ndomain: \u0026#34;http://127.0.0.1:5000\u0026#34; env: \u0026#34;dev\u0026#34; db: mysql: host: \u0026#34;localhost\u0026#34; port: \u0026#34;3306\u0026#34; user: \u0026#34;root\u0026#34; password: \u0026#34;123456\u0026#34; db: \u0026#34;sys\u0026#34; 取值时只需要引入env_vars fixture，像字典一样取值：\nfrom loguru import logger def test(env_vars): logger.info(env_vars[\u0026#34;domain\u0026#34;]) logger.info(env_vars[\u0026#34;env\u0026#34;]) logger.info(env_vars[\u0026#34;db\u0026#34;][\u0026#34;mysql\u0026#34;][\u0026#34;host\u0026#34;]) 激活哪个模板，就在tep.yaml中配置：\nenv: active: \u0026#34;dev\u0026#34; 工具包与示例代码 工具包有MySql访问、FastAPI Mock、常用函数、HTTP请求、流量录制：\n示例代码有断言、数据库访问、环境变量、faker造数据、常用函数、HTTP请求、从登陆到下单自动化用例：\n项目脚手架 tep startproject demo生成的项目脚手架如图所示：\n没有杂质，纯粹的pytest！\ntep技术内幕 化繁为简 tep只保留了“框架”层面的模块：\n三方库都抽出来到了脚手架的utils包里，譬如HTTP请求在utils/http_client里面。\n项目路径识别 在conftest加载插件时，使用inspect.stack()[1]反推项目路径：\ndef tep_plugins(): caller = inspect.stack()[1] Config.project_root_dir = os.path.dirname(caller.filename) plugins = fixture_paths() # +[其他插件] return plugins fixture自动导入 conftest.py加载时，将fixture路径作为插件加载，一个是项目目录下的fixtures包，一个是tep里面的fixture模块：\ndef fixture_paths(): \u0026#34;\u0026#34;\u0026#34; fixture路径，1、项目下的fixtures；2、tep下的fixture； :return: \u0026#34;\u0026#34;\u0026#34; _fixtures_dir = os.path.join(Config.project_root_dir, \u0026#34;fixtures\u0026#34;) paths = [] # 项目下的fixtures for root, _, files in os.walk(_fixtures_dir): for file in files: if file.startswith(\u0026#34;fixture_\u0026#34;) and file.endswith(\u0026#34;.py\u0026#34;): full_path = os.path.join(root, file) import_path = full_path.replace(_fixtures_dir, \u0026#34;\u0026#34;).replace(\u0026#34;\\\\\u0026#34;, \u0026#34;.\u0026#34;) import_path = import_path.replace(\u0026#34;/\u0026#34;, \u0026#34;.\u0026#34;).replace(\u0026#34;.py\u0026#34;, \u0026#34;\u0026#34;) paths.append(\u0026#34;fixtures\u0026#34; + import_path) # tep下的fixture paths.append(\u0026#34;tep.fixture\u0026#34;) return paths 环境变量 内置env_vars fixture：\n#!/usr/bin/python ## encoding=utf-8 \u0026#34;\u0026#34;\u0026#34; @Author : dongfanger @Date : 2020/12/30 9:30 @Desc : 预置fixture \u0026#34;\u0026#34;\u0026#34; import os import pytest import yaml from loguru import logger from tep.config import tep_config, Config class TepVars: \u0026#34;\u0026#34;\u0026#34;全局变量池\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.vars_ = {} def put(self, key, value): self.vars_[key] = value def get(self, key): value = \u0026#34;\u0026#34; try: value = self.vars_[key] except KeyError: logger.error(f\u0026#34;env_vars doesnt have this key: {key}\u0026#34;) return value @pytest.fixture(scope=\u0026#34;session\u0026#34;) def env_vars(): \u0026#34;\u0026#34;\u0026#34;环境变量，读取resources/env_vars下的变量模板\u0026#34;\u0026#34;\u0026#34; class Clazz(TepVars): def dict_(self): env_active = tep_config()[\u0026#39;env\u0026#39;][\u0026#34;active\u0026#34;] env_filename = f\u0026#34;env_vars_{env_active}.yaml\u0026#34; with open( os.path.join(Config.project_root_dir, \u0026#34;resources\u0026#34;, \u0026#34;env_vars\u0026#34;, env_filename)) as f: return yaml.load(f.read(), Loader=yaml.FullLoader) return Clazz().dict_() TepVars是全局变量池，支持动态put和get。\nenv_vars根据tep.yaml配置的active，获取对应的环境变量模板，解析yaml，返回字典。\n项目脚手架 脚手架代码完整的放在template下面：\n文件后缀均加上了.tep，这样就不会扰乱实际项目代码，在tep startproject时会将这些文件拷贝到目标项目，同时去除.tep后缀，快速生成自动化项目。\n这些模板的源码可以查阅另外一个项目：\nhttps://github.com/dongfanger/tep-template.git\n最新的tep预览功能也能在这个项目看到。\nTry Easy Pytest tep是Try Easy Pytest的首字母缩写，是依托于pytest的一款测试工具。本次工具架构改造是一个开端，tep还会持续迭代开发，逐步引入企业级的一系列技术实践，敬请期待，也欢迎加入我们，一起开发，共创美好未来！\ntep源码：https://github.com/dongfanger/tep\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000014-tep%E6%97%B6%E9%9A%948%E4%B8%AA%E6%9C%88%E8%BF%8E%E6%9D%A5%E9%87%8D%E5%A4%A7%E5%85%A8%E6%96%B0%E5%8D%87%E7%BA%A7/","summary":"\u003cstrong\u003etep此次更新，旨在从“工具”升级为“框架”，为此做了大量的代码整洁工作，重新设计了部分功能，项目脚手架也焕然一新。\u003c/strong\u003e","title":"tep时隔8个月迎来重大全新升级"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n大学，已经过去十年了。\n这周技术群有个讨论：怎么能够持续学习？想要抽时间总结工作经验，梳理技术知识，持续提升自己，可是没有动力让自己去做这件事。\n这个问题，也许我有些发言权，毕竟曾经身边有很多优秀的同学，现在身边也有很多优秀的同事，我也是个以学习为快乐的人。\n2个问题，怎么才能学习起来？怎么才能坚持学习？\n**怎么才能学习起来？**学习这件事，需要一定动力。我的动力很简单，“想成为优秀的人”。\n这是我写文章时才想出来的动力。这似乎很奇怪。有些人是这样的，要开始学习了，那先想个目标吧，一定要先找出能够一辈子坚持的终极目标，以终为始，才开始学习。实际上，这种目标很可能一辈子都找不到。反而会增加负能量，我竟然都不知道自己想要什么。\n**停止寻找学习目标，学习方法才是学习动力。**用方法驱动学习，就好比一艘风帆，在没有风的时候，是划不起来的。可一旦装上发动机，就能跑的和飞艇一样快，去往任何目的地。\n第一个学习方法是，时间管理。学习一下午很累，可以像上课一样，学习30分钟，休息10分钟，就要轻松很多。\n第二个学习方法是，费曼学习法。用自己的话把知识讲出来，这能让学习变得很有趣。\n如果实在不想学习，去自习室，去图书馆，呆一下，或许能找到属于自己的方法。\n怎么才能坚持学习？坚持学习的过程大概是这样：\n并不是一条直线，而是起起伏伏的状态。\n有时候不想学习，并不代表我就不学习了，等到状态回升，我会继续学习，没有内耗和焦虑。\n真正的坚持，是会忘记坚持的，就像每天吃饭一样。\n养成良好学习习惯，自然就会持续学习。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000014-%E6%8C%81%E7%BB%AD%E5%AD%A6%E4%B9%A0%E7%9A%84%E5%8A%A8%E5%8A%9B/","summary":"大学，已经过去十年了。","title":"持续学习的动力"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nnGrinder小众而实用 nGrinder在GitHub的star有1.6k，相比于JMeter6.4k来说，要少很多。在测试资料中，关于压测工具也是以JMeter居多，几乎没有专门针对nGrinder的学习资料。nGrinder既然这么小众，那为什么还要学它呢？因为听说咱们公司的压测平台，底层就是用的nGrinder，对于企业级应用来说，nGrinder还是很实用的，正如它的介绍：“Enterprise level performance testing solution based on The Grinder”。\nnGrinder有以下特点：\n我们编写压测脚本用到的就是第一个特点提到的Groovy脚本，跟Java语法类似，可以无成本切换。\n设计理念 nGrinder架构 nGrinder系统架构如下：\nnGrinder的底层是Grinder，基于Grinder的console和agent，nGrinder封装成了controller和agent：\ncontroller 提供了Web页面创建测试脚本和配置运行策略，这是nGrinder比较突出的特色了。\nagent 虚拟用户生成器，创建压测负载。\n当用户创建test后，会先创建一个console，AgentControllerServer会根据console分配相应的agent，AgentControllerServer管理着agent pool，负责agent的创建和释放。然后console将test脚本和资源多个已分配的agent，并控制整个test flow直到测试结束。测试结束后，agent会回收至AgentControllerServer以便其他test使用，console也会回收至ConsoleManager。\nnGrinder和Ginder最大的不同之处在于，nGrinder在controller中保留了多个console实例和agent。每个console都是跟其他console隔离的，能并发运行。\n不像其他压测工具在测试时，会保留agent以确保用户执行test时agent是可用的，也就是“预占”agent。nGrinder只有在真正测试执行时，动态分配agent，这个方案是nGrinder最独特的架构设计。它保证了多个压测任务能并发执行，并发上限只取决于空闲的agent数量。\nnGrinder3.1版本以前是单机架构的，如下图所示：\n1、nGrinder使用SVNkit，即svn server，来管理脚本文件。svn仓库存放在${NGRINDER_HOME}目录。\n2、nGrinder使用EhCache缓存，提升DB和SVN查询性能。\n3、nGrinder使用Spring Security保证安全，并使用Atlassian支持扩展。\nnGrinder3.1版本之后，支持集群：\n1、集群中所有console共享同样的DB和文件系统，并指向NFS共享的同一个${NGRINDER_HOME}。\n2、每个controller有它自己的${NGRINDER_EX_HOME}，存放特殊配置和日志。\n3、所有controller相互复制EhCache，让某些数据在集群中都是可见的。\nnGrinder技术栈 Client层\nBootstrap、Bootstrap-slider、Jquery、Jquery Validate、Datatables、Select2、JqPlot、CodeMirror。\nController层\nFreeMarker、Spring Security、Spring MVC、GSon、SVNKit Dav。\nService层\nThe Grinder、Atlassian Plugin Framework、Spring、EhCache。\nData层\nSpring Data、H2、Cubrid、Liquibase、SVNKit。\n快速入门 安装 在GitHub Release下载war包：\nhttps://github.com/naver/ngrinder/releases\n执行命令：\njava -jar ngrinder-controller-{version}.war 访问http://localhost:8080/即可。\n但是由于nGrinder需要占用大量内存，最好加上额外参数：\njava -XX:MaxPermSize=200m -jar ngrinder-controller-3.4.war 默认是8080端口，可以指定端口：\njava -XX:MaxPermSize=200m -jar ngrinder-controller-3.4.war --port 80 更多安装说明请参考：\nhttps://github.com/naver/ngrinder/wiki/Installation-Guide\n登录 默认用户名/密码为admin/admin。\n开始 只需要输入url，就能自动生成压测脚本。\n然后配置压测策略：\nAgent：需要多少个agent，也就是需要多少台机器。nGrinder默认一台机器一个agent。\nVuser per agent：每个agent创建多少个Vuser。Vuser相当于线程。\nDuration：持续压多少时间后停止。\nRun Count：运行多少次后停止。\n点击REV:HEAD后，能切换到脚本视图：\n填写好策略后，保存，设置“马上运行”还是“定时运行”：\n确认后在列表能看到压测任务：\n将鼠标悬浮在Status上能看到运行状态的明细：\n运行开始后会有一段启动时间，因为每个test都需要从console分发测试脚本和资源到agent上：\n运行结束后，能看到TPS走势图：\n点击Report in Detail按钮，能查看明细报告：\n包括TPS、Mean Test Time、Errors。如果在agent上安装了monitor，点击左下角的Perfomance Report能看到更详细的压测报告。\n更多关于nGrinder的内容请查阅官方手册和Wiki：\nhttps://github.com/naver/ngrinder/wiki/User-Guide\nnGrinder有三大优势，一是Web界面化操作；二是多压测任务并行；三是天然支持集群发压。值得大家学习使用。\n参考资料：\nnGrinder官网 https://naver.github.io/ngrinder/\nnGrinder架构 https://github.com/naver/ngrinder/wiki/Architecture\nnGrinder安装 https://github.com/naver/ngrinder/wiki/Installation-Guide\nnGrinder入门 https://github.com/naver/ngrinder/wiki/Quick-Start\nnGrinder手册 https://github.com/naver/ngrinder/wiki/User-Guide\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000014-ngrinder%E4%BD%A0%E7%9A%84%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%8E%8B%E6%B5%8B%E5%B7%A5%E5%85%B7%E5%8F%AF%E4%BB%A5%E6%98%AFngrinder/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"ngrinder小众而实用\"\u003enGrinder小众而实用\u003c/h1\u003e\n\u003cp\u003enGrinder在GitHub的star有1.6k，相比于JMeter6.4k来说，要少很多。在测试资料中，关于压测工具也是以JMeter居多，几乎没有专门针对nGrinder的学习资料。nGrinder既然这么小众，那为什么还要学它呢？因为听说咱们\u003cstrong\u003e公司的压测平台，底层就是用的nGrinder\u003c/strong\u003e，对于企业级应用来说，nGrinder还是很实用的，正如它的介绍：“Enterprise level performance testing solution based on The Grinder”。\u003c/p\u003e\n\u003cp\u003enGrinder有以下特点：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1cc334dacd5b43a18998748c60aaeccd.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我们编写压测脚本用到的就是第一个特点提到的Groovy脚本，跟Java语法类似，可以无成本切换。\u003c/p\u003e\n\u003ch1 id=\"设计理念\"\u003e设计理念\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4a5323e837144767bf9b77159c816859.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"ngrinder架构\"\u003enGrinder架构\u003c/h1\u003e\n\u003cp\u003enGrinder系统架构如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a492956cd0794cd68032605a2318fbb3.png\"\u003e\u003c/p\u003e\n\u003cp\u003enGrinder的底层是Grinder，基于Grinder的console和agent，nGrinder封装成了controller和agent：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003econtroller 提供了Web页面创建测试脚本和配置运行策略，这是nGrinder比较突出的特色了。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eagent 虚拟用户生成器，创建压测负载。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e当用户创建test后，会先创建一个console，AgentControllerServer会根据console分配相应的agent，AgentControllerServer管理着agent pool，负责agent的创建和释放。然后console将test脚本和资源多个已分配的agent，并控制整个test flow直到测试结束。测试结束后，agent会回收至AgentControllerServer以便其他test使用，console也会回收至ConsoleManager。\u003c/p\u003e\n\u003cp\u003enGrinder和Ginder最大的不同之处在于，nGrinder在controller中保留了多个console实例和agent。每个console都是跟其他console隔离的，能并发运行。\u003c/p\u003e\n\u003cp\u003e不像其他压测工具在测试时，会保留agent以确保用户执行test时agent是可用的，也就是“预占”agent。nGrinder只有在真正测试执行时，动态分配agent，这个方案是nGrinder最独特的架构设计。它保证了多个压测任务能并发执行，并发上限只取决于空闲的agent数量。\u003c/p\u003e\n\u003cp\u003enGrinder3.1版本以前是单机架构的，如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ddc8a508a37a4721a9af652d9fc8a7b3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1、nGrinder使用SVNkit，即svn server，来管理脚本文件。svn仓库存放在\u003ccode\u003e${NGRINDER_HOME}\u003c/code\u003e目录。\u003c/p\u003e\n\u003cp\u003e2、nGrinder使用EhCache缓存，提升DB和SVN查询性能。\u003c/p\u003e\n\u003cp\u003e3、nGrinder使用Spring Security保证安全，并使用Atlassian支持扩展。\u003c/p\u003e\n\u003cp\u003enGrinder3.1版本之后，支持集群：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3f38396937d14876b2ef552c3c50cb71.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1、集群中所有console共享同样的DB和文件系统，并指向NFS共享的同一个\u003ccode\u003e${NGRINDER_HOME}\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e2、每个controller有它自己的\u003ccode\u003e${NGRINDER_EX_HOME}\u003c/code\u003e，存放特殊配置和日志。\u003c/p\u003e\n\u003cp\u003e3、所有controller相互复制EhCache，让某些数据在集群中都是可见的。\u003c/p\u003e\n\u003ch1 id=\"ngrinder技术栈\"\u003enGrinder技术栈\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eClient层\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eBootstrap、Bootstrap-slider、Jquery、Jquery Validate、Datatables、Select2、JqPlot、CodeMirror。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eController层\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eFreeMarker、Spring Security、Spring MVC、GSon、SVNKit Dav。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eService层\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThe Grinder、Atlassian Plugin Framework、Spring、EhCache。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eData层\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eSpring Data、H2、Cubrid、Liquibase、SVNKit。\u003c/p\u003e\n\u003ch1 id=\"快速入门\"\u003e快速入门\u003c/h1\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003cp\u003e在GitHub Release下载war包：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/naver/ngrinder/releases\"\u003ehttps://github.com/naver/ngrinder/releases\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4549b3e0373b4d3c9a31f4a41d21cdfe.png\"\u003e\u003c/p\u003e\n\u003cp\u003e执行命令：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ejava -jar ngrinder-controller-\u003cspan style=\"color:#0550ae\"\u003e{\u003c/span\u003eversion\u003cspan style=\"color:#0550ae\"\u003e}\u003c/span\u003e.war\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e访问\u003ccode\u003ehttp://localhost:8080/\u003c/code\u003e即可。\u003c/p\u003e\n\u003cp\u003e但是由于nGrinder需要占用大量内存，最好加上额外参数：\u003c/p\u003e","title":"你的下一个压测工具可以是nGrinder"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n“字典这个数据结构活跃在所有Python程序的背后，即便你的源码里并没有直接用到它”，摘抄自《代码之美》第18章Python的字典类：如何打造全能战士。字典是Python语言的基石！在函数的关键字参数、实例的属性和模块的命名空间都能够看到它的身影，我们自己写代码时也经常会用到。\n“集合”这个概念在Python中算是比较年轻的，使用率也比较低，我只在元素去重和求差集并集时使用过。\n字典和集合有个共同点，它们都是基于同一种数据结构实现的：散列表，又叫做哈希表，Hash Table。要理解集合和字典，得先理解散列表。要理解散列表，得先理解可散列的数据类型。\n可散列的数据类型 在Python词汇表中，关于可散列类型的定义有这样一段话：\n“如果一个对象是可散列的，那么在这个对象的生命周期中，它的散列值是不变的，而且这个对象需要实现__hash__（）方法。另外可散列对象还要有__eq__()方法，这样才能跟其他键做比较。如果两个可散列对象是相等的，那么它们的散列值一定是一样的。”\n重点是散列值不变！\n字典的键必须是可散列的，否则变来变去就找不到映射了。\n于是可以得知原子不可变数据类型（str、bytes、和数值类型）都是可散列类型，frozenset冻结不可变集合，也是可散列的。元组有两种情况，一、如果所有元素都是可散列的数据类型，那么元组是可散列的，二、如果元组里面的元素是其他可变类型的引用，那么元组是不可散列的，示例：\n\u0026gt;\u0026gt;\u0026gt; tt = (1, 2, (30, 40)) \u0026gt;\u0026gt;\u0026gt; hash(tt) -3907003130834322577 \u0026gt;\u0026gt;\u0026gt; tl = (1, 2, [30, 40]) \u0026gt;\u0026gt;\u0026gt; hash(tl) Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; TypeError: unhashable type: \u0026#39;list\u0026#39; \u0026gt;\u0026gt;\u0026gt; tf = (1, 2, frozenset([30, 40])) \u0026gt;\u0026gt;\u0026gt; hash(tf) 5149391500123939311 其中tl元组包含了列表（可变），hash()函数报错了。\n散列表简介 假设你们班级100个同学每个人的学号是由院系-年级-班级-编号组成，例如学号为01100168表示是01系-10级-01班-68号。为了快速查找到68号的成绩信息，可以建立一张表，但是不能用学号作为下标，学号的数值实在太大。因此将学号除以1100100取余，即得到编号作为该表的下标。要查找学号为01100168的成绩的时候，只要直接访问表下标为68的数据即可。\n散列表就是一张表，它通过计算一个关于键值的函数，将所需查询的数据映射到表中一个位置来访问记录，这加快了查询速度。这个映射函数称作散列函数，存放记录的表称作散列表。再举个例子，比如下面这几个人物，按数组存储：\n这样我要找到沈嘉文的电话号码，需要顺序查找对比整个数组，第一个余罪，不是，第二个傅老大，不是，直到第三个才找到沈嘉文。换成散列表：\n左边是姓名首字母的Key，右边是电话号码的Value，当我们要查找沈嘉文的时候，通过计算，在s位置，1次查找就找到了。\n为了不让本文显得生硬，接下来先介绍字典和集合，最后再看看散列表是如何实现它们的。\n字典 Mapping和MutableMapping Mapping和MutableMapping是collections.abc模块中的两个抽象基类，它们的作用是作为形式化的文档，定义了构建一个映射类型所需要的最基本的接口。比如判断是否是映射类型：\n\u0026gt;\u0026gt;\u0026gt; from collections import abc \u0026gt;\u0026gt;\u0026gt; my_dict = {} \u0026gt;\u0026gt;\u0026gt; isinstance(my_dict, abc.Mapping) True 非抽象映射类型一般不会直接继承这两个抽象基类，而是会直接对dict或collections.UserDict进行扩展。正是如此，Python标准库里的所有映射类型都是利用dict来实现的。\ndict构造方法 dict构造方法如下：\n\u0026gt;\u0026gt;\u0026gt; a = dict(one=1, two=2, three=3) \u0026gt;\u0026gt;\u0026gt; b = {\u0026#34;one\u0026#34;: 1, \u0026#34;two\u0026#34;: 2, \u0026#34;three\u0026#34;: 3} \u0026gt;\u0026gt;\u0026gt; c = dict(zip([\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;], [1, 2, 3])) \u0026gt;\u0026gt;\u0026gt; d = dict([(\u0026#34;two\u0026#34;, 2), (\u0026#34;one\u0026#34;, 1), (\u0026#34;three\u0026#34;, 3)]) \u0026gt;\u0026gt;\u0026gt; e = dict({\u0026#34;three\u0026#34;: 3, \u0026#34;one\u0026#34;: 1, \u0026#34;two\u0026#34;: 2}) \u0026gt;\u0026gt;\u0026gt; a == b == c == d == e True 一共竟然有五种！还有第六种：字典推导，跟列表推导和生成器表达式类似：\n\u0026gt;\u0026gt;\u0026gt; my_list = [(\u0026#34;two\u0026#34;, 2), (\u0026#34;one\u0026#34;, 1), (\u0026#34;three\u0026#34;, 3)] \u0026gt;\u0026gt;\u0026gt; my_dict = {en: num for en, num in my_list} \u0026gt;\u0026gt;\u0026gt; my_dict {\u0026#39;two\u0026#39;: 2, \u0026#39;one\u0026#39;: 1, \u0026#39;three\u0026#39;: 3} 常见映射方法 除了dict，还有两个dict的变种：defaultdict和OrderedDict，它们对常见映射方法的支持区别如下：\n鸭子类型 鸭子类型是动态语言的说法，指一个对象只要“看起来像鸭子，走起路来像鸭子”，那它就可以被看做是鸭子。比如：\nclass Animal(object): def run(self): print(\u0026#34;The animal is running...\u0026#34;) class Dog(Animal): def run(self): print(\u0026#39;The dog is running...\u0026#39;) class Cat(Animal): def run(self): print(\u0026#39;The cat is running...\u0026#39;) def make_run(animal): animal.run() dog = Dog() cat = Cat() make_run(dog) make_run(cat) 对于 make_run() 函数来说，传入的参数并不一定需要是 Animal 类型的，只需要保证传入的对象有一个 run() 方法即可。\n在静态语言中，如果需要传入 Animal 类型，则传入的对象就必须是 Animal 类型或者它的子类，否则，将无法调用 run() 方法。\nupdate update方法用来更新字典里对应的条目，它处理参数m的方式，是典型的“鸭子类型”。函数首先检查m是否有keys方法，如果有，那么update函数就把它当作映射对象来处理，不关心是不是真的映射类型。如果没有，函数会把m当作包含了键值对(key, value)元素的迭代器。\nPython里大多数映射类型的构造方法都采用了类似的逻辑。\nsetdefault 当字典d[k]不能找到正确的键的时候，Python会抛出异常。也许每个Python使用者都知道可以用d.get(k, default)来代替d[k]，给找不到的键一个默认的返回值。但是要更新字典时，该怎么办呢？比如要在my_dict中添加键为b，值为列表[1, 2, 3, 4, 5, 6]的键值对：\nmy_dict = {\u0026#34;a\u0026#34;: 1} key = \u0026#34;b\u0026#34; my_list = range(2, 7) ## {\u0026#34;a\u0026#34;: 1, \u0026#34;b\u0026#34;: [2, 3, 4, 5, 6]} 不能用mylist[key] = my_list，必须用for循环动态append。\n方法1，先添加空列表，再append：\nmy_dict[key] = [] for i in my_list: my_dict[key].append(i) 方法2，第一次没有键，先用get查询返回空列表，再append，再赋值：\nfor i in my_list: temp = my_dict.get(key, []) temp.append(i) my_dict[key] = temp 方法3，先用if判断，再append：\nfor i in my_list: if key not in my_dict: my_dict[key] = [] my_dict[key].append(i) 方法4，一行代码：\nfor i in my_list: # 除了for循环，一行代码 my_dict.setdefault(key, []).append(i) Python骚操作总是这么多！setdefault你学会了么？\nsetdefault只需要进行一次键查询就可以完成操作，节省键查询，程序更高效。\ndefaultdict字典变种 有没有办法直接执行my_dict[key].append(i)呢？答案是有的，借助defaultdict可以实现：\nimport collections my_dict = collections.defaultdict(list) my_dict[\u0026#34;a\u0026#34;] = 1 key = \u0026#34;b\u0026#34; my_list = range(2, 7) for i in my_list: my_dict[key].append(i) my_dict[key]会按以下步骤执行：\n调用list()来建立一个新列表。 把这个新列表作为值，key作为它的键，放到my_dict中。 返回这个列表的引用。 通过列表引用继续执行append()函数。\ndefaultdict的__init__(self, default_factory=None, **kwargs)有个参数default_factory用来生成默认值，必须是可调用对象。比如：\ndef init_list(): return [0] my_dict = collections.defaultdict(init_list) 注意了！此时my_dict的值是{}空字典，default_factory只会在__getitem__里被调用，也就是说my_dict[key]时才会用这个默认值：\nprint(my_dict) # defaultdict(\u0026lt;function init_list at 0x014E84F0\u0026gt;, {}) print(my_dict[\u0026#34;b\u0026#34;]) # defaultdict(\u0026lt;function init_list at 0x014E84F0\u0026gt;, {\u0026#39;b\u0026#39;: [0]}) my_dict.get(\u0026ldquo;b\u0026rdquo;)不会调用__getitem__，不会使用default_factory，返回值为None。\n为什么get不会调用__getitem__?__getitem__是为[]提供的语法糖，get()已经是取值方法了，不需要这个语法糖。\ndefault_factory默认为None，如果不指定，查询不存在的键会触发KeyError，这个道理和[]取值是一样的。\n所有这一切背后的功臣其实是魔法方法__missing__。所有的映射类型在处理找不到的键的时候，都会牵扯到__missing__方法。基类dict并没有定义这个方法，但是dict是能知道它的，如果一个类继承了dict，然后实现了__missing__方法，Python就会自动调用它，而不是抛出一个KeyError异常。\n__missing__只会被__getitem__调用，这就是default_factory只对__getitem__有作用的原因！\n示例如下，当用非字符串键查询时，转换为字符串键查询：\nclass StrKeyDict0(dict): # \u0026lt;1\u0026gt; def __missing__(self, key): if isinstance(key, str): # \u0026lt;2\u0026gt; 不加这个判断，如果str(key)不存在，就会第3处再次调用__missing__无限递归 raise KeyError(key) return self[str(key)] # \u0026lt;3\u0026gt; def get(self, key, default=None): try: return self[key] # \u0026lt;4\u0026gt; except KeyError: return default # \u0026lt;5\u0026gt; # k in my_dict 会导致__contains__递归调用，所以这里用了self.keys() def __contains__(self, key): return key in self.keys() or str(key) in self.keys() # \u0026lt;6\u0026gt; 像k in my_dict.keys()这种操作在Python3中是很快的，而且即便映射类型对象很庞大也没关系，这是因为dict.keys()的返回值是一个“视图”。\nOrderdDict及其他字典变种 collections.OrderedDict\n在Django REST framework中的分页就用到了OrderedDict，返回分页数据必须是有序的，否则会提示UnOrdered。OrderedDict的popitem方法默认删除并返回字典里的最后一个元素（栈），如果加了参数OrderedDict(last=False)，那么它会删除并返回第一个被添加进度的元素（队列）。\ncollections.ChainMap\n示例：\nimport builtins pylookup = ChainMap(locals(), globals(), vars(builtins)) 该类型可以容纳多个不同的映射对象，在按键查找时，这些对象会被当作一个整体被逐一查找。\ncollections.Counter\n示例：\n\u0026gt;\u0026gt;\u0026gt; import collections \u0026gt;\u0026gt;\u0026gt; ct = collections.Counter(\u0026#34;abracadabra\u0026#34;) \u0026gt;\u0026gt;\u0026gt; ct Counter({\u0026#39;a\u0026#39;: 5, \u0026#39;b\u0026#39;: 2, \u0026#39;r\u0026#39;: 2, \u0026#39;c\u0026#39;: 1, \u0026#39;d\u0026#39;: 1}) \u0026gt;\u0026gt;\u0026gt; ct.update(\u0026#34;aaaaazzz\u0026#34;) \u0026gt;\u0026gt;\u0026gt; ct Counter({\u0026#39;a\u0026#39;: 10, \u0026#39;z\u0026#39;: 3, \u0026#39;b\u0026#39;: 2, \u0026#39;r\u0026#39;: 2, \u0026#39;c\u0026#39;: 1, \u0026#39;d\u0026#39;: 1}) \u0026gt;\u0026gt;\u0026gt; ct.most_common() [(\u0026#39;a\u0026#39;, 10), (\u0026#39;z\u0026#39;, 3), (\u0026#39;b\u0026#39;, 2), (\u0026#39;r\u0026#39;, 2), (\u0026#39;c\u0026#39;, 1), (\u0026#39;d\u0026#39;, 1)] \u0026gt;\u0026gt;\u0026gt; ct.most_common(2) [(\u0026#39;a\u0026#39;, 10), (\u0026#39;z\u0026#39;, 3)] Counter是用来给可散列表对象计数的。\ncollections.UserDict\n让用户继承写子类。\n它比dict更适合继承的原因是，后者有时会在某些方法的实现上走一些捷径，导致我们不得不在它的子类中重写这些方法，而UserDict就不需要。\n不可变映射类型 借助MappingProxyType，可以实现不可变字典。它返回的是一个只读的视图，会跟随源字典动态展示，但是无法对源字典做出改动。示例：\n\u0026gt;\u0026gt;\u0026gt; from types import MappingProxyType \u0026gt;\u0026gt;\u0026gt; d = {1: \u0026#34;A\u0026#34;} \u0026gt;\u0026gt;\u0026gt; d_proxy = MappingProxyType(d) \u0026gt;\u0026gt;\u0026gt; d_proxy mappingproxy({1: \u0026#39;A\u0026#39;}) \u0026gt;\u0026gt;\u0026gt; d_proxy[1] \u0026#39;A\u0026#39; \u0026gt;\u0026gt;\u0026gt; d_proxy[2] = \u0026#34;x\u0026#34; Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; TypeError: \u0026#39;mappingproxy\u0026#39; object does not support item assignment \u0026gt;\u0026gt;\u0026gt; d[2] = \u0026#34;B\u0026#34; \u0026gt;\u0026gt;\u0026gt; d_proxy mappingproxy({1: \u0026#39;A\u0026#39;, 2: \u0026#39;B\u0026#39;}) 小结 本文为上篇，主要介绍了散列表和字典，包含了一些Python骚操作，也用示例解释了什么是鸭子类型。下篇将介绍集合，以及散列表是如何实现dict和set的。\n参考资料：\n《流畅的Python》\nhttps://zhuanlan.zhihu.com/p/64853220\nhttps://www.jianshu.com/p/101c263cd93e\nhttp://www.woshipm.com/pmd/805326.html\nhttps://zhuanlan.zhihu.com/p/149463934?from_voters_page=true\nhttps://www.jianshu.com/p/e97044a8169a\nhttps://github.com/fluentpython/example-code/blob/master/03-dict-set/strkeydict0.py\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000014-python%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90python%E5%AD%97%E5%85%B8%E5%92%8C%E9%9B%86%E5%90%88%E4%B8%8A/","summary":"“字典这个数据结构活跃在所有Python程序的背后，即便你的源码里并没有直接用到它”，摘抄自《代码之美》第18章Python的字典类：如何打造全能战士。字典是Python语言的基石！在函数的关键字参数、实例的属性和模块的命名空间都能够看到它的身影，我们自己写代码时也经常会用到。","title":"深度剖析Python字典和集合（上）"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n9.1 基于评估降低AI应用的不确定性 9.1.1 从确定性到不确定性 传统软件在很大程度上是确定性的，而AI应用其行为本质上是非确定性和概率性的。不确定性，就意味着对于相同的输入，它们可能产生不同的输出。\n传统的QA流程，专为可预测的、基于规则的系统而设计，已无法充分应对这些由数据驱动的、自适应系统的挑战。\n9.1.2 幻觉和不确定性的根因 AI应用的核心功能是基于从海量数据中学到的统计模式，来预测序列中的下一个词，而非真正意义上的理解。这就导致了“幻觉”现象，即模型生成听起来合理但实际上不正确或无意义的输出。\n产生幻觉和不确定性的根本原因错综复杂：数据导致的缺陷、架构与建模的产物、错位与不确定性。\n9.1.3 AI评估的重要些 从服务客户上看，评估是构建信任的核心机制。从商业视角看，严格评估直接转化为竞争优势。\nAI的非确定性催生了评估经济，这是一个由可观测性平台、持续监控、自动化评估与治理即服务构成的新生态。评估不再是阶段性任务，而是与开发并行的持续工程实践，标志着AI原生时代质量保障的新范式。\n9.2 AI评估体系 9.2.1 评估方法的基础二分法 1、内在评估 vs 外在评估\n内在评估：侧重于孤立地评估模型输出的固有质量，而不考虑其在特定任务中的应用效果。评估的维度通常包括流畅性、连贯性、语法正确性以及事实准确性。\n外在评估：通过衡量模型在特定下游任务或应用中的表现来评估其质量。\n2、自动化评估 vs 人工评估\n自动化评估：利用计算指标或利用其他AI模型来对模型输出进行打分。\n人工评估：依赖人类评估员的判断来评估AI系统的输出质量，特别是在衡量帮助性、创造力、用户满意度等主观维度时。\n在实践中，最佳的评估策略通常是自动化和人工评估的混合模式，利用自动化的效率进行大规模初步筛选，并结合人工的深度洞察力对关键或模糊的案例进行精细评估。\n9.2.2 从静态到动态评估的演进 静态基准：静态基准是固定的、预先定义的数据集和任务组成。\n动态评估：动态评估旨在模拟真实世界场景的复杂性和不可预测性，通过变化的变量、意外的输入和演进的上下文来测试系统。主要的动态评估方法包括：基于模拟的测试、实时对抗性基准、程序化生成。\n9.2.3 现代AI评估的多范式全景 6个主要的评估范式：基准测试范式、“Evals”范式、心理测量学范式、人机交互范式、形式化方法范式、社会技术范式。\n一个组织的评估栈，即其选择、组合和优先排序不同评估范式的策略，深刻地揭示了该组织在性能、用户价值和风险规避之间的权衡。\n9.2.4 构建一个整体性的评估体系 一个完整的评估体系应包含8个核心组件：\n性能指标，包括准确率、精确率、召回率、F1分数等。 鲁棒性与泛化测试 偏见与公平性评估 可解释性 合理性与伦理考量 持续监控与模型飘移检测 决策框架 自动化与人在回路 9.3 基于LLM的自动化评估 9.3.1 传统指标的局限性 传统的自动化评估指标，其核心是基于词汇或短语的重叠度来计算分数，这种方法对于需要捕捉语义、风格、语气和创造力等细微差别的现代生成AI模型来说，存在根本性不足。\n另一方面，虽然人工评估被视为评估质量的“黄金标准”，但其高昂的成本、漫长的周期和固有的主观性，使其难以适应AI技术快速迭代的开发节奏。\n9.3.2 LLM-as-a-Judge范式介绍 LLM-as-a-Judge范式，利用一个功能强大的大型语言模型来扮演裁判的角色，对另一个AI模型或应用的输出进行评分、排序或选择。\n核心应用场景：数据标注、实时验证、为模型优化提供反馈。\n9.3.3 LLM裁判的评估模式 逐点评估：裁判独立地评估单个模型输出，并根据预定义的量表给出一个绝对分数。\n成对比较：裁判会同时看到两个来自不同模型的、匿名的输出，并被要求判断哪一个更好，或者两者是否相当。这是聊天机器人竞技场这样的众包评估平台所采用的核心方法。\n9.3.4 控制LLM裁判的负面影响 1、系统性偏见：位置偏见、冗长偏见、自我偏好偏见、情绪与语气偏见、评分粒度有限。\nLLM裁判产生的定量分数看似是精确的、数据驱动的，但实际上它们是通过一个充满主观偏见的过程生成的。因此一个来自LLM裁判的排行榜分数，并非对模型质量的纯粹客观测量，而是对裁判模型内在偏好和价值取向的反映。\n2、递归依赖：能力天花板、推理幻觉。\n一个较弱的LLM裁判无法公正地评估一个比它更强的候选模型。随着AI模型变得越来越强大，在评估其安全性和可靠性时，对人类专家和红队演练的依赖将会增加，而不是减少。\n9.3.5 提升裁判可靠性的缓解策略 1、提示工程技术：思维链与分布推理、少样本提示、清晰的评分指南。\n2、结构化缓解措施：交换位置、基于参考的评分、结束裁判输出。\n3、基于模型的解决方案：微调、使用多个裁判。\n4、高质量数据集：数据集、Bad Case集合。\n9.4 自动化评估落地实践 9.4.1 搭建评估系统的痛点 数据的采集、LLM Judger自身的准确性问题、Ground Truth的获取困难、特定领域黄金指标的定义与构建困难、评估成本高难以规模化、数据预处理的困难、从评估到可行动的断层。\n9.4.2 评估系统的不同Level 9.4.3 云原生评估系统 阿里云云原生可观测方案提供了一站式的数据采集、存储、评估、语义检索能力。\n9.4.4 评估系统总结 云原生集成了一站式的Trace数据采集，以及前处理、评估、后处理结合在同一个计算引擎中，开发者可以在一个系统内完成所有的评估任务。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005009-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E8%AF%84%E4%BC%B0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"91-基于评估降低ai应用的不确定性\"\u003e9.1 基于评估降低AI应用的不确定性\u003c/h1\u003e\n\u003ch2 id=\"911-从确定性到不确定性\"\u003e9.1.1 从确定性到不确定性\u003c/h2\u003e\n\u003cp\u003e传统软件在很大程度上是确定性的，而AI应用其行为本质上是非确定性和概率性的。不确定性，就意味着对于相同的输入，它们可能产生不同的输出。\u003c/p\u003e\n\u003cp\u003e传统的QA流程，专为可预测的、基于规则的系统而设计，已无法充分应对这些由数据驱动的、自适应系统的挑战。\u003c/p\u003e\n\u003ch2 id=\"912-幻觉和不确定性的根因\"\u003e9.1.2 幻觉和不确定性的根因\u003c/h2\u003e\n\u003cp\u003eAI应用的核心功能是基于从海量数据中学到的统计模式，来预测序列中的下一个词，而非真正意义上的理解。这就导致了“幻觉”现象，即模型生成听起来合理但实际上不正确或无意义的输出。\u003c/p\u003e\n\u003cp\u003e产生幻觉和不确定性的根本原因错综复杂：数据导致的缺陷、架构与建模的产物、错位与不确定性。\u003c/p\u003e\n\u003ch2 id=\"913-ai评估的重要些\"\u003e9.1.3 AI评估的重要些\u003c/h2\u003e\n\u003cp\u003e从服务客户上看，评估是构建信任的核心机制。从商业视角看，严格评估直接转化为竞争优势。\u003c/p\u003e\n\u003cp\u003eAI的非确定性催生了评估经济，这是一个由可观测性平台、持续监控、自动化评估与治理即服务构成的新生态。评估不再是阶段性任务，而是与开发并行的持续工程实践，标志着AI原生时代质量保障的新范式。\u003c/p\u003e\n\u003ch1 id=\"92-ai评估体系\"\u003e9.2 AI评估体系\u003c/h1\u003e\n\u003ch2 id=\"921-评估方法的基础二分法\"\u003e9.2.1 评估方法的基础二分法\u003c/h2\u003e\n\u003cp\u003e1、内在评估 vs 外在评估\u003c/p\u003e\n\u003cp\u003e内在评估：侧重于孤立地评估模型输出的固有质量，而不考虑其在特定任务中的应用效果。评估的维度通常包括流畅性、连贯性、语法正确性以及事实准确性。\u003c/p\u003e\n\u003cp\u003e外在评估：通过衡量模型在特定下游任务或应用中的表现来评估其质量。\u003c/p\u003e\n\u003cp\u003e2、自动化评估 vs 人工评估\u003c/p\u003e\n\u003cp\u003e自动化评估：利用计算指标或利用其他AI模型来对模型输出进行打分。\u003c/p\u003e\n\u003cp\u003e人工评估：依赖人类评估员的判断来评估AI系统的输出质量，特别是在衡量帮助性、创造力、用户满意度等主观维度时。\u003c/p\u003e\n\u003cp\u003e在实践中，最佳的评估策略通常是自动化和人工评估的混合模式，利用自动化的效率进行大规模初步筛选，并结合人工的深度洞察力对关键或模糊的案例进行精细评估。\u003c/p\u003e\n\u003ch2 id=\"922-从静态到动态评估的演进\"\u003e9.2.2 从静态到动态评估的演进\u003c/h2\u003e\n\u003cp\u003e静态基准：静态基准是固定的、预先定义的数据集和任务组成。\u003c/p\u003e\n\u003cp\u003e动态评估：动态评估旨在模拟真实世界场景的复杂性和不可预测性，通过变化的变量、意外的输入和演进的上下文来测试系统。主要的动态评估方法包括：基于模拟的测试、实时对抗性基准、程序化生成。\u003c/p\u003e\n\u003ch2 id=\"923-现代ai评估的多范式全景\"\u003e9.2.3 现代AI评估的多范式全景\u003c/h2\u003e\n\u003cp\u003e6个主要的评估范式：基准测试范式、“Evals”范式、心理测量学范式、人机交互范式、形式化方法范式、社会技术范式。\u003c/p\u003e\n\u003cp\u003e一个组织的评估栈，即其选择、组合和优先排序不同评估范式的策略，深刻地揭示了该组织在性能、用户价值和风险规避之间的权衡。\u003c/p\u003e\n\u003ch2 id=\"924-构建一个整体性的评估体系\"\u003e9.2.4 构建一个整体性的评估体系\u003c/h2\u003e\n\u003cp\u003e一个完整的评估体系应包含8个核心组件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e性能指标，包括准确率、精确率、召回率、F1分数等。\u003c/li\u003e\n\u003cli\u003e鲁棒性与泛化测试\u003c/li\u003e\n\u003cli\u003e偏见与公平性评估\u003c/li\u003e\n\u003cli\u003e可解释性\u003c/li\u003e\n\u003cli\u003e合理性与伦理考量\u003c/li\u003e\n\u003cli\u003e持续监控与模型飘移检测\u003c/li\u003e\n\u003cli\u003e决策框架\u003c/li\u003e\n\u003cli\u003e自动化与人在回路\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"93-基于llm的自动化评估\"\u003e9.3 基于LLM的自动化评估\u003c/h1\u003e\n\u003ch2 id=\"931-传统指标的局限性\"\u003e9.3.1 传统指标的局限性\u003c/h2\u003e\n\u003cp\u003e传统的自动化评估指标，其核心是基于词汇或短语的重叠度来计算分数，这种方法对于需要捕捉语义、风格、语气和创造力等细微差别的现代生成AI模型来说，存在根本性不足。\u003c/p\u003e\n\u003cp\u003e另一方面，虽然人工评估被视为评估质量的“黄金标准”，但其高昂的成本、漫长的周期和固有的主观性，使其难以适应AI技术快速迭代的开发节奏。\u003c/p\u003e\n\u003ch2 id=\"932-llm-as-a-judge范式介绍\"\u003e9.3.2 LLM-as-a-Judge范式介绍\u003c/h2\u003e\n\u003cp\u003eLLM-as-a-Judge范式，利用一个功能强大的大型语言模型来扮演裁判的角色，对另一个AI模型或应用的输出进行评分、排序或选择。\u003c/p\u003e\n\u003cp\u003e核心应用场景：数据标注、实时验证、为模型优化提供反馈。\u003c/p\u003e\n\u003ch2 id=\"933-llm裁判的评估模式\"\u003e9.3.3 LLM裁判的评估模式\u003c/h2\u003e\n\u003cp\u003e逐点评估：裁判独立地评估单个模型输出，并根据预定义的量表给出一个绝对分数。\u003c/p\u003e\n\u003cp\u003e成对比较：裁判会同时看到两个来自不同模型的、匿名的输出，并被要求判断哪一个更好，或者两者是否相当。这是聊天机器人竞技场这样的众包评估平台所采用的核心方法。\u003c/p\u003e\n\u003ch2 id=\"934-控制llm裁判的负面影响\"\u003e9.3.4 控制LLM裁判的负面影响\u003c/h2\u003e\n\u003cp\u003e1、系统性偏见：位置偏见、冗长偏见、自我偏好偏见、情绪与语气偏见、评分粒度有限。\u003c/p\u003e\n\u003cp\u003eLLM裁判产生的定量分数看似是精确的、数据驱动的，但实际上它们是通过一个充满主观偏见的过程生成的。因此一个来自LLM裁判的排行榜分数，并非对模型质量的纯粹客观测量，而是对裁判模型内在偏好和价值取向的反映。\u003c/p\u003e\n\u003cp\u003e2、递归依赖：能力天花板、推理幻觉。\u003c/p\u003e\n\u003cp\u003e一个较弱的LLM裁判无法公正地评估一个比它更强的候选模型。随着AI模型变得越来越强大，在评估其安全性和可靠性时，对人类专家和红队演练的依赖将会增加，而不是减少。\u003c/p\u003e\n\u003ch2 id=\"935-提升裁判可靠性的缓解策略\"\u003e9.3.5 提升裁判可靠性的缓解策略\u003c/h2\u003e\n\u003cp\u003e1、提示工程技术：思维链与分布推理、少样本提示、清晰的评分指南。\u003c/p\u003e","title":"AI评估"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n注解 注解的作用：\nInformation for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings. Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth. Runtime processing — Some annotations are available to be examined at runtime. 注解格式，使用@：\n@Entity 带key-value：\n@Author( name = \u0026#34;Benjamin Franklin\u0026#34;, date = \u0026#34;3/27/2003\u0026#34; ) class MyClass { ... } 只有1个key时可以省略key：\n@SuppressWarnings(value = \u0026#34;unchecked\u0026#34;) void myMethod() { ... } @SuppressWarnings(\u0026#34;unchecked\u0026#34;) void myMethod() { ... } 同时使用多个注解：\n@Author(name = \u0026#34;Jane Doe\u0026#34;) @EBook class MyClass { ... } The annotation type can be one of the types that are defined in the java.lang or java.lang.annotation packages of the Java SE API.\n哪里能用注解 ① Annotations can be applied to declarations: declarations of classes, fields, methods, and other program elements.\n② Java SE 8，annotations can also be applied to the use of types：\nClass instance creation expression:\nnew @Interned MyObject(); Type cast:\nmyString = (@NonNull String) str; implements clause:\nclass UnmodifiableList\u0026lt;T\u0026gt; implements @Readonly List\u0026lt;@Readonly T\u0026gt; { ... } Thrown exception declaration:\nvoid monitorTemperature() throws @Critical TemperatureException { ... } 自定义注解 使用@interface：\n@interface ClassPreamble { String author(); String date(); int currentRevision() default 1; String lastModified() default \u0026#34;N/A\u0026#34;; String lastModifiedBy() default \u0026#34;N/A\u0026#34;; // Note use of array String[] reviewers(); } 注解其实也是一种接口，只是要使用@来声明。\n使用：\n@ClassPreamble ( author = \u0026#34;John Doe\u0026#34;, date = \u0026#34;3/17/2002\u0026#34;, currentRevision = 6, lastModified = \u0026#34;4/12/2004\u0026#34;, lastModifiedBy = \u0026#34;Jane Doe\u0026#34;, // Note array notation reviewers = {\u0026#34;Alice\u0026#34;, \u0026#34;Bob\u0026#34;, \u0026#34;Cindy\u0026#34;} ) public class Generation3List extends Generation2List { // class code goes here } Java预置注解 @Deprecated\n// Javadoc comment follows /** * @deprecated * explanation of why it was deprecated */ @Deprecated static void deprecatedMethod() { } @Override\n// mark method as a superclass method // that has been overridden @Override int overriddenMethod() { } @SuppressWarnings\n// use a deprecated method and tell // compiler not to generate a warning @SuppressWarnings(\u0026#34;deprecation\u0026#34;) void useDeprecatedMethod() { // deprecation warning // - suppressed objectOne.deprecatedMethod(); } @SafeVarargs\n@FunctionalInterface\n注解上的注解：Annotations that apply to other annotations are called meta-annotations. There are several meta-annotation types defined in java.lang.annotation.\n@Retention\nRetentionPolicy.SOURCE – The marked annotation is retained only in the source level and is ignored by the compiler. RetentionPolicy.CLASS – The marked annotation is retained by the compiler at compile time, but is ignored by the Java Virtual Machine (JVM). RetentionPolicy.RUNTIME – The marked annotation is retained by the JVM so it can be used by the runtime environment. @Documented\n@Target\nElementType.ANNOTATION_TYPE can be applied to an annotation type. ElementType.CONSTRUCTOR can be applied to a constructor. ElementType.FIELD can be applied to a field or property. ElementType.LOCAL_VARIABLE can be applied to a local variable. ElementType.METHOD can be applied to a method-level annotation. ElementType.MODULE can be applied to a module declaration. ElementType.PACKAGE can be applied to a package declaration. ElementType.PARAMETER can be applied to the parameters of a method. ElementType.RECORD_COMPONENT can be applied to the component of a record. ElementType.TYPE can be applied to the declaration of a class, an abtract class, an interface, an annotation interface, an enumeration, or a record declaration. ElementType.TYPE_PARAMETER can be applied on the parameters of a type. ElementType.TYPE_USE can be applied where a type is used, for instance on the declaration of a field. @Inherited\n@Repeatable\n重复注解也是允许的：\n@Schedule(dayOfMonth=\u0026#34;last\u0026#34;) @Schedule(dayOfWeek=\u0026#34;Fri\u0026#34;, hour=\u0026#34;23\u0026#34;) public void doPeriodicCleanup() { ... } 重复注解的定义要用到@Repeatable：\n@Repeatable(Schedules.class) public @interface Schedule { String dayOfMonth() default \u0026#34;first\u0026#34;; String dayOfWeek() default \u0026#34;Mon\u0026#34;; int hour() default 12; } 并且，The containing annotation type must have a value element with an array type：\npublic @interface Schedules { Schedule[] value(); } the containing annotation type is @Schedules, so repeating @Schedule annotations is stored in an @Schedules annotation.\nSchedule[] value()是一个没有参数、返回类型为Schedule[]的方法的声明，它是一个抽象方法。事实上，这个方法声明是用来定义注解的属性的，与普通方法不同的是，它没有方法体，只有方法声明，而方法的具体实现则由使用该注解的代码来完成。在使用该注解时，也可以通过指定该属性的值来进行赋值操作，例如：\n@Schedules({ @Schedule(dayOfMonth=\u0026#34;last\u0026#34;), @Schedule(dayOfWeek=\u0026#34;Fri\u0026#34;, hour=\u0026#34;23\u0026#34;) }) public class MyScheduledTask { // ... } 在上述代码中，我们使用了@Schedules注解，并且指定了它的value属性，也就是给Schedule[] value()方法赋上相应的值。注意到，该属性的值是一个注解数组，因此需要使用大括号{}将多个注解组合起来。\n参考资料：\nAnnotations https://dev.java/learn/annotations/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000013-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B010%E6%B3%A8%E8%A7%A3/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"注解\"\u003e注解\u003c/h2\u003e\n\u003cp\u003e注解的作用：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eInformation for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.\u003c/li\u003e\n\u003cli\u003eCompile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.\u003c/li\u003e\n\u003cli\u003eRuntime processing — Some annotations are available to be examined at runtime.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e注解格式，使用\u003ccode\u003e@\u003c/code\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Entity\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e带key-value：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Author\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003ename\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Benjamin Franklin\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003edate\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;3/27/2003\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyClass\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e只有1个key时可以省略key：\u003c/p\u003e","title":"Java官方笔记10注解"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文将通过WebRTC本地音视频流压测脚本，来说明JMeter如何扩展Java请求。\nWebRTC WebRTC是Web Real-Time Communication缩写，指网页即时通讯，是一个支持Web浏览器进行实时语音或视频对话的API，实现了基于网页的视频会议，比如声网的Agora Web SDK就是基于WebRTC实现音视频通信的。与HTTP不同，WebRTC应用的主要压力是码流，JMeter没有找到提供WebRTC Sampler的第三方jar包，只能自己尝试写一个。\n正常情况是打开浏览器，打开摄像头和麦克风输入音视频流进行请求传输，测试模拟采用无头浏览器，读取本地文件作为音视频输入。\n无头浏览器是指没有界面的浏览器，通过调用浏览器API来模拟操作，比如Chrome在启动时添加--headless，就可以进入无头模式。\nWebRTC是使用JavaScript编写的，在前端领域生态相对来说丰富一些，有现成可用的Node库Puppeteer来支持无头浏览器：\n为了让JMeter能并发调用，需要编写Java代码调用Puppeteer，听着有点想象力，实际上已经有封装好的开源库了：jvppeteer。\n写Java代码 在pom.xml中添加依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.github.fanyong920\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jvppeteer\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.jmeter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;ApacheJMeter_core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.jmeter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;ApacheJMeter_java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; jvppeteer是Java封装Puppeteer包，ApacheJMeter_core和ApacheJMeter_java用来扩展JMeter。\n新建\\src\\main\\java\\App.java：\nimport com.ruiyun.jvppeteer.core.Puppeteer; import com.ruiyun.jvppeteer.core.browser.Browser; import com.ruiyun.jvppeteer.core.page.Page; import com.ruiyun.jvppeteer.options.LaunchOptions; import com.ruiyun.jvppeteer.options.LaunchOptionsBuilder; import org.apache.jmeter.config.Arguments; import org.apache.jmeter.protocol.java.sampler.JavaSamplerClient; import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext; import org.apache.jmeter.samplers.SampleResult; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; public class App implements JavaSamplerClient { Browser browser; public Arguments getDefaultParameters() { Arguments params = new Arguments(); params.addArgument(\u0026#34;chromePath\u0026#34;, \u0026#34;C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\u0026#34;); params.addArgument(\u0026#34;fakeVideoPath\u0026#34;, \u0026#34;D:\\\\test.y4m\u0026#34;); params.addArgument(\u0026#34;fakeAudioPath\u0026#34;, \u0026#34;D:\\\\test.wav\u0026#34;); params.addArgument(\u0026#34;isHeadless\u0026#34;, \u0026#34;true\u0026#34;); params.addArgument(\u0026#34;isLocalMedia\u0026#34;, \u0026#34;true\u0026#34;); params.addArgument(\u0026#34;isDefaultMedia\u0026#34;, \u0026#34;false\u0026#34;); params.addArgument(\u0026#34;meetingUrl\u0026#34;, \u0026#34;https://test.io\u0026#34;); return params; } @Override public void setupTest(JavaSamplerContext javaSamplerContext) { String chromePath = javaSamplerContext.getParameter(\u0026#34;chromePath\u0026#34;); String fakeVideoPath = javaSamplerContext.getParameter(\u0026#34;fakeVideoPath\u0026#34;); String fakeAudioPath = javaSamplerContext.getParameter(\u0026#34;fakeAudioPath\u0026#34;); String path = new String(chromePath.getBytes(), StandardCharsets.UTF_8); ArrayList\u0026lt;String\u0026gt; argList = new ArrayList\u0026lt;\u0026gt;(); argList.add(\u0026#34;--no-sandbox\u0026#34;); argList.add(\u0026#34;--disable-setuid-sandbox\u0026#34;); argList.add(\u0026#34;--ignore-certificate-errors\u0026#34;); argList.add(\u0026#34;--use-fake-ui-for-media-stream\u0026#34;); argList.add(\u0026#34;--use-fake-device-for-media-stream\u0026#34;); if (javaSamplerContext.getParameter(\u0026#34;isLocalMedia\u0026#34;).equals(\u0026#34;true\u0026#34;)) { argList.add(\u0026#34;--use-file-for-fake-video-capture=\u0026#34; + fakeVideoPath); argList.add(\u0026#34;--use-file-for-fake-audio-capture=\u0026#34; + fakeAudioPath); } boolean isHeadless = javaSamplerContext.getParameter(\u0026#34;isHeadless\u0026#34;).equals(\u0026#34;true\u0026#34;); LaunchOptions options = new LaunchOptionsBuilder().withArgs(argList).withHeadless(isHeadless).withExecutablePath(path).build(); try { browser = Puppeteer.launch(options); } catch (IOException e) { e.printStackTrace(); } } @Override public SampleResult runTest(JavaSamplerContext javaSamplerContext) { try { Page page = browser.newPage(); page.goTo(javaSamplerContext.getParameter(\u0026#34;meetingUrl\u0026#34;)); } catch (Exception e) { e.printStackTrace(); } return null; } @Override public void teardownTest(JavaSamplerContext javaSamplerContext) { browser.close(); } } App类继承了JavaSamplerClient类。getDefaultParameters()定义了JMeter界面参数。setupTest()是测试初始化，创建无头浏览器。runTest()是测试执行，访问会议URL进行推流。teardownTest()是测试清理，关闭无头浏览器。setupTest()和teardownTest()在运行时每个线程只会执行一次。\n无头浏览器核心参数配置如下：\n打Jar包 Java代码写好后，需要打成jar包提供给JMeter调用。打包过程如下：\n点击右上角Project Structure：\n打开Artifacts，点击+号，填写名字，在右边区域双击编译后输出文件目录到左边：\n点击OK确认后，从菜单栏找到Build Artifacts点击：\nBuild就可以了：\nJMeter使用 首先需要把jar包复制到lib\\ext目录下，webrtcTest.jar位置如下：\n除了webrtcTest.jar，还依赖3个jar包，在Settings\\Build\\Build Tools\\Maven找到Local repository本地仓库目录后打开，jvppeteer-1.1.2.jar存放位置如下：\ncommons-compress-1.20.jar存放位置如下：\nJava-WebSocket-1.5.0.jar存放位置如下：\n然后打开JMeter，添加线程组，添加Java请求，选择刚才创建的类，初始参数也加载出来了：\n默认音视频流是这样：\n本地音视频流是这样：\n脚本弄好了就可以开始对WebRTC应用进行压测了。\n小结 本文介绍了如何使用Java对Puppeteer封装的jvppeteer，实现对WebRTC进行本地音视频流的压测脚本，打包成jar包后，可以在JMeter中进行扩展，通过Java请求Sampler来调用。Headless Chrome对.y4m格式视频和.wav格式音频支持较好，建议用这两种格式做测试。\n参考资料：\nhttps://www.cnblogs.com/chenkx6/p/13639629.html\nhttps://baike.baidu.com/item/WebRTC/5522744?fr=aladdin\nhttps://github.com/puppeteer/puppeteer\nhttps://github.com/fanyong920/jvppeteer\n.y4m视频下载 https://media.xiph.org/video/derf/\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000013-jmeterjmeter%E6%89%A9%E5%B1%95java%E8%AF%B7%E6%B1%82/","summary":"本文将通过WebRTC本地音视频流压测脚本，来说明JMeter如何扩展Java请求。","title":"JMeter扩展Java请求"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在IntelliJ IDEA中，显示了Maven的Lifecycle：\n只需要学习这些命令，就能构建一个Maven项目。\n三个内置生命周期 Maven内置了三个生命周期：clean、default和site。生命周期是由多个顺序执行的阶段组成，它们的关系和说明如下：\nclean 清理文件。\npre-clean clean前置阶段 clean 移除上次构建生成的所有文件 post-clean clean后置阶段 default 主要生命周期，用来构建应用。\nvalidate 验证项目正确，必要信息有效 initialize 初始化构建状态，比如设置属性或创建目录 generate-sources 生成编译的源代码 process-sources 处理源代码，比如过滤任意值 generate-resources 生成包中的资源文件 process-resources 复制和处理资源文件到目标目录，为打开做准备 compile 编译项目的源码 process-classes 后处理（post-process）编译源代码生成的文件，比如给Java class文件做bytecode enhancement generate-test-sources 生成编译的测试代码 process-test-sources 处理测试代码，比如过滤任意值 generate-test-resources 创建测试需要的资源 process-test-resources 复制和处理资源文件到测试目录 test-compile 编译测试代码到测试目录 process-test-classes 后处理（post-process）编译测试代码生成的文件，比如给Java class文件做bytecode enhancement test 使用单元测试框架（如JUnit）执行测试，这些测试不能依赖已经打包（packaged）或部署（deployed）的代码 prepare-package 打包前准备，一般会生成处理过的未打包的前置版本包 package 将编译后的代码进行打包，比如jar包、war包 pre-integration-test 集成测试前置阶段，比如创建环境 integration-test 集成测试，在环境中进行测试 post-integration-test 集成测试后置阶段，比如清理环境 verify 验证包有效且质量达标 install 安装包到本地仓库，以便于本地项目依赖 deploy 复制包到远程仓库，共享给其他人使用 site 一般用来创建文档。\npre-site 前置阶段 site 生成项目的网站文档 post-site 后置阶段 site-deploy 把生成的网站文档部署到web服务器 命令行执行 Maven提供了一个mvn命令，把它的路径（如D:\\Program Files\\JetBrains\\IntelliJ IDEA 2020.3.3\\plugins\\maven\\lib\\maven3\\bin）添加到环境变量就能直接使用。\nmvn verify 如果不知道执行什么命令，可以执行这条命令，它会把前置命令都执行了：validate，compile，test，package。同时也会执行代码检查比如checkstyle，以及集成测试。 mvn clean deploy 常用来清理构建部署到仓库，如果项目有多个子模块，Maven会遍历所有模块执行。 对于pre-*, post-*, process-*等短横线命名的阶段则不能使用命令行直接执行，它们主要用来生成中间结果。比如单元测试覆盖率工具Jacoco和执行容器插件Docker可能会绑定到pre-integration-test来准备集成测试环境，然后在post-integration-test来收集覆盖率统计或者销毁容器。Failsafe和Code coverage插件绑定到了integration-test和verify两个阶段，在verify执行后生成测试报告。如果执行verify是没有问题的，但是如果直接执行integration-test，就可能导致无法生成测试报告，甚至集成测试环境也没有被彻底清理。\nIntelliJ IDEA集成了所有没有短横线命名的这些可以直接执行的命令。\nPlugin Goals Maven除了生命周期的阶段，还提供了一个更精细的任务，叫做插件目标（Plugin Goals）。示例：\nmvn clean dependency:copy-dependencies package dependency:copy-dependencies就是一个插件目标，表达式为plugin:goal。\n其实阶段都是由插件目标组成的，Maven默认进行了绑定，比如clean:clean、compiler:compile、deploy:deploy等。package阶段打包类型不同插件目标也有区别：jar:jar、war:war。\n一个典型的打jar包的阶段和插件目标如下图所示：\nPlugin Plugin是用来给Maven提供goals的，比如Compiler plugin包括2个goals：compile用来编译main源代码，testCompile用来编译测试代码。Plugin在pom.xml中进行配置，比如：\n\u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.companyname.projectgroup\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;project\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-antrun-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;id.validate\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;validate\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;run\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;tasks\u0026gt; \u0026lt;echo\u0026gt;validate phase\u0026lt;/echo\u0026gt; \u0026lt;/tasks\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;id.compile\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;compile\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;run\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;tasks\u0026gt; \u0026lt;echo\u0026gt;compile phase\u0026lt;/echo\u0026gt; \u0026lt;/tasks\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;id.test\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;test\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;run\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;tasks\u0026gt; \u0026lt;echo\u0026gt;test phase\u0026lt;/echo\u0026gt; \u0026lt;/tasks\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;id.package\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;run\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;tasks\u0026gt; \u0026lt;echo\u0026gt;package phase\u0026lt;/echo\u0026gt; \u0026lt;/tasks\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;id.deploy\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;deploy\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;run\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;tasks\u0026gt; \u0026lt;echo\u0026gt;deploy phase\u0026lt;/echo\u0026gt; \u0026lt;/tasks\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; 多个goals按配置的先后顺序运行。executions用来对goals进行配置，也可以添加id进行标识。\n实际的配置会复杂许多，可以参考开源项目：\nhttps://github.com/metersphere/metersphere\nhttps://github.com/macrozheng/mall\n小结 本文介绍了Maven内置的三个生命周期，生命周期是由多个阶段组成，IntelliJ IDEA集成了所有没有短横线命名的这些可以直接执行的阶段。阶段是由阶段目标（Plugin Goals）构成的，在pom.xml中进行配置。pom.xml是Maven一个很重要很常用的文件。\n参考资料：\nhttps://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html\nhttps://www.runoob.com/maven/maven-build-life-cycle.html\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000013-maven%E8%B7%9F%E7%9D%80%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E5%AD%A6maven%E6%9E%84%E5%BB%BA%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/","summary":"在IntelliJ IDEA中，显示了Maven的Lifecycle：","title":"跟着官方文档学Maven构建生命周期"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n如何提高测试竞争力，这个话题原本是想写“测试核心竞争力”的，酝酿了30天，迟迟无法落笔。所谓测试核心竞争力，主要有业务、技术、质量3个方向，我本想强调测试核心竞争力是质量，不能太过于追求业务和技术，要回归到质量保障这件事来。但是我错了，这是测试岗位的核心职责，并非测试人员的核心竞争力。\n测试人员的竞争力，是综合因素影响的，除了业务、技术、质量，还包括年龄、学历、关系、履历背景、性格、情商、情绪价值、工作成果、公司发展机遇等。在当前的大环境下，很难说哪一个是核心竞争力，能不能找到工作，能不能保住工作，看的是综合能力。\n测试行业大多数人都没有技术，测试行业本身就没有技术可言，不懂技术仍然可以在测试行业混得风生水起。这些话不是我说的，是道听途说来的，最近发现也的确如此。在前几年自动化测试、测试框架、测试平台的狂潮退去后，测试行业的技术分享越来越少。伴随着经济下行和公司内卷，测试工作也越来越趋向于QA的业务质量保障。测试有没有技术不重要，别出问题就行。问题恰好就在于怎么才能不出问题？\n**技术型测试的最大优势是，能通过技术手段保障质量。**测试无法表演真正的技术，在质量领域还是可以尽情发挥的。一方面是白盒测试，准确说是代码评审，精准把握代码改动和影响，高效测试，全面覆盖。另一方面是专项测试，自动化测试、性能测试、流量录制回放、精准测试、测试环境、测试数据等，有一定技术门槛，有的还需要开发。大道至简，越熟悉系统，越多测试手段，越能发现问题。\n还有一个有意思的优势，自动化办公。举个例子，在部署平台查看当前部署的代码分支，有5个应用，每次都需要切换网页，很繁琐。通过抓取网页接口和Cookie，写个脚本就能一次性快速查询，省不少事。这种小工具，不怕做不到，就怕想不到。测试工作本来就有很多是重复、机械、枯燥的，借助技术优势，让机器替你干活。\n技术同样能提高影响力，影响力和竞争力一样，要综合来看，不是技术单方面就能决定的。\n技术之路并不适合每个人，理性看待技术。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000013-%E6%8A%80%E6%9C%AF%E5%9E%8B%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BC%98%E5%8A%BF/","summary":"如何提高测试竞争力，这个话题原本是想写“测试核心竞争力”的，酝酿了30天，迟迟无法落笔。所谓测试核心竞争力，主要有业务、技术、质量3个方向，我本想强调测试核心竞争力是质量，不能太过于追求业务和技术，要回归到\u003cstrong\u003e质量保障\u003c/strong\u003e这件事来。但是我错了，\u003cstrong\u003e这是测试岗位的核心职责，并非测试人员的核心竞争力\u003c/strong\u003e。","title":"技术型测试的优势"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n接口自动化项目落地系列 找个开源网站或开源项目，用tep实现整套pytest接口自动化项目落地，归档到电子书，作为tep完整教程的项目篇一部分。自从tep完整教程发布以后，tep被越来越多小伙伴了解。教程只是纯理论，是骡子是马，拉出来遛遛才知道。做接口自动化项目落地，一方面是为了让自己脑海中的构想实实在在的呈现出来，现实和理想存在多少差距，不断尝试去弥补和修缮；另一方面也是方便读者朋友们学习使用，借助实际项目来练习，才能在赛道中弯道超车。\nHTTPBin网站 httpbin.org是一个简单的在线提供HTTP服务的网站：\n它能够用来对HTTP进行在线测试。\n测试报告 HTTPBin网站的接口自动化项目包含11个用例集：\n67条测试用例：\n自动化执行正确率98.5%，其中有1条错误结果，是我故意为之的，因为想展示下断言失败的效果。\n环境配置 包含http和https两套环境，因为HTTPBin支持HTTP\u0026amp;HTTPS：\nfixtures/fixture_env_vars.py\n#!/usr/bin/python ## encoding=utf-8 from tep.fixture import * @pytest.fixture(scope=\u0026#34;session\u0026#34;) def env_vars(config): class Clazz(TepVars): env = config[\u0026#34;env\u0026#34;] \u0026#34;\u0026#34;\u0026#34;变量定义开始\u0026#34;\u0026#34;\u0026#34; # 环境变量 mapping = { \u0026#34;http\u0026#34;: { # http环境 \u0026#34;domain\u0026#34;: \u0026#34;http://httpbin.org\u0026#34;, }, \u0026#34;https\u0026#34;: { # https环境 \u0026#34;domain\u0026#34;: \u0026#34;https://httpbin.org\u0026#34;, } # 继续添加 } # 定义类属性，敲代码时会自动补全 domain = mapping[env][\u0026#34;domain\u0026#34;] \u0026#34;\u0026#34;\u0026#34;变量定义结束\u0026#34;\u0026#34;\u0026#34; return Clazz() 配置默认为http环境：\nconf.yaml\nenv: http 用例集 http-methods import allure from tep.client import request @allure.title(\u0026#34;get请求\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/get\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 auth import allure from tep.client import request @allure.title(\u0026#34;Authorization以Bearer开头，认证成功\u0026#34;) def test(env_vars): # 描述 # http://httpbin.org/#/Auth/get_basic_auth__user___passwd_ # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/bearer\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;Authorization\u0026#39;: \u0026#39;Bearer ZG9uZ2ZhbmdlcjoxMjM0NTY=\u0026#39;, # 替换token \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 status-codes import allure from tep.client import request @allure.title(\u0026#34;post返回状态码300\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;post\u0026#34;, url=env_vars.domain + \u0026#34;/status/300\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;Content-Length\u0026#39;: \u0026#39;0\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;text/plain\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Origin\u0026#39;: \u0026#39;http://httpbin.org\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value\u0026#39;}, json={} ) # 提取 # 断言 assert response.status_code == 300 request_inspection import allure from tep.client import request @allure.title(\u0026#34;捕获请求信息--headers\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/headers\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 assert response.json()[\u0026#34;headers\u0026#34;] response_inspection import allure from tep.client import request @allure.title(\u0026#34;捕获响应信息--缓存\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/cache\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;Cache-Control\u0026#39;: \u0026#39;max-age=0\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;If-None-Match\u0026#39;: \u0026#39;1\u0026#39;, \u0026#39;If-Modified-Since\u0026#39;: \u0026#39;1\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code == 304 response_formats import allure from tep.client import request @allure.title(\u0026#34;txt文本text/plain\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/robots.txt\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;text/plain\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 assert response.headers[\u0026#34;content-type\u0026#34;] == \u0026#34;text/plain\u0026#34; dynamic_data import allure from tep.client import request @allure.title(\u0026#34;base64解码\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/base64/SFRUUEJJTiBpcyBhd2Vzb21l\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;text/html\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 assert \u0026#34;HTTPBIN is awesome\u0026#34; == response.text cookies import allure from tep.client import request @allure.title(\u0026#34;cookies\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/cookies\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 assert response.json()[\u0026#34;cookies\u0026#34;] images import allure from tep.client import request @allure.title(\u0026#34;图片\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/image\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;image/webp\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value; freeform=3; name=dongfanger\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 redirects import allure from tep.client import request @allure.title(\u0026#34;重定向\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/redirect/1\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;text/html\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;http://httpbin.org/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value; freeform=3; name=dongfanger\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code == 404 anything import allure from tep.client import request @allure.title(\u0026#34;返回所有数据\u0026#34;) def test(env_vars): # 描述 # 数据 # 请求 response = request( \u0026#34;delete\u0026#34;, url=env_vars.domain + \u0026#34;/anything\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Origin\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value; freeform=3; name=dongfanger\u0026#39;}, json={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 # 描述 # 数据 # 请求 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/anything\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value; freeform=3; name=dongfanger\u0026#39;}, params={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 # 描述 # 数据 # 请求 response = request( \u0026#34;patch\u0026#34;, url=env_vars.domain + \u0026#34;/anything\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Origin\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value; freeform=3; name=dongfanger\u0026#39;}, json={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 # 描述 # 数据 # 请求 response = request( \u0026#34;post\u0026#34;, url=env_vars.domain + \u0026#34;/anything\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;Content-Length\u0026#39;: \u0026#39;0\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Origin\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value; freeform=3; name=dongfanger\u0026#39;}, json={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 # 描述 # 数据 # 请求 response = request( \u0026#34;put\u0026#34;, url=env_vars.domain + \u0026#34;/anything\u0026#34;, headers={\u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;Proxy-Connection\u0026#39;: \u0026#39;keep-alive\u0026#39;, \u0026#39;Content-Length\u0026#39;: \u0026#39;0\u0026#39;, \u0026#39;accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;, \u0026#39;Origin\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;/\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Accept-Language\u0026#39;: \u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;stale_after=never; fake=fake_value; freeform=3; name=dongfanger\u0026#39;}, json={} ) # 提取 # 断言 assert response.status_code \u0026lt; 400 只花了3小时完成 通过mitmproxy来录制流量自动生成用例，效率得到了极大的提高，从原来的1天缩短到3小时就完成了整个项目落地。相比于手工编写用例，这次写HTTPBin的接口自动化，我使用了utils/mitm.py来录制流量，mitmproxy稍微不方便的是需要手动开启代理，不过适应了以后还是能接受。录制流量后就会生成自动化用例，但是还需要二次修改，才会变成最终的用例。主要修改的工作量是在添加断言，根据业务设置合理的断言。其次是替换url为env_vars.domain + \u0026quot;/api\u0026quot;拼接方式，直接批量Replace即可。然后就是修改文件名和@allure.title了，给用例加上标题。工欲善其事，必先利其器。\ntep共建 欢迎添加微信：cekaigang，分享交流tep实践案例，可以提供开源项目我来写，也可以写好后发我一起看看，优秀的项目会添加到tep完整教程的项目篇，以便更多测试同行们借鉴，大佬们赶快来加入我们吧。\n参考资料：\nHTTPBin接口自动化项目源码 https://github.com/dongfanger/httpbin\npostman https://www.postman.com/postman/workspace/httpbin/collection/\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000013-%E6%8E%A5%E5%8F%A3%E8%87%AA%E5%8A%A8%E5%8C%96%E9%A1%B9%E7%9B%AE%E8%90%BD%E5%9C%B0%E4%B9%8Bhttpbin%E7%BD%91%E7%AB%99/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"接口自动化项目落地系列\"\u003e接口自动化项目落地系列\u003c/h1\u003e\n\u003cp\u003e找个开源网站或开源项目，用\u003cstrong\u003etep\u003c/strong\u003e实现整套\u003cstrong\u003epytest\u003c/strong\u003e接口自动化项目落地，归档到电子书，作为\u003cstrong\u003etep完整教程\u003c/strong\u003e的\u003cstrong\u003e项目篇\u003c/strong\u003e一部分。自从tep完整教程发布以后，tep被越来越多小伙伴了解。教程只是纯理论，是骡子是马，拉出来遛遛才知道。做接口自动化项目落地，一方面是为了让自己脑海中的构想实实在在的呈现出来，现实和理想存在多少差距，不断尝试去弥补和修缮；另一方面也是方便读者朋友们学习使用，借助实际项目来练习，才能在赛道中弯道超车。\u003c/p\u003e\n\u003ch1 id=\"httpbin网站\"\u003eHTTPBin网站\u003c/h1\u003e\n\u003cp\u003ehttpbin.org是一个简单的在线提供HTTP服务的网站：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/216043454b2f43eeb4659ce9ac7b81a8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/acfb96edb6fe40008c3942d17482d2ee.png\"\u003e\u003c/p\u003e\n\u003cp\u003e它能够用来对HTTP进行在线测试。\u003c/p\u003e\n\u003ch1 id=\"测试报告\"\u003e测试报告\u003c/h1\u003e\n\u003cp\u003eHTTPBin网站的接口自动化项目包含\u003cstrong\u003e11个用例集\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/2b7c958062ed47d89e09e9e40d6abb5c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e67条测试用例\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c63e2d9a06bc4bdd8e7feed8b7ebc349.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e自动化执行正确率98.5%\u003c/strong\u003e，其中有1条错误结果，是我故意为之的，因为想展示下断言失败的效果。\u003c/p\u003e\n\u003ch1 id=\"环境配置\"\u003e环境配置\u003c/h1\u003e\n\u003cp\u003e包含http和https两套环境，因为HTTPBin支持HTTP\u0026amp;HTTPS：\u003c/p\u003e\n\u003cp\u003e\u003cem\u003efixtures/fixture_env_vars.py\u003c/em\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e#!/usr/bin/python\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## encoding=utf-8\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etep.fixture\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@pytest.fixture\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003escope\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;session\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eenv_vars\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eClazz\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eTepVars\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        env \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e config\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;env\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;变量定义开始\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#57606a\"\u003e# 环境变量\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        mapping \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# http环境\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://httpbin.org\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# https环境\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://httpbin.org\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# 继续添加\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#57606a\"\u003e# 定义类属性，敲代码时会自动补全\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        domain \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e mapping\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eenv\u003cspan style=\"color:#1f2328\"\u003e][\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;变量定义结束\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e Clazz\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e配置默认为http环境：\u003c/p\u003e\n\u003cp\u003e\u003cem\u003econf.yaml\u003c/em\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003eenv\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ehttp\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"用例集\"\u003e用例集\u003c/h1\u003e\n\u003ch2 id=\"http-methods\"\u003ehttp-methods\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/85caafcedc6e4c3bb04f47375fbf8015.png\"\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eallure\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etep.client\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e request\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@allure.title\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get请求\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eenv_vars\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 描述\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 数据\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 请求\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e request\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eenv_vars\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edomain \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Host\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;httpbin.org\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Proxy-Connection\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;keep-alive\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;accept\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;application/json\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                 \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;User-Agent\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                 \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Referer\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;http://httpbin.org/\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Accept-Encoding\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;gzip, deflate\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                 \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Accept-Language\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        params\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 提取\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 断言\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"auth\"\u003eauth\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/30f73a425db04174bb36bfcb19e79800.png\"\u003e\u003c/p\u003e","title":"接口自动化项目落地之HTTPBin网站"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n序列赋值是Python默认操作，如果使用不当，有可能会掉入语法陷阱。\n+ +是指把两个序列的元素拼接在一起。通常+号两侧的序列由相同类型的数据所构成，在拼接的过程中，两个被操作的序列都不会被修改，Python会新建一个包含同样类型数据的序列作为拼接的结果。比如：\na = [1] b = [2] c = a + b print(a, b, c) print(id(a), id(b), id(c)) 结果为：\n[1] [2] [1, 2] 2409610524480 2409610523520 2409610523648 * 如果想要把一个序列复制几份然后再拼接起来，更快捷的做法是把这个序列乘以一个整数。同样，这个操作会产生一个新序列：\n\u0026gt;\u0026gt;\u0026gt; l = [1] \u0026gt;\u0026gt;\u0026gt; l * 5 [1, 1, 1, 1, 1] \u0026gt;\u0026gt;\u0026gt; 5 * \u0026#34;a\u0026#34; \u0026#39;aaaaa\u0026#39; +和*都遵循这个规律，不修改原有的操作对象，而是构建一个全新的序列。\n列表套列表的陷进 猜猜这个结果会是啥：\nx = [\u0026#34;x\u0026#34;] my_list = [x] * 3 print(my_list) # [[\u0026#39;x\u0026#39;], [\u0026#39;x\u0026#39;], [\u0026#39;x\u0026#39;]] x2 = my_list[2] x2[0] = \u0026#34;y\u0026#34; print(my_list) 讲道理，应该是[['x'], ['x'], ['y']]，但是错了，实际是：\n[[\u0026#39;y\u0026#39;], [\u0026#39;y\u0026#39;], [\u0026#39;y\u0026#39;]] Unbelievable！给my_list的最后一个元素的列表赋值，结果所有三个元素的列表都被赋值了！这反映出my_list这三个元素不是3个列表，而是3个列表引用，指向了同一个相同的列表。相当于：\nx = [\u0026#34;x\u0026#34;] my_list = [] for i in range(3): my_list.append(x) # 追加相同对象 x2 = my_list[2] x2[0] = \u0026#34;y\u0026#34; print(my_list) # [[\u0026#39;y\u0026#39;], [\u0026#39;y\u0026#39;], [\u0026#39;y\u0026#39;]] 每次都追加了同一个对象到my_list。\n如果想生成3个不同列表，那么需要在每次迭代中新建列表：\nmy_list = [] for i in range(3): x = [\u0026#34;x\u0026#34;] # 新建列表 my_list.append(x) x2 = my_list[2] x2[0] = \u0026#34;y\u0026#34; print(my_list) # [[\u0026#39;x\u0026#39;], [\u0026#39;x\u0026#39;], [\u0026#39;y\u0026#39;]] 这样就符合预期了。可以用列表推导简化代码：\nx = [\u0026#34;x\u0026#34;] my_list = [x for i range(3)] x2 = my_list[2] x2[0] = \u0026#34;y\u0026#34; print(my_list) # [[\u0026#39;x\u0026#39;], [\u0026#39;x\u0026#39;], [\u0026#39;y\u0026#39;]] 教训：\n新建列表中的列表，使用列表推导，不要使用*运算符。\n如果a * n这个语句中，序列a里的元素是对其他可变对象的引用的话，就需要格外注意了，这可能不是你想要的效果。\n+= a += b虽然意思是a = a + b，但是它背后的特殊方法是__iadd__，如果一个类没有实现这个方法的话，Python才会退一步调用__add__。__iadd__方法会直接在原对象中追加，__add__方法会先生成新对象再赋值。\n*= +=的这些概念也适用于*=，只是后者对应的是__imul__。追加还是新对象，在作用到可变序列和不可变序列时效果明显，示例：\n## 可变序列，追加 \u0026gt;\u0026gt;\u0026gt; l = [1, 2, 3] \u0026gt;\u0026gt;\u0026gt; id(l) 2135319475136 \u0026gt;\u0026gt;\u0026gt; l *= 2 \u0026gt;\u0026gt;\u0026gt; l [1, 2, 3, 1, 2, 3] \u0026gt;\u0026gt;\u0026gt; id(l) 2135319475136 # id一样 ## 不可变序列，新对象 \u0026gt;\u0026gt;\u0026gt; t = (1, 2, 3) \u0026gt;\u0026gt;\u0026gt; id(t) 2135322139520 \u0026gt;\u0026gt;\u0026gt; t *= 2 \u0026gt;\u0026gt;\u0026gt; id(t) 2135321695424 # id不一样 元组套列表的陷进 \u0026gt;\u0026gt;\u0026gt; t = (1, 2, [30, 40]) \u0026gt;\u0026gt;\u0026gt; t[2] += [50, 60] 猜猜会发生下面4种情况中的哪一种？\na.t变成(1, 2, [30, 40, 50, 60])\nb.因为tuple不支持对它的元素赋值，所以会抛出TypeError异常\nc.以上两个都不是\nd.a和b都是对的\n因为元组不能赋值，所以我会毫不犹豫的选择b。但实际上答案是d！a和b都是对的，既会赋值成功，也会报错：\n\u0026gt;\u0026gt;\u0026gt; t = (1, 2, [30, 40]) \u0026gt;\u0026gt;\u0026gt; t[2] += [50, 60] Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; TypeError: \u0026#39;tuple\u0026#39; object does not support item assignment \u0026gt;\u0026gt;\u0026gt; t (1, 2, [30, 40, 50, 60]) Oh No！为什么？一、赋值成功，因为t[2]指向的是一个可变对象（列表[30, 40]），可变对象是能赋值的。二、报错，因为可变对象赋值给了不可变对象（元组t），不可变对象不能赋值。\n写成t[2].extend([50, 60])能避免这个异常。\n教训：\n不要把可变对象放在元组里面。 +=不是一个原子操作，虽然抛出了异常，但还是完成了操作。 这位巴西作者说到，在他15年的Python生涯中，他还没见过谁在这个地方吃过亏。\n小结 本文分别介绍了+、*和列表套列表的陷阱，+=、*=和元组套列表的陷阱，并分别得出了教训。这是动态语言的弊端，在运行后才能知道有没有类型错误，只能积累代码经验来避免。鱼与熊掌不可兼得，在享受Python语法简洁的便利同时，也得付出运行报错排查麻烦的代价。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000013-python%E5%BA%8F%E5%88%97%E8%B5%8B%E5%80%BC%E5%BC%95%E5%8F%91%E7%9A%84python%E5%88%97%E8%A1%A8%E9%99%B7%E8%BF%9B/","summary":"序列赋值是Python默认操作，如果使用不当，有可能会掉入语法陷阱。","title":"序列赋值引发的Python列表陷进"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n8.1 AI可观测 8.1.1 什么是AI可观测 AI应用具有非确定性，即便是相同的提示，在不同次的运行中也可能产生不同的输出。如果没有可观测，在发生幻觉虚假错误等严重问题时根本无从查起。相反，一个好用的可观测工具会记录每一次的提示与响应、追踪使用模式，并标记异常。AI可观测是确保AI应用高效稳定安全运行的基础能力。\n8.1.2 可观测 vs 监控：从“是什么”到“为什么” 监控关注“什么”：比如API响应时间、错误率、请求吞吐量和Token使用量等指标。\n可观测性探究“为什么”：比如一个聊天机器人的回答不正确且耗时过长，监控可能只会显示错误率上升或延迟过高，但可观测能揭示为什么：可能是一次长时间的RAG请求最终得到了错误的召回，导致模型给出不准确的答案。\nAI应用的每一次请求，都可以被弯针的追溯，包括调用工具的出入参、发送给大模型的具体提示词、任何中间步骤（如调用数据库或其他API），以及最终收到的输出。可观测工具就可以提供极其丰富的排查数据和上下文，帮助定位问题的根因。\n8.1.3 AI可观测应对的核心挑战 性能与可靠性问题：大模型是资源密集型的，延迟峰值和瓶颈时有发生。\n成本问题：许多大模型服务按Token使用量收费，若无控制，成本可能意外飙升。\n质量问题：大模型的可能输出从训练数据中继承偏见或有害内容，也很可能产生幻觉。\n8.1.4 AI可观测解决方案的关键能力 端到端全链路追踪：提供端到端的日志采集和链路追踪，可视化请求在整个AI应用中的执行路径。\n全栈可观测：包含应用、AI网关、推理引擎可观测3个维度。\n自动化评估功能：通过引入评估Agent，对应用和模型的输入输出进行自动化的评估，检测幻觉、不一致或答案质量下降等问题。\n8.2 端到端全链路追踪 一个典型的LLM应用架构可能包含用户终端、认证模块、会话管理、对话服务、大模型路由、流程编排等。需要具备3项可观测能力：标准化的数据语义规范、低成本高质量的数据采集、端到端全链路追踪。\n8.2.1 端到端全链路追踪的实现方式 面向AI应用的领域化Trace语义\n基于OpenTelemetry的高质量数据采集\n标准化协议\n8.2.2 核心技术路径 1、链路插桩技术\nPython探针，Monkey Patch\nJava探针，字节码增强技术。\nGo探针，编译时插桩技术。\n多语言兼容，其他语言通过OpenTelemetry开源框架支持。\n2、链路采集与加工\n数据采集策略\n流式场景优化\n3、LLM Trace查询与分析\n全链路透视\n高级过滤\n智能诊断\n8.3 全栈可观测：应用可观测 8.3.1 AI原生应用开发的痛点 工具选择盲区、错误排除困难、Token消耗黑洞、循环调用陷阱。\n8.3.2 AI原生应用可观测需要具备哪些能力 零代码接入、可视化工具选择过程、精准故障定位、Token成本分析、端到端链路追踪。\n8.3.3 演示场景架构 使用LangChain框架构建一个Agent，大模型使用Qwen Turbo。SLS MCP服务器通过SLS HTTP OpenAPI访问日志的接口，完成日志分析请求。LangChain Agent与MCP服务器产生的观测数据会自动采集到可观测平台中。\n8.3.4 场景演示 1、启动SLS MCP服务器\n2、启动Langchain Agent程序\n3、Agent观测\n4、MCP观测\n8.4 全栈可观测：AI网关可观测 8.4.1 观测场景：AI组件的多维可观测需求 1、性能与稳定性监控：保障AI应用的高可用\n关键性能指标：QPS、请求成功率、响应时间、流式与非流式请求分布。\n2、资源消耗与成本分析：实现精细化成本管控\nToken消耗数/s、按模型维度的Token使用统计、按消费者维度的Token使用统计。\n3、安全与合规审计：防范数据泄露与内容风险\n内容安全拦截日志、风险类型统计、风险消费者统计。\n4、治理策略执行追踪：确保限流、缓存、Fallback有效落地\n限流统计、缓存命中情况、Fallback执行路径。\n5、多租户与权限治理：实现调用者的精细化管理\n消费者身份识别、消费者级指标统计、异常消费者检测。\n8.4.2 观测实践：基于AI网关的可观测体系构建 1、观测数据：统一日志与指标\n2、可视化监控：多维度仪表盘\n3、深度分析：基于日志查询与SQL分析\n4、智能告警与自动化响应\n5、成本优化与治理闭环\n8.5 全栈可观测：推理引擎可观测 推理引擎，是AI算法和软硬件系统之间的桥梁，其主要功能包括优化LLM的性能以确保快速准确的推理、管理GPU内存等硬件资源、提供分布式和可扩展能力等。常见的推理引擎有vLLM、SGLang等。\n8.5.1 推理引擎需要观测什么 推理引擎vLLM的整体架构如下图：\n以下是API Server、模型输入输出、推理过程、推理引擎状态4个维度常见的观测项和含义：\n8.5.2 推理引擎需可观测的实践 首Token时间（TTFT）对客户体验比较重要。如果观测到比较大的TTFT，可以从提示词长度、并发请求排队、KV Cache使用率等因素去优化。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005008-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E5%8F%AF%E8%A7%82%E6%B5%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"81-ai可观测\"\u003e8.1 AI可观测\u003c/h1\u003e\n\u003ch2 id=\"811-什么是ai可观测\"\u003e8.1.1 什么是AI可观测\u003c/h2\u003e\n\u003cp\u003eAI应用具有非确定性，即便是相同的提示，在不同次的运行中也可能产生不同的输出。如果没有可观测，在发生幻觉虚假错误等严重问题时根本无从查起。相反，一个好用的可观测工具会记录每一次的提示与响应、追踪使用模式，并标记异常。AI可观测是确保AI应用高效稳定安全运行的基础能力。\u003c/p\u003e\n\u003ch2 id=\"812-可观测-vs-监控从是什么到为什么\"\u003e8.1.2 可观测 vs 监控：从“是什么”到“为什么”\u003c/h2\u003e\n\u003cp\u003e监控关注“什么”：比如API响应时间、错误率、请求吞吐量和Token使用量等指标。\u003c/p\u003e\n\u003cp\u003e可观测性探究“为什么”：比如一个聊天机器人的回答不正确且耗时过长，监控可能只会显示错误率上升或延迟过高，但可观测能揭示为什么：可能是一次长时间的RAG请求最终得到了错误的召回，导致模型给出不准确的答案。\u003c/p\u003e\n\u003cp\u003eAI应用的每一次请求，都可以被弯针的追溯，包括调用工具的出入参、发送给大模型的具体提示词、任何中间步骤（如调用数据库或其他API），以及最终收到的输出。可观测工具就可以提供极其丰富的排查数据和上下文，帮助定位问题的根因。\u003c/p\u003e\n\u003ch2 id=\"813-ai可观测应对的核心挑战\"\u003e8.1.3 AI可观测应对的核心挑战\u003c/h2\u003e\n\u003cp\u003e性能与可靠性问题：大模型是资源密集型的，延迟峰值和瓶颈时有发生。\u003c/p\u003e\n\u003cp\u003e成本问题：许多大模型服务按Token使用量收费，若无控制，成本可能意外飙升。\u003c/p\u003e\n\u003cp\u003e质量问题：大模型的可能输出从训练数据中继承偏见或有害内容，也很可能产生幻觉。\u003c/p\u003e\n\u003ch2 id=\"814-ai可观测解决方案的关键能力\"\u003e8.1.4 AI可观测解决方案的关键能力\u003c/h2\u003e\n\u003cp\u003e端到端全链路追踪：提供端到端的日志采集和链路追踪，可视化请求在整个AI应用中的执行路径。\u003c/p\u003e\n\u003cp\u003e全栈可观测：包含应用、AI网关、推理引擎可观测3个维度。\u003c/p\u003e\n\u003cp\u003e自动化评估功能：通过引入评估Agent，对应用和模型的输入输出进行自动化的评估，检测幻觉、不一致或答案质量下降等问题。\u003c/p\u003e\n\u003ch1 id=\"82-端到端全链路追踪\"\u003e8.2 端到端全链路追踪\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1209f5a13a5547eab6b48766b587a6e7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e一个典型的LLM应用架构可能包含用户终端、认证模块、会话管理、对话服务、大模型路由、流程编排等。需要具备3项可观测能力：标准化的数据语义规范、低成本高质量的数据采集、端到端全链路追踪。\u003c/p\u003e\n\u003ch2 id=\"821-端到端全链路追踪的实现方式\"\u003e8.2.1 端到端全链路追踪的实现方式\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e面向AI应用的领域化Trace语义\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/58734a9b5260416ea038ededef361970.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e基于OpenTelemetry的高质量数据采集\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/aea86fbca30c486facc5f20c5157b130.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e标准化协议\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8329ad7f600f43bfadb4613de6f7a7b7.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"822-核心技术路径\"\u003e8.2.2 核心技术路径\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e1、链路插桩技术\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003ePython探针，Monkey Patch\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bf8d024c90244cbeb754f3c20e89dd19.png\"\u003e\u003c/p\u003e\n\u003cp\u003eJava探针，字节码增强技术。\u003c/p\u003e\n\u003cp\u003eGo探针，编译时插桩技术。\u003c/p\u003e\n\u003cp\u003e多语言兼容，其他语言通过OpenTelemetry开源框架支持。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2、链路采集与加工\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e数据采集策略\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/26d6011ed8194420b7a170a9a7237589.png\"\u003e\u003c/p\u003e\n\u003cp\u003e流式场景优化\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fc1273e7f3ce44adad8996bf930b7059.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3、LLM Trace查询与分析\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e全链路透视\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/109b28eacf00489d87c80f28c1022ff3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e高级过滤\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/429353fd5ae342cda82ea84f9492b878.png\"\u003e\u003c/p\u003e\n\u003cp\u003e智能诊断\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8019f7b05a2b4e60b0b9c74bef580d62.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"83-全栈可观测应用可观测\"\u003e8.3 全栈可观测：应用可观测\u003c/h1\u003e\n\u003ch2 id=\"831-ai原生应用开发的痛点\"\u003e8.3.1 AI原生应用开发的痛点\u003c/h2\u003e\n\u003cp\u003e工具选择盲区、错误排除困难、Token消耗黑洞、循环调用陷阱。\u003c/p\u003e\n\u003ch2 id=\"832-ai原生应用可观测需要具备哪些能力\"\u003e8.3.2 AI原生应用可观测需要具备哪些能力\u003c/h2\u003e\n\u003cp\u003e零代码接入、可视化工具选择过程、精准故障定位、Token成本分析、端到端链路追踪。\u003c/p\u003e\n\u003ch2 id=\"833-演示场景架构\"\u003e8.3.3 演示场景架构\u003c/h2\u003e\n\u003cp\u003e使用LangChain框架构建一个Agent，大模型使用Qwen Turbo。SLS MCP服务器通过SLS HTTP OpenAPI访问日志的接口，完成日志分析请求。LangChain Agent与MCP服务器产生的观测数据会自动采集到可观测平台中。\u003c/p\u003e","title":"AI可观测"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在DRF官方教程的学习过程中，一个很明显的感受是框架在不断地进行封装，我们自己写框架/工具/脚本/平台也可以模仿模仿，先完成底层代码，再做多层封装，让使用者很容易就上手操作。本文是教程的最后一篇，介绍ViewSets和Routers。\nViewSets 先看看之前在给User模型创建Endpoint时，我们新增的2个视图：\nclass UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer DRF提供了rest_framework.viewsets：\n可以把它们合成一个视图。\nset是集合的意思，ViewSets就是视图集合。\nReadOnlyModelViewSet 我们先使用ReadOnlyModelViewSet把UserList和UserDetail视图合并成一个UserViewSet：\nfrom rest_framework import viewsets class UserViewSet(viewsets.ReadOnlyModelViewSet): \u0026#34;\u0026#34;\u0026#34; This viewset automatically provides `list` and `retrieve` actions. \u0026#34;\u0026#34;\u0026#34; queryset = User.objects.all() serializer_class = UserSerializer viewsets.ReadOnlyModelViewSet是只读视图集合，源码如下：\nclass ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet): \u0026#34;\u0026#34;\u0026#34; A viewset that provides default `list()` and `retrieve()` actions. \u0026#34;\u0026#34;\u0026#34; pass 它继承了mixins.RetrieveModelMixin、mixins.ListModelMixin和GenericViewSet：\nmixins.RetrieveModelMixin\nclass RetrieveModelMixin: \u0026#34;\u0026#34;\u0026#34; Retrieve a model instance. \u0026#34;\u0026#34;\u0026#34; def retrieve(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance) return Response(serializer.data) mixins.ListModelMixin\nclass ListModelMixin: \u0026#34;\u0026#34;\u0026#34; List a queryset. \u0026#34;\u0026#34;\u0026#34; def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) viewsets.GenericViewSet\nclass GenericViewSet(ViewSetMixin, generics.GenericAPIView): \u0026#34;\u0026#34;\u0026#34; The GenericViewSet class does not provide any actions by default, but does include the base set of generic view behavior, such as the `get_object` and `get_queryset` methods. \u0026#34;\u0026#34;\u0026#34; pass 从源码可以看出，它提供了list()和retrieve()2个方法，正好对应UserList和UserDetail。\nModelViewSet 再使用ModelViewSet把SnippetList、SnippetDetail和SnippetHighlight视图合并成一个SnippetViewSet：\nfrom rest_framework.decorators import action from rest_framework.response import Response from rest_framework import permissions class SnippetViewSet(viewsets.ModelViewSet): \u0026#34;\u0026#34;\u0026#34; This viewset automatically provides `list`, `create`, `retrieve`, `update` and `destroy` actions. Additionally we also provide an extra `highlight` action. \u0026#34;\u0026#34;\u0026#34; queryset = Snippet.objects.all() serializer_class = SnippetSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer]) def highlight(self, request, *args, **kwargs): snippet = self.get_object() return Response(snippet.highlighted) def perform_create(self, serializer): serializer.save(owner=self.request.user) @action装饰器用来创建除了create/update/delete以外的action，默认为GET请求，如果想改为POST请求，可以添加参数methods，它的源码如下：\ndef action(methods=None, detail=None, url_path=None, url_name=None, **kwargs): \u0026#34;\u0026#34;\u0026#34; Mark a ViewSet method as a routable action. `@action`-decorated functions will be endowed with a `mapping` property, a `MethodMapper` that can be used to add additional method-based behaviors on the routed action. :param methods: A list of HTTP method names this action responds to. Defaults to GET only. :param detail: Required. Determines whether this action applies to instance/detail requests or collection/list requests. :param url_path: Define the URL segment for this action. Defaults to the name of the method decorated. :param url_name: Define the internal (`reverse`) URL name for this action. Defaults to the name of the method decorated with underscores replaced with dashes. :param kwargs: Additional properties to set on the view. This can be used to override viewset-level *_classes settings, equivalent to how the `@renderer_classes` etc. decorators work for function- based API views. \u0026#34;\u0026#34;\u0026#34; methods = [\u0026#39;get\u0026#39;] if (methods is None) else methods methods = [method.lower() for method in methods] assert detail is not None, ( \u0026#34;@action() missing required argument: \u0026#39;detail\u0026#39;\u0026#34; ) # name and suffix are mutually exclusive if \u0026#39;name\u0026#39; in kwargs and \u0026#39;suffix\u0026#39; in kwargs: raise TypeError(\u0026#34;`name` and `suffix` are mutually exclusive arguments.\u0026#34;) def decorator(func): func.mapping = MethodMapper(func, methods) func.detail = detail func.url_path = url_path if url_path else func.__name__ func.url_name = url_name if url_name else func.__name__.replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;) # These kwargs will end up being passed to `ViewSet.as_view()` within # the router, which eventually delegates to Django\u0026#39;s CBV `View`, # which assigns them as instance attributes for each request. func.kwargs = kwargs # Set descriptive arguments for viewsets if \u0026#39;name\u0026#39; not in kwargs and \u0026#39;suffix\u0026#39; not in kwargs: func.kwargs[\u0026#39;name\u0026#39;] = pretty_name(func.__name__) func.kwargs[\u0026#39;description\u0026#39;] = func.__doc__ or None return func return decorator viewsets.ModelViewSet包含了增删改查视图集合，源码如下：\nclass ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet): \u0026#34;\u0026#34;\u0026#34; A viewset that provides default `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` and `list()` actions. \u0026#34;\u0026#34;\u0026#34; pass 它继承了mixins.CreateModelMixin、mixins.RetrieveModelMixin、mixins.UpdateModelMixin、mixins.DestroyModelMixin、mixins.ListModelMixin、GenericViewSet：\n其中mixins.RetrieveModelMixin、mixins.ListModelMixin和GenericViewSet在前面已经介绍过了\nmixins.CreateModelMixin\nclass CreateModelMixin: \u0026#34;\u0026#34;\u0026#34; Create a model instance. \u0026#34;\u0026#34;\u0026#34; def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() def get_success_headers(self, data): try: return {\u0026#39;Location\u0026#39;: str(data[api_settings.URL_FIELD_NAME])} except (TypeError, KeyError): return {} mixins.UpdateModelMixin\nclass UpdateModelMixin: \u0026#34;\u0026#34;\u0026#34; Update a model instance. \u0026#34;\u0026#34;\u0026#34; def update(self, request, *args, **kwargs): partial = kwargs.pop(\u0026#39;partial\u0026#39;, False) instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) if getattr(instance, \u0026#39;_prefetched_objects_cache\u0026#39;, None): # If \u0026#39;prefetch_related\u0026#39; has been applied to a queryset, we need to # forcibly invalidate the prefetch cache on the instance. instance._prefetched_objects_cache = {} return Response(serializer.data) def perform_update(self, serializer): serializer.save() def partial_update(self, request, *args, **kwargs): kwargs[\u0026#39;partial\u0026#39;] = True return self.update(request, *args, **kwargs) mixins.DestroyModelMixin\nclass DestroyModelMixin: \u0026#34;\u0026#34;\u0026#34; Destroy a model instance. \u0026#34;\u0026#34;\u0026#34; def destroy(self, request, *args, **kwargs): instance = self.get_object() self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) def perform_destroy(self, instance): instance.delete() 修改URLConf 视图改为ViewSet后，需要同时修改URLConf，编辑snippets/urls.py：\nfrom snippets.views import SnippetViewSet, UserViewSet, api_root from rest_framework import renderers snippet_list = SnippetViewSet.as_view({ \u0026#39;get\u0026#39;: \u0026#39;list\u0026#39;, \u0026#39;post\u0026#39;: \u0026#39;create\u0026#39; }) snippet_detail = SnippetViewSet.as_view({ \u0026#39;get\u0026#39;: \u0026#39;retrieve\u0026#39;, \u0026#39;put\u0026#39;: \u0026#39;update\u0026#39;, \u0026#39;patch\u0026#39;: \u0026#39;partial_update\u0026#39;, \u0026#39;delete\u0026#39;: \u0026#39;destroy\u0026#39; }) snippet_highlight = SnippetViewSet.as_view({ \u0026#39;get\u0026#39;: \u0026#39;highlight\u0026#39; }, renderer_classes=[renderers.StaticHTMLRenderer]) user_list = UserViewSet.as_view({ \u0026#39;get\u0026#39;: \u0026#39;list\u0026#39; }) user_detail = UserViewSet.as_view({ \u0026#39;get\u0026#39;: \u0026#39;retrieve\u0026#39; }) urlpatterns = format_suffix_patterns([ path(\u0026#39;\u0026#39;, api_root), path(\u0026#39;snippets/\u0026#39;, snippet_list, name=\u0026#39;snippet-list\u0026#39;), path(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;/\u0026#39;, snippet_detail, name=\u0026#39;snippet-detail\u0026#39;), path(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;/highlight/\u0026#39;, snippet_highlight, name=\u0026#39;snippet-highlight\u0026#39;), path(\u0026#39;users/\u0026#39;, user_list, name=\u0026#39;user-list\u0026#39;), path(\u0026#39;users/\u0026lt;int:pk\u0026gt;/\u0026#39;, user_detail, name=\u0026#39;user-detail\u0026#39;) ]) 注意，ViewSet需要绑定http methods和action，以指定请求方法对应的处理动作。\nRouters 使用ViewSet的一大好处是可以自动配置路由，DRF提供了rest_framework.routers：\n我们重新编写snippets/urls.py看看是什么效果：\nfrom django.urls import path, include from rest_framework.routers import DefaultRouter from snippets import views ## Create a router and register our viewsets with it. router = DefaultRouter() router.register(r\u0026#39;snippets\u0026#39;, views.SnippetViewSet) router.register(r\u0026#39;users\u0026#39;, views.UserViewSet) ## The API URLs are now determined automatically by the router. urlpatterns = [ path(\u0026#39;\u0026#39;, include(router.urls)), ] 真是厉害！这封装简直高级！而且DefaultRouter提供了API根目录的Endpoint，我们甚至可以把views.py中的api_root也删了。\n小结 本文是官方教程学习笔记的最后一篇了。可以从教程中了解到从函数视图到类视图，从View到ViewSet是如何封装起来的，实际运用中并不一定全部都得用类视图和ViewSet，还是需要平衡选择。后续还会有DRF系列的文章输出，不过作为阶段性成果，我把这7篇文章导成了PDF，和最后一版的源码，打包一起上传了，需要的话，可以在公众号后台回复【drf】获取：\n参考资料：\nhttps://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000012-djangodrf%E7%BB%88%E6%9E%81%E5%B0%81%E8%A3%85viewset%E5%92%8Crouter%E9%99%84%E6%95%99%E7%A8%8Bpdf%E6%BA%90%E7%A0%81/","summary":"在DRF官方教程的学习过程中，一个很明显的感受是框架在不断地进行封装，我们自己写框架/工具/脚本/平台也可以模仿模仿，先完成底层代码，再做多层封装，让使用者很容易就上手操作。本文是教程的最后一篇，介绍ViewSets和Routers。","title":"DRF终极封装ViewSet和Router附教程PDF源码"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nLambda Expression 有了Lambda Expression，就不用再写anonymous classes。\n写Lambda，首先要找到它的类型。\nThere is a restriction on the type of a lambda expression: it has to be a functional interface.\n函数接口，只有1个抽象方法的接口：\n@FunctionalInterface public interface Runnable { public abstract void run(); } 默认方法do not count，所以下面这个也是函数接口：\n@FunctionalInterface public interface Consumer\u0026lt;T\u0026gt; { void accept(T t); default Consumer\u0026lt;T\u0026gt; andThen(Consumer\u0026lt;? super T\u0026gt; after) { // the body of this method has been removed } } 静态方法也do not count，所以下面这个还是函数接口：\n@FunctionalInterface public interface Predicate\u0026lt;T\u0026gt; { boolean test(T t); default Predicate\u0026lt;T\u0026gt; and(Predicate\u0026lt;? super T\u0026gt; other) { // the body of this method has been removed } default Predicate\u0026lt;T\u0026gt; negate() { // the body of this method has been removed } default Predicate\u0026lt;T\u0026gt; or(Predicate\u0026lt;? super T\u0026gt; other) { // the body of this method has been removed } static \u0026lt;T\u0026gt; Predicate\u0026lt;T\u0026gt; isEqual(Object targetRef) { // the body of this method has been removed } static \u0026lt;T\u0026gt; Predicate\u0026lt;T\u0026gt; not(Predicate\u0026lt;? super T\u0026gt; target) { // the body of this method has been removed } } 接着，实现函数接口的抽象方法，比如Predicate的抽象方法：\nboolean test(T t); 实现Predicate\u0026lt;String\u0026gt;：\nPredicate\u0026lt;String\u0026gt; predicate = (String s) -\u0026gt; { return s.length() == 3; }; 省略了返回类型和方法名，只保留参数列表和方法体，用-\u0026gt;连接。\n再简化：\nPredicate\u0026lt;String\u0026gt; predicate = s -\u0026gt; s.length() == 3; 这就是最常见的Lambda，因为前面有String，编译器能够知道，所以这里把(String s)简写为s，然后只有一行，再把大括号和return省略掉。\n无return，跟省略掉return：\nConsumer\u0026lt;String\u0026gt; print = s -\u0026gt; System.out.println(s); 无参数，用()代替：\nRunnable runnable = () -\u0026gt; System.out.println(\u0026#34;I am running\u0026#34;); Lambda的使用：\nList\u0026lt;String\u0026gt; retainStringsOfLength3(List\u0026lt;String\u0026gt; strings) { Predicate\u0026lt;String\u0026gt; predicate = s -\u0026gt; s.length() == 3; List\u0026lt;String\u0026gt; stringsOfLength3 = new ArrayList\u0026lt;\u0026gt;(); for (String s: strings) { if (predicate.test(s)) { stringsOfLength3.add(s); } } return stringsOfLength3; } 因为Lambda实现了接口的抽象方法，所以predicate.test(s)调用的实际上是Lambda。\nLambda不能修改变量值：\nint calculateTotalPrice(List\u0026lt;Product\u0026gt; products) { int totalPrice = 0; Consumer\u0026lt;Product\u0026gt; consumer = product -\u0026gt; totalPrice += product.getPrice(); // 编译错误 for (Product product: products) { consumer.accept(product); } } 编译错误：Variable used in lambda expression should be final or effectively final\nThis process of accessing variable is called capturing: lambdas cannot capture variables, they can only capture values. A final variable is in fact a value.\n也就是Lambda只能读，不能写。\n使用Lambda Lambda是JDK8跨时代的语法技术，它引领了大量的JDK API重写。\njava.util.function中包含了很多可供实现的函数接口，functional interfaces：\nhttps://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/function/package-summary.html\n比如：\n@FunctionalInterface public interface Supplier\u0026lt;T\u0026gt; { T get(); } Lambda实现：\nSupplier\u0026lt;String\u0026gt; supplier = () -\u0026gt; \u0026#34;Hello Duke!\u0026#34;;` 使用：\nRandom random = new Random(314L); Supplier\u0026lt;Integer\u0026gt; newRandom = () -\u0026gt; random.nextInt(10); for (int index = 0; index \u0026lt; 5; index++) { System.out.println(newRandom.get() + \u0026#34; \u0026#34;); } 比如：\nList\u0026lt;String\u0026gt; strings = ...; // really any list of any kind of objects Consumer\u0026lt;String\u0026gt; printer = s -\u0026gt; System.out.println(s); strings.forEach(printer); 比如：\nList\u0026lt;String\u0026gt; immutableStrings = List.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;); List\u0026lt;String\u0026gt; strings = new ArrayList\u0026lt;\u0026gt;(immutableStrings); Predicate\u0026lt;String\u0026gt; isOddLength = s -\u0026gt; s.length() % 2 == 0; strings.removeIf(isOddLength); System.out.println(\u0026#34;strings = \u0026#34; + strings); 比如：\n@FunctionalInterface public interface Function\u0026lt;T, R\u0026gt; { R apply(U u); // default and static methods removed } Function\u0026lt;String, Integer\u0026gt; toLength = s -\u0026gt; s.length(); String word = ...; // any kind of word will do int length = toLength.apply(word); 以上是JDK中最经典的4种function interface：\nSupplier\u0026lt;T\u0026gt; Consumer\u0026lt;T\u0026gt; Predicate\u0026lt;T\u0026gt; Function\u0026lt;T, R\u0026gt; 总结一下，Lambda的使用就是先实现function interface的abstract method，然后①接口实例调用abstract method，或者②接口实例被forEach等调用。\nLambda使得function interface能够实例化：Predicate\u0026lt;String\u0026gt; isOddLength = s -\u0026gt; s.length() % 2 == 0;\nThe java.util.function package is now central in Java, because all the lambda expressions you are going to use in the Collections Framework or the Stream API implement one of the interfaces from that package.\n发现没有，把：\nList\u0026lt;String\u0026gt; strings = ...; // really any list of any kind of objects Consumer\u0026lt;String\u0026gt; printer = s -\u0026gt; System.out.println(s); strings.forEach(printer); 简写一下，去掉中间的接口实例：\nList\u0026lt;String\u0026gt; strings = ...; // really any list of any kind of objects strings.forEach(s -\u0026gt; System.out.println(s)); 这不就是常见到的Lambda的形态嘛。换句话说，Lambda的结果就是函数接口实例（Function interface instance），使用Lambda本质上就是调用函数，Java中没有函数的概念，通过function interface的abstract method，引入了函数，从而造就了Lambda。\nMethod References Sometimes people call these lambda expressions \u0026ldquo;anonymous methods\u0026rdquo;, since it is just that: a method that has no name, and that you can move around your application, store in a field or a variable, pass as an argument to a method or a constructor and return from a method.\nConsumer\u0026lt;String\u0026gt; printer = s -\u0026gt; System.out.println(s); 这里的Lambda没有什么逻辑，其实就是System.out.println()方法的引用，所以可以简写为Bound（System.out优化前后一致）：\nConsumer\u0026lt;String\u0026gt; printer = System.out::println; Static：\nIntBinaryOperator max = (a, b) -\u0026gt; Integer.max(a, b); IntBinaryOperator max = Integer::max; Unbound（优化前s，优化后String；优化前user，优化后User）：\nFunction\u0026lt;String, Integer\u0026gt; toLength = s -\u0026gt; s.length(); Function\u0026lt;String, Integer\u0026gt; toLength = String::length; Function\u0026lt;User, String\u0026gt; getName = user -\u0026gt; user.getName(); Function\u0026lt;String, Integer\u0026gt; toLength = User::getName; BiFunction\u0026lt;String, String, Integer\u0026gt; indexOf = (sentence, word) -\u0026gt; sentence.indexOf(word); BiFunction\u0026lt;String, String, Integer\u0026gt; indexOf = String::indexOf; Constructor：\nSupplier\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; newListOfStrings = () -\u0026gt; new ArrayList\u0026lt;\u0026gt;(); Supplier\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; newListOfStrings = ArrayList::new; 总结：\nCombining Lambda Expressions Function interface中的defualt method该登场了。\nPredicate\u0026lt;String\u0026gt; nonNull = s -\u0026gt; s != null; Predicate\u0026lt;String\u0026gt; nonEmpty = s -\u0026gt; s.isEmpty(); Predicate\u0026lt;String\u0026gt; shorterThan5 = s -\u0026gt; s.length() \u0026lt; 5; Predicate\u0026lt;String\u0026gt; p = nonNull.and(nonEmpty).and(shorterThan5); 这里的and()就是default method，它实现了Lambda的组合，也就是链式调用的既视感。\n稍复杂点的：\nPredicate\u0026lt;String\u0026gt; isNull = Objects::isNull; Predicate\u0026lt;String\u0026gt; isEmpty = String::isEmpty; Predicate\u0026lt;String\u0026gt; isNullOrEmpty = isNull.or(isEmpty); Predicate\u0026lt;String\u0026gt; isNotNullNorEmpty = isNullOrEmpty.negate(); Predicate\u0026lt;String\u0026gt; shorterThan5 = s -\u0026gt; s.length() \u0026lt; 5; Predicate\u0026lt;String\u0026gt; p = isNotNullNorEmpty.and(shorterThan5); Comparators @FunctionalInterface public interface Comparator\u0026lt;T\u0026gt; { int compare(T o1, T o2); } Lambda实现：\nComparator\u0026lt;Integer\u0026gt; comparator = (i1, i2) -\u0026gt; Integer.compare(i1, i2); Comparator\u0026lt;Integer\u0026gt; comparator = Integer::compare; Comparator的静态方法：\nComparator\u0026lt;String\u0026gt; comparator = Comparator.comparing(String::length); This comparing() method is a static method of the Comparator interface. It takes a Function as an argument, that should return a type that is an extension of Comparable.\n使用：\nList\u0026lt;User\u0026gt; users = ...; // this is your list Comparator\u0026lt;User\u0026gt; byName = Comparator.comparing(User::getName); users.sort(byName); 还有默认方法thenComparing：\nComparator\u0026lt;User\u0026gt; byFirstNameThenLastName = Comparator.comparing(User::getFirstName) .thenComparing(User::getLastName); 参考资料：\nLambda Expressions https://dev.java/learn/lambdas/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000012-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B09lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"lambda-expression\"\u003eLambda Expression\u003c/h2\u003e\n\u003cp\u003e有了Lambda Expression，就不用再写anonymous classes。\u003c/p\u003e\n\u003cp\u003e写Lambda，首先要找到它的类型。\u003c/p\u003e\n\u003cp\u003eThere is a restriction on the type of a lambda expression: it has to be a functional interface.\u003c/p\u003e\n\u003cp\u003e函数接口，只有1个抽象方法的接口：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@FunctionalInterface\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eRunnable\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eabstract\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e默认方法do not count，所以下面这个也是函数接口：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@FunctionalInterface\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eConsumer\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eaccept\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eT\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003et\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edefault\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eConsumer\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eandThen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eConsumer\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;?\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eT\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eafter\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// the body of this method has been removed\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e静态方法也do not count，所以下面这个还是函数接口：\u003c/p\u003e","title":"Java官方笔记9Lambda表达式"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter断言用来对服务器的响应数据进行验证，其中的响应断言基本能完成大多数的验证工作，但是JMeter还是提供了适用于多个不同场景的断言元件，一共16个，如下图所示：\nResponse Assertion 最最最常用的断言，足够满足工作需要。\nApply to：断言应用范围。\n子取样器可能是JMeter手动设置的，可能是重定向，可能是嵌入资源（images、iframe等）。\nField to Test：哪部分响应域参与断言。\nIgnore Status：多个响应断言，如果当前断言失败了，勾选此选项可以忽略此响应结果，继续下一个断言。\nPatterns to Test：\nContains：实际响应包括预期匹配内容，支持正则表达式。 Matchs：实际响应完全匹配预期匹配内容，大小写不敏感，支持正则表达式。 Equals：实际响应完全等于预期匹配内容，大小写敏感，只支持字符串，不支持正则表达式。 Substring：实际响应包括预期匹配内容，大小写敏感，只支持字符串，不支持正则表达式。 Not：对测试模式进行否定。\nOr：将多个测试模式以逻辑“或”组合起来。\nAdd：添加需要匹配的字符串或者正则表达式。\nCustom failure message：自定义失败信息。\n多模式断言，Add可以添加多个断言模式，假设有模式p1，p2，p3：\n默认And：p1 and p2 and p3 只选Not：(not p1) and (not p2) and (not p3) 只选Or：p1 or p2 or p3 同时选Not和Or：(not p1) or (not p2) or (not p3) JSON Assertion JSONPath是一个JSON查询语言，可以用来快速查找节点，语法参考：\nhttps://github.com/alibaba/fastjson/wiki/JSONPath\nAssert JSON Path exists：JSONPath表达式。\nAdditionally assert value：普通匹配。\nMatch as regular expression：正则匹配。\nExpected Value：预期值。\nExpect null：预期为null。\nInvert assertion（will fail if above conditions met）：反转断言，如果上面的条件满足就断言失败。\nSize Assertion 验证响应数据Size大小。\nApply to：断言应用范围。\nResponse Size Field to Test：哪部分响应域参与断言。\nSize to Assert：断言时Size的阀值设置。\nJSR223 Assertion 支持其他脚本语言如JavaScript。\nXPath2 Assertion 2.0版本的XPath断言，可以验证WebServices协议接口响应的XML格式数据。\nBinary Response Assertion 验证二进制响应数据。\nCompare Assertion 因为会消耗较多资源，一般不建议使用，仅仅用来调试。\nCompare Assertion进行设置，需要配合Comparision Assertion Visualizer进行断言结果显示。\nDuration Assertion 验证Sampler运行测试消耗的时间，比如断言持续10s，如果实际耗时30秒，就会断言失败。\nHTML Assertion 用的比较少。\nJTidy是HTML Tidy的Java实现版本，提供了HTML的语法检查器，可以用来解析HTML代码，可以用来清除格式不好和不对的HTML，类似于一个处理HTML文件的DOM解析器。\nJSON JMESPath Assertion 跟JSONPath一样，是另外一个JSON查询语言，语法参考：\nhttps://jmespath.org/\nMD5Hex Assertion 对返回的MD5结果进行断言。\nMD5Hex：直接填入MD5值。\nSMIME Assertion 不常用。\n对加密和签名的邮件进行断言。\nXML Assertion 不常用。\n验证响应数据是正确的XML格式。\nXML Schema Assertion 不常用。\nXML Schema的作用是定义一份XML文档的合法模板。\nXPath Assertion 可以验证WebServices协议接口响应的XML格式数据。\nBeanShell Assertion BeanShell作为脚本语言能够方便地调用Java类，在断言元件中，可以访问JMeter的诸多属性，具体有：\nlog，写日志。 SampleResult，获取响应数据、响应码等。 Response，获取响应数据。 Failure，设置断言成功与否，Boolean类型。 FailureMessage，设置失败信息。 ResponseData，响应数据。 ResponseCode，响应码。 ResponseMessage，响应信息。 ResponseHeaders，响应头信息。 RequestHeaders，请求头信息。 SampleLabel，取样器Label信息。 SamplerData，发送给服务器的数据。 Ctx（JMeterContext），JMeter上下文信息，获取线程数、线程号等信息。 Vars（JMeterVariables），获取JMeter中定义的变量，或者设置变量。 Props（JMeterProperties），获取JMeter中的属性，或者设置属性。 示例：\nSampleResult.getRequestHeaders() SampleResult.setSuccessful(false) SampleResult.getResponseDataAsString() SampleResult.getResponseCode() SampleResult.getResponseMessage() 小结 本文对JMeter所有断言元件进行了介绍，其中的响应断言已经足够使用了，对于RESTful接口来说，JSON格式响应数据可以使用JSON Assertion和JSON JMESPath Assertion，其他元件则用的比较少。另外BeanShell Assertion在编写基于JMeter的测试平台时可能会用到。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\nhttps://zhuanlan.zhihu.com/p/72917999\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000012-jmeterjmeter%E5%93%8D%E5%BA%94%E6%96%AD%E8%A8%80%E5%A4%9F%E7%94%A8%E4%BA%86/","summary":"JMeter断言用来对服务器的响应数据进行验证，其中的响应断言基本能完成大多数的验证工作，但是JMeter还是提供了适用于多个不同场景的断言元件，一共16个，如下图所示：","title":"JMeter响应断言够用了"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nWhat We Mean by \u0026ldquo;Spring\u0026rdquo; The term \u0026ldquo;Spring\u0026rdquo; means different things in different contexts. It can be used to refer to the Spring Framework project itself, which is where it all started. Over time, other Spring projects have been built on top of the Spring Framework. Most often, when people say \u0026ldquo;Spring\u0026rdquo;, they mean the entire family of projects. This reference documentation focuses on the foundation: the Spring Framework itself.\nIntroduction IoC，the Inversion of Control. IoC is also known as dependency injection (DI). （依赖注入实现了控制反转，为此Spring设计了The IoC Container这项技术）\nIt is a process whereby objects define their dependencies (that is, the other objects they work with) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method. The container then injects those dependencies when it creates the bean. This process is fundamentally the inverse (hence the name, Inversion of Control) of the bean itself controlling the instantiation or location of its dependencies by using direct construction of classes or a mechanism such as the Service Locator pattern.\n这段描述是关于\u0026quot;依赖注入\u0026quot;（Dependency Injection，DI）的解释。依赖注入是软件工程中的一种设计模式，它通过构造函数参数、工厂方法的参数或者在对象实例构造或从工厂方法返回后设置的属性来定义对象的依赖关系。容器在创建对象时注入这些依赖。这个过程本质上是对象本身通过直接构造类或使用服务定位器模式等机制来控制其依赖项的实例化或定位的反转（因此得名“控制反转”）。\n传统上，对象可能会直接实例化或定位其所依赖的对象，导致紧耦合，并且难以修改或测试单个组件。依赖注入通过将依赖管理的责任从对象本身移交给外部容器或框架来解决这个问题。\n应用依赖注入，对象可以更具可重用性和可维护性，因为它们只关注履行自己的职责，而依赖关系从外部进行注入。这样可以更好地分离关注点，并且可以更容易地修改依赖关系，而无需修改依赖对象。\n通过依赖注入实现的控制反转还有助于测试，因为在单元测试期间，可以轻松地使用模拟对象替代依赖项，实现对各个组件的隔离测试。\n总体而言，依赖注入是一种强大的技术，通过将依赖的创建和管理从依赖对象本身解耦，促进模块化、松耦合和可测试的软件设计。\nThe org.springframework.beans and org.springframework.context packages are the basis for Spring Framework’s IoC container.\nIn Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container.\nContainer Overview The org.springframework.context.ApplicationContext interface represents the Spring IoC container and is responsible for instantiating, configuring, and assembling the beans.\nIn stand-alone applications, it is common to create an instance of ClassPathXmlApplicationContext or FileSystemXmlApplicationContext.\nConfiguration Metadata\nThe configuration metadata is represented in XML, Java annotations, or Java code. This configuration metadata represents how you, as an application developer, tell the Spring container to instantiate, configure, and assemble the objects in your application.\nXML-based metadata is not the only allowed form of configuration metadata. Other forms of metadata with the Spring container, see:\nAnnotation-based configuration: define beans using annotation-based configuration metadata.\nJava-based configuration: define beans external to your application classes by using Java rather than XML files. To use these features, see the@Configuration, @Bean, @Import, and @DependsOn annotations.\nXML-based configuration metadata configures these beans as \u0026lt;bean/\u0026gt; elements inside a top-level \u0026lt;beans/\u0026gt; element. Java configuration typically uses @Bean-annotated methods within a @Configuration class.\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd\u0026#34;\u0026gt; \u0026lt;bean id=\u0026#34;...\u0026#34; class=\u0026#34;...\u0026#34;\u0026gt; \u0026lt;!-- collaborators and configuration for this bean go here --\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;...\u0026#34; class=\u0026#34;...\u0026#34;\u0026gt; \u0026lt;!-- collaborators and configuration for this bean go here --\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- more bean definitions go here --\u0026gt; \u0026lt;/beans\u0026gt; Instantiating a Container\nApplicationContext context = new ClassPathXmlApplicationContext(\u0026#34;services.xml\u0026#34;, \u0026#34;daos.xml\u0026#34;); services.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd\u0026#34;\u0026gt; \u0026lt;!-- services --\u0026gt; \u0026lt;bean id=\u0026#34;petStore\u0026#34; class=\u0026#34;org.springframework.samples.jpetstore.services.PetStoreServiceImpl\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;accountDao\u0026#34; ref=\u0026#34;accountDao\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;itemDao\u0026#34; ref=\u0026#34;itemDao\u0026#34;/\u0026gt; \u0026lt;!-- additional collaborators and configuration for this bean go here --\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- more bean definitions for services go here --\u0026gt; \u0026lt;/beans\u0026gt; The property name element refers to the name of the JavaBean property, and the ref element refers to the name of another bean definition.\nThis linkage between id and ref elements expresses the dependency between collaborating objects.\ndaos.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd\u0026#34;\u0026gt; \u0026lt;bean id=\u0026#34;accountDao\u0026#34; class=\u0026#34;org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao\u0026#34;\u0026gt; \u0026lt;!-- additional collaborators and configuration for this bean go here --\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;itemDao\u0026#34; class=\u0026#34;org.springframework.samples.jpetstore.dao.jpa.JpaItemDao\u0026#34;\u0026gt; \u0026lt;!-- additional collaborators and configuration for this bean go here --\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- more bean definitions for data access objects go here --\u0026gt; \u0026lt;/beans\u0026gt; Composing XML-based Configuration Metadata\nOften, each individual XML configuration file represents a logical layer or module in your architecture.\nUse one or more occurrences of the \u0026lt;import/\u0026gt; element to load bean definitions from another file or files. The following example shows how to do so:\n\u0026lt;beans\u0026gt; \u0026lt;import resource=\u0026#34;services.xml\u0026#34;/\u0026gt; \u0026lt;import resource=\u0026#34;resources/messageSource.xml\u0026#34;/\u0026gt; \u0026lt;import resource=\u0026#34;/resources/themeSource.xml\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;bean1\u0026#34; class=\u0026#34;...\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;bean2\u0026#34; class=\u0026#34;...\u0026#34;/\u0026gt; \u0026lt;/beans\u0026gt; The Groovy Bean Definition DSL\nbeans { dataSource(BasicDataSource) { driverClassName = \u0026#34;org.hsqldb.jdbcDriver\u0026#34; url = \u0026#34;jdbc:hsqldb:mem:grailsDB\u0026#34; username = \u0026#34;sa\u0026#34; password = \u0026#34;\u0026#34; settings = [mynew:\u0026#34;setting\u0026#34;] } sessionFactory(SessionFactory) { dataSource = dataSource } myService(MyService) { nestedBean = { AnotherBean bean -\u0026gt; dataSource = dataSource } } } Using the Container\nBy using the method T getBean(String name, Class\u0026lt;T\u0026gt; requiredType), you can retrieve instances of your beans.\n// create and configure beans ApplicationContext context = new ClassPathXmlApplicationContext(\u0026#34;services.xml\u0026#34;, \u0026#34;daos.xml\u0026#34;); // retrieve configured instance PetStoreService service = context.getBean(\u0026#34;petStore\u0026#34;, PetStoreService.class); // use configured instance List\u0026lt;String\u0026gt; userList = service.getUsernameList(); Indeed, your application code should have no calls to the getBean() method at all and thus have no dependency on Spring APIs at all. For example, Spring’s integration with web frameworks provides dependency injection for various web framework components such as controllers and JSF-managed beans, letting you declare a dependency on a specific bean through metadata (such as an autowiring annotation).\nBean Overview Beans are created with the configuration metadata that you supply to the container (for example, in the form of XML \u0026lt;bean/\u0026gt; definitions).\nClass只是Class，但是如果配置到了XML的\u0026lt;bean/\u0026gt;，那么它就成为了the Spring Bean。\nBeanDefinition：\nA package-qualified class name: typically, the actual implementation class of the bean being defined.\nBean behavioral configuration elements, which state how the bean should behave in the container (scope, lifecycle callbacks, and so forth).\nReferences to other beans that are needed for the bean to do its work. These references are also called collaborators or dependencies.\nOther configuration settings to set in the newly created object — for example, the size limit of the pool or the number of connections to use in a bean that manages a connection pool.\nNaming Beans\nIn XML-based configuration metadata, you use the id attribute, the name attribute, or both to specify bean identifiers.\nAliasing a Bean outside the Bean Definition\n\u0026lt;alias name=\u0026#34;fromName\u0026#34; alias=\u0026#34;toName\u0026#34;/\u0026gt; Instantiating Beans\nIf you use XML-based configuration metadata, you specify the type (or class) of object that is to be instantiated in the class attribute of the \u0026lt;bean/\u0026gt; element.\nInstantiation with a Constructor\n\u0026lt;bean id=\u0026#34;exampleBean\u0026#34; class=\u0026#34;examples.ExampleBean\u0026#34;/\u0026gt; \u0026lt;bean name=\u0026#34;anotherExample\u0026#34; class=\u0026#34;examples.ExampleBeanTwo\u0026#34;/\u0026gt; Instantiation with a Static Factory Method\n\u0026lt;bean id=\u0026#34;clientService\u0026#34; class=\u0026#34;examples.ClientService\u0026#34; factory-method=\u0026#34;createInstance\u0026#34;/\u0026gt; public class ClientService { private static ClientService clientService = new ClientService(); private ClientService() {} public static ClientService createInstance() { return clientService; } } Instantiation by Using an Instance Factory Method\n\u0026lt;!-- the factory bean, which contains a method called createInstance() --\u0026gt; \u0026lt;bean id=\u0026#34;serviceLocator\u0026#34; class=\u0026#34;examples.DefaultServiceLocator\u0026#34;\u0026gt; \u0026lt;!-- inject any dependencies required by this locator bean --\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- the bean to be created via the factory bean --\u0026gt; \u0026lt;bean id=\u0026#34;clientService\u0026#34; factory-bean=\u0026#34;serviceLocator\u0026#34; factory-method=\u0026#34;createClientServiceInstance\u0026#34;/\u0026gt; public class DefaultServiceLocator { private static ClientService clientService = new ClientServiceImpl(); public ClientService createClientServiceInstance() { return clientService; } } In Spring documentation, \u0026ldquo;factory bean\u0026rdquo; refers to a bean that is configured in the Spring container and that creates objects through an instance or static factory method.\nDetermining a Bean’s Runtime Type\nThe recommended way to find out about the actual runtime type of a particular bean is a BeanFactory.getType call for the specified bean name.\n参考资料：\nThe IoC Container https://docs.spring.io/spring-framework/reference/core/beans.html\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000012-spring%E5%AF%B9ioc%E5%AE%B9%E5%99%A8%E5%92%8Cbean%E7%9A%84%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"what-we-mean-by-spring\"\u003eWhat We Mean by \u0026ldquo;Spring\u0026rdquo;\u003c/h1\u003e\n\u003cp\u003eThe term \u0026ldquo;Spring\u0026rdquo; means different things in different contexts. It can be used to refer to the Spring Framework project itself, which is where it all started. Over time, other Spring projects have been built on top of the Spring Framework. Most often, when people say \u0026ldquo;Spring\u0026rdquo;, they mean the entire family of projects. This reference documentation focuses on the foundation: the Spring Framework itself.\u003c/p\u003e","title":"对IoC容器和Bean的学习笔记"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n这个版本质量好不好？\n每次遇到这种问题，都很难给出让人满意的答案。\n本文梳理下当前我对版本质量的思考，未来会不断迭代更新我的认知。\n软件产品质量模型，描述了8个特性：功能适用性、性能效率、兼容性、可用性、可靠性、安全性、可维护性、可移植性。这个模型可以从全局视角，对软件产品质量进行评估。\n总不能每次发完版，就套用这个模型来给产品质量打个分吧。\n更现实的做法是，定义量化指标多维度进行评估。\n指标库是个庞大和复杂的体系，涵盖了项目、产品、需求、开发、测试、质量等多个方面的关键性能指标（Key Performance Indicators，简称KPIs）。\n这里只罗列版本质量相关的指标：\n线上问题数量及问题级别：系统发布触发用户问题。质量好不好，必然要看问题多不多，影响大不大。\n需求缺陷密度：需求缺陷数 / 完成需求数。缺陷少，质量高。想必我们都体验过测出来一堆问题，上线后还是一堆问题的烂质量。\n缺陷关闭率：已关闭缺陷数 / 有效缺陷总数。要想提高质量，就要解决问题。曾经历过遗留30个bug不处理就上线的，初创团队抢占市场可以理解，稳定产品的缺陷关闭率低就需要引起重视了。\n缺陷关闭时长：所有缺陷(关闭时间 - 创建时间)求和 / 有效缺陷总数。长时间、跨版本都没有解决的问题，一拖再拖，说不定哪天就在线上暴雷了。所有问题都要及时关注，尽快修复。\n缺陷逃逸率：上线后发现缺陷数 / 有效缺陷总数。测试同学注意了，灵魂三问，没有测过吗？为什么没测到？为什么不知道？遇到过测试马虎了事的，测试不给力，保不住质量下限。\n除了怎么衡量版本质量以外，还有个问题是怎么交付出高质量产品，是让开发少写点bug，还是让测试多找点bug？\n有个很有意思的说法是，高质量产品是用出来的。这个认知没有局限于开发、测试谁背锅的责任归属，而是从产品迭代的角度出发，更多的功能，更多的缺陷，就会投入更多的努力去修复这些质量问题，从而提高产品质量。\n让更多客户使用，发现更多问题，持续完善产品，才能打磨出高质量产品。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000012-%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E8%B4%A8%E9%87%8F%E7%9A%84%E6%80%9D%E8%80%83v1.0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/41b06258d80d4711a81d97d4be317ce4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这个版本质量好不好？\u003c/p\u003e\n\u003cp\u003e每次遇到这种问题，都很难给出让人满意的答案。\u003c/p\u003e\n\u003cp\u003e本文梳理下当前我对版本质量的思考，未来会不断迭代更新我的认知。\u003c/p\u003e\n\u003cp\u003e软件产品质量模型，描述了8个特性：功能适用性、性能效率、兼容性、可用性、可靠性、安全性、可维护性、可移植性。这个模型可以从全局视角，对软件产品质量进行评估。\u003c/p\u003e\n\u003cp\u003e总不能每次发完版，就套用这个模型来给产品质量打个分吧。\u003c/p\u003e\n\u003cp\u003e更现实的做法是，定义\u003cstrong\u003e量化指标\u003c/strong\u003e多维度进行评估。\u003c/p\u003e\n\u003cp\u003e指标库是个庞大和复杂的体系，涵盖了项目、产品、需求、开发、测试、质量等多个方面的关键性能指标（Key Performance Indicators，简称KPIs）。\u003c/p\u003e\n\u003cp\u003e这里只罗列\u003cstrong\u003e版本质量相关\u003c/strong\u003e的指标：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e线上问题数量及问题级别\u003c/strong\u003e：系统发布触发用户问题。质量好不好，必然要看问题多不多，影响大不大。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e需求缺陷密度\u003c/strong\u003e：需求缺陷数 / 完成需求数。缺陷少，质量高。想必我们都体验过测出来一堆问题，上线后还是一堆问题的烂质量。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺陷关闭率\u003c/strong\u003e：已关闭缺陷数 / 有效缺陷总数。要想提高质量，就要解决问题。曾经历过遗留30个bug不处理就上线的，初创团队抢占市场可以理解，稳定产品的缺陷关闭率低就需要引起重视了。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺陷关闭时长\u003c/strong\u003e：所有缺陷(关闭时间 - 创建时间)求和 / 有效缺陷总数。长时间、跨版本都没有解决的问题，一拖再拖，说不定哪天就在线上暴雷了。所有问题都要及时关注，尽快修复。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺陷逃逸率\u003c/strong\u003e：上线后发现缺陷数 / 有效缺陷总数。测试同学注意了，灵魂三问，没有测过吗？为什么没测到？为什么不知道？遇到过测试马虎了事的，测试不给力，保不住质量下限。\u003c/p\u003e\n\u003cp\u003e除了怎么衡量版本质量以外，还有个问题是怎么交付出高质量产品，是让开发少写点bug，还是让测试多找点bug？\u003c/p\u003e\n\u003cp\u003e有个很有意思的说法是，\u003cstrong\u003e高质量产品是用出来的\u003c/strong\u003e。这个认知没有局限于开发、测试谁背锅的责任归属，而是从产品迭代的角度出发，更多的功能，更多的缺陷，就会投入更多的努力去修复这些质量问题，从而提高产品质量。\u003c/p\u003e\n\u003cp\u003e让更多客户使用，发现更多问题，持续完善产品，才能打磨出高质量产品。\u003c/p\u003e","title":"关于版本质量的思考V1.0"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n创建上图的pytest项目结构，只需要一条命令即可：\ntep startproject demo 前提是安装tep0.9.8版本：\npip install tep==0.9.8 示例代码单独放在了samples文件夹下，可以放心将整个目录删除。其中的db演示了连接MySQL数据库，http演示了requests常见用法和tep request的猴子补丁，login_pay演示了从登陆到下单该如何编写脚本，包括httprunner、mvc（不推荐）、tep（强烈推荐）三种写法。\nfixtures从conftest.py文件中单独拎了出来。这个设计与Django不谋而合，Django也有个叫做fixtures的文件夹，用来存放测试数据。其实pytest的fixture的本意是可以用来做setup和teardown的，从这个意义来说，fixtures就是存放测试数据的，比如封装登录接口为用例提供token。\nutils存放了一些工具。比如自带的fastapi项目，request猴子补丁，mitmproxy抓包自动生成用例。request猴子补丁可以自定义日志输出格式，在发送请求前后完成任何你想做的操作。\ntep0.9.8版本正式发布，对项目脚手架进行了精简，是为接下来的**《tep完整教程帮你突破pytest》**做好准备。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000012-%E6%B0%91%E9%97%B4%E6%B5%81%E8%BD%AC%E7%9A%84pytest%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d890fa29667248acb99bcd84bb794f0a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e创建上图的pytest项目结构，\u003cstrong\u003e只需要一条命令即可\u003c/strong\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etep startproject demo\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8ef1c1ae74b04a1295e9a3c91a44aeac.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e前提是安装tep0.9.8版本：\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epip install \u003cspan style=\"color:#953800\"\u003etep\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e0.9.8\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e示例代码单独放在了samples文件夹下，可以放心将整个目录删除。其中的db演示了连接MySQL数据库，http演示了requests常见用法和tep request的猴子补丁，login_pay演示了\u003cstrong\u003e从登陆到下单该如何编写脚本\u003c/strong\u003e，包括httprunner、mvc（不推荐）、tep（强烈推荐）三种写法。\u003c/p\u003e\n\u003cp\u003efixtures从conftest.py文件中单独拎了出来。这个设计与Django不谋而合，Django也有个叫做fixtures的文件夹，用来存放测试数据。其实pytest的fixture的本意是可以用来做setup和teardown的，从这个意义来说，fixtures就是存放测试数据的，比如封装登录接口为用例提供token。\u003c/p\u003e\n\u003cp\u003eutils存放了一些工具。比如自带的fastapi项目，request猴子补丁，mitmproxy抓包自动生成用例。request猴子补丁可以自定义日志输出格式，在发送请求前后完成任何你想做的操作。\u003c/p\u003e\n\u003cp\u003etep0.9.8版本正式发布，对项目脚手架进行了精简，是为接下来的**《tep完整教程帮你突破pytest》**做好准备。\u003c/p\u003e","title":"民间流转的pytest项目结构设计"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n切片，就像面包，给几刀，切成一片一片，可以做成吐司，也可以做成三明治，口味更佳：\n列表（list）、元组（tuple）、字符串（str）都能进行切片，得到子片段，实际上切片操作比想象的要强大很多，能取值，亦能赋值。\n忽略最后一个元素 切片是用下标和冒号来描述的，比如s[2:13]。对于2, 3, ..., 12这个序列，表达为[2, 13)，左闭右开，比[2, 12]和(1, 13)都更合理，理由如下：\n上限减去下限等于元素个数，比如13 - 2 = 11，刚好就有11个元素。 连续的范围没有重叠，比如[2, 13) 和 [13, 25) 是两个连续的范围，13只会包含在后一个里。 下标从0开始 对于10个元素，写成[0, 10)比[1, 11)更合理，理由如下：\nN个元素，[0, N)比[1, N+1)写法更简洁，不需要+1。\n某个元素的下标等于排在它前面元素的个数，方便使用，比如：\n0 1 2 3 4 5 6 7 8 9 ^ 前面有4个元素 好用的切片 以上两个数学理论给切片使用带来了很多好处：\n当只有最后一个位置信息时，可以快速看出有几个元素，比如my_list[:3]返回3个元素。\n当起止位置信息都可见时，可以快速计算出长度，用stop - start就可以了，比如my_list[1:3]长度为2。\n利用任意一个下标把序列切割成不重叠的两部分，只要写成my_list[:x]和my_list[x:]就可以了，比如\n\u0026gt;\u0026gt;\u0026gt; my_list = [10, 20, 30, 40, 50, 60] \u0026gt;\u0026gt;\u0026gt; my_list[:3] [10, 20, 30] \u0026gt;\u0026gt;\u0026gt; my_list[3:] [40, 50, 60] Python里的范围（range）也是忽略最后一个元素，下标从0开始的。\n切片间隔 切片除了s[a:b]，还有第三个下标s[a:b:c]，意思是对s在a和b之间以c为间隔取值，c还可以为负，负值意味着反向取值。比如：\n\u0026gt;\u0026gt;\u0026gt; s = \u0026#34;bicycle\u0026#34; \u0026gt;\u0026gt;\u0026gt; s[::3] \u0026#34;bye\u0026#34; \u0026gt;\u0026gt;\u0026gt; s[::-1] \u0026#34;elcycib\u0026#34; \u0026gt;\u0026gt;\u0026gt; s[::-2] \u0026#34;eccb\u0026#34; a:b:c更严谨的描述是start:stop:step。\n语法如此简洁，用脚想也知道是Python魔法方法干的好事！在对s[a:b:c]进行求值的时候，Python实际上会调用s.__getitem__(slice(a, b, c))，熟悉的配方，熟悉的味道。slice(a, b, c)是a:b:c用在[]中返回的切片对象，slice()是Python内置函数，示例：\ninvoice = \u0026#34;Mini Kit $34.95 1 $ 34.95\u0026#34; SKU = slice(0, 8) print(invoice[SKU]) 切片赋值 切片有一个强大功能是给切片赋值，如果把切片放在赋值语句的左边，或把它作为del操作的对象，我们就可以对序列进行嫁接、切除或就地修改操作。示例：\n\u0026gt;\u0026gt;\u0026gt; l = list(range(10)) \u0026gt;\u0026gt;\u0026gt; l [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] \u0026gt;\u0026gt;\u0026gt; del l[5:7] \u0026gt;\u0026gt;\u0026gt; l [0, 1, 2, 3, 4, 7, 8, 9] \u0026gt;\u0026gt;\u0026gt; l[3:2] = [11, 22] \u0026gt;\u0026gt;\u0026gt; l [0, 1, 2, 11, 22, 3, 4, 7, 8, 9] \u0026gt;\u0026gt;\u0026gt; l[2:5] = [100] \u0026gt;\u0026gt;\u0026gt; l [0, 1, 100, 3, 4, 7, 8, 9] 注意，如果赋值的对象是一个切片，那么赋值语句的右侧必须是个可迭代对象，即使只有单独一个值，否则会报错：\n\u0026gt;\u0026gt;\u0026gt; l[2:5] = 100 Traceback (most recent call last): File \u0026#34;\u0026lt;input\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; TypeError: can only assign an iterable 多维切片 除了一维切片，Python还支持多维切片，这在多维数组中能体现出来。NumPy是Python第三方库，提供了高阶数组，使得Python成为科学计算应用的主流语言。示例：\n\u0026gt;\u0026gt;\u0026gt; import numpy \u0026gt;\u0026gt;\u0026gt; a = numpy.arange(12) \u0026gt;\u0026gt;\u0026gt; a array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) \u0026gt;\u0026gt;\u0026gt; a.shape (12,) \u0026gt;\u0026gt;\u0026gt; a.shape = 3, 4 \u0026gt;\u0026gt;\u0026gt; a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) \u0026gt;\u0026gt;\u0026gt; a[:, 1] array([1, 5, 9]) \u0026gt;\u0026gt;\u0026gt; a[1:2, 2:3] array([[6]]) \u0026gt;\u0026gt;\u0026gt; a[1:3, 2:4] array([[ 6, 7], [10, 11]]) 在NumPy中，省略号...用作多维数组切片的快捷方式，如果x是四维数组，那么x[i, ...]就是x[i, :, :, :]的缩写，比如：\n\u0026gt;\u0026gt;\u0026gt; a.shape = 2, 2, 3 \u0026gt;\u0026gt;\u0026gt; a array([[[ 0, 1, 2], [ 3, 4, 5]], [[ 6, 7, 8], [ 9, 10, 11]]]) \u0026gt;\u0026gt;\u0026gt; a[:, :, 1] array([[ 1, 4], [ 7, 10]]) \u0026gt;\u0026gt;\u0026gt; a[..., 1] array([[ 1, 4], [ 7, 10]]) 小结 本文介绍了Python强大的切片操作，因为忽略最后一个元素和下标从0开始，所以切片用起来特别顺手，除了开始和结尾，还能设置切片间隔，间隔为负可以反向取值。切片赋值是切片另一个强大功能，需要注意的是赋值语句的右侧必须是个可迭代对象。\n参考资料：\n《流畅的Python》\nhttps://blog.wz52.cn/archives/174.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000012-python%E8%83%BD%E5%8F%96%E5%80%BC%E4%BA%A6%E8%83%BD%E8%B5%8B%E5%80%BC%E7%9A%84python%E5%88%87%E7%89%87/","summary":"切片，就像面包，给几刀，切成一片一片，可以做成吐司，也可以做成三明治，口味更佳：","title":"能取值亦能赋值的Python切片"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n7.1 AI应用运行时的演进趋势 7.1.1 从云原生到AI原生 云原生时代的核心是应用。将单体应用拆解为更小、更独立、更具弹性的微服务，并高效地在云基础设施上进行部署、扩展和管理。\n而Agent的工作流，不再是执行一段固定的main函数，而是一个动态的、长周期的、充满不确定性的“思考-行动”循环。需要极致弹性和成本效益。需要数小时上下文记忆、拥有完整文件系统、并能与浏览器进行复杂交互。\n7.1.2 Agentic AI应用的典型场景 场景一：交互式智能内容创作助手\n对轮对话、上下文记忆、模型自托管、异构任务流、流式响应。\n场景二：个性化AI客服\n事件驱动、企业数据、实时在线。\n场景三：通用Agent平台+病毒式传播的AIGC创意应用\nAgent智能体、代码执行验证、部署与分享、脉冲式流量、大文件处理。\n7.1.3 AI原生应用运行时的核心能力 1、面向会话的状态管理与安全隔离\n2、大规模实时弹性与精益成本管理\n3、异构算力与标准化工具连接能力\n7.2 模型运行时 7.2.1 企业模型使用的核心痛点 资源浪费：业务流量波动大，高峰10万，平日几千，预留峰值会导致算力闲置。\n冷启动：低频模型无法保持实例存活，使用时冷启动耗时长。\n定制化：业务产品需要定制化，产品迭代周期长。\n7.2.2 Serverless模型运行时的核心能力 Serverless是什么？\nServerless不是完全没有服务器，而是开发者不需要关心服务器的管理。对于开发者来说，只需关注代码，其他的交给云平台。主流Serverless平台包括阿里云函数计算、腾讯云云函数等。\nServerless和云服务器的区别？\n云服务器只是一台远程的服务器；Serverless只上传代码片段（函数），不管服务器，当有请求触发时，代码才会运行。\n1、异构算力和1/N卡切分使用\n在算力层，Serverless模型运行时通过GPU虚拟化技术将单张GPU显卡划分为N个独立计算单元，每个实例具备隔离的显存空间与算力资源，不同实例通过GPU分时复用技术实现并行推理。\nServerless模型运行时通过池化技术，将CPU/GPU/XPU统一纳管到一个资源池，开发者可根据模型特性按需配置算力模型，比如语音识别等轻量任务分配CPU，图像生成等算力密集型任务分配GPU碎片，大语言模型等显存密集型任务分配GPU整卡或多卡。\n2、负载感知调度和毫秒级闲置唤醒\n在调度层，Serverless模型运行时通过负载感知调度系统实时监测请求队列深度、GPU显存占有率、实例健康状态等多维指标，基于池化技术构建三级响应机制：请求优先分配至活跃实例；当资源吃紧时，毫秒级唤醒闲置实例了；仅在极端流量下触发冷启动。\n其核心技术突破在于利用CRID（用户空间检查点/恢复）技术冻结显存状态，并将显存数据临时置换至内存，并在新的请求调度前实现毫秒级/秒级状态恢复，较传统虚机/容器方案提速百倍。\n3、集成加速框架和开发调试工具链\n在开发层，Serverless模型运行时预集成加速框架深度优化模型运行效率：vLLM框架的Paged Attention技术通过显存分页管理提升3倍吞吐量；SGLang的RadixAttention实现注意力机制并行编译，降低60%推理延迟；TensorRT-LLM的量化融合策略提升2倍能效比。\n开发工具链提供DevPod交互式环境，开发环节实现白屏化操作和实时反馈，集成在线IDE如VSCode/JupyterLab/SSH终端，开发者在云端环境具备比本地环境更高的生产效率；生产部署环节实现革命性简化——上传模型文件后，系统在30秒内自动生成Dockerfile、构建推理服务、输出OpenAPI文档及SDK。\n总结，GPU碎片化技术提供原子级算力单元，为智能调度奠定资源基础；负载感知引擎通过毫秒级实例弹性，将碎片化算力转化为即时服务能力；开发工具链则构建自动化流水线，使技术红利直达开发者工作台。\n7.2.3 Serverless模型运行时——AI大脑的终极载体 Serverless模型运行时实现了异构算力革命、智能调度进化、开发范式升级三重突破，是承载AI大脑的核心基座。\n7.3 智能体运行时 7.3.1 AI应用形态的持续演进 1、“请求-响应”模式：无状态的事务性AI任务\n通过快速复制无状态的计算实例来应对流量变化。\n2、“对话”模式：有状态的协作式Agentic AI应用\n7.3.2 Agent运行时的核心架构目标 1、围绕会话请求和资源调度模型，维持长时运行的状态延续\n2、实现面向Active-Idle资源管理，解决长时运行的成本困境\n7.3.3 现有架构支撑Agent运行时对比分析 7.3.4 为AI原生的“会话式”Serverless运行时 AI应用的会话既需要状态持久化，又需要高性能的本地计算和会话维度隔离能力。\n一个会话，一个独立运行时：\n原生会话状态保持 灵活可靠的安全隔离 毫秒级的弹性速度 7.4 工具与云沙箱 7.4.1 AI Agent与工具：从概念到能力 Agent常用工具类型：\n代码解释器、浏览器使用和计算机使用这类复杂工具，已经不是传统的单次、无状态的API调用，而是需要一个有状态的、沙箱化的环境。AI Sandbox，正是一个被严格控制的隔离环境。\n7.4.2 复杂工具运行时的核心诉求 隔离与安全（Isolation \u0026amp; Security） 状态管理与成本（State Management \u0026amp; Cost） 可扩展性与运维（Scalability \u0026amp; Operations） 7.4.3 Serverless作为AI Sandbox的理想基座 1、计算隔离，硬件级与内核级双重保障\n比如阿里云函数计算采用的“神龙裸金属+MicroVM安全容器”架构。\n2、会话管理，原生支持有状态应用\n核心能力是强会话亲和性（Session Affinity）、会话物理隔离（Session Isolation）、会话管理接口（Session Management Interface）。\n3、存储隔离，解决状态持久化难题\n本地临时存储，利用快照技术实现极速恢复。持久化共享存储，提供会话级别的数据沙箱。\n4、对极致存储性能的持续探索\n支持在会话粒度上为每一个独立的Sandbox动态挂载专属的高性能存储盘。\n7.5 AI应用运行时的降本路线 7.5.1 阶段一：从资源租用到按请求计费 只为代码运行时刻付费。\n关键技术包括：精准识别请求边界；按请求分配独占资源；低延时大并发的冷启动能力；1ms完成活跃/闲置状态转化。\n7.5.2 阶段二：多并发+毫秒级计费——面相Web应用的优化 突破单并发限制，按函数实例的活跃时间段计费，并将粒度精细化到1ms。从“为单个请求买单”转变为“为活跃区间买单”。\n关键技术包括：识别活跃时间段作为计费边界；引入Custom Runtime/Container Runtime；缩短计费粒度；极致优化平台全链路延迟。\n7.5.3 阶段三：按实际资源消耗计费——AI时代的价值计费 在识别请求边界的基础上，引入按实际资源消耗动态区分活跃/闲置的计费模型。从“为活跃区间买单”演化为“按资源消耗分层计费”。\n关键技术包括：支持会话亲和性；按实际资源消耗判断活跃/闲置；执行期间低负载的减免机制；支持不冻结，允许后台任务持续运行。\n7.5.4 函数计算的演化方向是把产品形态与用户价值更紧密地对齐 在AI时代，函数计算一直坚持走向让开发者只关心业务逻辑，云厂商自动完成一切资源管理与调度的愿景，最终让计算像水、电一样随时可得、按实际使用价值付费。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005007-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E5%BA%94%E7%94%A8%E8%BF%90%E8%A1%8C%E6%97%B6/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"71-ai应用运行时的演进趋势\"\u003e7.1 AI应用运行时的演进趋势\u003c/h1\u003e\n\u003ch2 id=\"711-从云原生到ai原生\"\u003e7.1.1 从云原生到AI原生\u003c/h2\u003e\n\u003cp\u003e云原生时代的核心是应用。将单体应用拆解为更小、更独立、更具弹性的微服务，并高效地在云基础设施上进行部署、扩展和管理。\u003c/p\u003e\n\u003cp\u003e而Agent的工作流，不再是执行一段固定的main函数，而是一个动态的、长周期的、充满不确定性的“思考-行动”循环。需要极致弹性和成本效益。需要数小时上下文记忆、拥有完整文件系统、并能与浏览器进行复杂交互。\u003c/p\u003e\n\u003ch2 id=\"712-agentic-ai应用的典型场景\"\u003e7.1.2 Agentic AI应用的典型场景\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e场景一：交互式智能内容创作助手\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e对轮对话、上下文记忆、模型自托管、异构任务流、流式响应。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e场景二：个性化AI客服\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e事件驱动、企业数据、实时在线。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e场景三：通用Agent平台+病毒式传播的AIGC创意应用\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAgent智能体、代码执行验证、部署与分享、脉冲式流量、大文件处理。\u003c/p\u003e\n\u003ch2 id=\"713-ai原生应用运行时的核心能力\"\u003e7.1.3 AI原生应用运行时的核心能力\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4bf9ca763c2f4739ab21c899e6643766.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1、面向会话的状态管理与安全隔离\u003c/p\u003e\n\u003cp\u003e2、大规模实时弹性与精益成本管理\u003c/p\u003e\n\u003cp\u003e3、异构算力与标准化工具连接能力\u003c/p\u003e\n\u003ch1 id=\"72-模型运行时\"\u003e7.2 模型运行时\u003c/h1\u003e\n\u003ch2 id=\"721-企业模型使用的核心痛点\"\u003e7.2.1 企业模型使用的核心痛点\u003c/h2\u003e\n\u003cp\u003e资源浪费：业务流量波动大，高峰10万，平日几千，预留峰值会导致算力闲置。\u003c/p\u003e\n\u003cp\u003e冷启动：低频模型无法保持实例存活，使用时冷启动耗时长。\u003c/p\u003e\n\u003cp\u003e定制化：业务产品需要定制化，产品迭代周期长。\u003c/p\u003e\n\u003ch2 id=\"722-serverless模型运行时的核心能力\"\u003e7.2.2 Serverless模型运行时的核心能力\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eServerless是什么？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eServerless不是完全没有服务器，而是开发者不需要关心服务器的管理。对于开发者来说，只需关注代码，其他的交给云平台。主流Serverless平台包括阿里云函数计算、腾讯云云函数等。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eServerless和云服务器的区别？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e云服务器只是一台远程的服务器；Serverless只上传代码片段（函数），不管服务器，当有请求触发时，代码才会运行。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1、异构算力和1/N卡切分使用\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e在算力层，Serverless模型运行时通过GPU虚拟化技术将单张GPU显卡划分为N个独立计算单元，每个实例具备隔离的显存空间与算力资源，不同实例通过GPU分时复用技术实现并行推理。\u003c/p\u003e\n\u003cp\u003eServerless模型运行时通过池化技术，将CPU/GPU/XPU统一纳管到一个资源池，开发者可根据模型特性按需配置算力模型，比如语音识别等轻量任务分配CPU，图像生成等算力密集型任务分配GPU碎片，大语言模型等显存密集型任务分配GPU整卡或多卡。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2、负载感知调度和毫秒级闲置唤醒\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e在调度层，Serverless模型运行时通过负载感知调度系统实时监测请求队列深度、GPU显存占有率、实例健康状态等多维指标，基于池化技术构建三级响应机制：请求优先分配至活跃实例；当资源吃紧时，毫秒级唤醒闲置实例了；仅在极端流量下触发冷启动。\u003c/p\u003e\n\u003cp\u003e其核心技术突破在于利用CRID（用户空间检查点/恢复）技术冻结显存状态，并将显存数据临时置换至内存，并在新的请求调度前实现毫秒级/秒级状态恢复，较传统虚机/容器方案提速百倍。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3、集成加速框架和开发调试工具链\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e在开发层，Serverless模型运行时预集成加速框架深度优化模型运行效率：vLLM框架的Paged Attention技术通过显存分页管理提升3倍吞吐量；SGLang的RadixAttention实现注意力机制并行编译，降低60%推理延迟；TensorRT-LLM的量化融合策略提升2倍能效比。\u003c/p\u003e\n\u003cp\u003e开发工具链提供DevPod交互式环境，开发环节实现白屏化操作和实时反馈，集成在线IDE如VSCode/JupyterLab/SSH终端，开发者在云端环境具备比本地环境更高的生产效率；生产部署环节实现革命性简化——上传模型文件后，系统在30秒内自动生成Dockerfile、构建推理服务、输出OpenAPI文档及SDK。\u003c/p\u003e\n\u003cp\u003e总结，GPU碎片化技术提供原子级算力单元，为智能调度奠定资源基础；负载感知引擎通过毫秒级实例弹性，将碎片化算力转化为即时服务能力；开发工具链则构建自动化流水线，使技术红利直达开发者工作台。\u003c/p\u003e\n\u003ch2 id=\"723-serverless模型运行时ai大脑的终极载体\"\u003e7.2.3 Serverless模型运行时——AI大脑的终极载体\u003c/h2\u003e\n\u003cp\u003eServerless模型运行时实现了异构算力革命、智能调度进化、开发范式升级三重突破，是承载AI大脑的核心基座。\u003c/p\u003e\n\u003ch1 id=\"73-智能体运行时\"\u003e7.3 智能体运行时\u003c/h1\u003e\n\u003ch2 id=\"731-ai应用形态的持续演进\"\u003e7.3.1 AI应用形态的持续演进\u003c/h2\u003e\n\u003cp\u003e1、“请求-响应”模式：无状态的事务性AI任务\u003c/p\u003e\n\u003cp\u003e通过快速复制无状态的计算实例来应对流量变化。\u003c/p\u003e\n\u003cp\u003e2、“对话”模式：有状态的协作式Agentic AI应用\u003c/p\u003e\n\u003ch2 id=\"732-agent运行时的核心架构目标\"\u003e7.3.2 Agent运行时的核心架构目标\u003c/h2\u003e\n\u003cp\u003e1、围绕会话请求和资源调度模型，维持长时运行的状态延续\u003c/p\u003e\n\u003cp\u003e2、实现面向Active-Idle资源管理，解决长时运行的成本困境\u003c/p\u003e\n\u003ch2 id=\"733-现有架构支撑agent运行时对比分析\"\u003e7.3.3 现有架构支撑Agent运行时对比分析\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b32ade2409ca4fd58002287e7cd9bb7e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/275c7b565fb745e598f0f3f2b505f44f.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"734-为ai原生的会话式serverless运行时\"\u003e7.3.4 为AI原生的“会话式”Serverless运行时\u003c/h2\u003e\n\u003cp\u003eAI应用的会话既需要状态持久化，又需要高性能的本地计算和会话维度隔离能力。\u003c/p\u003e\n\u003cp\u003e一个会话，一个独立运行时：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e原生会话状态保持\u003c/li\u003e\n\u003cli\u003e灵活可靠的安全隔离\u003c/li\u003e\n\u003cli\u003e毫秒级的弹性速度\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"74-工具与云沙箱\"\u003e7.4 工具与云沙箱\u003c/h1\u003e\n\u003ch2 id=\"741-ai-agent与工具从概念到能力\"\u003e7.4.1 AI Agent与工具：从概念到能力\u003c/h2\u003e\n\u003cp\u003eAgent常用工具类型：\u003c/p\u003e","title":"AI应用运行时"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n很多API并不是真正的实现了RESTful，而应该叫做RPC (Remote Procedure Call 远程过程调用)，Roy Fielding曾经提到了它们的区别，原文如下：\nI am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating. What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed? — Roy Fielding https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven 大概意思是，如果应用状态引擎（API）不是超文本驱动的，那么就不是RESTful。\n我的理解是，像超文本一样携带一个地址，可以寻址定位信息，如超文本的link属性。\n超链接（Hypermedia）API Hypermedia指的是，返回结果中提供链接，连向其他API方法，使得用户不查文档，也知道下一步应该做什么。比如，当用户向api.example.com的根目录发出请求，会得到这样一个文档：\n{\u0026#34;link\u0026#34;: { \u0026#34;rel\u0026#34;: \u0026#34;collection https://www.example.com/zoos\u0026#34;, \u0026#34;href\u0026#34;: \u0026#34;https://api.example.com/zoos\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;List of zoos\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;application/vnd.yourformat+json\u0026#34; }} 上面代码表示，文档中有一个link属性，用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系（collection关系，并给出该collection的网址），href表示API的路径，title表示API的标题，type表示返回类型。\n摘取自：http://www.ruanyifeng.com/blog/2014/05/restful_api.html\n创建api_root的Endpoint 回到教程的例子。在前面我们已经为snippets和users创建了Endpoint，现在来创建根目录的Endpoint，编辑snippets/views.py：\nfrom rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.reverse import reverse @api_view([\u0026#39;GET\u0026#39;]) def api_root(request, format=None): return Response({ \u0026#39;users\u0026#39;: reverse(\u0026#39;user-list\u0026#39;, request=request, format=format), \u0026#39;snippets\u0026#39;: reverse(\u0026#39;snippet-list\u0026#39;, request=request, format=format) }) reverse()函数用来返回snippets/urls.py中viewname对应的url，如path('users/', views.UserList.as_view(), name='user-list')。\n然后添加到snippets/urls.py中：\npath(\u0026#39;\u0026#39;, views.api_root), 创建SnippetHighlight的Endpoint 还记得在上篇文章中提到的Snippet.highlighted字段么：\n我们现在为它创建Endpoint，继续编辑snippets/views.py：\nfrom rest_framework import renderers from rest_framework.response import Response class SnippetHighlight(generics.GenericAPIView): queryset = Snippet.objects.all() renderer_classes = [renderers.StaticHTMLRenderer] def get(self, request, *args, **kwargs): snippet = self.get_object() return Response(snippet.highlighted) 然后添加到snippets/urls.py中：\npath(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;/highlight/\u0026#39;, views.SnippetHighlight.as_view()), 因为snippet.highlighted不是JSON而是HTML，所以用[renderers.StaticHTMLRenderer]返回预渲染的（pre-rendered）HTML。\nHyperlinkedModelSerializer 在Web API设计中，一般有以下几种方式来表示实体之间的关系：\n主键 超链接 关系实体（the related entity），唯一标识符字段（a unique identifying slug field） 关系实体，默认字符串（the default string representation） 关系实体，嵌入到父类中（the parent representation） 其他自定义 前2个比较熟悉，后面几个有点不太懂，我理解是类似于数据库的关联关系表。\nDRF支持以上所有方式，这里我们用DRF的HyperlinkedModelSerializer来实现真正的RESTful。在snippets/serializers.py中把我们之前的代码：\nclass SnippetSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source=\u0026#39;owner.username\u0026#39;) class Meta: model = Snippet fields = [\u0026#39;id\u0026#39;, \u0026#39;title\u0026#39;, \u0026#39;code\u0026#39;, \u0026#39;linenos\u0026#39;, \u0026#39;language\u0026#39;, \u0026#39;style\u0026#39;, \u0026#39;owner\u0026#39;] class UserSerializer(serializers.ModelSerializer): snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all()) class Meta: model = User fields = [\u0026#39;id\u0026#39;, \u0026#39;username\u0026#39;, \u0026#39;snippets\u0026#39;] 修改为：\nclass SnippetSerializer(serializers.HyperlinkedModelSerializer): owner = serializers.ReadOnlyField(source=\u0026#39;owner.username\u0026#39;) highlight = serializers.HyperlinkedIdentityField(view_name=\u0026#39;snippet-highlight\u0026#39;, format=\u0026#39;html\u0026#39;) class Meta: model = Snippet fields = [\u0026#39;url\u0026#39;, \u0026#39;id\u0026#39;, \u0026#39;highlight\u0026#39;, \u0026#39;owner\u0026#39;, \u0026#39;title\u0026#39;, \u0026#39;code\u0026#39;, \u0026#39;linenos\u0026#39;, \u0026#39;language\u0026#39;, \u0026#39;style\u0026#39;] class UserSerializer(serializers.HyperlinkedModelSerializer): snippets = serializers.HyperlinkedRelatedField(many=True, view_name=\u0026#39;snippet-detail\u0026#39;, read_only=True) class Meta: model = User fields = [\u0026#39;url\u0026#39;, \u0026#39;id\u0026#39;, \u0026#39;username\u0026#39;, \u0026#39;snippets\u0026#39;] 其中ModelSerializer换成了HyperlinkedModelSerializer，后者的区别如下：\n默认不包含id字段\n包含url字段，用HyperlinkedIdentityField表示\n源码：serializer_url_field = HyperlinkedIdentityField\n关系用HyperlinkedRelatedField表示，而不是PrimaryKeyRelatedField\n源码：serializer_related_field = HyperlinkedRelatedField\n由于用了HyperlinkedModelSerializer，SnippetSerializer和UserSerializer的url字段默认指向的是'{model_name}-detail' url pattern，这是DRF定义的，在示例中就是'snippet-detail' 和'user-detail'。新增的highlight字段和url字段是一样的类型，它指向的是'snippet-highlight'，而不是'snippet-detail'。\n修改url pattern 既然已经提到了url pattern，那么在snippets/urls.py中修改一下：\nfrom django.urls import path from rest_framework.urlpatterns import format_suffix_patterns from snippets import views ## API endpoints urlpatterns = format_suffix_patterns([ path(\u0026#39;\u0026#39;, views.api_root), path(\u0026#39;snippets/\u0026#39;, views.SnippetList.as_view(), name=\u0026#39;snippet-list\u0026#39;), path(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;/\u0026#39;, views.SnippetDetail.as_view(), name=\u0026#39;snippet-detail\u0026#39;), path(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;/highlight/\u0026#39;, views.SnippetHighlight.as_view(), name=\u0026#39;snippet-highlight\u0026#39;), path(\u0026#39;users/\u0026#39;, views.UserList.as_view(), name=\u0026#39;user-list\u0026#39;), path(\u0026#39;users/\u0026lt;int:pk\u0026gt;/\u0026#39;, views.UserDetail.as_view(), name=\u0026#39;user-detail\u0026#39;) ]) name就是在serializers.py和views.py中用到的。\n添加分页 REST设计基本原则提到了：处理好分页。DRF添加分页的方式很简单，编辑tutorial/settings.py文件：\nREST_FRAMEWORK = { \u0026#39;DEFAULT_PAGINATION_CLASS\u0026#39;: \u0026#39;rest_framework.pagination.PageNumberPagination\u0026#39;, \u0026#39;PAGE_SIZE\u0026#39;: 10 } 小结 我之前是在学SpringBoot的时候了解过RESTful API的超链接API，文章开头的那一段介绍就是当时写的笔记，DRF提供了HyperlinkedModelSerializer来实现，还是比较好理解的，其中的细节需要在实战中再多多熟悉。\n参考资料：\nhttps://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/\nhttps://spring.io/guides/tutorials/rest/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000011-djangodrf%E4%BD%BF%E7%94%A8%E8%B6%85%E9%93%BE%E6%8E%A5api%E5%AE%9E%E7%8E%B0%E7%9C%9F%E6%AD%A3restful/","summary":"很多API并不是真正的实现了RESTful，而应该叫做RPC (Remote Procedure Call 远程过程调用)，Roy Fielding曾经提到了它们的区别，原文如下：","title":"DRF使用超链接API实现真正RESTful"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDispatcherServlet简介 DispatcherServlet是Spring MVC框架的核心组件，充当前端控制器（Front Controller）的角色。它负责接收所有的HTTP请求，并将它们分发给适当的处理器（控制器）。DispatcherServlet是整个Spring MVC应用程序的入口点，管理请求的整个生命周期。\n通过源码查看DispatcherServlet定义：\npublic class DispatcherServlet extends FrameworkServlet public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware DispatcherServlet继承了FrameworkServlet，继承了HttpServletBean，继承了HttpServlet。HttpServlet来自jakarta.servlet.http.HttpServlet，也就是新版Servlet：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;jakarta.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jakarta.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;6.1.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 果然，Spring MVC是基于Servlet的！\nDispatcherServlet注册 DispatcherServlet是个Servlet，需要注册后才能使用。有3种注册方式：web.xml、Spring MVC、Spring Boot\n① web.xml\n\u0026lt;web-app\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;org.springframework.web.context.ContextLoaderListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;/WEB-INF/app-context.xml\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;app\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.DispatcherServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;load-on-startup\u0026gt;1\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;app\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/app/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;/web-app\u0026gt; ② Spring MVC\npublic class MyWebApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) { // Load Spring web application configuration AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(AppConfig.class); // Create and register the DispatcherServlet DispatcherServlet servlet = new DispatcherServlet(context); ServletRegistration.Dynamic registration = servletContext.addServlet(\u0026#34;app\u0026#34;, servlet); registration.setLoadOnStartup(1); registration.addMapping(\u0026#34;/app/*\u0026#34;); } } ③ Spring Boot\nimport org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @Bean public ServletRegistrationBean\u0026lt;DispatcherServlet\u0026gt; dispatcherServletRegistration(WebApplicationContext context) { AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext(); webContext.register(AppConfig.class); DispatcherServlet dispatcherServlet = new DispatcherServlet(webContext); ServletRegistrationBean\u0026lt;DispatcherServlet\u0026gt; registration = new ServletRegistrationBean\u0026lt;\u0026gt;(dispatcherServlet, \u0026#34;/app/*\u0026#34;); registration.setName(\u0026#34;app\u0026#34;); registration.setLoadOnStartup(1); return registration; } } Spring Boot通常会自动配置DispatcherServlet。Spring Boot的自动配置机制会根据应用程序的依赖和配置自动创建和注册DispatcherServlet，并将其映射到根路径（/）。这使得开发者无需手动配置DispatcherServlet，从而简化了开发过程。Spring Boot通过spring-boot-starter-web依赖自动配置DispatcherServlet。当你在项目中引入这个依赖时，Spring Boot会自动扫描并配置Web相关的组件，包括DispatcherServlet。\nContext层级 DispatcherServlet的构造方法，参数是WebApplicationContext。WebApplicationContext定义如下：\npackage org.springframework.web.context; import jakarta.servlet.ServletContext; import org.springframework.context.ApplicationContext; import org.springframework.lang.Nullable; public interface WebApplicationContext extends ApplicationContext { String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + \u0026#34;.ROOT\u0026#34;; String SCOPE_REQUEST = \u0026#34;request\u0026#34;; String SCOPE_SESSION = \u0026#34;session\u0026#34;; String SCOPE_APPLICATION = \u0026#34;application\u0026#34;; String SERVLET_CONTEXT_BEAN_NAME = \u0026#34;servletContext\u0026#34;; String CONTEXT_PARAMETERS_BEAN_NAME = \u0026#34;contextParameters\u0026#34;; String CONTEXT_ATTRIBUTES_BEAN_NAME = \u0026#34;contextAttributes\u0026#34;; @Nullable ServletContext getServletContext(); } 有个getServletContext方法，返回了熟悉的ServletContext，来自jakarta.servlet.ServletContext。没错，Spring MVC的底层确实是Servlet！\nWebApplicationContext继承自ApplicationContext，ApplicationContext的定义如下：\npackage org.springframework.context; import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.lang.Nullable; public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver { @Nullable String getId(); String getApplicationName(); String getDisplayName(); long getStartupDate(); @Nullable ApplicationContext getParent(); AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException; } 在使用框架时，Context可以是层级结构的：\nServlet WebApplicationContext是特定于某个DispatcherServlet实例的上下文，包含特定于该Servlet的Bean。Root WebApplicationContext是整个Web应用程序的根上下文，包含共享的基础设施Bean。多个DispatcherServlet实例之间共享一个Root WebApplicationContext，每个实例都有自己的Servlet WebApplicationContext。\nContext也可以是单一的，如果只需要一个Context，那么配置Root WebApplicationContext即可。\n特殊Bean类型 在Spring框架中，Bean是Spring IoC容器管理的对象。特殊Bean通常指那些实现了特定接口或继承了特定类的Bean，这些接口或类定义了某些框架契约。\nHandlerMapping\nHandlerMapping是Spring MVC中用于将请求URL映射到相应处理器（Controller）的方法。它的主要职责是根据请求找到合适的处理器。常见实现： RequestMappingHandlerMapping：基于注解的映射，如@RequestMapping SimpleUrlHandlerMapping：基于URL路径的映射\nHandlerAdapter\nHandlerAdapter是用于执行处理器（Controller）的方法。它的主要职责是调用处理器并返回一个ModelAndView对象。常见实现：\nRequestMappingHandlerAdapter：处理基于注解的控制器方法\nSimpleControllerHandlerAdapter：处理实现了Controller接口的控制器\nHandlerExceptionResolver\nHandlerExceptionResolver用于处理在处理请求过程中抛出的异常。它的主要职责是将异常转换为适当的视图或响应。常见实现：\nExceptionHandlerExceptionResolver：处理使用@ExceptionHandler注解的方法\nResponseStatusExceptionResolver：处理使用@ResponseStatus注解的方法\nViewResolver\nViewResolver用于将逻辑视图名称解析为实际的视图对象。它的主要职责是找到合适的视图来渲染模型数据。常见实现：\nInternalResourceViewResolver：解析JSP视图\nThymeleafViewResolver：解析Thymeleaf视图\nLocaleResolver, LocaleContextResolver\nLocaleResolver和LocaleContextResolver用于解析和设置请求的区域信息（Locale）。常见实现：\nAcceptHeaderLocaleResolver：基于请求头的区域信息\nCookieLocaleResolver：基于Cookie的区域信息\nTimeZoneAwareLocaleContextResolver：支持时间区域的解析，LocaleContextResolver：扩展了LocaleResolver\nThemeResolver\nThemeResolver用于解析和设置请求的主题信息。主题通常用于定义应用的外观和感觉。常见实现：\nFixedThemeResolver：使用固定的主题\nCookieThemeResolver：基于Cookie的主题解析\nMultipartResolver\nMultipartResolver用于处理文件上传请求。它的主要职责是将多部分请求解析为文件和表单字段。常见实现：\nCommonsMultipartResolver：基于Apache Commons FileUpload\nStandardServletMultipartResolver：基于Servlet 3.0的文件上传\nFlashMapManager\nFlashMapManager用于在重定向请求之间传递临时数据。它的主要职责是管理Flash属性。常见实现：\nSessionFlashMapManager：基于HTTP会话的FlashMap管理\n关于HandlerMapping和HandlerAdapter\n查看它们的源码：\npackage org.springframework.web.servlet; import jakarta.servlet.http.HttpServletRequest; import org.springframework.lang.Nullable; public interface HandlerMapping { String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + \u0026#34;.bestMatchingHandler\u0026#34;; /** @deprecated */ @Deprecated String LOOKUP_PATH = HandlerMapping.class.getName() + \u0026#34;.lookupPath\u0026#34;; String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + \u0026#34;.pathWithinHandlerMapping\u0026#34;; String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + \u0026#34;.bestMatchingPattern\u0026#34;; String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + \u0026#34;.introspectTypeLevelMapping\u0026#34;; String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + \u0026#34;.uriTemplateVariables\u0026#34;; String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + \u0026#34;.matrixVariables\u0026#34;; String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + \u0026#34;.producibleMediaTypes\u0026#34;; default boolean usesPathPatterns() { return false; } @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; } package org.springframework.web.servlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.lang.Nullable; public interface HandlerAdapter { boolean supports(Object handler); @Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; /** @deprecated */ @Deprecated long getLastModified(HttpServletRequest request, Object handler); } 结合这个图来理解：\n先看HandlerMapping这块，DispatcherServlet接收到请求，调用HandlerMapping来查找合适的处理器，HandlerMapping返回处理器及其相关的拦截器信息。\ngetHandler方法返回了HandlerExecutionChain类，包含了Handler和HandlerInterceptor：\n再看HandlerAdapter这块，DispatcherServlet调用HandlerAdapter来执行处理器的方法，HandlerAdapter调用处理器的方法并返回ModelAndView对象，DispatcherServlet根据ModelAndView对象来选择视图进行渲染。handle方法入参request、response、handler，返回ModelAndView：\n其中SimpleControllerHandlerAdapter类的源码：\npackage org.springframework.web.servlet.mvc; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.lang.Nullable; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.ModelAndView; public class SimpleControllerHandlerAdapter implements HandlerAdapter { public SimpleControllerHandlerAdapter() { } public boolean supports(Object handler) { return handler instanceof Controller; } @Nullable public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return ((Controller)handler).handleRequest(request, response); } public long getLastModified(HttpServletRequest request, Object handler) { if (handler instanceof LastModified lastModified) { return lastModified.getLastModified(request); } else { return -1L; } } } DispatcherServlet有几个方法入参为ModelAndView，选择视图进行渲染：\n感兴趣可以到源码跟踪细节：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-webmvc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;6.1.10\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 它们分别应用了不同的设计模式：\nHandlerMapping应用了策略模式（Strategy Pattern）\nHandlerMapping接口定义了查找处理器的策略，而具体的实现类（如RequestMappingHandlerMapping、SimpleUrlHandlerMapping等）则提供了不同的查找策略。这样，Spring MVC可以根据配置或需求选择不同的HandlerMapping实现，而不需要修改核心代码。\nHandlerAdapter应用了适配器模式（Adapter Pattern）\nHandlerAdapter接口定义了执行处理器的方法，而具体的实现类（如RequestMappingHandlerAdapter、SimpleControllerHandlerAdapter等）则适配了不同类型的处理器。这样，Spring MVC可以通过统一的接口来调用不同类型的处理器，而不需要关心处理器的具体实现细节。\n123\n参考资料：\nhttps://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet.html\nChatGPT\n《跟开涛学SpringMVC》\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000011-javaweb%E5%BC%80%E5%8F%91%E8%BF%9B%E9%98%B6springmvc%E7%9A%84dispatcherservlet/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"dispatcherservlet简介\"\u003eDispatcherServlet简介\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003eDispatcherServlet\u003c/code\u003e是Spring MVC框架的核心组件，充当前端控制器（Front Controller）的角色。它负责接收所有的HTTP请求，并将它们分发给适当的处理器（控制器）。\u003ccode\u003eDispatcherServlet\u003c/code\u003e是整个Spring MVC应用程序的入口点，管理请求的整个生命周期。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a9f8ce1877e7455d89b640b5cf26bec5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e通过源码查看DispatcherServlet定义：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eDispatcherServlet\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFrameworkServlet\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eabstract\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eFrameworkServlet\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eHttpServletBean\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eApplicationContextAware\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eabstract\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eHttpServletBean\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eHttpServlet\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eEnvironmentCapable\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eEnvironmentAware\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eDispatcherServlet继承了FrameworkServlet，继承了HttpServletBean，继承了HttpServlet。HttpServlet来自\u003ccode\u003ejakarta.servlet.http.HttpServlet\u003c/code\u003e，也就是新版Servlet：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ejakarta.servlet\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003ejakarta.servlet-api\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e6.1.0\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eprovided\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e果然，Spring MVC是基于Servlet的！\u003c/p\u003e\n\u003ch1 id=\"dispatcherservlet注册\"\u003eDispatcherServlet注册\u003c/h1\u003e\n\u003cp\u003eDispatcherServlet是个Servlet，需要注册后才能使用。有3种注册方式：web.xml、Spring MVC、Spring Boot\u003c/p\u003e\n\u003cp\u003e① web.xml\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;web-app\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;listener\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;listener-class\u0026gt;\u003c/span\u003eorg.springframework.web.context.ContextLoaderListener\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/listener-class\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/listener\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;context-param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;param-name\u0026gt;\u003c/span\u003econtextConfigLocation\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/param-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;param-value\u0026gt;\u003c/span\u003e/WEB-INF/app-context.xml\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/param-value\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/context-param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;servlet\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;servlet-name\u0026gt;\u003c/span\u003eapp\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/servlet-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;servlet-class\u0026gt;\u003c/span\u003eorg.springframework.web.servlet.DispatcherServlet\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/servlet-class\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;init-param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;param-name\u0026gt;\u003c/span\u003econtextConfigLocation\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/param-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;param-value\u0026gt;\u0026lt;/param-value\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/init-param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;load-on-startup\u0026gt;\u003c/span\u003e1\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/load-on-startup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/servlet\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;servlet-mapping\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;servlet-name\u0026gt;\u003c/span\u003eapp\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/servlet-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;url-pattern\u0026gt;\u003c/span\u003e/app/*\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/url-pattern\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/servlet-mapping\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/web-app\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e② Spring MVC\u003c/p\u003e","title":"JavaWeb开发进阶SpringMVC的DispatcherServlet"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n泛型 为什么需要泛型？generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods.\n说白了就像Python动态语言的变量，是动态的，可以指向任意类型。\n泛型有个好处是不需要类型转换：\nList list = new ArrayList(); list.add(\u0026#34;hello\u0026#34;); String s = (String) list.get(0); List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); list.add(\u0026#34;hello\u0026#34;); String s = list.get(0); // no cast 这个例子的泛型，是指List的实现使用了泛型\u0026lt;T\u0026gt;，从而给使用带来了好处。\n定义\nclass name\u0026lt;T1, T2, ..., Tn\u0026gt; { /* ... */ } 比如定义class List\u0026lt;T\u0026gt;，使用List\u0026lt;String\u0026gt;\n将以下代码：\npublic class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; } } 改为泛型实现：\n/** * Generic version of the Box class. * @param \u0026lt;T\u0026gt; the type of the value being boxed */ public class Box\u0026lt;T\u0026gt; { // T stands for \u0026#34;Type\u0026#34; private T t; public void set(T t) { this.t = t; } public T get() { return t; } } T必须是非基本数据类型：any class type, any interface type, any array type, or even another type variable\ntype parameter命名采用单个大写字母：\nE - Element (used extensively by the Java Collections Framework) K - Key N - Number T - Type V - Value S, U, V etc. - 2nd, 3rd, 4th types 这样能很好的跟其他命名区分开来。\n使用泛型，必须要指定具体的值，比如这里的Integer：\nBox\u0026lt;Integer\u0026gt; integerBox; 这就跟方法调用传参是一个道理。\nDiamond，将：\nBox\u0026lt;Integer\u0026gt; integerBox = new Box\u0026lt;Integer\u0026gt;(); 简写为：\nBox\u0026lt;Integer\u0026gt; integerBox = new Box\u0026lt;\u0026gt;(); 多个type parameters：\npublic interface Pair\u0026lt;K, V\u0026gt; { public K getKey(); public V getValue(); } public class OrderedPair\u0026lt;K, V\u0026gt; implements Pair\u0026lt;K, V\u0026gt; { private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } Pair\u0026lt;String, Integer\u0026gt; p1 = new OrderedPair\u0026lt;String, Integer\u0026gt;(\u0026#34;Even\u0026#34;, 8); // 这里的int类型8，自动装箱为了I Pair\u0026lt;String, String\u0026gt; p2 = new OrderedPair\u0026lt;String, String\u0026gt;(\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;); 简写为diamond：\nOrderedPair\u0026lt;String, Integer\u0026gt; p1 = new OrderedPair\u0026lt;\u0026gt;(\u0026#34;Even\u0026#34;, 8); OrderedPair\u0026lt;String, String\u0026gt; p2 = new OrderedPair\u0026lt;\u0026gt;(\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;); 嵌套：\nOrderedPair\u0026lt;String, Box\u0026lt;Integer\u0026gt;\u0026gt; p = new OrderedPair\u0026lt;\u0026gt;(\u0026#34;primes\u0026#34;, new Box\u0026lt;Integer\u0026gt;(...)); Raw Types\nA raw type is the name of a generic class or interface without any type arguments.\n比如：\npublic class Box\u0026lt;T\u0026gt; { public void set(T t) { /* ... */ } // ... } Box\u0026lt;Integer\u0026gt; intBox = new Box\u0026lt;\u0026gt;(); Box rawBox = new Box(); // 这个就是rawType 在IDEA有时候会碰到警告Raw use of parameterized class \u0026lsquo;List\u0026rsquo;，就是指的这个玩意。这是老式写法，raw types会绕过泛型的type checks，应该避免使用。\nGeneric Methods\n定义，泛型位置在return type的前面：\npublic class Util { public static \u0026lt;K, V\u0026gt; boolean compare(Pair\u0026lt;K, V\u0026gt; p1, Pair\u0026lt;K, V\u0026gt; p2) { return p1.getKey().equals(p2.getKey()) \u0026amp;\u0026amp; p1.getValue().equals(p2.getValue()); } } public class Pair\u0026lt;K, V\u0026gt; { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } 使用：\nPair\u0026lt;Integer, String\u0026gt; p1 = new Pair\u0026lt;\u0026gt;(1, \u0026#34;apple\u0026#34;); Pair\u0026lt;Integer, String\u0026gt; p2 = new Pair\u0026lt;\u0026gt;(2, \u0026#34;pear\u0026#34;); boolean same = Util.\u0026lt;Integer, String\u0026gt;compare(p1, p2); 调用泛型方法时也可以省略泛型入参：\nPair\u0026lt;Integer, String\u0026gt; p1 = new Pair\u0026lt;\u0026gt;(1, \u0026#34;apple\u0026#34;); Pair\u0026lt;Integer, String\u0026gt; p2 = new Pair\u0026lt;\u0026gt;(2, \u0026#34;pear\u0026#34;); boolean same = Util.compare(p1, p2); // 这里省略了泛型入参 Bounded Type Parameters\n有点像Python的typing，限制动态变量的类型，使用extends关键字：\npublic class Box\u0026lt;T\u0026gt; { private T t; public void set(T t) { this.t = t; } public T get() { return t; } public \u0026lt;U extends Number\u0026gt; void inspect(U u){ System.out.println(\u0026#34;T: \u0026#34; + t.getClass().getName()); System.out.println(\u0026#34;U: \u0026#34; + u.getClass().getName()); } public static void main(String[] args) { Box\u0026lt;Integer\u0026gt; integerBox = new Box\u0026lt;Integer\u0026gt;(); integerBox.set(new Integer(10)); integerBox.inspect(\u0026#34;some text\u0026#34;); // error: this is still String! } } 这样还能进一步调用bounded type parameters的方法：\npublic class NaturalNumber\u0026lt;T extends Integer\u0026gt; { private T n; public NaturalNumber(T n) { this.n = n; } public boolean isEven() { return n.intValue() % 2 == 0; // intValue()是Integer的方法 } // ... } Multiple Bounds\nClass A { /* ... */ } interface B { /* ... */ } interface C { /* ... */ } class D \u0026lt;T extends A \u0026amp; B \u0026amp; C\u0026gt; { /* ... */ } class的位置必须在interface前面。\nBounded Type Parameters的用途之一，比如：\npublic static \u0026lt;T\u0026gt; int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e \u0026gt; elem) // compiler error ++count; return count; } 会编译报错，因为\u0026gt;符号只适用基本数据类型，如果想支持Object，怎么办呢：\npublic static \u0026lt;T extends Comparable\u0026lt;T\u0026gt;\u0026gt; int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e.compareTo(elem) \u0026gt; 0) ++count; return count; } extends Comparable\u0026lt;T\u0026gt;以后调用compareTo()方法，就能既支持基本数据类型又能支持Object了。\n泛型在继承时有个注意的点： Box\u0026lt;Integer\u0026gt; and Box\u0026lt;Double\u0026gt; are not subtypes of Box\u0026lt;Number\u0026gt;\n正确的方式：\nYou can subtype a generic class or interface by extending or implementing it.\ninterface PayloadList\u0026lt;E,P\u0026gt; extends List\u0026lt;E\u0026gt; { void setPayload(int index, P val); ... } Type Inference 类型推断：Type inference is a Java compiler\u0026rsquo;s ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable.（换个理解方式，就是动态变量需要知道绑定哪个类型）\nDiamond就是一种Type Inference：\nMap\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; myMap = new HashMap\u0026lt;\u0026gt;(); 在构造方法中进行推断：\nclass MyClass\u0026lt;X\u0026gt; { \u0026lt;T\u0026gt; MyClass(T t) { // ... } } MyClass\u0026lt;Integer\u0026gt; myObject = new MyClass\u0026lt;\u0026gt;(\u0026#34;\u0026#34;); X推断为Integer，T推断为String。\nLambda Expressions也会根据上下文推断target type：\npublic static void printPersons(List\u0026lt;Person\u0026gt; roster, CheckPerson tester) public void printPersonsWithPredicate(List\u0026lt;Person\u0026gt; roster, Predicate\u0026lt;Person\u0026gt; tester) printPersons( people, p -\u0026gt; p.getGender() == Person.Sex.MALE \u0026amp;\u0026amp; p.getAge() \u0026gt;= 18 \u0026amp;\u0026amp; p.getAge() \u0026lt;= 25); // 自动推断为CheckPerson printPersonsWithPredicate( people, p -\u0026gt; p.getGender() == Person.Sex.MALE \u0026amp;\u0026amp; p.getAge() \u0026gt;= 18 \u0026amp;\u0026amp; p.getAge() \u0026lt;= 25);) // 自动推断Predicate\u0026lt;Person\u0026gt; Variable declarations Assignments Return statements Array initializers Method or constructor arguments Lambda expression bodies Conditional expressions, ?: Cast expressions 再看个例子：\npublic interface Runnable { void run(); } public interface Callable\u0026lt;V\u0026gt; { V call(); } void invoke(Runnable r) { r.run(); } \u0026lt;T\u0026gt; T invoke(Callable\u0026lt;T\u0026gt; c) { return c.call(); } String s = invoke(() -\u0026gt; \u0026#34;done\u0026#34;); // Lambda 实际推断使用哪个？答案是Callable，因为它有return，而Runnable没有。\nWildcards 使用?，extends表示上限:\nList\u0026lt;? extends Number\u0026gt; 可以是 List\u0026lt;Integer\u0026gt;, List\u0026lt;Double\u0026gt;, and List\u0026lt;Number\u0026gt;\n这里的extends既是class的extends，也是interface的implements。\nList\u0026lt;Object\u0026gt;和List\u0026lt;?\u0026gt;有什么区别？\n①子类型\npublic static void printList(List\u0026lt;Object\u0026gt; list) { for (Object elem : list) System.out.println(elem + \u0026#34; \u0026#34;); System.out.println(); } it prints only a list of Object instances; it cannot print List\u0026lt;Integer\u0026gt;, List\u0026lt;String\u0026gt;, List\u0026lt;Double\u0026gt;, and so on, because they are not subtypes of List\u0026lt;Object\u0026gt;.\npublic static void printList(List\u0026lt;?\u0026gt; list) { for (Object elem: list) System.out.print(elem + \u0026#34; \u0026#34;); System.out.println(); } Because for any concrete type A, List\u0026lt;A\u0026gt; is a subtype of List\u0026lt;?\u0026gt;, you can use printList() to print a list of any type.\n②值\nYou can insert an Object, or any subtype of Object, into a List\u0026lt;Object\u0026gt;. But you can only insert null into a List\u0026lt;?\u0026gt;.\n使用?，super表示下限:\npublic static void addNumbers(List\u0026lt;? super Integer\u0026gt; list) { for (int i = 1; i \u0026lt;= 10; i++) { list.add(i); } } 可以是List\u0026lt;Integer\u0026gt;, List\u0026lt;Number\u0026gt;, and List\u0026lt;Object\u0026gt; — anything that can hold Integer values\n?能支持集合子类型：\nType Erasure Type Erasure是Java编译器为了实现泛型做的：\nReplace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods. Insert type casts if necessary to preserve type safety. Generate bridge methods to preserve polymorphism in extended generic types. Restriction on Generics 1、不能使用基本数据类型：\nclass Pair\u0026lt;K, V\u0026gt; { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // ... } Pair\u0026lt;int, char\u0026gt; p = new Pair\u0026lt;\u0026gt;(8, \u0026#39;a\u0026#39;); // compile-time error 只能使用包装类：\nPair\u0026lt;Integer, Character\u0026gt; p = new Pair\u0026lt;\u0026gt;(8, \u0026#39;a\u0026#39;); 2、不能创建泛型实例：\npublic static \u0026lt;E\u0026gt; void append(List\u0026lt;E\u0026gt; list) { E elem = new E(); // compile-time error list.add(elem); } 只能通过类来创建实例：\npublic static \u0026lt;E\u0026gt; void append(List\u0026lt;E\u0026gt; list, Class\u0026lt;E\u0026gt; cls) throws Exception { E elem = cls.newInstance(); // OK list.add(elem); } List\u0026lt;String\u0026gt; ls = new ArrayList\u0026lt;\u0026gt;(); append(ls, String.class); 3、static不能使用泛型\npublic class MobileDevice\u0026lt;T\u0026gt; { private static T os; // 在实例化后，会同时代表3种类型，显然不合理 // ... } MobileDevice\u0026lt;Smartphone\u0026gt; phone = new MobileDevice\u0026lt;\u0026gt;(); MobileDevice\u0026lt;Pager\u0026gt; pager = new MobileDevice\u0026lt;\u0026gt;(); MobileDevice\u0026lt;TabletPC\u0026gt; pc = new MobileDevice\u0026lt;\u0026gt;(); 4、不能instanceof\npublic static \u0026lt;E\u0026gt; void rtti(List\u0026lt;E\u0026gt; list) { if (list instanceof ArrayList\u0026lt;Integer\u0026gt;) { // compile-time error // ... } } 使用?可以：\npublic static void rtti(List\u0026lt;?\u0026gt; list) { if (list instanceof ArrayList\u0026lt;?\u0026gt;) { // OK; instanceof requires a reifiable type // ... } } 5、不能创建泛型数组：\nList\u0026lt;Integer\u0026gt;[] arrayOfLists = new List\u0026lt;Integer\u0026gt;[2]; // compile-time error 6、Cannot Create, Catch, or Throw Objects of Parameterized Types\n// Extends Throwable indirectly class MathException\u0026lt;T\u0026gt; extends Exception { /* ... */ } // compile-time error // Extends Throwable directly class QueueFullException\u0026lt;T\u0026gt; extends Throwable { /* ... */ // compile-time error public static \u0026lt;T extends Exception, J\u0026gt; void execute(List\u0026lt;J\u0026gt; jobs) { try { for (J job : jobs) // ... } catch (T e) { // compile-time error // ... } } throws可以：\nclass Parser\u0026lt;T extends Exception\u0026gt; { public void parse(File file) throws T { // OK // ... } } 7、重载方法不能有擦除后相同的泛型：\npublic class Example { public void print(Set\u0026lt;String\u0026gt; strSet) { } public void print(Set\u0026lt;Integer\u0026gt; intSet) { } // 编译错误 } 参考资料：\nGenerics https://dev.java/learn/generics/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000011-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B08%E6%B3%9B%E5%9E%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"泛型\"\u003e泛型\u003c/h2\u003e\n\u003cp\u003e为什么需要泛型？generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e说白了就像Python动态语言的变量，是动态的，可以指向任意类型。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e泛型有个好处是不需要类型转换：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eList\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003elist\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eArrayList\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elist\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eadd\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;hello\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003es\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003elist\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eget\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e0\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eString\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003elist\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eArrayList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eString\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elist\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eadd\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;hello\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003es\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003elist\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eget\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e0\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// no cast\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这个例子的泛型，是指List的实现使用了泛型\u003ccode\u003e\u0026lt;T\u0026gt;\u003c/code\u003e，从而给使用带来了好处。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e定义\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eT1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eT2\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e...,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTn\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e/* ... */\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e比如定义\u003ccode\u003eclass List\u0026lt;T\u0026gt;\u003c/code\u003e，使用\u003ccode\u003eList\u0026lt;String\u0026gt;\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e将以下代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eBox\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eobject\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eobject\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eobject\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eobject\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eget\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eobject\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e改为泛型实现：\u003c/p\u003e","title":"Java官方笔记8泛型"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter定时器一般用来设置延迟与同步。它的作用域和优先级如下：\n定时器的优先级高于Sampler。 在同一作用域（比如控制器下）有多个定时器存在，每个定时器都会执行。 在某一Sampler节点下的定时器，只对这个Sampler有效。 JMeter一共有9种定时器：\nConstant Timer 固定值，多用来模拟思考时间。\nUniform Random Timer 随机时间，分布会更均匀。\nRandom Delay Maximum：随机延迟时间。\nConstant Delay Offset：固定延迟时间。\n示例：\n会让线程延迟在2.0到2.1秒之间。\nPrecise Throughput Timer 用的比较少，比Constant Throughput Timer更复杂更高级的用来控制吞吐量的一个定时器。\nConstant Throughput Timer 控制吞吐量，尽量保持Sampler产生的吞吐量（RPS）在一个固定的范围内。\nTarget throughput：目标吞吐量。JMeter会根据吞吐量、并发线程数来计算迭代的时间间隔，如果吞吐量大过设定值，就增加；反之减少。\nCalculate Throughput based on：\nthis thread only：仅针对当前线程，每个线程互不干扰，分开计算需要延迟多少时间。\nall active threads：把所有线程的吞吐量合在一起作为因子计算。\nall active threads in current thread group：针对当前线程组中的所有线程。\nall active threads (shared)：在所有线程中，随便获取一个线程上次运行的实际来进行计算，结果随机不一定准确，只能努力控制吞吐量在某一个范围。\nall active threads in current thread group (shared)：在上面意思一样，范围是当前线程组。\nGaussian Random Timer 生成一个呈高斯分布的时间间隔，可以用来离散请求。\nDeviation：一个浮动范围的偏差值。\nConstant Delay Offset：固定延迟时间。\nJSR223 Timer 跟BeanShell类似，可以运行JavaScript等其他语言。\nJDK集成了JS的运行引擎，可以运行JavaScript脚本，JSR223是一个标准，为脚本引擎提供了统一的接口、统一的访问模式。\nPoisson Random Timer 随机时间，呈泊松分布。\nLambda：泊松分布的期望和方差。\nConstant Delay Offset：固定延迟时间。\nSynchronizing Timer 同步定时器，用来模拟多用户并发同一时刻发起请求。\nNumber of Simulated Users to Group by：等待x个用户（线程）集合后开始释放（发起请求）。\n默认为0，等同于设置为线程租中的线程数量。\nTimeout in milliseconds：等待时长，如果等待这么长时间，还没有达到上个选项设置的数量，那么不再等待，立即释放已到达的线程。\n默认为0，等待线程数达到上个选项设置的数量才释放。\nBeanShell Timer Reset Interpreter：是否每次迭代重置解释器，默认False。官方建议在长时间运行的脚本中才设置为True。\nParameters：入参，可以是单个变量，也可以是数组。\nFile Name：从脚本文件中读取BeanShell脚本。\nScript：\nctx：通过JmeterContent实例获取运行时信息，如log.error(ctx.getThreadNum().toString());。\nvars：访问或设置变量，如vars.get(\u0026quot;name\u0026quot;)，vars.put(\u0026quot;name\u0026quot;, \u0026quot;dongfanger\u0026quot;)。\nprops：访问JMeter属性（%JMETER_HOME%/bin/jmeter.properties），如props.get(\u0026quot;log_level.jmeter\u0026quot;)，props.put(\u0026quot;log_level.jmeter\u0026quot;, \u0026quot;ERROR\u0026quot;)。\nlog：在代码中调用log类来写日志，如log.error(\u0026quot;loadtest\u0026quot;);。\nprev：可以访问前面Sampler的结果。\n示例：\n小结 本文首先介绍了JMeter定时器的作用域和优先级，然后分别介绍了各式各样的定时器，既能设置延迟，也能同步集合，可以固定可以随机，配置与编程并存，五花八门。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\nhttps://blog.csdn.net/hwhua1986/article/details/78214923\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000011-jmeterjmeter%E5%AE%9A%E6%97%B6%E5%99%A8%E8%AE%BE%E7%BD%AE%E5%BB%B6%E8%BF%9F%E4%B8%8E%E5%90%8C%E6%AD%A5/","summary":"JMeter定时器一般用来设置延迟与同步。它的作用域和优先级如下：","title":"JMeter定时器设置延迟与同步"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n元组拆包 元组是不可变列表，列表是通过索引取值的，元组也是：\ntuple_test = (1, 2, 3) a = tuple_test[0] b = tuple_test[1] c = tuple_test[2] 但Python是出了名的一行代码解决问题，元组拆包就是精髓技术之一：\na, b, c = tuple_test print(\u0026#34;%s %s %s\u0026#34; % tuple_test) 把元组一一对应拆出来，就叫做元组拆包。拆包有个要求，元组中的元素数量必须跟接受这些元素的空挡数一致，否则会报错：\ntuple_test = (1, 2, 3) a, b = tuple_test # ValueError: too many values to unpack (expected 2) _占位符\n使用_占位符可以解决这个问题：\ntuple_test = (1, 2, 3) a, b, _ = tuple_test 这样就只获取到部分数据了，这在取函数返回值时特别有用，比如：\nimport os _, filename = os.path.split(\u0026#34;/home/dongfanger/.ssh/idrsa.pub\u0026#34;) print(filename) # \u0026#34;idrsa.pub\u0026#34; *前缀\n当返回值特别多时，_占位符写起来麻烦，可以用*来处理剩下的元素：\n\u0026gt;\u0026gt;\u0026gt; a, b, *rest = range(5) \u0026gt;\u0026gt;\u0026gt; a, b, *rest (0, 1, [2, 3, 4]) 注意rest是个列表，如果没有足够元素，会返回空列表：\n\u0026gt;\u0026gt;\u0026gt; a, b, *rest = range(2) \u0026gt;\u0026gt;\u0026gt; a, b, *rest (0, 1, []) *前缀变量能放在任意位置，比如，放在中间：\n\u0026gt;\u0026gt;\u0026gt; a, *body, c, d = range(5) \u0026gt;\u0026gt;\u0026gt; a, body, c, d (0, [1, 2], 3, 4) 放在前面：\n\u0026gt;\u0026gt;\u0026gt; *head, b, c, d = range(5) \u0026gt;\u0026gt;\u0026gt; head, b, c, d ([0, 1], 2, 3, 4) 实在是妙啊。\n*还有一个作用，把元组拆开作为函数参数：\n\u0026gt;\u0026gt;\u0026gt; divmod(20, 8) (2, 4) \u0026gt;\u0026gt;\u0026gt; t = (20, 8) \u0026gt;\u0026gt;\u0026gt; divmod(*t) (2, 4) 经典写法*args就是这个道理。\n嵌套元组拆包 嵌套元组是指元组中有元组，比如(1, 2, 3, (4, 5))，对于嵌套元组，你可能会想要拆两遍：\ntuple_nest_test = (1, 2, 3, (4, 5)) a, b, c, d = tuple_nest_test x, y = d print(a, b, c, x, y) 实际上能一步到位：\ntuple_nest_test = (1, 2, 3, (4, 5)) a, b, c, (x, y) = tuple_nest_test print(a, b, c, x, y) 交换两个变量的值 元组拆包提供了语法糖，对于交换两个变量的值的常规写法：\ntemp = a a = b b = temp 可以切换为优雅写法：\nb, a = a, b 具名元组 元组很像数据库表记录，除了没有表名和字段名，collections.namedtuple具名元组补偿了这个缺憾，它是一个工厂函数，可以用来构建一个带字段名的元组和一个有名字的类，比如：\nimport collections # 定义 Card = collections.namedtuple(\u0026#34;Card\u0026#34;, [\u0026#34;rank\u0026#34;, \u0026#34;suit\u0026#34;]) # 初始化 card_test = Card(\u0026#34;J\u0026#34;, \u0026#34;hearts\u0026#34;) # 使用 print(card_test.rank) # J print(card_test[1]) # hearts Card是表名，有两个表字段rank和suit。\n定义具名元组需要2个参数，第1个参数是类名，第2个参数是字段名，既可以是可迭代对象（如列表和元组），也可以是空格间隔的字符串：\nCard = collections.namedtuple(\u0026#34;Card\u0026#34;, (\u0026#34;rank\u0026#34;, \u0026#34;suit\u0026#34;)) Card = collections.namedtuple(\u0026#34;Card\u0026#34;, \u0026#34;rank suit\u0026#34;) 初始化时以一串参数形式传入构造函数：\ncard_test = Card(\u0026#34;J\u0026#34;, \u0026#34;hearts\u0026#34;) 既可以通过.运算符，也可以用索引来取值：\nprint(card_test.rank) print(card_test[1]) 这个带名字的元组，对调试程序有很大帮助。\n列表与元组 元组是不可变列表，它们就像双胞胎，长相类似，内在性格却有不同：\n黄色列表独有，红色元组特有，元组竟然还多了个s.__getnewargs__()方法！从表中可以清楚地看到，除了跟增减元素相关的方法之外，元组支持列表的其他所有方法。\n列表也能拆 既然列表和元组是孪生兄弟，那必然也有共同技能：\nlist_test = [1, 2, 3] a, b, c = list_test \u0026gt;\u0026gt;\u0026gt; divmod(20, 8) (2, 4) \u0026gt;\u0026gt;\u0026gt; t = [20, 8] # 换成列表 \u0026gt;\u0026gt;\u0026gt; divmod(*t) (2, 4) 列表拆包，也是ok的。\n小结 本文介绍了Python神奇操作元组拆包，借助_占位符和*前缀可以进行更加灵活的取值，具名元组实际用的还比较少，不过看一些源码是有的。文章最后比较了列表和元组的差异，列表也能拆包。列表（list）、元组（tuple），以及字符串（str），都有一个共同操作：切片。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000011-pythonpython%E5%85%83%E7%BB%84%E6%8B%86%E5%8C%85%E6%8D%A1%E5%88%B08%E5%80%8D%E9%95%9C%E5%BF%AB%E5%87%86%E7%8B%A0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"元组拆包\"\u003e元组拆包\u003c/h1\u003e\n\u003cp\u003e元组是不可变列表，列表是通过索引取值的，元组也是：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etuple_test \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ea \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e tuple_test\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eb \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e tuple_test\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ec \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e tuple_test\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e但Python是出了名的一行代码解决问题，元组拆包就是精髓技术之一：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ea\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e c \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e tuple_test\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e%s\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e%s\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e%s\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e%\u003c/span\u003e tuple_test\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e把元组一一对应拆出来，就叫做元组拆包。拆包有个要求，元组中的元素数量必须跟接受这些元素的空挡数一致，否则会报错：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etuple_test \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ea\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e tuple_test  \u003cspan style=\"color:#57606a\"\u003e# ValueError: too many values to unpack (expected 2)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e_\u003c/code\u003e占位符\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e使用\u003ccode\u003e_\u003c/code\u003e占位符可以解决这个问题：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etuple_test \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ea\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e _ \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e tuple_test\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这样就只获取到部分数据了，这在取函数返回值时特别有用，比如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eos\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e_\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e filename \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003esplit\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/home/dongfanger/.ssh/idrsa.pub\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efilename\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# \u0026#34;idrsa.pub\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e*\u003c/code\u003e前缀\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e当返回值特别多时，\u003ccode\u003e_\u003c/code\u003e占位符写起来麻烦，可以用\u003ccode\u003e*\u003c/code\u003e来处理剩下的元素：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003erest \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erange\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e5\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003erest\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e注意\u003ccode\u003erest\u003c/code\u003e是个列表，如果没有足够元素，会返回空列表：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003erest \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erange\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003erest\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003e*\u003c/code\u003e前缀变量能放在任意位置，比如，放在中间：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e c\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e d \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erange\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e5\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e a\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e body\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e c\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e d\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e放在前面：\u003c/p\u003e","title":"Python元组拆包捡到8倍镜快准狠"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nContainer Images FROM eclipse-temurin:17-jre as builder WORKDIR application ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} application.jar RUN java -Djarmode=layertools -jar application.jar extract FROM eclipse-temurin:17-jre WORKDIR application COPY --from=builder application/dependencies/ ./ COPY --from=builder application/spring-boot-loader/ ./ COPY --from=builder application/snapshot-dependencies/ ./ COPY --from=builder application/application/ ./ ENTRYPOINT [\u0026#34;java\u0026#34;, \u0026#34;org.springframework.boot.loader.JarLauncher\u0026#34;] $ docker build --build-arg JAR_FILE=path/to/myapp.jar . Production-ready Features The spring-boot-actuator module provides all of Spring Boot’s production-ready features.\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Endpoints\nActuator endpoints let you monitor and interact with your application. Spring Boot includes a number of built-in endpoints and lets you add your own. For example, the health endpoint provides basic application health information.\nMonitoring and Management Over HTTP\nIf you are developing a web application, Spring Boot Actuator auto-configures all enabled endpoints to be exposed over HTTP. The default convention is to use the id of the endpoint with a prefix of /actuator as the URL path. For example, health is exposed as /actuator/health.\nLoggers\nTRACE\nDEBUG\nINFO\nWARN\nERROR\nFATAL\nOFF\nnull\nDeploying Spring Boot Applications You can deploy Spring Boot applications to a variety of cloud platforms, to virtual/real machines, or make them fully executable for Unix systems.\nKubernetes\nSpring Boot auto-detects Kubernetes deployment environments by checking the environment for \u0026quot;*_SERVICE_HOST\u0026quot; and \u0026quot;*_SERVICE_PORT\u0026quot; variables. You can override this detection with the spring.main.cloud-platform configuration property.\n总结 快速刷完SpringBoot官方网站，刚开始还比较能看懂，越到后面越发现，整个内容偏向于“配置”说明，能吸收和理解的知识很少，从我做的笔记也是能看出来这点。\n这跟SpringBoot本身的设计也是一致的，即SpringBoot是在Spring Framework基础之上做的“自动配置”，从繁杂的xml配置中解脱出来，SpringBoot本身就是一个外壳。\n学习SpirngBoot，需要对它的内核进行学习，也就是Spring Framework，还好也有官方文档，虽然有点难找：\nhttps://docs.spring.io/spring-framework/reference/overview.html\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000011-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B08%E5%85%B6%E4%BB%96/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"container-images\"\u003eContainer Images\u003c/h1\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eFROM eclipse-temurin:17-jre as builder\nWORKDIR application\nARG JAR_FILE=target/*.jar\nCOPY ${JAR_FILE} application.jar\nRUN java -Djarmode=layertools -jar application.jar extract\n\nFROM eclipse-temurin:17-jre\nWORKDIR application\nCOPY --from=builder application/dependencies/ ./\nCOPY --from=builder application/spring-boot-loader/ ./\nCOPY --from=builder application/snapshot-dependencies/ ./\nCOPY --from=builder application/application/ ./\nENTRYPOINT [\u0026#34;java\u0026#34;, \u0026#34;org.springframework.boot.loader.JarLauncher\u0026#34;]\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e$ docker build --build-arg JAR_FILE=path/to/myapp.jar .\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"production-ready-features\"\u003eProduction-ready Features\u003c/h1\u003e\n\u003cp\u003eThe \u003ca href=\"https://github.com/spring-projects/spring-boot/tree/v3.1.1/spring-boot-project/spring-boot-actuator\"\u003e\u003ccode\u003espring-boot-actuator\u003c/code\u003e\u003c/a\u003e module provides all of Spring Boot’s production-ready features.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-starter-actuator\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eEndpoints\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eActuator endpoints let you monitor and interact with your application. Spring Boot includes a number of built-in endpoints and lets you add your own. For example, the \u003ccode\u003ehealth\u003c/code\u003e endpoint provides basic application health information.\u003c/p\u003e","title":"SpringBoot官方笔记8其他"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n很多朋友是因为teprunner，也就是这个小众的pytest内核测试平台关注的公众号。为了让大家更好的上手teprunner，我更新了它的README，希望能让小伙伴们根据这些文档内容，一步一步的在自己本地电脑上把项目跑起来。项目跑起来之后，就可以参考前面一系列的学习教程，自己动手做一遍，在做的过程中和teprunner进行对比，不懂的点逐一突破，由点到面，完整实现。这种学习方式能更快速的掌握测试平台开发技能哦。\n前端项目teprunner-frontend 源码地址：https://github.com/dongfanger/teprunner-frontend\n下载源码 方式①：\ngit clone git@github.com:dongfanger/teprunner-frontend.git 方式②：下载zip压缩包后解压。\n安装依赖包 在项目目录打开cmd，执行命令：\nnpm install 启动服务 等待依赖包安装完成后，启动前端服务：\nnpm run serve 访问系统 打开浏览器，输入localhost:8080：\n用户名admin，密码qa123456。此时还无法登陆，需要部署后端服务。\n后端项目teprunner-backend 源码地址：https://github.com/dongfanger/teprunner-backend\n下载源码 方式①：\ngit clone git@github.com:dongfanger/teprunner-backend.git 方式②：下载zip压缩包后解压。\n准备数据库连接 以下两者任选其一即可。\nSQLite\nSQLite数据库是Django自带的，不需要另外安装。由于会用到models.JSONField，SQLite默认不兼容，所以需要下载sqlite3.dll文件替换下：https://www.sqlite.org/download.html\n根据Python版本选择相应文件，比如我的windows安装的Python38-32，下载了sqlite-dll-win32-x86-3340100.zip这个软件包，解压后将D:\\Program Files (x86)\\Python38-32\\DLLs\\sqlite3.dll替换。\nMySQL\n也可以使用MySQL数据库，前提是已经安装并创建好了数据库。修改mysite/settings.py中的数据库配置：\nDATABASES = { \u0026#39;default\u0026#39;: { \u0026#39;ENGINE\u0026#39;: \u0026#39;django.db.backends.mysql\u0026#39;, \u0026#39;HOST\u0026#39;: \u0026#39;主机\u0026#39;, \u0026#39;PORT\u0026#39;: 端口, \u0026#39;NAME\u0026#39;: \u0026#39;数据库名\u0026#39;, \u0026#39;USER\u0026#39;: \u0026#39;用户名\u0026#39;, \u0026#39;PASSWORD\u0026#39;: \u0026#39;密码\u0026#39; } } Django默认通过底层调用mysqlclient这个库和MySQL交互。但是mysqlclient非常不好安装，我们改用 pymysql。先安装pymysql：\npip install pymysql 然后在与mysite/settings.py文件同级的__init__文件中添加代码：\nimport pymysql pymysql.version_info = (1, 4, 0, \u0026#34;final\u0026#34;, 0) pymysql.install_as_MySQLdb() 迁移数据库 数据库准备好以后，就可以执行以下命令，创建表结构：\npython manage.py makemigrations python manage.py migrate 然后执行以下命令，初始化用户数据：\npython manage.py loaddata user 启动服务 数据准备好了，执行以下命令，启动后端服务：\npython manage.py runserver 启动成功后，确保前端服务也已解决启动成功后，就可以打开localhost:8080，输入用户名admin，密码qa123456登录测试平台体验啦。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000011-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E5%85%A5%E9%97%A8%E7%BA%A7%E9%83%A8%E7%BD%B2%E6%89%8B%E5%86%8C%E5%8F%91%E5%B8%83%E5%95%A6/","summary":"很多朋友是因为teprunner，也就是这个小众的pytest内核测试平台关注的公众号。为了让大家更好的上手teprunner，我更新了它的README，希望能让小伙伴们根据这些文档内容，一步一步的在自己本地电脑上把项目跑起来。项目跑起来之后，就可以参考前面一系列的学习教程，自己动手做一遍，在做的过程中和teprunner进行对比，不懂的点逐一突破，由点到面，完整实现。**这种学习方式能更快速的掌","title":"teprunner测试平台入门级部署手册发布啦"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n使用 操作过程非常简单，流程如下：\n①配置过滤域名\n必须配置，不然会有很多无效流量造成数据冗余。\n②启动代理\n「示例」使用了反向代理，tep自带FastApi启动Mock服务：\n「实际」使用正向代理，并手动配置代理。命令说明在utils/mitm.py文件中有：\n③录制流量\n「示例」通过Postman发起请求：\n「实际」在网页上点点点。\n自动生成用例\n用例就在tests/mitm实时生成好了，用例文件名为当前时间：\n每录制一个请求，就能在测试用例中看到实时添加了一条测试步骤：\n# 接口描述 # 数据 # 请求 response = request( \u0026#34;{method}\u0026#34;, url=\u0026#34;{url}\u0026#34;, headers={headers}, {body_grammar} ) # 数据提取 # var = response.jmespath(\u0026#34;expression\u0026#34;) # 断言 assert response.status_code \u0026lt; 400 录制完成以后，就可以再次编辑测试用例完善它。\n原理 utils/mitm.py源码如下：\n#!/usr/bin/python ## encoding=utf-8 ## mitmproxy录制流量自动生成用例 import os import time from mitmproxy import ctx project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) tests_dir = os.path.join(project_dir, \u0026#34;tests\u0026#34;) ## tests/mitm mitm_dir = os.path.join(tests_dir, \u0026#34;mitm\u0026#34;) if not os.path.exists(mitm_dir): os.mkdir(mitm_dir) ## 当前时间作为文件名 filename = time.strftime(\u0026#34;%Y%m%d_%H_%M_%S\u0026#34;, time.localtime()) + \u0026#34;.py\u0026#34; case_file = os.path.join(mitm_dir, filename) ## 生成用例文件 template = \u0026#34;\u0026#34;\u0026#34;from tep.client import request def test(): \u0026#34;\u0026#34;\u0026#34; if not os.path.exists(case_file): with open(case_file, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf8\u0026#34;) as fw: fw.write(template) class Record: def __init__(self, domains): self.domains = domains def response(self, flow): if self.match(flow.request.url): # method method = flow.request.method.lower() ctx.log.error(method) # url url = flow.request.url ctx.log.error(url) # headers headers = dict(flow.request.headers) ctx.log.error(headers) # body body = flow.request.text or {} ctx.log.error(body) with open(case_file, \u0026#34;a\u0026#34;, encoding=\u0026#34;utf8\u0026#34;) as fa: fa.write(self.step(method, url, headers, body)) def match(self, url): if not self.domains: ctx.log.error(\u0026#34;必须配置过滤域名\u0026#34;) exit(-1) for domain in self.domains: if domain in url: return True return False def step(self, method, url, headers, body): if method == \u0026#34;get\u0026#34;: body_grammar = f\u0026#34;params={body}\u0026#34; else: body_grammar = f\u0026#34;json={body}\u0026#34; return f\u0026#34;\u0026#34;\u0026#34; # 接口描述 # 数据 # 请求 response = request( \u0026#34;{method}\u0026#34;, url=\u0026#34;{url}\u0026#34;, headers={headers}, {body_grammar} ) # 数据提取 # var = response.jmespath(\u0026#34;expression\u0026#34;) # 断言 assert response.status_code \u0026lt; 400 \u0026#34;\u0026#34;\u0026#34; ## ==================================配置开始================================== addons = [ Record( # 过滤域名 [ \u0026#34;http://www.httpbin.org\u0026#34;, \u0026#34;http://127.0.0.1:5000\u0026#34; ], ) ] ## ==================================配置结束================================== \u0026#34;\u0026#34;\u0026#34; ==================================命令说明开始================================== ## 正向代理 mitmdump -s mitm.py ## 反向代理 mitmdump -s mitm.py --mode reverse:http://127.0.0.1:5000 --listen-host 127.0.0.1 --listen-port 8000 ==================================命令说明结束================================== \u0026#34;\u0026#34;\u0026#34; 通过mitmproxy命令启动代理后，获取当前时间作为文件名在tests/mitm下生成用例文件，给addons添加插件Record类，通过response钩子捕获请求信息，对请求进行域名过滤，域名匹配成功的请求就使用测试步骤代码模板，填充method、url、headers、body数据，追加到测试用例文件中。\n**自动生成的用例只支持tep风格。**我也考虑过同时兼容tep、mvc、httprunner，但是实际编写过代码后，我仍然坚持认为tep风格是我最喜欢的编写方式，尽量贴近原生，可以减少出错风险，同时获得更多代码自由。\n想体验的大佬可以升级或安装tep的0.9.7版本，使用tep startproject demo创建项目即可。\n参考资料：\n基于mitmproxy实现接口录制 https://testerhome.com/topics/29457\nmitmproxy生成httprunner脚本 https://testerhome.com/topics/30204\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000011-tep%E9%9B%86%E6%88%90mitmproxy%E5%BD%95%E5%88%B6%E6%B5%81%E9%87%8F%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90%E7%94%A8%E4%BE%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"使用\"\u003e使用\u003c/h1\u003e\n\u003cp\u003e操作过程非常简单，流程如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/878aab2dbe594c36b9daadf8c46baf8b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e①\u003cstrong\u003e配置过滤域名\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e必须配置，不然会有很多无效流量造成数据冗余。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/aecdc4689cd145c5b79e049fbc5e18e1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e②\u003cstrong\u003e启动代理\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e「示例」使用了反向代理，tep自带FastApi启动Mock服务：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5477922c30864b6686005cac4c30d1a0.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e「\u003cstrong\u003e实际\u003c/strong\u003e」使用正向代理，并手动配置代理。命令说明在\u003ccode\u003eutils/mitm.py\u003c/code\u003e文件中有：\u003cimg loading=\"lazy\" src=\"../assets/4a6bbaf69b9c4321960f445ff9c07b4e.png\"\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e③\u003cstrong\u003e录制流量\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e「示例」通过Postman发起请求：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e75f4777b1514b22bd450704670603bf.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e「\u003cstrong\u003e实际\u003c/strong\u003e」在网页上点点点。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/cf5ebc04eea34b0cbd710dcb35b9d378.png\"\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e自动生成用例\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e用例就在\u003ccode\u003etests/mitm\u003c/code\u003e\u003cstrong\u003e实时生成\u003c/strong\u003e好了，用例文件名为当前时间：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/423aec3928b24f778066d05885ebd279.png\"\u003e\u003c/p\u003e\n\u003cp\u003e每录制一个请求，就能在测试用例中看到实时添加了一条测试步骤：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e    # 接口描述\n    # 数据\n    # 请求\n    response = request(\n        \u0026#34;{method}\u0026#34;,\n        url=\u0026#34;{url}\u0026#34;,\n        headers={headers},\n        {body_grammar}\n    )\n    # 数据提取\n    # var = response.jmespath(\u0026#34;expression\u0026#34;)\n    # 断言\n    assert response.status_code \u0026lt; 400\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e录制完成以后，就可以再次编辑测试用例完善它。\u003c/p\u003e\n\u003ch1 id=\"原理\"\u003e原理\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003eutils/mitm.py\u003c/code\u003e源码如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e#!/usr/bin/python\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## encoding=utf-8\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## mitmproxy录制流量自动生成用例\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eos\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etime\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003emitmproxy\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e ctx\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eproject_dir \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edirname\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eos\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edirname\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eos\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eabspath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#953800\"\u003e__file__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etests_dir \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eproject_dir\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;tests\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## tests/mitm\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emitm_dir \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003etests_dir\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;mitm\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eexists\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emitm_dir\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003emkdir\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emitm_dir\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 当前时间作为文件名\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efilename \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e time\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estrftime\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%Y%m\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e%d\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e_%H_%M_%S\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e time\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elocaltime\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;.py\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecase_file \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejoin\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emitm_dir\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e filename\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## 生成用例文件\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etemplate \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;from tep.client import request\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003edef test():\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e os\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epath\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eexists\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecase_file\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ewith\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eopen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecase_file\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;w\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e encoding\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;utf8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eas\u003c/span\u003e fw\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        fw\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewrite\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003etemplate\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eRecord\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__init__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e domains\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edomains \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e domains\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eresponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e flow\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ematch\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eflow\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erequest\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# method\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            method \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e flow\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erequest\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003emethod\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elower\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ctx\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elog\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eerror\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emethod\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# url\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e flow\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erequest\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eurl\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ctx\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elog\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eerror\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# headers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            headers \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003edict\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eflow\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erequest\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ctx\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elog\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eerror\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# body\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e flow\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erequest\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etext \u003cspan style=\"color:#0550ae\"\u003eor\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ctx\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elog\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eerror\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003ewith\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eopen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecase_file\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;a\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e encoding\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;utf8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eas\u003c/span\u003e fa\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                fa\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewrite\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estep\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emethod\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e body\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ematch\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edomains\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ctx\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003elog\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eerror\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;必须配置过滤域名\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            exit\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e domain \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edomains\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e domain \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e url\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eFalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003estep\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e method\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e body\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e method \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            body_grammar \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003ef\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;params=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003ebody\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            body_grammar \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003ef\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;json=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003ebody\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003ef\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    # 接口描述\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    # 数据\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    # 请求\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    response = request(\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e        \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003emethod\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e        url=\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003eurl\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e        headers=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003eheaders\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{\u003c/span\u003ebody_grammar\u003cspan style=\"color:#0a3069\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    )\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    # 数据提取\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    # var = response.jmespath(\u0026#34;expression\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    # 断言\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    assert response.status_code \u0026lt; 400\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## ==================================配置开始==================================\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaddons \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Record\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#57606a\"\u003e# 过滤域名\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://www.httpbin.org\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://127.0.0.1:5000\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e## ==================================配置结束==================================\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e==================================命令说明开始==================================\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e## 正向代理\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003emitmdump -s mitm.py\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e## 反向代理\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003emitmdump -s mitm.py --mode reverse:http://127.0.0.1:5000 --listen-host 127.0.0.1 --listen-port 8000\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e==================================命令说明结束==================================\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e通过mitmproxy命令启动代理后，获取当前时间作为文件名在\u003ccode\u003etests/mitm\u003c/code\u003e下生成用例文件，给addons添加插件Record类，通过response钩子捕获请求信息，对请求进行域名过滤，域名匹配成功的请求就使用测试步骤代码模板，填充method、url、headers、body数据，追加到测试用例文件中。\u003c/p\u003e","title":"tep集成mitmproxy录制流量自动生成用例"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n“ChatGPT是倾盆大雨前的第一场雨”，OpenAI首席科学家Ilya Sutskever。\n大模型是指在人工智能领域，尤其是机器学习和深度学习中，具有大量参数和复杂结构的模型。\n大模型通过在大量数据上进行预训练，然后针对特定任务进行微调，最终部署到应用中以执行复杂任务。\n最近百度推出的“萝卜快跑”打车（Robotaxi），就是大模型应用之一。收集大量驾驶数据，包括视频、图像、雷达、车辆传感器等数据，对数据进行标注、归一化和增强等预处理。然后对大模型进行预训练，学习道路环境、交通规则、物体识别等。根据自动驾驶需求，对模型进行微调，保持车道、避让障碍物、识别交通标识等。将训练好的模型集成到自动驾驶系统中，在模拟环境和封闭场地进行测试。经过充分的模拟和实车测试后，部署到出租车上，实现无人驾驶。\n未来世界，到底是个什么样的世界啊。\n截止2024年初，已有诞生了近百个参数规模大于100亿的模型，在图像语言、推荐系统、医疗诊断、办公软件等各行各业，掀起了滔天巨浪，甚至有人感叹第四次工业革命是不是就要到来。\n从个人体验来说，日常工作中使用ChatGPT时间已经远远超过了搜索引擎。测试工作虽然没有颠覆式改变，但可以预见未来会有不少变化。\n以下纯属我的猜想。\n功能用例。AI可以根据需求文档分析出功能用例。只要针对垂直业务进行大量训练，一个既懂业务，又懂测试用例设计的机器人，是很可能出现的。\n自动化用例。AI可以自动生成，自动执行，自动断言，自动完成自动化测试。流量录制回放已经替代了部分回归测试工作，有AI能力加持，很难不去想象，未来是不是会出现“无人回归测试”。\n代码评审。AI可以分析增量代码，计算出影响范围，给出改动点、影响点和测试点。ChatGPT已经很懂代码了，再结合精准测试分析，这项技术会愈加成熟。\n测试数据。AI可以生成各种各样的测试数据。文件数据、内存动态数据、数据库存储数据，想要什么给你什么。\n测试工具。AI可以快速生成测试工具。ChatGPT具备强大的代码生成能力，创建工具将变得简单快捷。\nAI未来是否会取代人类？测试工作的特点之一是重复性，AI恰好就是解决重复问题的，所以很多重复性测试工作，一定会被AI取代。但是AI始终无法取代测试人员，为什么？\n因为AI不能替测试人员背锅呀。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000011-%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%97%B6%E4%BB%A3%E6%B5%8B%E8%AF%95%E5%B7%A5%E4%BD%9C%E4%BC%9A%E6%9C%89%E4%BB%80%E4%B9%88%E5%8F%98%E5%8C%96/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3f169471792b4d8dad8dde37a0989a1e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e“ChatGPT是倾盆大雨前的第一场雨”，OpenAI首席科学家\u003cem\u003eIlya\u003c/em\u003e Sutskever。\u003c/p\u003e\n\u003cp\u003e大模型是指在人工智能领域，尤其是机器学习和深度学习中，具有大量参数和复杂结构的模型。\u003c/p\u003e\n\u003cp\u003e大模型通过在大量数据上进行预训练，然后针对特定任务进行微调，最终部署到应用中以执行复杂任务。\u003c/p\u003e\n\u003cp\u003e最近百度推出的“萝卜快跑”打车（Robotaxi），就是大模型应用之一。收集大量驾驶数据，包括视频、图像、雷达、车辆传感器等数据，对数据进行标注、归一化和增强等预处理。然后对大模型进行预训练，学习道路环境、交通规则、物体识别等。根据自动驾驶需求，对模型进行微调，保持车道、避让障碍物、识别交通标识等。将训练好的模型集成到自动驾驶系统中，在模拟环境和封闭场地进行测试。经过充分的模拟和实车测试后，部署到出租车上，实现无人驾驶。\u003c/p\u003e\n\u003cp\u003e未来世界，到底是个什么样的世界啊。\u003c/p\u003e\n\u003cp\u003e截止2024年初，已有诞生了近百个参数规模大于100亿的模型，在图像语言、推荐系统、医疗诊断、办公软件等各行各业，掀起了滔天巨浪，甚至有人感叹第四次工业革命是不是就要到来。\u003c/p\u003e\n\u003cp\u003e从个人体验来说，日常工作中使用ChatGPT时间已经远远超过了搜索引擎。测试工作虽然没有颠覆式改变，但可以预见未来会有不少变化。\u003c/p\u003e\n\u003cp\u003e以下纯属我的猜想。\u003c/p\u003e\n\u003cp\u003e功能用例。AI可以根据需求文档分析出功能用例。只要针对垂直业务进行大量训练，一个既懂业务，又懂测试用例设计的机器人，是很可能出现的。\u003c/p\u003e\n\u003cp\u003e自动化用例。AI可以自动生成，自动执行，自动断言，自动完成自动化测试。流量录制回放已经替代了部分回归测试工作，有AI能力加持，很难不去想象，未来是不是会出现“无人回归测试”。\u003c/p\u003e\n\u003cp\u003e代码评审。AI可以分析增量代码，计算出影响范围，给出改动点、影响点和测试点。ChatGPT已经很懂代码了，再结合精准测试分析，这项技术会愈加成熟。\u003c/p\u003e\n\u003cp\u003e测试数据。AI可以生成各种各样的测试数据。文件数据、内存动态数据、数据库存储数据，想要什么给你什么。\u003c/p\u003e\n\u003cp\u003e测试工具。AI可以快速生成测试工具。ChatGPT具备强大的代码生成能力，创建工具将变得简单快捷。\u003c/p\u003e\n\u003cp\u003eAI未来是否会取代人类？测试工作的特点之一是重复性，AI恰好就是解决重复问题的，所以很多重复性测试工作，一定会被AI取代。但是AI始终无法取代测试人员，为什么？\u003c/p\u003e\n\u003cp\u003e因为AI不能替测试人员背锅呀。\u003c/p\u003e","title":"大模型时代，测试工作会有什么变化"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n6.1 网关的演进历程 是什么在推动软件架构的演进呢？业务规模。业务从互联网、移动互联网以及即将到来的万物互联，其规模仍然在高速增长，也就意味着软件架构的复杂度也会越来越高，网关的特性也会越来越多。越来越复杂、越来越智能。\n6.1.1 流量网关 代表软件是Nginx，将客户请求分配到不同的服务器上，从而实现流量负债均衡，避免单点故障，确保服务的稳定性和连续性。\n6.1.2 ESB网关 ESB，企业服务总线，通过集中管理消息路由、转换和安全，实现服务的快速部署和高效运作。\n6.1.3 微服务网关 微服务网关可以实现负载均衡、限流、熔断、降级、身份验证等功能，通过统一入口管理和优化各微服务间的交互。Spring Cloud Gateway是一个广泛应用的微服务网关。\n6.1.4 云原生网关 云原生网关是伴随K8S的广泛应用而诞生的一种创新网关。既能有流量网关的特性来处理海量请求，又具备微服务网关的特性来做服务发现与服务治理，同时要求网关也具备弹性扩缩容能力解决容量调度问题。\n6.1.5 AI网关 之前的几种类型网关处理的流量基本是以HTTP、RPC为主，多采用长连接，长连接采用request-response模式。AI采用的协议以SSE/Websocket为主，虽然也是长连接，但跟request-response模式相比，Server可以主动发送数据给Client，Websocket更是一种全双工协议，主要在语音类的实时通讯场景中使用。\nAI场景对网关有新诉求，比如：多模型代理、智能路由、模型增强、安全防护、流式传输、无损变更等。\nHigress网关已经在阿里云内部大规模生产落地，可以访问项目地址了解：https://higress.ai/\n6.1.6 API网关 从流量网关，再到AI网关，都是API网关在不同软件架构下的网关形态。HTTP/gRPC/Dubbo/Websocket都可以称之为API，MCP、A2A也可以称之为API。\n6.2 AI网关的定义、特点与应用场景 AI网关是提供多模型流量调度，MCP和Agent管理，智能路由和AI治理的下一代网关。AI网关不仅完整继承了API网关的通用能力，如安全认证、路由转发、流量控制等，也演进出了大模型Fallback、大模型负载均衡、Token级别的精细化流量管控、语义化缓存、内容安全、联网搜索、MCP协议转化和管理、工具精选和搜索效果优化等面向AI场景的能力。\n6.2.1 AI应用的流量特征 高延时\n大带宽与流式传输\n长连接\nAPI驱动\n6.2.2 AI网关的应用场景 1、模型服务提供商（MaaS）的接入层\n2、AI应用的开发网关\n3、企业内部的中央AI网关\n4、MCP工具生态的统一入口\n5、构建企业AI能力货币化的统一开放平台\n6.3 AI网关的核心能力和最佳实践 6.3.1 核心能力 1、多模型代理\n2、多模型回退/容灾\n3、消费者认证\n4、内容安全防护\n5、Token限流\n6、语义缓存\n7、可观测性\n8、MCP代理\n9、工具的动态组装\n10、工具的智能路由\n6.3.2 最佳实践 AI网关的原子能力可以总结为面向LLM、Agent、MCP和AI开放平台四大类，这里总结了8类常见的实践，多模型代理、消费者认证、内容安全与防护、Token限流、语义缓存、多模型容灾、多模型可观测和AI开放平台。\n1、多模型代理\n需求场景：\n文本、图像、音频、3D等多模态业务场景。\n企业不同垂直领域，需要调用专用模型。\n任务需要多个模型分工协作。\n医疗机构，处理患者数据使用私有模型，其他和患者无关使用通用模型。\n核心价值：\n业务需求适配：根据业务复杂性或性能要求选择不同模型。\n数据隐私与合规性：在处理敏感数据时，可能需要切换到符合特定法规的模型，确保数据处理的安全性。\n性能优化：根据实时性能需求，可能会切换到更快的模型以减少延迟。\n成本与性能平衡：根据预算动态选择性价比最优的模型。\n领域特定需求：针对特定领域（如法律、医学），可能需要切换到在相关领域微调过的模型，以提高推理准确性。\n容灾与故障转移：主模型服务异常时快速切换备用模型。\n2、消费者认证\n需求场景：\n企业为不同部门提供共享的大模型服务时，通过API Key区分租户，确保数据隔离和权限管控。\n企业内部不同角色差异化访问模型能力，比如金融企业限制风险评估模型仅限风控部门调用。\n核心价值：\n身份可信：确保请求方为注册/授权用户或系统。\n风险拦截：防止恶意攻击、非法调用与资源滥用。\n合规保障：满足数据安全法规及企业审计要求。\n成本控制：基于鉴权实现精准计费与API配额管理。\n3、内容安全防护\n需求场景：\n金融行业审核用户输入的金融交易指令、投资咨询内容。\n医疗行业电子病历生成内容防止泄露患者隐私。\n社交媒体实时审核用户发布的内容拦截涉黄、暴恐、虚假信息。\n政务服务审核公众提交的政务咨询内容防止恶意攻击或敏感信息传播。\n电商直播审核商品描述、直播弹幕内容拦截虚假宣传、违禁品信息。\n核心价值：\n防止攻击：验证输入可以阻止恶意提示注入，防止模型生成有害内容。\n维护模型完整性：避免输入操纵模型，导致错误或偏见输出。\n用户安全：确保输出没有有害或误导性内容，保护用户免受不良影响。\n内容适度：过滤掉不适当的内容，如仇恨言论或不雅语言，特别是在公共应用中。\n法律法规：确保输出符合法律和伦理标准，尤其在医疗或金融领域。\n4、Token限流\n需求场景：10000人企业，只需要配置7000人硬件资源，超出部分进行限流。限制Token数量来减少垃圾请求或攻击，以免受到资损。\n核心价值：\n成本管理：LLM的费用通常基于Token数量计算，限流帮助用户避免超支。例如，服务提供商可能按Token使用量提供不同定价层。\n资源管理：LLM需要大量计算资源，限流防止系统过载，确保所有用户都能获得稳定性能，尤其在高峰期。\n用户分层：可以基于ConsumerId或者API Key进行Token限流。\n防止恶意使用：通过限制Token数量来减少垃圾请求或攻击。\n5、语义缓存\n需求场景：\n客服系统、智能助手等，用户常提出重复问题，缓存常见问题的回答。\n法律文件分析、教育教材解析，需对通一长文本多次提问，缓存上下文。\n数据分析与生成场景，对相同数据集的多次分析结果缓存。\nRAG缓存知识库检索结果。\n核心价值：\n提高效率：如果相同的输入反复出现，缓存可以避免重复运行模型，从而加快响应速度，特别是在处理常见问题时。\n降低成本：减少模型调用次数可以节省计算资源，尤其对大型模型来说成本较高。\n保持一致性：缓存确保相同输入产生相同输出，有助于测试和合规性场景。\n6、多模型容灾\n需求场景：\n大模型生成不稳定。\n使用者请求参数有误导致连接超时或中断。\n请求频次过高触发限流。\n依赖服务故障。\n核心价值：\n当主LLM服务因为各种原因出现异常，不能提供服务时，网关侧可以快速将请求Fallback到配置的其他LLM服务，虽然可能推理质量有所下降，但是保证了业务的连续性，争取了排查LLM服务的时间。 7、多模型可观测\n需求场景：成本控制和稳定性场景。\n核心价值：AI网关通过支持在应用、网关、后端LLM服务上开启OT服务来进行全链路的跟踪，通过TracId来串联各个地方的日志、请求参数等信息。除了提供QPS、RT、错误率等传统观测指标，AI网关还能集成更多元化的监控指标：\n基于Consumer的Token消耗统计。\n基于模型的Token消耗统计。\n限流指标：每单位时间内有多少次请求因为限流被拦截，限流消费者统计（是哪些消费者在被限流）。\n缓存命中情况。\n安全统计：风险类统计、风险消费者统计。\n8、AI开放平台\n需求场景：工具提供方、Agent开发者、安全合规、财务运营等角色各自为政，产生了能力目录分散、上架流程不一致、权限与配额难统一、成本难计量与分摊、内容安全也难以落实到每一次调用等一系列问题。\n核心价值：\nAI开放平台管理后台（for管理者/运营）：在这里将底层的模型服务、MCP Server、Agent等多样化的AI能力，以API的形式轻松打包成标准化的“AI产品”，并配上完善的文档、示例，最终一键发布到门户。\nAI开放平台门户（for开发者）：门户是面向内外开发者的“店面”。开发者可以在此完成开发者注册、创建消费者、获取凭证、浏览和订阅AI产品、在线测试，并清晰地监控自己的调用状态和成本。\nAI网关：作为Higress AI社区的子项目，Higress AI网关承载所有AI调用的认证、安全、流控、协议转换以及可观测性等能力。\nNacos：Nacos作为MCP Registry为门户提供全面的MCP Server元信息托管，版本管理，服务发现，密钥托管等能力。通过动态服务发现和动态配置变更实现系统灵活扩展和变更，通过Nacos MCP Router智能路由简化多MCP服务管理和调用。在日常开发测试的场景中，通过动态Prompt变更实现高效的MCP服务调试，在生成环境中通过多版本管理实现灰度发布，通过加密存储和动态配置变更提升安全能力。\n6.4 使用AI网关快速构建AI应用 本文基于Higress、通义千问以及ChatGPT-Next-Web，演示如何逐步搭建一个体系完整的AI应用。\n架构图：\n6.4.1 AI代理插件 应用架构\n首先，通过网关快速部署一个可以进行对话的聊天应用。\n架构如下：\nLLM服务使用通义千问，服务类型为DNS：\n插件配置\n插件效果\n6.4.2 AI可观测插件 应用架构\n网关提供了对Token的观测机制，包含路由级、服务级、模型级的Token用量观测。\n插件配置\n插件效果\n6.4.3 AI内容安全插件 应用架构\n插件配置\n在网关配置内容安全的服务：\n开启内容安全插件，选择对LLM路由生效：\n插件效果\n6.4.4 AI Token限流插件 应用架构\n创建一个Redis服务并且在网关进行配置：\n插件配置\n插件效果\n6.5.4 AI缓存 应用架构\n插件配置\n插件效果\n6.4.6 AI RAG 应用架构\n插件配置\n插件效果\n6.4.7 其他 Prompt工程相关插件\nPrompt模版，定义模版指定参数进行访问\nPrompt装饰器，在请求之前和之后插入message\nAI请求/响应智能转换\n插件配置\n插件效果\n6.5 API和Agent的货币化 AI应用正在越过“聊天即产品”的早期阶段，进入以任务完成为目标的Agent时代。\n6.5.1 AI开放平台 AI网关负责抽象并统一上游推理与下游工具，提供稳定的路由、编排与策略面；协议层对工具与资源进行自描述，收敛参数、错误与权限语义，消弭供应商差异；在此基础上，以API货币化为抓手，构建可计量、可计费、可分成、可审计的商业框架，最终形成Agent开放平台或Agent市场，建立“供给-分发-变现-治理”的闭环。\n开源项目HiMarket AI开放平台：\nhttps://github.com/higress-group/himarket\n6.5.2 AI网关 AI网关是AI开放平台的底层基础设施。\n6.5.3 API货币化 首先让一切可被度量，随后基于这些指标设计价格计划。\n6.5.4 API开发者门户 面向开发者提供自助式上架到变现的全链路体验。\n6.5.5 Agent市场 API和Agent的货币化绝不是“加个计费按钮”，而是一套以AI网关为中枢、以协议为通用语言、以市场为分发载体的系统工程。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005006-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E7%BD%91%E5%85%B3/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"61-网关的演进历程\"\u003e6.1 网关的演进历程\u003c/h1\u003e\n\u003cp\u003e是什么在推动软件架构的演进呢？业务规模。业务从互联网、移动互联网以及即将到来的万物互联，其规模仍然在高速增长，也就意味着软件架构的复杂度也会越来越高，网关的特性也会越来越多。越来越复杂、越来越智能。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5bf22b56b5b4464f8a6277cc8c17edd9.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"611-流量网关\"\u003e6.1.1 流量网关\u003c/h2\u003e\n\u003cp\u003e代表软件是Nginx，将客户请求分配到不同的服务器上，从而实现流量负债均衡，避免单点故障，确保服务的稳定性和连续性。\u003c/p\u003e\n\u003ch2 id=\"612-esb网关\"\u003e6.1.2 ESB网关\u003c/h2\u003e\n\u003cp\u003eESB，企业服务总线，通过集中管理消息路由、转换和安全，实现服务的快速部署和高效运作。\u003c/p\u003e\n\u003ch2 id=\"613-微服务网关\"\u003e6.1.3 微服务网关\u003c/h2\u003e\n\u003cp\u003e微服务网关可以实现负载均衡、限流、熔断、降级、身份验证等功能，通过统一入口管理和优化各微服务间的交互。Spring Cloud Gateway是一个广泛应用的微服务网关。\u003c/p\u003e\n\u003ch2 id=\"614-云原生网关\"\u003e6.1.4 云原生网关\u003c/h2\u003e\n\u003cp\u003e云原生网关是伴随K8S的广泛应用而诞生的一种创新网关。既能有流量网关的特性来处理海量请求，又具备微服务网关的特性来做服务发现与服务治理，同时要求网关也具备弹性扩缩容能力解决容量调度问题。\u003c/p\u003e\n\u003ch2 id=\"615-ai网关\"\u003e6.1.5 AI网关\u003c/h2\u003e\n\u003cp\u003e之前的几种类型网关处理的流量基本是以HTTP、RPC为主，多采用长连接，长连接采用request-response模式。AI采用的协议以SSE/Websocket为主，虽然也是长连接，但跟request-response模式相比，Server可以主动发送数据给Client，Websocket更是一种全双工协议，主要在语音类的实时通讯场景中使用。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/10f08f06650847409694087017f7f9c0.png\"\u003e\u003c/p\u003e\n\u003cp\u003eAI场景对网关有新诉求，比如：多模型代理、智能路由、模型增强、安全防护、流式传输、无损变更等。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ca7cb41081da44c7a5342061aebe0352.png\"\u003e\u003c/p\u003e\n\u003cp\u003eHigress网关已经在阿里云内部大规模生产落地，可以访问项目地址了解：https://higress.ai/\u003c/p\u003e\n\u003ch2 id=\"616-api网关\"\u003e6.1.6 API网关\u003c/h2\u003e\n\u003cp\u003e从流量网关，再到AI网关，都是API网关在不同软件架构下的网关形态。HTTP/gRPC/Dubbo/Websocket都可以称之为API，MCP、A2A也可以称之为API。\u003c/p\u003e\n\u003ch1 id=\"62-ai网关的定义特点与应用场景\"\u003e6.2 AI网关的定义、特点与应用场景\u003c/h1\u003e\n\u003cp\u003eAI网关是提供多模型流量调度，MCP和Agent管理，智能路由和AI治理的下一代网关。AI网关不仅完整继承了API网关的通用能力，如安全认证、路由转发、流量控制等，也演进出了大模型Fallback、大模型负载均衡、Token级别的精细化流量管控、语义化缓存、内容安全、联网搜索、MCP协议转化和管理、工具精选和搜索效果优化等面向AI场景的能力。\u003c/p\u003e\n\u003ch2 id=\"621-ai应用的流量特征\"\u003e6.2.1 AI应用的流量特征\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e高延时\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e大带宽与流式传输\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e长连接\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eAPI驱动\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"622-ai网关的应用场景\"\u003e6.2.2 AI网关的应用场景\u003c/h2\u003e\n\u003cp\u003e1、模型服务提供商（MaaS）的接入层\u003c/p\u003e\n\u003cp\u003e2、AI应用的开发网关\u003c/p\u003e\n\u003cp\u003e3、企业内部的中央AI网关\u003c/p\u003e\n\u003cp\u003e4、MCP工具生态的统一入口\u003c/p\u003e\n\u003cp\u003e5、构建企业AI能力货币化的统一开放平台\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/af7c740bb4d94809a6e3f28a6efdbaa8.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"63-ai网关的核心能力和最佳实践\"\u003e6.3 AI网关的核心能力和最佳实践\u003c/h1\u003e\n\u003ch2 id=\"631-核心能力\"\u003e6.3.1 核心能力\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6a9d044807414b7cac37a40da5ada5a3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1、多模型代理\u003c/p\u003e\n\u003cp\u003e2、多模型回退/容灾\u003c/p\u003e\n\u003cp\u003e3、消费者认证\u003c/p\u003e\n\u003cp\u003e4、内容安全防护\u003c/p\u003e\n\u003cp\u003e5、Token限流\u003c/p\u003e\n\u003cp\u003e6、语义缓存\u003c/p\u003e\n\u003cp\u003e7、可观测性\u003c/p\u003e\n\u003cp\u003e8、MCP代理\u003c/p\u003e\n\u003cp\u003e9、工具的动态组装\u003c/p\u003e\n\u003cp\u003e10、工具的智能路由\u003c/p\u003e\n\u003ch2 id=\"632-最佳实践\"\u003e6.3.2 最佳实践\u003c/h2\u003e\n\u003cp\u003eAI网关的原子能力可以总结为面向LLM、Agent、MCP和AI开放平台四大类，这里总结了8类常见的实践，多模型代理、消费者认证、内容安全与防护、Token限流、语义缓存、多模型容灾、多模型可观测和AI开放平台。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1、多模型代理\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e需求场景：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e文本、图像、音频、3D等多模态业务场景。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e企业不同垂直领域，需要调用专用模型。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e任务需要多个模型分工协作。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e医疗机构，处理患者数据使用私有模型，其他和患者无关使用通用模型。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e","title":"AI网关"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDjango可以用LoginRequiredMixin和PermissionRequiredMixin给类视图添加认证和权限，DRF做了高级封装，提供了更简洁的实现方式。我们通过继续学习官网教程来进行了解。\n更新model 首先修改Snippet模型，添加2个字段：owner，存储snippet创建者，highlighted，存储高亮HTML。同时重写save方法，在同步数据库的时候，使用pygments包把code格式化后存到highlighted字段。修改后的snippets/models.py完整代码如下：\nfrom django.db import models from pygments.lexers import get_all_lexers from pygments.styles import get_all_styles from pygments.lexers import get_lexer_by_name from pygments.formatters.html import HtmlFormatter from pygments import highlight LEXERS = [item for item in get_all_lexers() if item[1]] LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS]) STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()]) class Snippet(models.Model): created = models.DateTimeField(auto_now_add=True) title = models.CharField(max_length=100, blank=True, default=\u0026#39;\u0026#39;) code = models.TextField() linenos = models.BooleanField(default=False) language = models.CharField(choices=LANGUAGE_CHOICES, default=\u0026#39;python\u0026#39;, max_length=100) style = models.CharField(choices=STYLE_CHOICES, default=\u0026#39;friendly\u0026#39;, max_length=100) owner = models.ForeignKey(\u0026#39;auth.User\u0026#39;, related_name=\u0026#39;snippets\u0026#39;, on_delete=models.CASCADE) highlighted = models.TextField() class Meta: ordering = [\u0026#39;created\u0026#39;] def save(self, *args, **kwargs): \u0026#34;\u0026#34;\u0026#34; Use the `pygments` library to create a highlighted HTML representation of the code snippet. \u0026#34;\u0026#34;\u0026#34; lexer = get_lexer_by_name(self.language) linenos = \u0026#39;table\u0026#39; if self.linenos else False options = {\u0026#39;title\u0026#39;: self.title} if self.title else {} formatter = HtmlFormatter(style=self.style, linenos=linenos, full=True, **options) self.highlighted = highlight(self.code, lexer, formatter) super(Snippet, self).save(*args, **kwargs) 接着删除数据库和migrations，重新迁移数据库：\nrm -f db.sqlite3 rm -r snippets/migrations python manage.py makemigrations snippets python manage.py migrate 并创建超级管理员：\npython manage.py createsuperuser User添加Endpoint Endpoint，表示API的具体网址。我们按照models.py→serializers.py→views.py→urls.py的代码编写顺序，给User模型添加Endpoint。\nmodels.py\n直接使用Django默认User模型，不需要修改代码。\nserializers.py\n添加UserSerializer，由于User没有snippets字段，所以需要显式添加：\nfrom django.contrib.auth.models import User class UserSerializer(serializers.ModelSerializer): snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all()) class Meta: model = User fields = [\u0026#39;id\u0026#39;, \u0026#39;username\u0026#39;, \u0026#39;snippets\u0026#39;] views.py\n添加只读的列表视图UserList和详情视图UserDetail，分别用到了ListAPIView和RetrieveAPIView：\nfrom django.contrib.auth.models import User from snippets.serializers import UserSerializer class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer urls.py\n添加访问路径：\npath(\u0026#39;users/\u0026#39;, views.UserList.as_view()), path(\u0026#39;users/\u0026lt;int:pk\u0026gt;/\u0026#39;, views.UserDetail.as_view()), 关联User和Snippet 如果使用POST方法请求http://127.0.0.1:8000/snippets/，尝试添加1条数据：\n会发现接口报错了：\nowner_id不能为空？因为前面只给Snippet添加了owner字段，还没有写反序列化更新模型的代码，所以通过请求访问视图，再尝试反序列化的时候，报错了。我们先修改视图SnippetList来修复这个问题：\ndef perform_create(self, serializer): serializer.save(owner=self.request.user) 在SnippetList视图中重写perform_create()方法，意思是在保存时，把request.user值赋给owner字段。perform_create()方法的源码是：\nclass CreateModelMixin: \u0026#34;\u0026#34;\u0026#34; Create a model instance. \u0026#34;\u0026#34;\u0026#34; def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() 再修改snippets/serializers.py，添加owner字段，支持序列化：\nclass SnippetSerializer(serializers.ModelSerializer): # ReadOnlyField表示只能序列化为JSON，不能反序列化更新模型 # 也可以改成CharField(read_only=True) owner = serializers.ReadOnlyField(source=\u0026#39;owner.username\u0026#39;) class Meta: model = Snippet fields = [\u0026#39;id\u0026#39;, \u0026#39;title\u0026#39;, \u0026#39;code\u0026#39;, \u0026#39;linenos\u0026#39;, \u0026#39;language\u0026#39;, \u0026#39;style\u0026#39;, \u0026#39;owner\u0026#39;] 注意Meta.fields也要加上owner哦。\n再请求一次：\n刚才的错误没有了，但是报了个新的错误：Snippet.owner必须是User实例，给它赋值的是AnonymousUser（匿名用户），导致ValueError了。这个报错是发生这条代码：\nserializer.save(owner=self.request.user) 也就是说请求访问视图后，进行反序列化了，但是反序列化失败了。非常奇怪！我们的请求中并没有用户信息，正常来说在访问视图的时候就该被拦截了。\n给视图添加认证 我们需要让API更符合常规，让未认证的用户不能执行视图中的代码。DRF提供了rest_framework .permissions来给视图添加认证：\n其中IsAuthenticatedOrReadOnly表示只有认证了才能读写，否则只能读。把它添加到SnippetList和SnippetDetail视图中：\nfrom rest_framework import permissions permission_classes = [permissions.IsAuthenticatedOrReadOnly] 再请求试试，刚才的错误没有了，API返回的是需要提供用户凭证：\n登录视图 如果用浏览器打开http://127.0.0.1:8000/snippets/，会发现只有GET方法没有POST，这是因为需要添加DRF登录视图，在tutorial/urls.py中添加rest_framework.urls：\nurlpatterns += [ path(\u0026#39;api-auth/\u0026#39;, include(\u0026#39;rest_framework.urls\u0026#39;)), ] api-auth/可以自定义。\n刷新页面右上角就会出现Log in按钮，登录后就能POST了。\n对象级权限 为了更细粒度的控制权限，让用户只能编辑自己创建的snippet，新建snippets/permissions.py：\nfrom rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): \u0026#34;\u0026#34;\u0026#34; Custom permission to only allow owners of an object to edit it. \u0026#34;\u0026#34;\u0026#34; def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we\u0026#39;ll always allow GET, HEAD or OPTIONS requests. if request.method in permissions.SAFE_METHODS: return True # Write permissions are only allowed to the owner of the snippet. return obj.owner == request.user 新增IsOwnerOrReadOnly权限，继承了permissions.BasePermission，重写了has_object_permission()方法。接着在snippets/views.py中给SnippetDetail加上：\nfrom snippets.permissions import IsOwnerOrReadOnly permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] 试下访问其他用户创建的snippet，发现只能查看：\n访问自己创建的snippet，可以修改和删除：\n自定义权限 以上是官网的示例，我在Postman测试了下，发现超管dongfanger可以创建snippet：\n普通用户player也可以创建snippet：\n我想让普通用户不能创建，只能超管创建。仿照官网示例，在snippets/permissions.py中添加IsAdminOrReadOnly：\nclass IsAdminOrReadOnly(permissions.BasePermission): def has_permission(self, request, view): return request.user.is_superuser 接着给SnippetList加上：\npermission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAdminOrReadOnly] 用普通用户尝试创建，提示没有权限：\n用超级管理员尝试创建，成功：\n其他认证方式 本文使用的认证方式是默认的SessionAuthentication和BasicAuthentication，只要数据库的用户名、密码和请求中的用户凭证（用户名、密码）匹配上了，就认为认证成功。如果要实现token或jwt认证，需要使用到rest_framework.authentication：\n或rest_framework_jwt.authentication：\npip install djangorestframework-jwt\n这一部分内容官网教程中并没有提及，等我们把教程学完了，以后再找时间来介绍。\n小结 DRF实现认证和权限的关键在于新增permissions.py模块，编写class，继承permissions.BasePermission，重写has_permission()或has_object_permission()方法，再添加class到类视图的permission_classes中。这块的内容比Django的认证系统那套简洁，但是有点混淆，另外我之前参照网上实现了一版JWT，也有点不一样。看来还得写篇对比的文章才行。\n参考资料：\nhttps://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000010-djangodrf%E6%AF%94django%E7%9A%84%E8%AE%A4%E8%AF%81%E5%92%8C%E6%9D%83%E9%99%90%E9%AB%98%E5%9C%A8%E5%93%AA%E9%87%8C/","summary":"Django可以用\u003ccode\u003eLoginRequiredMixin\u003c/code\u003e和\u003ccode\u003ePermissionRequiredMixin\u003c/code\u003e给类视图添加认证和权限，DRF做了高级封装，提供了更简洁的实现方式。我们通过继续学习官网教程来进行了解。","title":"DRF比Django的认证和权限高在哪里"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nSpring MVC，又称为Spring Web MVC，名字来源于Spring模块spring-webmvc，是一个基于Servlet API开发的Web框架。\nSpring MVC遵循经典的MVC设计模式：\n模型（Model） 职责：负责应用程序的数据和业务逻辑 功能： 处理数据的存储、检索和操作 通常与数据库交互 包含业务规则和数据验证逻辑 示例：在一个电商应用中，模型可能包括用户、产品和订单等实体类 视图（View） 职责：负责呈现数据给用户 功能： 显示模型中的数据 提供用户界面元素，如文本框、按钮和表格 不包含业务逻辑，只负责展示数据 示例：在一个电商应用中，视图可能包括产品列表页面、购物车页面和订单确认页面 控制器（Controller） 职责：负责处理用户输入并更新模型和视图 功能： 接收用户输入（如点击按钮、提交表单） 调用模型层处理数据 更新视图以反映模型的变化 示例：在一个电商应用中，控制器可能包括处理用户登录、添加商品到购物车和提交订单的逻辑 我们可以在Maven pom.xml中添加配置：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-webmvc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;6.1.10\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 查看Spring MVC源码：\n接下来，将跟随Spring MVC官方文档，学习以下内容：\nDispatcherServlet\nDispatcherServlet 是Spring MVC的前端控制器（Front Controller），负责接收所有的HTTP请求，并将它们分发给适当的处理器（控制器）。它是整个Spring MVC框架的核心组件\nFilters\n过滤器（Filters）用于在请求到达控制器之前或响应返回客户端之前对请求和响应进行预处理或后处理。常见的过滤器包括身份验证过滤器、日志记录过滤器等\nAnnotated Controllers\n注解控制器（Annotated Controllers）使用注解（如@Controller、@RequestMapping）来定义控制器和处理请求的方法。它们使得配置更加简洁和直观\nFunctional Endpoints\n功能性端点（Functional Endpoints）是Spring 5引入的一种新的定义路由和处理请求的方式，使用函数式编程风格来替代传统的注解方式\nURI Links\nURI链接（URI Links）用于定义和生成应用程序中的URL。Spring提供了多种方式来生成和管理URI链接，以确保链接的正确性和一致性\nAsynchronous Requests\n异步请求（Asynchronous Requests）允许控制器在处理请求时不阻塞当前线程，从而提高应用程序的性能和响应能力。Spring MVC支持异步请求处理，通过DeferredResult、Callable等方式实现\nCORS\n跨域资源共享（CORS, Cross-Origin Resource Sharing）是一种机制，允许来自不同源的请求访问资源。Spring提供了对CORS的支持，可以通过注解或配置来实现\nError Responses\n错误响应（Error Responses）用于处理和返回错误信息。Spring MVC提供了多种方式来处理错误，包括@ExceptionHandler注解、自定义错误页面等\nWeb Security\nWeb安全（Web Security）涉及到保护Web应用程序免受各种攻击，如XSS、CSRF、SQL注入等。Spring Security是一个强大的安全框架，提供了全面的安全解决方案\nHTTP Caching\nHTTP缓存（HTTP Caching）用于提高应用程序的性能，通过缓存响应来减少服务器负载。Spring提供了多种缓存机制，如Cache-Control 头、ETag等\nView Technologies\n视图技术（View Technologies）用于生成和呈现用户界面。Spring MVC支持多种视图技术，包括 JSP、Thymeleaf、FreeMarker等\nMVC Config\nMVC配置（MVC Config）用于配置Spring MVC应用程序的各种组件和行为。可以通过Java配置类或XML配置文件来实现\nHTTP/2\nHTTP/2是HTTP协议的第二个主要版本，提供了更高效的传输性能和更好的用户体验。Spring支持HTTP/2，可以通过配置来启用\n参考资料：\nhttps://docs.spring.io/spring-framework/reference/web/webmvc.html\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000010-javaweb%E5%BC%80%E5%8F%91%E8%BF%9B%E9%98%B6springmvc%E7%AE%80%E4%BB%8B/","summary":"Spring MVC，又称为Spring Web MVC，名字来源于Spring模块\u003ccode\u003espring-webmvc\u003c/code\u003e，是一个基于Servlet API开发的Web框架。","title":"JavaWeb开发进阶SpringMVC简介"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n接口 接口只能包含：constants, method signatures（abstract）, default methods, static methods, and nested types\n方法体只存在于：default methods and static methods\n接口不能实例化，只能被类实现，或者被其他接口继承（接口可以多继承）。\n定义接口：\npublic interface OperateCar { // constant declarations, if any // method signatures // An enum with values RIGHT, LEFT int turn(Direction direction, double radius, double startSpeed, double endSpeed); int changeLanes(Direction direction, double startSpeed, double endSpeed); int signalTurn(Direction direction, boolean signalOn); int getRadarFront(double distanceToCar, double speedOfCar); int getRadarRear(double distanceToCar, double speedOfCar); ...... // more method signatures } 类实现接口：\npublic class OperateBMW760i implements OperateCar { // the OperateCar method signatures, with implementation -- // for example: public int signalTurn(Direction direction, boolean signalOn) { // code to turn BMW\u0026#39;s LEFT turn indicator lights on // code to turn BMW\u0026#39;s LEFT turn indicator lights off // code to turn BMW\u0026#39;s RIGHT turn indicator lights on // code to turn BMW\u0026#39;s RIGHT turn indicator lights off } // other members, as needed -- for example, helper classes not // visible to clients of the interface } 如果接口不是public，那么只能被同一个包里面的类实现。\n实现接口 接口：\npublic interface Relatable { // this (object calling isLargerThan()) // and other must be instances of // the same class returns 1, 0, -1 // if this is greater than, // equal to, or less than other public int isLargerThan(Relatable other); } 实现：\npublic class RectanglePlus implements Relatable { public int width = 0; public int height = 0; public Point origin; // four constructors public RectanglePlus() { origin = new Point(0, 0); } public RectanglePlus(Point p) { origin = p; } public RectanglePlus(int w, int h) { origin = new Point(0, 0); width = w; height = h; } public RectanglePlus(Point p, int w, int h) { origin = p; width = w; height = h; } // a method for moving the rectangle public void move(int x, int y) { origin.x = x; origin.y = y; } // a method for computing // the area of the rectangle public int getArea() { return width * height; } // a method required to implement // the Relatable interface public int isLargerThan(Relatable other) { RectanglePlus otherRect = (RectanglePlus)other; if (this.getArea() \u0026lt; otherRect.getArea()) return -1; else if (this.getArea() \u0026gt; otherRect.getArea()) return 1; else return 0; } } 对于接口：\npublic interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); } 如果想添加新方法，为了避免影响已经实现了该接口的所有类，有以下几种方式：\n①继承\npublic interface DoItPlus extends DoIt { boolean didItWork(int i, double x, String s); } ②default method\npublic interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); default boolean didItWork(int i, double x, String s) { // Method body } } 如果继承了接口，那么子接口的默认方法有可能出现：\n无声明，继承父接口的默认方法 声明，变为abstract method，实现类，必须实现该方法（也就是父类默认，但是子类不默认了） 声明并重写，以子类的默认方法为准 ③static method\npublic interface TimeClient { // ... static public ZoneId getZoneId (String zoneString) { try { return ZoneId.of(zoneString); } catch (DateTimeException e) { System.err.println(\u0026#34;Invalid time zone: \u0026#34; + zoneString + \u0026#34;; using default time zone instead.\u0026#34;); return ZoneId.systemDefault(); } } default public ZonedDateTime getZonedDateTime(String zoneString) { return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString)); } } 接口的static method，就相当于把多个class的static method抽到了一个公共的地方，实现该接口的类，都具有static method。\n接口类型 接口也可以作为类型：\npublic Object findLargest(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) \u0026gt; 0) return object1; else return object2; } 接口类型的赋值对象，必须是实现该接口的类的实例。\n参考资料：\nhttps://dev.java/learn/interfaces/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000010-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B07%E6%8E%A5%E5%8F%A3/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"接口\"\u003e接口\u003c/h2\u003e\n\u003cp\u003e接口只能包含：constants, method signatures（abstract）, default methods, static methods, and nested types\u003c/p\u003e\n\u003cp\u003e方法体只存在于：default methods and static methods\u003c/p\u003e\n\u003cp\u003e接口不能实例化，只能被类实现，或者被其他接口继承（接口可以多继承）。\u003c/p\u003e\n\u003cp\u003e定义接口：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eOperateCar\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// constant declarations, if any\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// method signatures\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// An enum with values RIGHT, LEFT\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eturn\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eDirection\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edirection\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eradius\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartSpeed\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eendSpeed\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003echangeLanes\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eDirection\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edirection\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartSpeed\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eendSpeed\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esignalTurn\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eDirection\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edirection\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eboolean\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esignalOn\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetRadarFront\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edistanceToCar\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                     \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003espeedOfCar\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetRadarRear\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edistanceToCar\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003espeedOfCar\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e         \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e......\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// more method signatures\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e类实现接口：\u003c/p\u003e","title":"Java官方笔记7接口"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter逻辑控制器可以对元件的执行逻辑进行控制，就像编程一样，实现业务需求。\nJMeter包括了以下逻辑控制器：\n一共17种。除了仅一次控制器外，其他控制器下可以嵌套别的种类的逻辑控制器。\nIf Controller 控制此节点下的元件是否运行。\nExpression：可以使用变量表达式或JavaScript。\n勾选Interpret Condition as Variable Expression表示使用变量表达式，建议勾选上。\nUse status of last Sample表示判断上个取样器是否成功，点击后会把${JMeterThread.last_sample_ok}添加到Expression输入框中：\nEvaluate for all children：判断条件是否针对所有的子节点，默认不勾选，只在If Controller入口处判断一次，否则每次节点都会进行判断。\nTransaction Controller 事务控制器能够帮我们把一堆请求归到一个大的事务中去，在报告分析时更方便。\nGenerate parent sample：是否生成父取样结果，勾选后有两个效果，一是Aggregate Report会看到Transaction Controller字样，它把节点下的取样器的运行结果（如消耗时间）累加在一起（注意事务控制器下如果有多个取样器，全部取样器都运行成功，整个事务控制器才算成功），比如：\n二是View Return Tree中会看到多一个结果，比如：\nInclude duration of timer and pre-post processors in generated sample：包括定时器和前置-后置处理器的耗时，建议不用勾选，不然会影响统计结果。\nLoop Controller 跟线程组的Loop设置一样。最终执行次数 = 线程组执行次数 x 循环控制器执行次数，比如线程组设置3次，循环控制器设置6次，那么控制器下面的元件会执行18次。\nWhile Controller 其子节点下的元件将一直运行直到While条件为false。\nCondition：接受变量表达式与变量，比如${inputVar}\u0026lt;10，当inputVar=7时，它的子节点将一直运行下去。\n另外提供以下三个常量：\nBlank：当循环中有取样器失败后停止。 LAST：当循环前有取样器失败则不进入循环。 Otherwise：当判断条件为false时停止循环。 Critical Section Controller 加锁让多线程顺序执行。\n比如，不加锁，3个线程是并行执行的。：\n加锁以后，变成顺序执行了：\nForEach Controller Input variable prefix：输入变量前缀。可以是用户自定义变量里面的多个变量如id_1, id_2, id_3（这里就填写id）：\n也可以是正则表达式提取器匹配到的多个值（这里就填写varName）：\nStart index for loop (exclusive)：循环变量下标起点（不包括自己）。\nEnd index for loop (inclusive)：循环变量下标终点（包括自己）。\nOutput variable name：输出变量名，比如returnVar，通过${returnVar}引用。\nAdd \u0026quot;_\u0026quot; before number：变量前缀后是否加“_”作为分隔符。\n示例：\nInclude Controller 类似于程序中的函数，可以把其他.jmx文件包含进来。\n被导入的测试计划有特殊要求：不能有线程组，只能包含简单控制器及控制器下的元件。\n比如一个查询订单的业务操作用Sampler来模拟，然后放到简单控制器中作为一个执行单元，别的地方也要用到时，就可以不重写直接引用过来。\nInterleave Controller 其节点下的取样器会交替执行。\n比如：\n1个线程迭代3次，执行顺序是1 3, 2 3, 1 3。\nIgnore sub-controller blocks：不执行子控制器，只执行当前这个交替控制器。比如：\n1个线程迭代4次，执行顺序是1, 2, 3, 1，Loop Controller失效。注意，1 2 3是交替执行的，每次迭代只执行其中一个。（如果不勾选，Loop设置为2，那么执行顺序是：1, 2, 3 3, 1）\n嵌套的例子：\n1个线程迭代5次，执行顺序是：1, 3, 2, 4, 1。\nInterleave across threads：跨线程交替运行。比如：\n3个线程2次迭代，不勾选的执行顺序：1 2, 1 2, 1 2；勾选的执行顺序：1 2, 2 3, 3 4。\nOnce Only Controller 其子元件只运行一次。比如只登录一次，只读取一次CSV，只连一次数据库。\n注意，子节点放个CSV Data Set Config，可以让每个线程只读一次，然后使用同一条数据进行反复请求。\n多个线程是一行一行取值的，不会重复。\n比如：\n2个线程3次迭代，那么：\n线程a只读一次，取到尾号1845这条数据，然后用这条数据迭代请求3次。 线程b只读一次，取到尾号0740这条数据，然后用这条数据迭代请求3次。 Random Controller 节点下的元件随机运行，运行顺序不定。\nIgnore sub-controller blocks：不执行子控制器，只执行当前这个交替控制器。\nRandom Order Controller 节点下的元件随机执行，且每个元件只执行一次。\nRecording Controller 没用。\nRuntime Controller 用来控制其子元件的执行时长。\nRuntime单位为秒，默认为1，去掉1或者设置为0，它的子元件不执行。\nSimple Controller 简单控制器很简单，就是用来给Sampler分组的。它指定了一个执行单元，不会改变元件的执行顺序。\n示例：\nThroughput Controller 控制子元件的执行次数（不能控制吞吐量，想控制吞吐量可以使用Constant Throughput Timer）。\nPercent Executions：按执行次数的百分比来计算控制的执行次数，此时Throughput取值是0~100。\nTotal Executions：按Throughput的值来指定执行次数。\nPer User：只对Total Executions生效，勾选后每个线程单独计算，不勾选则所有线程加起来计算。比如2个线程，每个线程迭代10次，Throughput值为6,，勾选Per User则分别计算分别执行6次共12次；不勾选Per User则加起来计算，共执行6次。\nModule Controller 在当前测试计划中引入新的测试片段，测试片段由控制器、取样器及辅助元件构成，能够完成负载的模拟。\n示例：\n把测试片段里面的Loop Controller引了进来。\nModule To Run：下拉列表，选择引入哪一个脚本片段（既包括测试片段也包括当前测试计划的控制器）。\nSwitch Controller 类似于高级语言中的Switch逻辑控制语句。\nSwitch Value：可以为数字，匹配取样器编号（子节点中取样器编号从0开始），不指定或指定编号超出了子节点数，则运行第0个取样器；也可以为字符，匹配取样器名称，匹配不上就会默认查找名称为default的取样器，如果没有则不运行。\n小结 本文对JMeter所有控制器进行了介绍，从这些控制器能感受到JMeter之所以这么流行，是因为它实在太成熟了，这里面的技术需要多少沉淀才能做得出来呀。有时间一定得看看JMeter的源码深度学习下。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000010-jmeterjmeter%E9%80%BB%E8%BE%91%E6%8E%A7%E5%88%B6%E5%99%A8%E5%AE%8C%E6%95%B4%E4%BB%8B%E7%BB%8D/","summary":"JMeter逻辑控制器可以对元件的执行逻辑进行控制，就像编程一样，实现业务需求。","title":"JMeter逻辑控制器完整介绍"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n数据模型==对象模型 Python官方文档说法是“Python数据模型”，大多数Python书籍作者说法是“Python对象模型”，它们是一个意思，表示“计算机编程语言中对象的属性”。这句话有点抽象，只要知道对象是Python对数据的抽象，在Python中万物皆对象就可以了。\n官方文档严谨说法，Python程序中的所有数据都是用对象或对象之间的关系来表示的。\n对象三要素 对象有三个要素：编号（identity）、类型（type）、值（value）。\nidentity\n编号就是对象的内存地址，从创建后就不会改变。is运算符用来比较2个对象的编号。id()函数返回对象编号的整数表示。\nidentity也可以翻译为身份，对象身份。\ntype\n类型决定了可以对对象做哪些操作，也定义了对象的可能值，比如int和bool类型的值就不一样。type()函数返回对象类型。很多人会误以为Python是弱类型语言，其实Python是强类型语言，这个误解的真实原因是，Python不需要编译，不需要提前知道变量的类型，在运行时才检查类型，这应该叫做动态语言。\nJavaScript是弱类型语言，在Python中1+\u0026quot;2\u0026quot;会运行失败，在JavaScript中能运行成功。\n类型在创建后也不会改变，虽然可以进行类型转换，但转换实际会产生新对象：\na = 1 print(id(a)) print(id(str(a))) print(id(a)) 结果为：\n140715000207008 2136059506160 140715000207008 value\n对象三要素中唯一能改变的就只有值了。官网有一句正确的废话：允许改变值的对象是mutable（可变的），不允许改变值的对象是immutable（不可变的）。它的意义在于当不可变对象的值是对可变对象的引用时，该如何判断对象的可变性？结论是不可变对象仍然是不可变的，因为这个引用是不能变的，但是我们通常会认为这个不可变对象的值是可变的，尤其是对象容器。对象类型也会决定是否可变，比如numbers、strings、tuples是不可变的，dictionaries、lists是可变的。\n对象容器 有些对象包含了对其他对象的引用，这叫做containers对象容器，比如tuples、lists、dictionaries。大多数情况下，我们说容器的值，说的是引用的对象的值，而不是引用编号。但讨论容器可变性时，则仅仅是指容器直接包含的引用编号。比如tuple是不可变的，如果它包含了对一个可变对象的引用后，当该可变对象改变时容器的值也会改变。\n对象回收 对象不会被显式销毁，但是它们会被当做垃圾回收，只要不存在对象引用，这就是Python垃圾回收机制！垃圾回收机制比较复杂，比如循环引用，实际上已经没有使用了，但是还存在引用等，涉及到算法规则，等写完Python进阶，在写Python原理时做进一步研究。\nPython垃圾回收机制不是银弹，不能解决所有问题，所以在引用外部资源，比如打开文件后，我们需要注意显式close，防止资源始终占用内存，无法释放，造成内存泄漏。close除了手动调用close()方法外，也可以使用with来自动close。\n使用try...except可能会让对象继续存活。\n类型决定一切 对象的类型几乎决定了对象的一切行为，甚至是对象编号，比如对于不可变类型：\na = 1 b = 1 a和b可能会指向同一个值为1的对象，也可能会指向两个不同的值为1的对象，这取决于具体实现。\n但是对于可变类型：\nc = [] d = [] c和d一定会指向两个不同的单独的空列表。\n注意c = d = [] 则是将同一个对象赋值给 c 和 d。\n小结 Python数据模型就是常说的对象模型，万物皆对象，有编号、类型、值三个要素。了解了对象模型后，Python另一个重要概念即将浮出水面，它就是数据结构。\n参考资料：\n《流畅的Python》\nhttps://docs.python.org/3/reference/datamodel.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000010-pythonpython%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%E4%B8%8Epython%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"数据模型对象模型\"\u003e数据模型==对象模型\u003c/h1\u003e\n\u003cp\u003ePython官方文档说法是“Python数据模型”，大多数Python书籍作者说法是“Python对象模型”，它们是一个意思，表示“计算机编程语言中对象的属性”。这句话有点抽象，只要知道对象是Python对数据的抽象，在Python中万物皆对象就可以了。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e官方文档严谨说法，Python程序中的所有数据都是用对象或对象之间的关系来表示的。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"对象三要素\"\u003e对象三要素\u003c/h1\u003e\n\u003cp\u003e对象有三个要素：编号（identity）、类型（type）、值（value）。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eidentity\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e编号就是对象的内存地址，从创建后就不会改变。\u003ccode\u003eis\u003c/code\u003e运算符用来比较2个对象的编号。\u003ccode\u003eid()\u003c/code\u003e函数返回对象编号的整数表示。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eidentity也可以翻译为身份，对象身份。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003etype\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e类型决定了可以对对象做哪些操作，也定义了对象的可能值，比如\u003ccode\u003eint\u003c/code\u003e和\u003ccode\u003ebool\u003c/code\u003e类型的值就不一样。\u003ccode\u003etype()\u003c/code\u003e函数返回对象类型。很多人会误以为Python是弱类型语言，其实Python是强类型语言，这个误解的真实原因是，Python不需要编译，不需要提前知道变量的类型，在运行时才检查类型，这应该叫做动态语言。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eJavaScript是弱类型语言，在Python中\u003ccode\u003e1+\u0026quot;2\u0026quot;\u003c/code\u003e会运行失败，在JavaScript中能运行成功。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e类型在创建后也不会改变，虽然可以进行类型转换，但转换实际会产生新对象：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ea \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eid\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ea\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eid\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003estr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ea\u003cspan style=\"color:#1f2328\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eid\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ea\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e结果为：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e140715000207008\n2136059506160\n140715000207008\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003evalue\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e对象三要素中唯一能改变的就只有值了。官网有一句正确的废话：允许改变值的对象是\u003ccode\u003emutable\u003c/code\u003e（可变的），不允许改变值的对象是\u003ccode\u003eimmutable\u003c/code\u003e（不可变的）。它的意义在于当不可变对象的值是对可变对象的引用时，该如何判断对象的可变性？结论是不可变对象仍然是不可变的，因为这个引用是不能变的，但是我们通常会认为这个不可变对象的值是可变的，尤其是对象容器。对象类型也会决定是否可变，比如\u003ccode\u003enumbers\u003c/code\u003e、\u003ccode\u003estrings\u003c/code\u003e、\u003ccode\u003etuples\u003c/code\u003e是不可变的，\u003ccode\u003edictionaries\u003c/code\u003e、\u003ccode\u003elists\u003c/code\u003e是可变的。\u003c/p\u003e\n\u003ch1 id=\"对象容器\"\u003e对象容器\u003c/h1\u003e\n\u003cp\u003e有些对象包含了对其他对象的引用，这叫做\u003ccode\u003econtainers\u003c/code\u003e对象容器，比如\u003ccode\u003etuples\u003c/code\u003e、\u003ccode\u003elists\u003c/code\u003e、\u003ccode\u003edictionaries\u003c/code\u003e。大多数情况下，我们说容器的值，说的是引用的对象的值，而不是引用编号。但讨论容器可变性时，则仅仅是指容器直接包含的引用编号。比如\u003ccode\u003etuple\u003c/code\u003e是不可变的，如果它包含了对一个可变对象的引用后，当该可变对象改变时容器的值也会改变。\u003c/p\u003e\n\u003ch1 id=\"对象回收\"\u003e对象回收\u003c/h1\u003e\n\u003cp\u003e对象不会被显式销毁，但是它们会被当做垃圾回收，只要不存在对象引用，这就是Python垃圾回收机制！垃圾回收机制比较复杂，比如循环引用，实际上已经没有使用了，但是还存在引用等，涉及到算法规则，等写完Python进阶，在写Python原理时做进一步研究。\u003c/p\u003e\n\u003cp\u003ePython垃圾回收机制不是银弹，不能解决所有问题，所以在引用外部资源，比如打开文件后，我们需要注意显式close，防止资源始终占用内存，无法释放，造成内存泄漏。close除了手动调用\u003ccode\u003eclose()\u003c/code\u003e方法外，也可以使用\u003ccode\u003ewith\u003c/code\u003e来自动close。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e使用\u003ccode\u003etry...except\u003c/code\u003e可能会让对象继续存活。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"类型决定一切\"\u003e类型决定一切\u003c/h1\u003e\n\u003cp\u003e对象的类型几乎决定了对象的一切行为，甚至是对象编号，比如对于不可变类型：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ea \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eb \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003ea\u003c/code\u003e和\u003ccode\u003eb\u003c/code\u003e可能会指向同一个值为\u003ccode\u003e1\u003c/code\u003e的对象，也可能会指向两个不同的值为\u003ccode\u003e1\u003c/code\u003e的对象，这取决于具体实现。\u003c/p\u003e\n\u003cp\u003e但是对于可变类型：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ec \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ed \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003ec\u003c/code\u003e和\u003ccode\u003ed\u003c/code\u003e一定会指向两个不同的单独的空列表。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注意\u003ccode\u003ec = d = []\u003c/code\u003e 则是将同一个对象赋值给 \u003ccode\u003ec\u003c/code\u003e 和 \u003ccode\u003ed\u003c/code\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"小结\"\u003e小结\u003c/h1\u003e\n\u003cp\u003ePython数据模型就是常说的对象模型，万物皆对象，有编号、类型、值三个要素。了解了对象模型后，Python另一个重要概念即将浮出水面，它就是数据结构。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003e《流畅的Python》\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://docs.python.org/3/reference/datamodel.html\"\u003ehttps://docs.python.org/3/reference/datamodel.html\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Python数据模型与Python对象模型"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nCaching Spring Boot auto-configures the cache infrastructure as long as caching support is enabled by using the @EnableCaching annotation.\nimport org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; @Component public class MyMathService { @Cacheable(\u0026#34;piDecimals\u0026#34;) public int computePiDecimal(int precision) { ... } } Before invoking computePiDecimal, the abstraction looks for an entry in the piDecimals cache that matches the i argument. If an entry is found, the content in the cache is immediately returned to the caller, and the method is not invoked. Otherwise, the method is invoked, and the cache is updated before returning the value.\nRedis If Redis is available and configured, a RedisCacheManager is auto-configured.\nspring.cache.cache-names=cache1,cache2 spring.cache.redis.time-to-live=10m import java.time.Duration; import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; @Configuration(proxyBeanMethods = false) public class MyRedisCacheManagerConfiguration { @Bean public RedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer() { return (builder) -\u0026gt; builder .withCacheConfiguration(\u0026#34;cache1\u0026#34;, RedisCacheConfiguration .defaultCacheConfig().entryTtl(Duration.ofSeconds(10))) .withCacheConfiguration(\u0026#34;cache2\u0026#34;, RedisCacheConfiguration .defaultCacheConfig().entryTtl(Duration.ofMinutes(1))); } } If you need to disable caching altogether in certain environments, force the cache type to none to use a no-op implementation, as shown in the following example:\nspring.cache.type=none Sending Email The Spring Framework provides an abstraction for sending email by using the JavaMailSender interface, and Spring Boot provides auto-configuration for it as well as a starter module.\nspring.mail.properties[mail.smtp.connectiontimeout]=5000 spring.mail.properties[mail.smtp.timeout]=3000 spring.mail.properties[mail.smtp.writetimeout]=5000 Web Services spring.webservices.wsdl-locations=classpath:/wsdl 参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#io\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000010-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B07io/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"caching\"\u003eCaching\u003c/h1\u003e\n\u003cp\u003eSpring Boot auto-configures the cache infrastructure as long as caching support is enabled by using the \u003ccode\u003e@EnableCaching\u003c/code\u003e annotation.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.springframework.cache.annotation.Cacheable\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.springframework.stereotype.Component\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Component\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyMathService\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Cacheable\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;piDecimals\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003ecomputePiDecimal\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eprecision\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBefore invoking \u003ccode\u003ecomputePiDecimal\u003c/code\u003e, the abstraction looks for an entry in the \u003ccode\u003epiDecimals\u003c/code\u003e cache that matches the \u003ccode\u003ei\u003c/code\u003e argument. If an entry is found, the content in the cache is immediately returned to the caller, and the method is not invoked. Otherwise, the method is invoked, and the cache is updated before returning the value.\u003c/p\u003e","title":"SpringBoot官方笔记7IO"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\ntep0.9.5更新了以下内容：\n自定义request请求日志 Allure报告添加request描述 猴子补丁扩展request fixtures支持多层级目录 FastAPI替代Flask 升级tep到0.9.5版本，使用tep startproject demo创建项目脚手架，开箱即用以上新功能。\n1.自定义request请求日志 tep默认的request请求日志如下所示：\n2022-01-22 20:00:26.335 | INFO | tep.client:tep_request_monkey_patch:44 - method:\u0026#34;post\u0026#34; url:\u0026#34;http://127.0.0.1:5000/login\u0026#34; headers:{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;} json:{\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;} status:200 response:{\u0026#34;token\u0026#34;:\u0026#34;de2e3ffu29\u0026#34;} elapsed:0.000s 全部都在一行，假如想换行显示，那么可以在utils/http_client.py文件中修改request_monkey_patch代码：\n在测试代码中把from tep.client import request改成from utils.http_client import request：\n日志就会变成换行显示了：\n2022-01-22 20:04:05.379 | INFO | utils.http_client:request_monkey_patch:38 - method:\u0026#34;post\u0026#34; headers:{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;} json:{\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;} status:200 response:{\u0026#34;token\u0026#34;:\u0026#34;de2e3ffu29\u0026#34;} elapsed:0.000s 2.Allure报告添加request描述 tep的Allure报告默认会有个request \u0026amp; response：\n可以给request添加desc参数，在Allure测试报告中添加描述：\n运行以下命令，然后打开Allure测试报告：\npytest -s test_request_monkey_patch.py --tep-reports 3.猴子补丁扩展request 前面的“自定义request请求日志”和“Allure报告添加request描述”已经展示了如何通过猴子补丁扩展日志和扩展报告，您还可以为request扩展更多想要的功能，只需要实现utils/http_client.py里面的request_monkey_patch函数即可：\n#!/usr/bin/python ## encoding=utf-8 import decimal import json import time import allure from loguru import logger from tep import client from tep.client import TepResponse def request_monkey_patch(req, *args, **kwargs): start = time.process_time() desc = \u0026#34;\u0026#34; if \u0026#34;desc\u0026#34; in kwargs: desc = kwargs.get(\u0026#34;desc\u0026#34;) kwargs.pop(\u0026#34;desc\u0026#34;) response = req(*args, **kwargs) end = time.process_time() elapsed = str(decimal.Decimal(\u0026#34;%.3f\u0026#34; % float(end - start))) + \u0026#34;s\u0026#34; log4a = \u0026#34;{}\\n{}status:{}\\nresponse:{}\\nelapsed:{}\u0026#34; try: kv = \u0026#34;\u0026#34; for k, v in kwargs.items(): # if not json, str() try: v = json.dumps(v, ensure_ascii=False) except TypeError: v = str(v) kv += f\u0026#34;{k}:{v}\\n\u0026#34; if args: method = f\u0026#39;\\nmethod:\u0026#34;{args[0]}\u0026#34; \u0026#39; else: method = \u0026#34;\u0026#34; request_response = log4a.format(method, kv, response.status_code, response.text, elapsed) logger.info(request_response) allure.attach(request_response, f\u0026#39;{desc} request \u0026amp; response\u0026#39;, allure.attachment_type.TEXT) except AttributeError: logger.error(\u0026#34;request failed\u0026#34;) except TypeError: logger.warning(log4a) return TepResponse(response) def request(method, url, **kwargs): client.tep_request_monkey_patch = request_monkey_patch return client.request(method, url, **kwargs) 4.fixtures支持多层级目录 tep之前一直只能支持fixtures的根目录的fixture_*.py文件自动导入，现在能支持多层级目录了：\n测试代码test_multi_fixture.py：\n#!/usr/bin/python ## encoding=utf-8 def test(fixture_second, fixture_three): pass 能运行成功。自动导入多层目录的代码实现如下：\n## 自动导入fixtures _fixtures_dir = os.path.join(_project_dir, \u0026#34;fixtures\u0026#34;) for root, _, files in os.walk(_fixtures_dir): for file in files: if file.startswith(\u0026#34;fixture_\u0026#34;) and file.endswith(\u0026#34;.py\u0026#34;): full_path = os.path.join(root, file) import_path = full_path.replace(_fixtures_dir, \u0026#34;\u0026#34;).replace(\u0026#34;\\\\\u0026#34;, \u0026#34;.\u0026#34;).replace(\u0026#34;/\u0026#34;, \u0026#34;.\u0026#34;).replace(\u0026#34;.py\u0026#34;, \u0026#34;\u0026#34;) try: fixture_path = \u0026#34;fixtures\u0026#34; + import_path exec(f\u0026#34;from {fixture_path} import *\u0026#34;) except: fixture_path = \u0026#34;.fixtures\u0026#34; + import_path exec(f\u0026#34;from {fixture_path} import *\u0026#34;) 5.FastAPI替代Flask 因为HttpRunner用的FastAPI，所以我也把Flask替换成了FastAPI，在utils/fastapi_mock.py文件中可以找到代码实现的简易Mock：\n#!/usr/bin/python ## encoding=utf-8 import uvicorn from fastapi import FastAPI, Request app = FastAPI() @app.post(\u0026#34;/login\u0026#34;) async def login(req: Request): body = await req.json() if body[\u0026#34;username\u0026#34;] == \u0026#34;dongfanger\u0026#34; and body[\u0026#34;password\u0026#34;] == \u0026#34;123456\u0026#34;: return {\u0026#34;token\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} return \u0026#34;\u0026#34; @app.get(\u0026#34;/searchSku\u0026#34;) def search_sku(req: Request): if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and req.query_params.get(\u0026#34;skuName\u0026#34;) == \u0026#34;电子书\u0026#34;: return {\u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;2.3\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/addCart\u0026#34;) async def add_cart(req: Request): body = await req.json() if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;skuId\u0026#34;] == \u0026#34;222\u0026#34;: return {\u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;2.3\u0026#34;, \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34;, \u0026#34;totalPrice\u0026#34;: \u0026#34;6.9\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/order\u0026#34;) async def order(req: Request): body = await req.json() if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;skuId\u0026#34;] == \u0026#34;222\u0026#34;: return {\u0026#34;orderId\u0026#34;: \u0026#34;333\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/pay\u0026#34;) async def pay(req: Request): body = await req.json() if req.headers.get(\u0026#34;token\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;orderId\u0026#34;] == \u0026#34;333\u0026#34;: return {\u0026#34;success\u0026#34;: \u0026#34;true\u0026#34;} return \u0026#34;\u0026#34; if __name__ == \u0026#39;__main__\u0026#39;: uvicorn.run(\u0026#34;fastapi_mock:app\u0026#34;, host=\u0026#34;127.0.0.1\u0026#34;, port=5000) 最后，感谢@zhangwk02提交的Pull requests，虽然写的代码被我全部优化了，但是提供了很棒的想法和动力。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000010-tep0.9.5%E6%94%AF%E6%8C%81%E8%87%AA%E5%AE%9A%E4%B9%89%E6%89%A9%E5%B1%95request/","summary":"tep0.9.5更新了以下内容：","title":"tep0.9.5支持自定义扩展request"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n经过Python测试交流群的小伙伴群策群力，teprunner添加了一个重要功能，把PyCharm中的代码，通过Git同步到测试平台中，生成测试用例。这样，teprunner就成了一个名副其实的pytest脚本在线管理平台。\n效果展示 项目添加Git仓库和Git分支：\n点击同步按钮即可进行Git同步：\n同步说明 为什么要把平台用例和Git用例独立？如果平台上面的用例可以通过Git进行提交代码，那么代码冲突会很难解决，这是其一。其二是平台用例的好处就是规避了Git管理代码的冲突问题，一般测试人员代码能力没有开发人员那么强，代码冲突解决起来是真的费时费力。其三是如果用例在平台和Git中都有修改，那么同步时并不知道哪一份是最新的，无法判断。\n前端开发内容 router添加路由：\n给项目增加两个输入框：\n表格增加两列：\n给测试用例的描述添加一个链接，查看用例：\n接口响应添加来源字段：\n根据来源区分编辑和删除的操作权限：\n用例查看页面：\nGit同步菜单：\nGit同步页面及说明：\n请求后端gitSync接口：\n后端开发内容 Project新增字段：\nCase新增字段：\n其中filename用于缓存在数据库中的用例代码对应的文件名（实际上是相对于tests目录的路径）。\n数据迁移：\npython manage.py makemigrations python manage.py migrate ProjectSerializer添加字段：\nCaseSerializer和CaseListSerializer添加字段：\nurl添加路由：\ngit_sync视图：\nGit同步后端配置：\n从Git拉代码：\n如果docker中没有就clone，如果已经存在就checkout到指定分支再git pull。\n同步用例：\n根据数据库中的filenames和git的filenames进行集合化后求差集/交集，得出需要删除/添加/更新的用例集，然后分别操作数据库缓存。\n读取git文件内容：\n从文件内容提取描述和创建人：\n小结 本文介绍了如何使用Git把PyCharm中的pytest脚本，同步到测试平台进行管理。至此，teprunner测试平台V1.0.0正式完成。后续会逐渐完善部署文档和用户手册，欢迎持续关注。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000010-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E9%87%8D%E7%A3%85%E6%9B%B4%E6%96%B0git%E6%89%93%E9%80%9Apycharm%E4%B8%8E%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0/","summary":"经过Python测试交流群的小伙伴群策群力，teprunner添加了一个重要功能，把PyCharm中的代码，通过Git同步到测试平台中，生成测试用例。这样，teprunner就成了一个名副其实的\u003cstrong\u003epytest脚本在线管理平台\u003c/strong\u003e。","title":"teprunner重磅更新Git打通PyCharm与测试平台"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n想当初，测试流程很纯粹，就是研发把代码写完了，功能交给测试，测试提bug，测完了上线，再发个测试报告，完事。\n现在的测试流程是这样的：需求评审，研发测试排期，设计评审，代码评审，编写用例，用例评审，研发自测，研发提测，流水线，测试环境，冒烟测试，测试执行，缺陷管理，精准测试，合并主干，用例归档，回归测试，性能测试，流量录制回放，测试报告，上线checklist，系统上线，线上验证。\n为什么会这样？原因很简单，质量和效率。\n为了追求质量和效率，在测试流程方面下功夫，节点变得越来越多，走完一条流程，花在流程本身的时间越来越长。\n需求评审：产品负责给研发和测试讲懂接下来要做什么。测试要能够提前发现需求问题，测试左移。\n排期：开发和测试，预估工作量，排个时间，什么时候提测，什么时候测完，什么时候上线。业务和产品关心的是尽快上线，不然就要向上升级。\n设计评审：研发负责讲解设计思路，评审人员看看合理不合理。测试要能够提前发现设计问题，测试左移。\n代码评审：研发负责讲解代码实现，评审人员看看正确不正确。测试要能够提前发现代码问题，测试左移。\n编写用例：测试人员编写测试用例，参考需求文档、设计文档等，考虑功能、非功能，用户场景、测试点，系统实现，采用等价类、边界值、场景法等用例设计方法，把用例一条一条写下来。\n用例评审：测试人员负责讲清楚测什么和怎么测，包括功能用例和回归用例，既要看用例质量，也要看用例条数。评审完要发邮件，评审结论是否通过，有无待办。同时更新用例。这是大家共识的测试内容，将来可能会作为复盘证据。\n研发自测：测试得要求研发完成自测，否则会浪费大把测试时间，测试不是保姆。\n研发提测：研发觉得没啥大问题了，交给测试。\n流水线：触发自动执行，代码检查，安全审计，部署环境，接口自动化，等等等，有很多原子可以配置。\n冒烟测试：冒烟是指机器插上电，看看会不会冒烟。研发提测后，测试要看主要流程通不通。不通可以驳回提测，让研发重新自测。研发可以让测试提供冒烟用例，大家对齐冒烟目标。\n测试环境：测试环境、预发环境、生产环境，要有一个完全隔离的测试环境，避免影响线上。\n测试执行：按照用例执行，标记通过不通过，执行过程中探索测试，以用例为基础，持续测试，尽可能多的测试，尽可能多的发现问题。有可能会发现需求问题，及时跟产品和研发讨论。这个阶段，测试是主角，推动起来。\n缺陷管理：发现bug，及时记录，让bug流转起来，直到关闭。不能关闭的转为遗留项，评估好风险，及时报备。\n精准测试：看看测试执行的代码覆盖率，哪些逻辑还没覆盖到，补充测试。根据代码改动自动评估影响范围，自动推荐用例，减少无意义的测试，让测试越来越精准。\n合并主干：功能分支测试完成，可以合并到主干了。测试要审批研发的Pull Request。\n用例归档：功能用例完善得差不多了，需要合入回归用例，把用例库建设起来。既包括手动用例，也包括自动用例。要完成自动化用例编写。\n回归测试：研发把主干合并后部署起来，测试要回归一遍，如果历史功能回归不到位，导致了线上问题，也是要算漏测的。\n性能测试：有些需求涉及到性能，需要压测，评估性能。\n流量录制回放：人工回归始终没有那么完善，通过真实流量，高保真的回归业务场景。\n测试报告：测试完成了，编写测试报告，明确测试结论、遗留项、风险。可以上线了。\n产品业务UAT：测试测完了，那么产品和业务也可以验收，看看最终效果是不是当初想的那样。\n上线checklist：研发负责把上线的检查项罗列出来，拉上所有人挨个检查一遍，这些检查项都是历史问题的经验教训。\n系统上线：在一个月黑风高的夜晚，趁没有多少人使用系统的时候，把新功能发布上去。发布也不能直接梭哈，要灰度，观察没问题了，再全发。\n线上验证：上完线，测试还要继续在线上验证，说不定会发现问题，这种半夜惊喜时不时会来一个。如果真有问题，第一时间止损，按照预案，要么回滚，要么紧急修复。同时和研发一起，观察线上日志，监听系统报警。测试右移。\n忙活到现在，总算是把新功能按照测试流程，发布上去了。\n第二天，下一个需求开始了。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000010-%E6%B5%8B%E8%AF%95%E6%B5%81%E7%A8%8B%E5%88%AB%E5%86%8D%E5%B1%82%E5%B1%82%E5%8A%A0%E7%A0%81%E4%BA%86/","summary":"想当初，测试流程很纯粹，就是研发把代码写完了，功能交给测试，测试提bug，测完了上线，再发个测试报告，完事。","title":"测试流程，别再层层加码了"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n5.1 AI工具简介 5.1.1 Function Calling Function Calling需要编写代码，并且存在规格碎片化、可靠性挑战、工程治理缺位、厂商锁定与迁移成本高等问题。\n5.1.2 MCP MCP，Model Context Protocol，模型上下文协议，是一个开放标准，帮助连接AI助手与数据所在的系统，包括内容存储库、业务工具和开发环境。\nMCP可以看作是AI应用程序的“USB-C端口”，就像USB-C为连接设备与各种外设提供了标准化方式，MCP为AI模型连接不同数据源和工具提供了标准化方法。\nMCP把工具（Tools）、资源（Resources/用于检索与长上下文读写）、提示（Prompts）、会话（Sessions）、取样/推理（Sampling）等能力统一纳入协议语义，并标准化“枚举-描述-请求-响应-权限-审计”的全链路。传输层上，MCP不限定具体载体，既可以通过HTTP/WebSocket，也可以通过stdio等方式交换消息。\n5.1.3 MCP就足够了吗 无论是RAG、Function Call还是MCP，其实都是为了让模型获取更多知识，调用更多工具来完成更复杂的事情。MCP并不是万能的，短期内存在不确定性与不同声音，国内是否成为事实标准亦未定。以MCP为协议底座，叠加AI工具接入的最佳实践与企业级安全治理，才能把“可接入”变成“可规模、可移植、可审计”的Agent能力。\n5.2 AI工具标准化 5.2.1 MCP协议介绍 MCP协议遵循客户端-服务器（Client-Server）架构，允许AI应用程序通过MCP客户端与多个MCP服务器建立连接。MCP架构包含以下几个部分：\nMCP Host：协调和管理一个或多个MCP客户端的AI应用程序，如Claude Desktop、Visual Studio Code等，需要通过MCP协议访问数据。\nMCP Client：MCP客户端，运行在MCP Host内部，负责与MCP Server建立并维护连接。MCP Host会为每一个MCP Server单独创建一个Client，Client与Server保持一对一连接。\nMCP Server：MCP服务器，轻量级程序，通过MCP协议公开特定功能。MCP协议规定了MCP Server可为Client提供工具、资源文件以及提示词模板。\nMCP协议采用JSON-RPC 2.0作为通信规范，所有消息均使用JSON格式进行序列化。消息类型包括：Request、Result、Error、Notification。示例如下：\n请求：\n响应：\nAI应用程序与MCP Server交互的整体流程如下：\n5.2.2 MCP的核心优势 标准化模型与工具的连接方式\n灵活的资源调度方式\n支持双向、异步通信\n支持能力共享\n构建方式便捷\n5.2.3 MCP面临的挑战 安全问题\n大规模应用问题\n集中管理问题\n5.2.4 可行的解决方案 1、AI网关解决MCP安全问题\n下一章将详细介绍AI网关。\n2、工具优选和语义检索提升批量MCP的调用质量、降低调用耗时\n工具优选：当模型请求在经过网关调用LLM时，携带含有大量工具的tool_calls数组时，网关侧基于Qwen3 Embedding和Qwen3 Reranker的方案，提供工具的精选能力，将tool_calls的数量压缩至目标数量，以提升模型响应速度与工具选择精确性。\n语义检索：基于Higress网关的WASM插件机制，通过创建一个“All-in-One”的MCP Server，将用户在网关实例中注册的所有MCP工具进行统一聚合和管理，并提供智能的语义化检索能力。\n3、Nacos解决“MCP爆炸”问题\nMCP Registry允许用户将已有的MCP服务统一注册到Nacos上，Router则可以根据用户任务的语义描述和关键词，从MCP Registry中筛选出最匹配的MCP服务，然后将这些服务提供给模型进行决策。\n4、HiMarket解决MCP管理问题\nHiMarket是一个开箱即用的AI开放平台，可用于帮助用户实现MCP服务的集中化管理。\n5.3 MCP实践 5.3.1 构建MCP的实践 1、从零快速构建MCP Server\n框架：\nSpring AI MCP（Java）\nFastMCP（Python）\nMCP-Framework（TypeScript）\n更多技术选型可以参考官方文档：https://mcp-docs.cn/introduction\n代码示例：\n2、基于存量OpenAPI构建MCP Server\n构建一个中间转换层，将MCP协议的请求转换为对现有的OpenAPI的调用，并将API的响应转换为MCP协议的格式。\n目前已经有成熟的API网关和开源工具提供了自动化解决方案，以下可以参考：\n利用Higress AI网关实现零代码接入\n结合Nacos提供MCP Registry能力\n3、MCP Server优化与扩展\n数据处理优化：数据聚合与精简、数据结构转换\n性能与可靠性：缓存与异步、连接管理、可观测性\n安全与合规：身份认证与授权、日志记录、持续升级策略\n功能扩展\n5.3.2 从第一个MCP Server开始 MCP Server的开发之旅，从技术上讲并不复杂，但要构建一个真正生产可用、性能卓越、智能高效的服务器，则需要在协议理解、业务封装、性能优化和安全防护等多个维度上精益求精。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005005-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E5%B7%A5%E5%85%B7/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"51-ai工具简介\"\u003e5.1 AI工具简介\u003c/h1\u003e\n\u003ch2 id=\"511-function-calling\"\u003e5.1.1 Function Calling\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/95cc2317230e4760a9adb9435283c508.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ee6df274f39347e1992064268a75e991.png\"\u003e\u003c/p\u003e\n\u003cp\u003eFunction Calling需要编写代码，并且存在规格碎片化、可靠性挑战、工程治理缺位、厂商锁定与迁移成本高等问题。\u003c/p\u003e\n\u003ch2 id=\"512-mcp\"\u003e5.1.2 MCP\u003c/h2\u003e\n\u003cp\u003eMCP，Model Context Protocol，模型上下文协议，是一个开放标准，帮助连接AI助手与数据所在的系统，包括内容存储库、业务工具和开发环境。\u003c/p\u003e\n\u003cp\u003eMCP可以看作是AI应用程序的“USB-C端口”，就像USB-C为连接设备与各种外设提供了标准化方式，MCP为AI模型连接不同数据源和工具提供了标准化方法。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/037d99bd2bdd48669370af85c3568941.png\"\u003e\u003c/p\u003e\n\u003cp\u003eMCP把工具（Tools）、资源（Resources/用于检索与长上下文读写）、提示（Prompts）、会话（Sessions）、取样/推理（Sampling）等能力统一纳入协议语义，并标准化“枚举-描述-请求-响应-权限-审计”的全链路。传输层上，MCP不限定具体载体，既可以通过HTTP/WebSocket，也可以通过stdio等方式交换消息。\u003c/p\u003e\n\u003ch2 id=\"513-mcp就足够了吗\"\u003e5.1.3 MCP就足够了吗\u003c/h2\u003e\n\u003cp\u003e无论是RAG、Function Call还是MCP，其实都是为了让模型获取更多知识，调用更多工具来完成更复杂的事情。MCP并不是万能的，短期内存在不确定性与不同声音，国内是否成为事实标准亦未定。以MCP为协议底座，叠加AI工具接入的最佳实践与企业级安全治理，才能把“可接入”变成“可规模、可移植、可审计”的Agent能力。\u003c/p\u003e\n\u003ch1 id=\"52-ai工具标准化\"\u003e5.2 AI工具标准化\u003c/h1\u003e\n\u003ch2 id=\"521-mcp协议介绍\"\u003e5.2.1 MCP协议介绍\u003c/h2\u003e\n\u003cp\u003eMCP协议遵循客户端-服务器（Client-Server）架构，允许AI应用程序通过MCP客户端与多个MCP服务器建立连接。MCP架构包含以下几个部分：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eMCP Host：协调和管理一个或多个MCP客户端的AI应用程序，如Claude Desktop、Visual Studio Code等，需要通过MCP协议访问数据。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eMCP Client：MCP客户端，运行在MCP Host内部，负责与MCP Server建立并维护连接。MCP Host会为每一个MCP Server单独创建一个Client，Client与Server保持一对一连接。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eMCP Server：MCP服务器，轻量级程序，通过MCP协议公开特定功能。MCP协议规定了MCP Server可为Client提供工具、资源文件以及提示词模板。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/39989b3e403744a0b0290c1e38e72e11.png\"\u003e\u003c/p\u003e\n\u003cp\u003eMCP协议采用JSON-RPC 2.0作为通信规范，所有消息均使用JSON格式进行序列化。消息类型包括：Request、Result、Error、Notification。示例如下：\u003c/p\u003e\n\u003cp\u003e请求：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/991306dcc55744aebde7b9ccac15ff2e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e响应：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/425df5c698d441489eeefb3caa30011e.png\"\u003e\u003c/p\u003e\n\u003cp\u003eAI应用程序与MCP Server交互的整体流程如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a2eaeb531e6246718d2f701cfa94736b.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"522-mcp的核心优势\"\u003e5.2.2 MCP的核心优势\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e标准化模型与工具的连接方式\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e灵活的资源调度方式\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e支持双向、异步通信\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e支持能力共享\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e构建方式便捷\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"523-mcp面临的挑战\"\u003e5.2.3 MCP面临的挑战\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e安全问题\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e大规模应用问题\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e集中管理问题\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"524-可行的解决方案\"\u003e5.2.4 可行的解决方案\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e1、AI网关解决MCP安全问题\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ffedcbf1a0044fd5aac0a9228f77175c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e下一章将详细介绍AI网关。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2、工具优选和语义检索提升批量MCP的调用质量、降低调用耗时\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e工具优选：当模型请求在经过网关调用LLM时，携带含有大量工具的tool_calls数组时，网关侧基于Qwen3 Embedding和Qwen3 Reranker的方案，提供工具的精选能力，将tool_calls的数量压缩至目标数量，以提升模型响应速度与工具选择精确性。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e语义检索：基于Higress网关的WASM插件机制，通过创建一个“All-in-One”的MCP Server，将用户在网关实例中注册的所有MCP工具进行统一聚合和管理，并提供智能的语义化检索能力。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e","title":"AI工具"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n刚开始写views.py模块的代码，一般都是用def定义的函数视图，不过DRF更推荐使用class定义的类视图，这能让我们的代码更符合DRY（Don\u0026rsquo;t Repeat Yourself）设计原则：\n使用APIView rest_framework.views.APIView是DRF封装的API视图，继承了django.views.generic.base.View：\n我们用它把函数视图改写成类视图，编辑snippets/views.py：\nfrom snippets.models import Snippet from snippets.serializers import SnippetSerializer from django.http import Http404 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status class SnippetList(APIView): \u0026#34;\u0026#34;\u0026#34; List all snippets, or create a new snippet. \u0026#34;\u0026#34;\u0026#34; def get(self, request, format=None): snippets = Snippet.objects.all() serializer = SnippetSerializer(snippets, many=True) return Response(serializer.data) def post(self, request, format=None): serializer = SnippetSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class SnippetDetail(APIView): \u0026#34;\u0026#34;\u0026#34; Retrieve, update or delete a snippet instance. \u0026#34;\u0026#34;\u0026#34; def get_object(self, pk): try: return Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: raise Http404 def get(self, request, pk, format=None): snippet = self.get_object(pk) serializer = SnippetSerializer(snippet) return Response(serializer.data) def put(self, request, pk, format=None): snippet = self.get_object(pk) serializer = SnippetSerializer(snippet, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, pk, format=None): snippet = self.get_object(pk) snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) 类视图的代码跟函数视图是非常类似的，区别在于GET、POST等方法是用的函数而不是if语句，可以更好的解耦代码。\n改了views.py代码后，需要同时修改snippets/urls.py：\nfrom django.urls import path from rest_framework.urlpatterns import format_suffix_patterns from snippets import views urlpatterns = [ path(\u0026#39;snippets/\u0026#39;, views.SnippetList.as_view()), path(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;/\u0026#39;, views.SnippetDetail.as_view()), ] urlpatterns = format_suffix_patterns(urlpatterns) 为什么要加个as_view()方法？\n因为path()的参数必须是可调用的，在源码中能看到elif callable(view)：\ndef _path(route, view, kwargs=None, name=None, Pattern=None): if isinstance(view, (list, tuple)): # For include(...) processing. pattern = Pattern(route, is_endpoint=False) urlconf_module, app_name, namespace = view return URLResolver( pattern, urlconf_module, kwargs, app_name=app_name, namespace=namespace, ) # callable判断 elif callable(view): pattern = Pattern(route, name=name, is_endpoint=True) return URLPattern(pattern, view, kwargs, name) else: raise TypeError(\u0026#39;view must be a callable or a list/tuple in the case of include().\u0026#39;) as_view()方法返回了一个内部定义的可调用函数：\n@classonlymethod def as_view(cls, **initkwargs): \u0026#34;\u0026#34;\u0026#34;Main entry point for a request-response process.\u0026#34;\u0026#34;\u0026#34; for key in initkwargs: if key in cls.http_method_names: raise TypeError( \u0026#39;The method name %s is not accepted as a keyword argument \u0026#39; \u0026#39;to %s().\u0026#39; % (key, cls.__name__) ) if not hasattr(cls, key): raise TypeError(\u0026#34;%s() received an invalid keyword %r. as_view \u0026#34; \u0026#34;only accepts arguments that are already \u0026#34; \u0026#34;attributes of the class.\u0026#34; % (cls.__name__, key)) # 内部定义了可调用函数 def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *args, **kwargs) if not hasattr(self, \u0026#39;request\u0026#39;): raise AttributeError( \u0026#34;%s instance has no \u0026#39;request\u0026#39; attribute. Did you override \u0026#34; \u0026#34;setup() and forget to call super()?\u0026#34; % cls.__name__ ) return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) return view 使用mixins DRF提供了rest_framework.mixins模块，封装了类视图常用的增删改查方法：\n比如新增CreateModelMixin：\nclass CreateModelMixin: \u0026#34;\u0026#34;\u0026#34; Create a model instance. \u0026#34;\u0026#34;\u0026#34; def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() def get_success_headers(self, data): try: return {\u0026#39;Location\u0026#39;: str(data[api_settings.URL_FIELD_NAME])} except (TypeError, KeyError): return {} 类视图继承了Mixin后，可以直接使用它的.create()方法，类似的还有.list()、.retrieve()、.update()和.destroy()。我们按照这个思路来简化snippets/views.py代码：\nfrom snippets.models import Snippet from snippets.serializers import SnippetSerializer from rest_framework import mixins from rest_framework import generics class SnippetList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) class SnippetDetail(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) 瞬间少了好多代码，真够DRY的。\n什么是mixin？\n维基百科的解释：\nIn object-oriented programming languages, a mixin (or mix-in) is a class that contains methods for use by other classes without having to be the parent class of those other classes. 不太好理解。\n换句话说，mixin类提供了一些方法，我们不会直接用这些方法，而是把它添加到其他类来使用。\n还是有点抽象。\n再简单点说，mixin只不过是实现多重继承的一个技巧而已。\n这下应该清楚了。\n使用generics 如果仔细看snippets/views.py的代码，就会发现我们用到了from rest_framework import generics：\n和generics.GenericAPIView：\n这是DRF提供的通用API类视图，mixins只提供了处理方法，views.py中的类要成为视图，还需要继承GenericAPIView，GenericAPIView继承了本文第一小节提到的rest_framework.views.APIView。除了GenericAPIView，我们还可以用其他的类视图进一步简化代码：\nfrom snippets.models import Snippet from snippets.serializers import SnippetSerializer from rest_framework import generics class SnippetList(generics.ListCreateAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer 看看ListCreateAPIView的源码：\nclass ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView): \u0026#34;\u0026#34;\u0026#34; Concrete view for listing a queryset or creating a model instance. \u0026#34;\u0026#34;\u0026#34; def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) 真DRY！\n小结 学到这里，已经开始感受到了Django REST framework的强大之处了，我觉得学一个框架，不仅要看如何使用，还需要了解它的设计思路和底层实现，这样才能更好的总结为自己的编程思想，写出更漂亮的代码。\n参考资料：\nhttps://www.django-rest-framework.org/tutorial/3-class-based-views/#tutorial-3-class-based-views\nhttps://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful\nhttps://www.zhihu.com/question/20778853\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000009-djangodrf%E7%B1%BB%E8%A7%86%E5%9B%BE%E8%AE%A9%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%A0%81dry%E8%B5%B7%E6%9D%A5/","summary":"刚开始写\u003ccode\u003eviews.py\u003c/code\u003e模块的代码，一般都是用\u003ccode\u003edef\u003c/code\u003e定义的函数视图，不过DRF更推荐使用\u003ccode\u003eclass\u003c/code\u003e定义的类视图，这能让我们的代码更符合DRY（Don\u0026rsquo;t Repeat Yourself）设计原则：","title":"DRF类视图让你的代码DRY起来"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nFilter是什么 Filter用于对请求和响应进行预处理和后处理。Filter链（Filter Chain）允许多个Filter依次处理同一个请求和响应。\nFilter的使用场景 日志记录：记录请求和响应的详细信息，例如请求时间、响应时间、请求参数等 安全检查：检查用户是否有权限访问特定资源，例如验证用户身份、检查用户角色等 压缩：对响应内容进行压缩，例如使用GZIP压缩响应数据 字符编码设置：设置请求和响应的字符编码，例如将请求和响应的字符编码设置为UTF-8 输入验证：对请求参数进行验证，例如检查参数是否为空、参数格式是否正确等 Filter Interface Filter接口有3个方法：\npackage javax.servlet; import java.io.IOException; public interface Filter { void init(FilterConfig var1) throws ServletException; void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException; void destroy(); } 容器在启动时创建Filter实例，并调用其init(FilterConfig filterConfig)方法进行初始化。每次请求到达时，容器调用Filter的doFilter(ServletRequest request, ServletResponse response, FilterChain chain)方法进行过滤。容器在关闭时调用Filter的destroy()方法进行清理。\nFilter可以通过两种方式进行配置：\n1.在web.xml中配置：\n\u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;MyFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;com.example.MyFilter\u0026lt;/filter-class\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;MyFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; 如果采用web.xml，那么去掉Filter只需要修改web.xml即可，不需要修改代码。\nweb.xml在低版本Servlet是必须的，在高版本Servlet已经不再需要。\n2.使用注解配置：\nimport javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter(\u0026#34;/*\u0026#34;) public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化操作 } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 预处理操作 System.out.println(\u0026#34;Request received at \u0026#34; + System.currentTimeMillis()); // 继续执行下一个过滤器或目标资源 chain.doFilter(request, response); // 后处理操作 System.out.println(\u0026#34;Response sent at \u0026#34; + System.currentTimeMillis()); } @Override public void destroy() { // 清理操作 } } FilterChain Interface Filter接口的doFilter方法，第3个参数类型是FilterChain，它的定义如下：\npackage javax.servlet; import java.io.IOException; public interface FilterChain { void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException; } FilterChain用于在Servlet过滤器链中传递请求和响应对象。FilterChain的实现由Servlet容器（如Tomcat、Jetty等）负责。容器在部署应用时，会根据web.xml配置文件或注解扫描来确定过滤器的顺序，并创建一个FilterChain对象来管理这些过滤器。当chain.doFilter(request, response)被调用时，FilterChain会递增索引并调用下一个过滤器的doFilter方法。如果没有更多的过滤器，FilterChain会将请求传递给最终的目标资源（如Servlet）。\nFilterConfig Interface FilterConfig用于在过滤器初始化时提供配置信息，它包含了过滤器的初始化参数和Servlet上下文。主要方法如下：\nString getFilterName()：返回过滤器的名称 ServletContext getServletContext()：返回Servlet上下文对象 String getInitParameter(String name)：根据参数名称获取初始化参数的值 Enumeration\u0026lt;String\u0026gt; getInitParameterNames()：返回所有初始化参数的名称 Filter示例 验证用户身份：\nimport javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class AuthenticationFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化代码 } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // 获取身份验证信息，例如从请求头中获取令牌 String authToken = httpRequest.getHeader(\u0026#34;Authorization\u0026#34;); // 验证令牌 if (isValidToken(authToken)) { // 如果令牌有效，继续处理请求 chain.doFilter(request, response); } else { // 如果令牌无效，返回401未授权状态 httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, \u0026#34;Unauthorized\u0026#34;); } } private boolean isValidToken(String token) { // 令牌验证逻辑，例如检查令牌是否存在和有效 return token != null \u0026amp;\u0026amp; token.equals(\u0026#34;valid-token\u0026#34;); } @Override public void destroy() { // 清理代码 } } 统计访问用户数：\nimport java.io.*; import javax.servlet.*; public class MyFilter implements Filter { static int count = 0; public void init(FilterConfig arg0) throws ServletException {} public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { PrintWriter out = res.getWriter(); // 继续传递请求和响应 chain.doFilter(req, res); // 输出总访问人数 out.print(\u0026#34;\u0026lt;br/\u0026gt;Total visitors \u0026#34; + (++count)); out.close(); } public void destroy() {} } 计算响应时长：\nimport java.io.*; import javax.servlet.*; public class MyFilter implements Filter { static int count = 0; public void init(FilterConfig arg0) throws ServletException {} public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { PrintWriter out = res.getWriter(); long before = System.currentTimeMillis(); // 继续传递请求和响应 chain.doFilter(req, res); long after = System.currentTimeMillis(); out.print(\u0026#34;\u0026lt;br/\u0026gt;Total response time \u0026#34; + (after - before) + \u0026#34; milliseconds\u0026#34;); out.close(); } public void destroy() {} } 直接发送响应并关闭输出流：\nimport java.io.*; import javax.servlet.*; public class MyFilter implements Filter { public void init(FilterConfig arg0) throws ServletException {} public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { PrintWriter out = res.getWriter(); out.print(\u0026#34;\u0026lt;br/\u0026gt;This site is under construction..\u0026#34;); out.close(); } public void destroy() {} } 总结，Filter是一个强大的工具，可以在请求和响应的生命周期中进行各种预处理和后处理操作。通过合理使用Filter，可以实现许多常见的Web应用需求，如日志记录、身份验证、授权、压缩等。\n参考资料：\nhttps://www.javatpoint.com/servlet-filter\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000009-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servletfilter/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"filter是什么\"\u003eFilter是什么\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/cff2e7895dba4a7f9f6d22bea57906de.png\"\u003e\u003c/p\u003e\n\u003cp\u003eFilter用于对请求和响应进行预处理和后处理。Filter链（Filter Chain）允许多个Filter依次处理同一个请求和响应。\u003c/p\u003e\n\u003ch1 id=\"filter的使用场景\"\u003eFilter的使用场景\u003c/h1\u003e\n\u003col\u003e\n\u003cli\u003e日志记录：记录请求和响应的详细信息，例如请求时间、响应时间、请求参数等\u003c/li\u003e\n\u003cli\u003e安全检查：检查用户是否有权限访问特定资源，例如验证用户身份、检查用户角色等\u003c/li\u003e\n\u003cli\u003e压缩：对响应内容进行压缩，例如使用GZIP压缩响应数据\u003c/li\u003e\n\u003cli\u003e字符编码设置：设置请求和响应的字符编码，例如将请求和响应的字符编码设置为UTF-8\u003c/li\u003e\n\u003cli\u003e输入验证：对请求参数进行验证，例如检查参数是否为空、参数格式是否正确等\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"filter-interface\"\u003eFilter Interface\u003c/h1\u003e\n\u003cp\u003eFilter接口有3个方法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejava.io.IOException\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eFilter\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003einit\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eFilterConfig\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar1\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletException\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edoFilter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar2\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFilterChain\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar3\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletException\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edestroy\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e容器在启动时创建Filter实例，并调用其\u003ccode\u003einit(FilterConfig filterConfig)\u003c/code\u003e方法进行初始化。每次请求到达时，容器调用Filter的\u003ccode\u003edoFilter(ServletRequest request, ServletResponse response, FilterChain chain)\u003c/code\u003e方法进行过滤。容器在关闭时调用Filter的\u003ccode\u003edestroy()\u003c/code\u003e方法进行清理。\u003c/p\u003e\n\u003cp\u003eFilter可以通过两种方式进行配置：\u003c/p\u003e\n\u003cp\u003e1.在web.xml中配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;filter\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;filter-name\u0026gt;\u003c/span\u003eMyFilter\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/filter-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;filter-class\u0026gt;\u003c/span\u003ecom.example.MyFilter\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/filter-class\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/filter\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;filter-mapping\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;filter-name\u0026gt;\u003c/span\u003eMyFilter\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/filter-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;url-pattern\u0026gt;\u003c/span\u003e/*\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/url-pattern\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/filter-mapping\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e如果采用web.xml，那么去掉Filter只需要修改web.xml即可，不需要修改代码。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eweb.xml在低版本Servlet是必须的，在高版本Servlet已经不再需要。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e2.使用注解配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.Filter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.FilterChain\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.FilterConfig\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.ServletException\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.ServletRequest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.ServletResponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.annotation.WebFilter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejava.io.IOException\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@WebFilter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/*\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyFilter\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFilter\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003einit\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eFilterConfig\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003efilterConfig\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 初始化操作\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edoFilter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFilterChain\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003echain\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 预处理操作\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Request received at \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecurrentTimeMillis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 继续执行下一个过滤器或目标资源\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003echain\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003edoFilter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 后处理操作\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response sent at \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecurrentTimeMillis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edestroy\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 清理操作\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"filterchain-interface\"\u003eFilterChain Interface\u003c/h1\u003e\n\u003cp\u003eFilter接口的doFilter方法，第3个参数类型是FilterChain，它的定义如下：\u003c/p\u003e","title":"JavaWeb开发基础ServletFilter"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n继承 Java只有单继承，最顶级的父类是Object。\n子类会继承父类的fields和methods，而不会继承constructors，因为constructors不属于methods，但是子类可以通过super调用父类的constructor。\n子类继承父类的范围是：public、protected、package-private\n隐式转换，子类转父类（只有1个爸爸）：\nObject obj = new MountainBike(); 显示转换，父类转子类（有多个子女，所以要明确指定）：\nMountainBike myBike = (MountainBike)obj; 类有field，而接口没有，所以在多继承时就有问题：如果多个类有相同的field，那么子类将不知道用哪一个，而接口不存在这个问题。Java不支持继承多个类，但是可以实现多个接口。\n重载 子类的方法跟父类有完全相同的签名和返回类型（也可以是子类），将会覆盖父类方法Override。\n如果子类定义了1个static方法，跟父类完全相同，那么父类方法会被隐藏Hide。\nOverride和Hide是不同的：\npublic class Animal { public static void testClassMethod() { System.out.println(\u0026#34;The static method in Animal\u0026#34;); } public void testInstanceMethod() { System.out.println(\u0026#34;The instance method in Animal\u0026#34;); } } public class Cat extends Animal { public static void testClassMethod() { System.out.println(\u0026#34;The static method in Cat\u0026#34;); } public void testInstanceMethod() { System.out.println(\u0026#34;The instance method in Cat\u0026#34;); } public static void main(String[] args) { Cat myCat = new Cat(); Animal myAnimal = myCat; // 隐式转换 Animal.testClassMethod(); // 调的父类 myAnimal.testInstanceMethod(); // 父类对象引用，还是调的子类 } } The static method in Animal The instance method in Cat Override只会调子类方法，而Hide取决于调用方是父还是子，比如这里的myCat，隐式转换为父类Animal后，会调父类的static方法，而调的实例方法却是子类的。（如果不是隐式转换，而是直接给父类实例化，那肯定还是调父类方法）\n类instance方法优先于接口default方法：\npublic class Horse { public String identifyMyself() { return \u0026#34;I am a horse.\u0026#34;; } } public interface Flyer { default public String identifyMyself() { return \u0026#34;I am able to fly.\u0026#34;; } } public interface Mythical { default public String identifyMyself() { return \u0026#34;I am a mythical creature.\u0026#34;; } } public class Pegasus extends Horse implements Flyer, Mythical { public static void main(String... args) { Pegasus myApp = new Pegasus(); System.out.println(myApp.identifyMyself()); } } 输出为I am a horse.\nOverride的优先：\npublic interface Animal { default public String identifyMyself() { return \u0026#34;I am an animal.\u0026#34;; } } public interface EggLayer extends Animal { default public String identifyMyself() { return \u0026#34;I am able to lay eggs.\u0026#34;; } } public interface FireBreather extends Animal { } public class Dragon implements EggLayer, FireBreather { public static void main (String... args) { Dragon myApp = new Dragon(); System.out.println(myApp.identifyMyself()); } } 输出为I am able to lay eggs\n如果实现多接口，有同名的，需要显示指定调用方：\npublic interface OperateCar { // ... default public int startEngine(EncryptedKey key) { // Implementation } } public interface FlyCar { // ... default public int startEngine(EncryptedKey key) { // Implementation } } public class FlyingCar implements OperateCar, FlyCar { // ... public int startEngine(EncryptedKey key) { FlyCar.super.startEngine(key); // 显示指定，并且使用super OperateCar.super.startEngine(key); } } 总结下，如果子类方法签名+return跟父类方法一样，有以下4种情况：\n注意compile-time error，static方法不能和instance方法一样，因为它们是不同级别的。\n多态 MountainBike和RoadBike都继承Bicycle，虽然都有printDescription，但它们有多样的形态：\npublic class MountainBike extends Bicycle { private String suspension; public MountainBike( int startCadence, int startSpeed, int startGear, String suspensionType){ super(startCadence, startSpeed, startGear); this.setSuspension(suspensionType); } public String getSuspension(){ return this.suspension; } public void setSuspension(String suspensionType) { this.suspension = suspensionType; } public void printDescription() { super.printDescription(); System.out.println(\u0026#34;The \u0026#34; + \u0026#34;MountainBike has a\u0026#34; + getSuspension() + \u0026#34; suspension.\u0026#34;); } } public class RoadBike extends Bicycle{ // In millimeters (mm) private int tireWidth; public RoadBike(int startCadence, int startSpeed, int startGear, int newTireWidth){ super(startCadence, startSpeed, startGear); this.setTireWidth(newTireWidth); } public int getTireWidth(){ return this.tireWidth; } public void setTireWidth(int newTireWidth){ this.tireWidth = newTireWidth; } public void printDescription(){ super.printDescription(); System.out.println(\u0026#34;The RoadBike\u0026#34; + \u0026#34; has \u0026#34; + getTireWidth() + \u0026#34; MM tires.\u0026#34;); } } 所谓的虚拟方法调用，名字很高大上，其实质就是，子类重载了父类方法，在调用子类实例方法时，先调子类实现：\n注意，子类的field如果和父类的一样，那么父类的field会被hide，即使type不一样。如果要使用父类的field，需要关键字super。同名fileld是不好的设计，应该尽量避免。\nsuper关键字\npublic class Superclass { public void printMethod() { System.out.println(\u0026#34;Printed in Superclass.\u0026#34;); } } public class Subclass extends Superclass { // overrides printMethod in Superclass public void printMethod() { super.printMethod(); System.out.println(\u0026#34;Printed in Subclass\u0026#34;); } public static void main(String[] args) { Subclass s = new Subclass(); s.printMethod(); } } 在子类constructor中，如果没有显式super，那么会调用默认的super()\nObject Java中的所有类，都终极继承了Object。（继承链的顶端）\ntoString()\nSystem.out.println(firstBook.toString()); equals()\npublic class Book { String ISBN; public String getISBN() { return ISBN; } public boolean equals(Object obj) { if (obj instanceof Book) return ISBN.equals((Book)obj.getISBN()); else return false; } } hashCode()\n如果两个对象相等，那么它们的hashCode一定相等。重写equals()，必须重写hashCode()\ngetClass()\nvoid printClassName(Object obj) { System.out.println(\u0026#34;The object\u0026#39;s\u0026#34; + \u0026#34; class is \u0026#34; + obj.getClass().getSimpleName()); } clone()\naCloneableObject.clone(); finalize()\n垃圾回收时调用。它的调用时机是不确定的，不要使用它来做逻辑。\n抽象 abstract class，抽象类，不能被实例化，但是可以继承。\nabstract void moveTo(double deltaX, double deltaY); 如果抽象类有method，那么也必须是abstract：\npublic abstract class GraphicObject { // declare fields // declare nonabstract methods abstract void draw(); } 子类必须抽象类的所有方法，否则子类也必须是abstract。\n抽象类：（not static、final） field，（public、protected、private） method\n接口：public static final field，public method\nJava中，抽象类的例子是AbstractMap、接口的例子是HashMap实现的Serializable, Cloneable, and Map\u0026lt;K, V\u0026gt;接口。\n如果类没有实现接口中的所有方法，可以定义为abstract，然后由子类来实现剩余的全部方法：\nabstract class X implements Y { // implements all but one method of Y } class XX extends X { // implements the remaining method in Y } X没有实现全部方法，所以是abstract，子类XX实现剩余全部方法。\n参考资料：\nInheritance https://dev.java/learn/inheritance/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000009-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B06%E7%BB%A7%E6%89%BF/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"继承\"\u003e继承\u003c/h2\u003e\n\u003cp\u003eJava只有单继承，最顶级的父类是Object。\u003c/p\u003e\n\u003cp\u003e子类会继承父类的fields和methods，而不会继承constructors，因为constructors不属于methods，但是子类可以通过super调用父类的constructor。\u003c/p\u003e\n\u003cp\u003e子类继承父类的范围是：public、protected、package-private\u003c/p\u003e\n\u003cp\u003e隐式转换，子类转父类（只有1个爸爸）：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eobj\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMountainBike\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e显示转换，父类转子类（有多个子女，所以要明确指定）：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eMountainBike\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyBike\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMountainBike\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003eobj\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e类有field，而接口没有，所以在多继承时就有问题：如果多个类有相同的field，那么子类将不知道用哪一个，而接口不存在这个问题。Java不支持继承多个类，但是可以实现多个接口。\u003c/p\u003e\n\u003ch2 id=\"重载\"\u003e重载\u003c/h2\u003e\n\u003cp\u003e子类的方法跟父类有完全相同的签名和返回类型（也可以是子类），将会覆盖父类方法\u003cstrong\u003eOverride\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e如果子类定义了1个static方法，跟父类完全相同，那么父类方法会被隐藏\u003cstrong\u003eHide\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eOverride和Hide是不同的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eAnimal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etestClassMethod\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;The static method in Animal\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etestInstanceMethod\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;The instance method in Animal\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eCat\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eAnimal\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etestClassMethod\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;The static method in Cat\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etestInstanceMethod\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;The instance method in Cat\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003emain\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#0550ae\"\u003e[]\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eargs\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eCat\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyCat\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCat\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eAnimal\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyAnimal\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyCat\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 隐式转换\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eAnimal\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003etestClassMethod\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 调的父类\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003emyAnimal\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003etestInstanceMethod\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 父类对象引用，还是调的子类\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eThe\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emethod\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ein\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eAnimal\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eThe\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003einstance\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emethod\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ein\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCat\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOverride只会调子类方法，而Hide取决于调用方是父还是子，比如这里的myCat，\u003cstrong\u003e隐式转换\u003c/strong\u003e为父类Animal后，会调父类的static方法，而调的实例方法却是子类的。（如果不是隐式转换，而是直接给父类实例化，那肯定还是调父类方法）\u003c/p\u003e","title":"Java官方笔记6继承"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter内置了36个函数，这些函数可以通过函数助手进行编辑和测试。了解这些函数，不仅能提高JMeter的使用熟练度，也有助于知晓测试工具或测试框架通用的函数有哪些，在自主设计时，作为参考借鉴。\nJMeter函数调用的写法是${__function()}，注意函数名前面需要添加双下划线前缀。\n__BeanShell脚本语言 执行BeanShell脚本。BeanShell是Java编写的Java源码解释器，小型、免费、可嵌入，可以像写脚本语言一样写Java，无需编译，立即运行。\n示例，字符串拼接：\n输入表达式后，点击Generate \u0026amp; Copy to clipboard按钮，会生成函数语法并复制到粘贴板，同时输出函数运行结果。\n__char 把数字转化成Unicode字符。\n示例，数字65转化成字符A：\n__counter 统计线程的迭代次数。它有两个参数：\n第一个参数，如果为true，那么每个线程单独统计；如果为false，那么所有线程合在一起统计。 第二个参数，变量名。 示例：\n__CSVRead 从CSV文件中读取数据。\n固定取值 始终取第n列第一行的值。\n示例：\n动态取值 使用next每次迭代取下一行数据。注意必须先取列，再取行。\n示例：\n文件别名 给文件名起个别名。\n示例：\n__escapeHtml HTML转义。\n示例：\n__escapeOroRegrexChars 把一些Java正则表达式引擎不识别的正则表达式转换一下，这样就可以识别了。转换过程中使用了ORO正则表达式库。\n示例：\n__eval 计算表达式。\n示例：\n__evalVar 把表达式的结果存入变量。\n示例：\n__FileToString 读取文件全部内容，以字符串形式保存到变量中。\n示例：\n__intSum int型求和。\n示例：\n__longSum 跟上个一样，只是换成了long型。\n__javaScript 执行JavaScript脚本。\n示例：\n__jexl JEXL全称是Jakarta Commons Jexl，它是一种表达式语言解释器。jexl能直接访问JMeter中的部分变量：\nlog：直接调用logger函数 ctx：获取JMeterContent对象 vars：获取JMeter定义的变量 props：获取JMeter配置属性 threadName：获取JMeter线程名 sampler：获取Sampler实例 sampleResult：获取SamplerResult实例 OUT：OUT.println()，类似System.out.println 示例：\n__log 记录日志，并返回输入的字符串。\n日志级别包括：\n控制台指JMeter GUI的感叹号打开的控制台；标准输出窗口指打开JMeter时的CMD窗口。\nOUT：打印到控制台和标准输出窗口，相当于System.out.print() ERR：打印到控制台和标准输出窗口，相当于System.err DEBUG：打印到控制台，DEBUG级别以上 INFO：打印到控制台，INFO及ERROR级别 WARN：打印到控制台，WARN、INFO、ERROR级别 ERROR：打印到控制台，仅ERROR级别 后四种级别DEBUG、INFO、WARN、ERROR是否写入JMeter的运行时日志，需要通过jmeter.property文件进行配置。\n示例：\n__logn 与上一个类似，区别是它只记录日志，不返回值。\n__machineIP 本机的IP。\n__machineName 本机的计算机名。\n__P 获取命令行中定义的属性，默认值为1。\n示例：\n在非GUI方式运行测试计划时，可以用这个函数来做参数化，由运行命令动态指定参数值，方便跟Jenkins、Maven或者Ant集成。\n__property 获取jmeter.properties文件中设置的JMeter属性。\n示例：\n__Random 随机值。\n示例：\n__RandomString 随机字符串。\n示例：\n__regexFunction 跟后置处理器的正则表达式提取器用法一样。\n示例：\n__samplerName 获取当前sampler名称。\n__setProperty 动态设置JMeter属性。\n示例：\n__split 拆分字符串。\n__StringFromFile 不如使用CSV Data Set Config。\n__TestPlanName 获取测试计划的名字。\n__threadNum 返回当前线程号，从1开始递增。\n示例：\n__time 返回当前时间，由SimpleDateFormat类来处理函数格式。\n年：yyyy 月：MM 日：dd 时：hh 分：mm 秒：ss 示例：\n__unescape 返转escape字符，如\\r\\n转成CRLF。\n__unescapeHtml 反转HTML，如\u0026amp;nbsp;\u0026amp;nbsp;转成空格。\n__urldecode 反转URL中的Unicode编码字符，如word%22school%22转成word\u0026quot;school\u0026quot;。\n__urlencode 转成Unicode编码字符，如word\u0026quot;school\u0026quot;转成word%22school%22。\n__UUID 生成唯一字符串。\n__V 执行变量表达式并返回结果，如果需要嵌套的使用变量时，就可以用到它。\n比如定义变量a1=2, b1=1，${a1}``${b1}都可以调用成功。\n但是如果想通过${a${b1}}来调用${a1}就不能成功。\n使用${__V(a${b1})}等价于${__V(a1)}等价于${a1}就可以了。\n__XPath 匹配XML文件内容。\n示例：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;company\u0026gt; \u0026lt;name\u0026gt; \u0026lt;address\u0026gt; shanghai \u0026lt;/address\u0026gt; \u0026lt;/name\u0026gt; \u0026lt;/company\u0026gt; XPath表达式/company/name/address/text()取值shanghai。\nXPath可以参考https://www.w3school.com.cn/xpath/index.asp\n最近新增 除了上述36个内置函数，JMeter最近还新增了以下函数：\nchangeCase 改变大小写\ndateTimeConvert 日期格式转换\ndigest 加密算法，如SHA-1, SHA-256, MD5等\nescapeXml 转义XML\ngroovy groovy表达式\nisPropDefined 属性是否定义\nisVarDefined 变量是否定义\nRandomDate 随机日期\nRandomFromMultipleVars 从多个变量中取随机值\nStringToFile 把字符串写入文件中\ntimeShift 时间偏移，比如计算某一天的前三天\n小结 本文对36个内置函数，以及11个新增函数进行了介绍，灵活选择使用这些函数，能起到事半功倍的效果。函数助手可以很方便的对函数进行预览和测试。**最新最全的函数及其使用介绍可以到官网查阅：**https://jmeter.apache.org/usermanual/functions.html\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000009-jmeterjmeter36%E4%B8%AA%E5%86%85%E7%BD%AE%E5%87%BD%E6%95%B0%E5%8F%8A11%E4%B8%AA%E6%96%B0%E5%A2%9E%E5%87%BD%E6%95%B0%E4%BB%8B%E7%BB%8D/","summary":"JMeter内置了36个函数，这些函数可以通过函数助手进行编辑和测试。了解这些函数，不仅能提高JMeter的使用熟练度，也有助于知晓测试工具或测试框架通用的函数有哪些，在自主设计时，作为参考借鉴。","title":"JMeter36个内置函数及11个新增函数介绍"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n序列 序列是指一组数据，按存放类型分为容器序列与扁平序列，按能否被修改分为不可变序列与可变序列。\n容器序列与扁平序列\n容器序列存放的是对象的引用，包括list、tuple、collections.deque。\n扁平序列存放的是对象的值，包括str、bytes、bytearray、memoryview和array.array。\n扁平序列的值是字符、字节和数值这种基础类型。\n不可变序列与可变序列\n不可变序列，包括tuple、str、bytes。\n可变序列，包括list、bytearray、array.array、collection.deque、memoryview。\n下图左边是父类，右边是子类，可以看出可变序列是从不可变序列继承来的，扩展了可变方法：\n列表推导 Python语言魅力在于简洁，这能从最常见的创建列表体现出来，比如我们想把字符串\u0026quot;abc\u0026quot;转换成新列表[\u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;, \u0026quot;c\u0026quot;]，常规写法：\nsymbols = \u0026#34;abc\u0026#34; codes = [] for symbol in symbols: codes.append(symbol) print(codes) # [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;] 用到了for循环和列表append方法。实际上可以不用append方法，直接：\nsymbols = \u0026#34;abc\u0026#34; codes = [symbol for symbol in symbols] 这叫做列表推导，是更加Pythonic的写法。\n无论是编写效率还是可阅读性，列表推导都更胜一筹，可以说是构建列表的快捷方式。但是不能滥用，通用原则是，如果列表推导的代码超过了两行，就要考虑用append了。这不是规定，完全可以凭借自我喜好来选择。\n笛卡尔积是指多个序列中元素所有组合，我们用列表推导来实现笛卡尔积：\ncolors = [\u0026#34;black\u0026#34;, \u0026#34;white\u0026#34;] sizes = [\u0026#34;S\u0026#34;, \u0026#34;M\u0026#34;, \u0026#34;L\u0026#34;] tshirts = [(color, size) for color in colors for size in sizes] 一行代码搞定！Life is short，use Python，list comprehension is wonderful，amazing。\n注意这行代码有两个for循环，等价于：\nfor color in colors: for size in sizes: 运行结果是：\n[(\u0026#39;black\u0026#39;, \u0026#39;S\u0026#39;), (\u0026#39;black\u0026#39;, \u0026#39;M\u0026#39;), (\u0026#39;black\u0026#39;, \u0026#39;L\u0026#39;), (\u0026#39;white\u0026#39;, \u0026#39;S\u0026#39;), (\u0026#39;white\u0026#39;, \u0026#39;M\u0026#39;), (\u0026#39;white\u0026#39;, \u0026#39;L\u0026#39;)] 如果换一下顺序：\n[(color, size) for color in colors for size in sizes] 等价于：\nfor size in sizes: for color in colors: 运行结果是不同的，观察第2个元素：\n[(\u0026#39;black\u0026#39;, \u0026#39;S\u0026#39;), (\u0026#39;white\u0026#39;, \u0026#39;S\u0026#39;), (\u0026#39;black\u0026#39;, \u0026#39;M\u0026#39;), (\u0026#39;white\u0026#39;, \u0026#39;M\u0026#39;), (\u0026#39;black\u0026#39;, \u0026#39;L\u0026#39;), (\u0026#39;white\u0026#39;, \u0026#39;L\u0026#39;)] 生成器表达式 一般接触到生成器时，都要讲yield关键字，看似有点复杂，然而却很简单，生成器就像列表推导一样，只不过是用来生成其他类型序列的，比如元组：\nsymbols = \u0026#34;abc\u0026#34; codes = (symbol for symbol in symbols) 它的语法非常简单，把列表推导的中括号[]换成小括号()，就可以了。\n语法相似，本质上却有很大区别，我们试着用生成器表达式来实现笛卡尔积，看看会有什么变化：\ncolors = [\u0026#34;black\u0026#34;, \u0026#34;white\u0026#34;] sizes = [\u0026#34;S\u0026#34;, \u0026#34;M\u0026#34;, \u0026#34;L\u0026#34;] tshirts = ((color, size) for color in colors for size in sizes) 运行结果是：\n\u0026lt;generator object \u0026lt;genexpr\u0026gt; at 0x000001FD57D2DB30\u0026gt; generator object，结果是一个生成器对象。因为生成器表达式在每次迭代时才会逐个产出元素，所以这里的结果并不是已经创建好的元组。列表推导才会一次性产生新列表所有元素。\n通过迭代把生成器表达式结果输出：\nfor tshirt in tshirts: print(tshirt) (\u0026#39;black\u0026#39;, \u0026#39;S\u0026#39;) (\u0026#39;white\u0026#39;, \u0026#39;S\u0026#39;) (\u0026#39;black\u0026#39;, \u0026#39;M\u0026#39;) (\u0026#39;white\u0026#39;, \u0026#39;M\u0026#39;) (\u0026#39;black\u0026#39;, \u0026#39;L\u0026#39;) (\u0026#39;white\u0026#39;, \u0026#39;L\u0026#39;) 生成器表达式可以提升程序性能，比如要计算两个各有1000个元素的列表的笛卡尔积，生成器表达式可以帮忙省掉运行for循环的开销，即一个包含100万个元素的列表。\nyield作用和return差不多，后面会讲到。\nTips 本小节内容是我看《流畅的Python》第一遍时记录的知识点：\nPython标准库用C实现了丰富的序列类型。 列表推导，就是指a = [x for x in something]这种写法。 生成器表达式用于生成列表外的其他类型的序列，它跟列表推导的区别仅仅在于方括号换成圆括号，如b = tuple(x for x in something) 。 array.array('I', x for x in something) ，array构造方法的第一个参数指定了数组中数字的存储方式。 for tshirt in [c, s for c in colors for s in sizes]，列表推导会一次性生成这个列表，存储在内存中，占用资源。for tshirt in ('%s %s' for c in colors for s in sizes)，生成器表达式只在循环时逐个产出元素，避免额外的内存占用，省掉了运行for循环的开销。 小结 本文首先介绍了序列的概念，然后演示了Python常规骚操作——列表推导，最后引出了生成器表达式这个看似复杂实则简单的语法。列表是可变的，它有个不可变的孪生兄弟，元组。\n参考资料：\n《流畅的Python》\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000009-pythonpython%E9%AA%9A%E6%93%8D%E4%BD%9C%E4%BB%8E%E5%88%97%E8%A1%A8%E6%8E%A8%E5%AF%BC%E5%92%8C%E7%94%9F%E6%88%90%E5%99%A8%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%BC%80%E5%A7%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"序列\"\u003e序列\u003c/h1\u003e\n\u003cp\u003e序列是指一组数据，按存放类型分为容器序列与扁平序列，按能否被修改分为不可变序列与可变序列。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e容器序列与扁平序列\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e容器序列存放的是对象的引用，包括\u003ccode\u003elist\u003c/code\u003e、\u003ccode\u003etuple\u003c/code\u003e、\u003ccode\u003ecollections.deque\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e扁平序列存放的是对象的值，包括\u003ccode\u003estr\u003c/code\u003e、\u003ccode\u003ebytes\u003c/code\u003e、\u003ccode\u003ebytearray\u003c/code\u003e、\u003ccode\u003ememoryview\u003c/code\u003e和\u003ccode\u003earray.array\u003c/code\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e扁平序列的值是字符、字节和数值这种基础类型。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e不可变序列与可变序列\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e不可变序列，包括\u003ccode\u003etuple\u003c/code\u003e、\u003ccode\u003estr\u003c/code\u003e、\u003ccode\u003ebytes\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e可变序列，包括\u003ccode\u003elist\u003c/code\u003e、\u003ccode\u003ebytearray\u003c/code\u003e、\u003ccode\u003earray.array\u003c/code\u003e、\u003ccode\u003ecollection.deque\u003c/code\u003e、\u003ccode\u003ememoryview\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e下图左边是父类，右边是子类，可以看出可变序列是从不可变序列继承来的，扩展了可变方法：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/99443fcff54247bf8dd12bc527c7c17f.webp\"\u003e\u003c/p\u003e\n\u003ch1 id=\"列表推导\"\u003e列表推导\u003c/h1\u003e\n\u003cp\u003ePython语言魅力在于简洁，这能从最常见的创建列表体现出来，比如我们想把字符串\u003ccode\u003e\u0026quot;abc\u0026quot;\u003c/code\u003e转换成新列表\u003ccode\u003e[\u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;, \u0026quot;c\u0026quot;]\u003c/code\u003e，常规写法：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esymbols = \u0026#34;abc\u0026#34;\ncodes = []\nfor symbol in symbols:\n    codes.append(symbol)\nprint(codes)  # [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e用到了for循环和列表append方法。实际上可以不用append方法，直接：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esymbols = \u0026#34;abc\u0026#34;\ncodes = [symbol for symbol in symbols]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这叫做列表推导，是更加Pythonic的写法。\u003c/p\u003e\n\u003cp\u003e无论是编写效率还是可阅读性，列表推导都更胜一筹，可以说是构建列表的快捷方式。但是不能滥用，通用原则是，如果列表推导的代码超过了两行，就要考虑用append了。这不是规定，完全可以凭借自我喜好来选择。\u003c/p\u003e\n\u003cp\u003e笛卡尔积是指多个序列中元素所有组合，我们用列表推导来实现笛卡尔积：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecolors = [\u0026#34;black\u0026#34;, \u0026#34;white\u0026#34;]\nsizes = [\u0026#34;S\u0026#34;, \u0026#34;M\u0026#34;, \u0026#34;L\u0026#34;]\ntshirts = [(color, size) for color in colors for size in sizes]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e一行代码搞定！Life is short，use Python，list comprehension is wonderful，amazing。\u003c/p\u003e","title":"Python骚操作从列表推导和生成器表达式开始"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nThe Spring Framework provides extensive support for integrating with messaging systems, from simplified use of the JMS API using JmsTemplate to a complete infrastructure to receive messages asynchronously. Spring AMQP provides a similar feature set for the Advanced Message Queuing Protocol. Spring Boot also provides auto-configuration options for RabbitTemplate and RabbitMQ. Spring WebSocket natively includes support for STOMP messaging, and Spring Boot has support for that through starters and a small amount of auto-configuration. Spring Boot also has support for Apache Kafka.\nAMQP The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the spring-boot-starter-amqp “Starter”.\nRabbitMQ\nspring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=admin spring.rabbitmq.password=secret spring.rabbitmq.addresses=amqp://admin:secret@localhost Sending a Message\nimport org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.stereotype.Component; @Component public class MyBean { private final AmqpAdmin amqpAdmin; private final AmqpTemplate amqpTemplate; public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { this.amqpAdmin = amqpAdmin; this.amqpTemplate = amqpTemplate; } public void someMethod() { this.amqpAdmin.getQueueInfo(\u0026#34;someQueue\u0026#34;); } public void someOtherMethod() { this.amqpTemplate.convertAndSend(\u0026#34;hello\u0026#34;); } } Sending a Message To A Stream\nspring.rabbitmq.stream.name=my-stream Receiving a Message\nimport org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class MyBean { @RabbitListener(queues = \u0026#34;someQueue\u0026#34;) public void processMessage(String content) { // ... } } Apache Kafka spring.kafka.bootstrap-servers=localhost:9092 spring.kafka.consumer.group-id=myGroup Sending a Message\nimport org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; @Component public class MyBean { private final KafkaTemplate\u0026lt;String, String\u0026gt; kafkaTemplate; public MyBean(KafkaTemplate\u0026lt;String, String\u0026gt; kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; } public void someMethod() { this.kafkaTemplate.send(\u0026#34;someTopic\u0026#34;, \u0026#34;Hello\u0026#34;); } } Receiving a Message\nimport org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; @Component public class MyBean { @KafkaListener(topics = \u0026#34;someTopic\u0026#34;) public void processMessage(String content) { // ... } } WebSockets Spring Boot provides WebSockets auto-configuration for embedded Tomcat, Jetty, and Undertow. If you deploy a war file to a standalone container, Spring Boot assumes that the container is responsible for the configuration of its WebSocket support.\n参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#messaging\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000009-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B06%E6%B6%88%E6%81%AF/","summary":"The Spring Framework provides extensive support for integrating with messaging systems, from simplified use of the JMS API using \u003ccode\u003eJmsTemplate\u003c/code\u003e to a complete infrastructure to receive messages asynchro","title":"SpringBoot官方笔记6消息"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nteprunner测试平台已经有一个多月没有更新了，主要原因是定时任务不够稳定，经过反复试错，找到了解决办法，这次终于稳定了。\n本文开发内容 作为测试平台而言，定时任务算是必备要素了，只有跑起来的自动化，才能算是真正的自动化。本文将给测试计划添加定时任务功能，具体如下：\n前端添加测试计划的定时任务开关 采用crontab表达式设置计划时间 后端集成django-apschedule，在数据库中记录任务明细和执行详情。 定时清理执行记录。 前端效果图：\n前端开发内容 编辑src/views/teprunner/plan/PlanEditor.vue文件：\n运行环境用el-select实现了下拉框，用el-switch实现了开关按钮。\nel-pophover实现了帮助描述，可以参考编写crontab表达式。\n在data中添加了表单项taskRunEnv、taskStatus、taskCrontab，必填规则，以及其他变量。\n页面创建时读取localStorage中的计划信息。\n并获取运行环境下拉框选项。\n开关按钮的文字是根taskStatus进行设置的。\n在保存时，给请求添加上新的这3个参数。\n后端开发内容 第一步是安装django-apscheduler，要么直接安装：\npip install django-apscheduler 要么更新项目代码后通过requirements.txt安装：\npip install -r requirements.txt 然后编辑teprunnerbackend/settings.py文件：\n在INSTALLED_APPS中添加django_apscheduler。\n接着迁移数据库，创建两张任务表，一张任务明细表，一张任务执行情况表：\npython manage.py migrate 编辑teprunner/models.py文件：\n给Plan模型添加3个字段。\n编辑teprunner/serializers.py文件：\n同样的，给PlanSerializer添加3个字段。\n新建teprunner/views/task.py文件：\n创建BackgroundScheduler的对象实例，Background指的是在后台运行。并添加DjangoJobStore，把任务通过Django保存到数据库中。\n添加一个定时删除执行记录的任务，max_age是最大保存时间，这里设置为7天。scheduler.add_job()用来添加定时任务，trigger是触发器，也就是计划时间，这里设置为每周一0点。id是任务的标识符。max_instances指同时最多只有一个实例。replace_existing设置为True，每次都更新已存在的任务，防止重启服务导致scheduler.add_job()报错。\n启动任务。\n编辑teprunner/views/run.py文件：\n为了手动执行测试计划和定时任务执行测试计划共用，这里把执行代码抽取了部分作为run_plan_engine()函数。\n编辑teprunner/views/plan.py文件：\n重写create方法，先根据测试计划的名字判断是否已存在，如果存在就直接返回500。接着判断开关如果开启，那么就通过scheduler.add_job()添加任务。跟刚才添加任务的有点区别是，通过args参数指定了func函数的参数。最后把任务添加日志写到响应中返回。\n重写update方法，先判断测试计划是否已经存在，判断规则是根据名字去查找已存在记录，如果找到同名计划，且id不是自己，那么就认为已存在同名计划，直接返回500。\n然后判断如果开关打开，就新增任务；如果开关关闭，就删除任务，删除任务使用scheduler.remove_job()。\n最后重写destroy方法，在删除测试计划时，一并删除定时任务。\n猴子补丁解决pymysql连接问题 为什么定时任务会不稳定？因为我用的pymysql库，它不会进行数据库连接断开后重试。Django和MySQL建立建立后，何时断开连接通过CONNECT_MAX_AGE来设置，默认是0，表示使用完马上断开连接。Django只会对Web请求采取这个策略，使用signals.request_started.connect(close_old_connections)和signals.request_finished.connect(close_old_connections)来关闭旧连接。**但定时任务不是Web请求，而是直接连接数据库，Django并不会去主动断开这个连接。**而MySQL默认8小时会把连接断掉，于是当Django拿着已经被MySQL断开的连接对象去请求MySQL，就报错了。\n当我在本地安装了MySQL后，重启MySQL就能复现这个问题。\n解决办法一是把旧连接复活，进行断线重连，但是会导致连接占用可能越来越多，耗费资源。解决办法二是像Django处理Web请求一样，每次用完就断开，下次使用再重新连接，占用资源少。\n猴子补丁是指不修改第三方库的基础上，对库的功能进行扩展。我给django-apscheduler写了个猴子补丁，实现第二个解决办法，用完就断开连接：\n并且通过issue方式，告诉了它的作者：\n这开启了我在GitHub上英文交流技术的大门。\n比如我又给loguru提了个bug，此时已经和loguru的作者英文交流了5个回合。\n小结 本文给测试计划添加了定时任务功能，为teprunner测试平台补上了一块重要拼图。从此它不但能批量执行用例了，还能按照计划时间，定时执行，实现了真正的自动化。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000009-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E8%BF%99%E6%AC%A1%E7%BB%88%E4%BA%8E%E7%A8%B3%E4%BA%86/","summary":"teprunner测试平台已经有一个多月没有更新了，主要原因是定时任务不够稳定，经过反复试错，找到了解决办法，这次终于稳定了。","title":"teprunner测试平台定时任务这次终于稳了"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n大家好，我是刚哥。\ntep已经支持了两种开发模式：用例数据一体和HttpRunner（第三版）。HttpRunner已经众所周知，此处不做过多介绍。用例数据一体指的是把用例和数据都写在一个test.py文件里面，这样的好处是简单直接明了，特别适合刚开始用pytest写自动化代码的同学，从上往下一气呵成就完成了一个自动化用例，收获成就感的同时，也能很方便的共享给其他人使用，单个文件就能跑起来。\n但是当我在公司用SpringBoot开发了一个后端服务后，尝到了MVC架构所带来的高可维护性，加上在跟各位大佬聊天时，也听说很多公司对接口自动化做了分层设计，于是让tep支持用例数据分离的开发，已经变得很重要了。用例数据分离指的是用例只有逻辑没有数据，这样在修改数据的时候，用例基本不需要变化就能适应，用例看起来是非常简洁的，就像这样：\nclass Test: case_vars = TepVars() case_vars.vars_ = { \u0026#34;domain\u0026#34;: \u0026#34;http://127.0.0.1:5000\u0026#34;, \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34; } def test(self): # 登录 Login(Test).post() # 搜索商品 SearchSku(Test).get() # 添加购物车 AddCart(Test).post() # 下单 Order(Test).post() # 支付 Pay(Test).post() 这就是tep支持的用例数据分离的用例的样子。\n我增加了一个services文件夹，用于存放接口：\n先看下登录Login.py：\nclass Login(BaseRequest): def post(self): response = self.request( \u0026#34;post\u0026#34;, url=self.case_vars.get(\u0026#34;domain\u0026#34;) + \u0026#34;/login\u0026#34;, headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 self.case_vars.put(\u0026#34;token\u0026#34;, response.jmespath(\u0026#34;token\u0026#34;)) 必须继承BaseRequest。 self.request和requests.request用法完全一致。 self.case_vars用于在测试用例的测试步骤之间传递变量，有get和put两个操作。 response.jmespath(\u0026quot;token\u0026quot;)是封装了requests.Response后添加的方法，替代jmespath.search(\u0026quot;token\u0026quot;, response.json())的写法。 BaseRequest的定义如下：\nimport jmespath from requests import Response from tep.client import request class TepResponse(Response): def __init__(self, response): super().__init__() for k, v in response.__dict__.items(): self.__dict__[k] = v def jmespath(self, expression): return jmespath.search(expression, self.json()) class BaseRequest: def __init__(self, clazz): self.case_vars = clazz.case_vars def request(self, method, url, **kwargs): response = request(method, url, **kwargs) return TepResponse(response) TepResponse对requests.Response做了封装，添加了jmespath方法。 BaseRequest的__init__在初始化时传入测试类（test.py里面测试用例的类），提取case_vars对象，这样接口类继承以后，就能用self.case_vars来get和put变量了。 同时调用tep.client.request，并把response以封装后的TepResponse返回。 那么问题来了，**如何在接口之间传递参数呢？**请看test_login_pay_mvc.py、Login和SearchSku这三段代码：\n## test_login_pay_mvc.py class Test: case_vars = TepVars() case_vars.vars_ = { \u0026#34;domain\u0026#34;: \u0026#34;http://127.0.0.1:5000\u0026#34;, \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34; } def test(self): # 登录 Login(Test).post() # 搜索商品 SearchSku(Test).get() # 添加购物车 AddCart(Test).post() # 下单 Order(Test).post() # 支付 Pay(Test).post() ## Login.py class Login(BaseRequest): def post(self): response = self.request( \u0026#34;post\u0026#34;, url=self.case_vars.get(\u0026#34;domain\u0026#34;) + \u0026#34;/login\u0026#34;, headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 self.case_vars.put(\u0026#34;token\u0026#34;, response.jmespath(\u0026#34;token\u0026#34;)) ## SearchSku.py class SearchSku(BaseRequest): def get(self): response = self.request( \u0026#34;get\u0026#34;, url=self.case_vars.get(\u0026#34;domain\u0026#34;) + \u0026#34;/searchSku\u0026#34;, headers={\u0026#34;token\u0026#34;: self.case_vars.get(\u0026#34;token\u0026#34;)}, params={\u0026#34;skuName\u0026#34;: \u0026#34;电子书\u0026#34;} ) self.case_vars.put(\u0026#34;skuId\u0026#34;, response.jmespath(\u0026#34;skuId\u0026#34;)) self.case_vars.put(\u0026#34;skuPrice\u0026#34;, response.jmespath(\u0026#34;price\u0026#34;)) assert response.status_code \u0026lt; 400 test_login_pay_mvc.py文件的Test测试用例里面，定义了用例级别的变量：domain。 Login.py文件通过self.case_vars.get(\u0026quot;domain\u0026quot;)获取domain，然后把token通过self.case_vars.put(\u0026quot;token\u0026quot;, response.jmespath(\u0026quot;token\u0026quot;))存入token。 SearchSku.py文件通过self.case_vars.get(\u0026ldquo;token\u0026rdquo;)获取token，并用self.case_vars.put()存入skuId和skuPrice，供下一个接口AddCart使用，以此类推。 用例数据分离的写法遵从了MVC架构，并借鉴了HttpRunner和JMeter的部分设计，相比于用例数据一体，可能编写体验没有那么方便，但是维护性和可复用性都很高，其中接口类的get和post可以根据数据需要定义多个如get_a和post_b等，以支持多种数据，略微有一丢丢复杂，适合有代码经验的同学。\n**如何才能使用用例数据一体、用例数据分离、HttpRunner3种开发模式呢？**等tep0.9.4版本发布后进行升级，用tep startproject demo命令创建项目脚手架，就会包含登录到下单流程的全部3种开发模式示例代码，开箱即用，一键运行成功。\n从此刻开始，tep实用性已经提升了一个档次，但是我将停下它的脚步，将注意力放到HttpRunner3的源码学习中，并产出一系列文章，站在巨人的肩膀上，才能看得更远。为什么HttpRunner如此重要？因为毕竟tep的技术就是从HttpRunner学来的。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000009-tep%E6%94%AF%E6%8C%81mvc%E6%9E%B6%E6%9E%84%E5%AE%9E%E7%8E%B0%E7%94%A8%E4%BE%8B%E6%95%B0%E6%8D%AE%E5%88%86%E7%A6%BB/","summary":"大家好，我是刚哥。","title":"tep支持MVC架构实现用例数据分离"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n伪命题，是指那些表面上看似正确，但实际上是错误的、没有意义的或者根本无法验证的命题。\n自动化用例，在设计上就不能存在依赖关系，而是用例彼此独立，能够随意并行执行的。\n交流群偶尔还是看到有人提出接口自动化用例依赖问题。这个问题反映出了对测试用例这个概念，认识得还不够清晰。测试用例，是由测试标题、前提条件、测试步骤、预期结果组成的最小可执行单元。无论是功能用例还是自动化用例，都要遵循这条基础理论。执行1条用例，只覆盖1个案例，这叫做单一责任原则，保证测试原子性。\n用例是由多个步骤组成的。在编写功能用例时，基本都会按照这种方式来写。编写自动化用例，把重点放在代码框架上，反而忽略了基本的概念。比如处理接口依赖，默认把1个接口视为1条用例，然后为了支持接口依赖，想尽各种办法优化框架，实现用例依赖。为什么1个接口就必须是1条用例？事实上，1个接口只能算作1个步骤。\n1条用例，1个步骤，1个接口，这是单接口用例。1条用例，多个步骤，每个步骤1个接口，才是多接口用例。接口依赖问题，是步骤依赖问题，不是用例依赖问题。用例依赖问题，是个伪命题。步骤依赖问题比用例依赖问题更容易解决，因为步骤一定是按照先后顺序执行的，无论用例是串行还是并行。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000009-%E8%87%AA%E5%8A%A8%E5%8C%96%E7%94%A8%E4%BE%8B%E4%BE%9D%E8%B5%96%E9%97%AE%E9%A2%98%E6%98%AF%E4%B8%AA%E4%BC%AA%E5%91%BD%E9%A2%98/","summary":"伪命题，是指那些表面上看似正确，但实际上是错误的、没有意义的或者根本无法验证的命题。","title":"自动化用例依赖问题，是个伪命题"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDjango视图是用来处理请求和响应的，Django默认是按Form和Template来设计的，如果要处理以JSON格式为主的RESTful API，那么就需要对Django请求和响应的处理代码进行优化改造，本文就来介绍DRF在这一部分的技术升级。\nRequest DRF把Django的HttpRequest扩展成了Request：\n其中最核心的属性是request.data，它和request.POST的区别如下：\nrequest.POST # 只处理表单（Form）数据，只支持POST方法 request.data # 处理任何数据，支持POST、PUT、PATCH方法 Response DRF的Response继承自Django的django.template.response.SimpleTemplateResponse：\nResponse可以根据客户端的请求render合适的content type：\nreturn Response(data) 我摘取了rendered_content()函数的代码：\n@property def rendered_content(self): renderer = getattr(self, \u0026#39;accepted_renderer\u0026#39;, None) accepted_media_type = getattr(self, \u0026#39;accepted_media_type\u0026#39;, None) context = getattr(self, \u0026#39;renderer_context\u0026#39;, None) assert renderer, \u0026#34;.accepted_renderer not set on Response\u0026#34; assert accepted_media_type, \u0026#34;.accepted_media_type not set on Response\u0026#34; assert context is not None, \u0026#34;.renderer_context not set on Response\u0026#34; context[\u0026#39;response\u0026#39;] = self media_type = renderer.media_type charset = renderer.charset content_type = self.content_type if content_type is None and charset is not None: content_type = \u0026#34;{}; charset={}\u0026#34;.format(media_type, charset) elif content_type is None: content_type = media_type self[\u0026#39;Content-Type\u0026#39;] = content_type ret = renderer.render(self.data, accepted_media_type, context) if isinstance(ret, str): assert charset, ( \u0026#39;renderer returned unicode, and did not specify \u0026#39; \u0026#39;a charset value.\u0026#39; ) return ret.encode(charset) if not ret: del self[\u0026#39;Content-Type\u0026#39;] return ret Status codes 如果在代码中直接写数字形式的状态码如400，是不容易阅读的，于是DRF提供了标识符如HTTP_400_BAD_REQUEST来替代。我列一些常见的状态码标识符：\nHTTP_200_OK = 200 HTTP_201_CREATED = 201 HTTP_204_NO_CONTENT = 204 HTTP_400_BAD_REQUEST = 400 HTTP_401_UNAUTHORIZED = 401 HTTP_403_FORBIDDEN = 403 HTTP_404_NOT_FOUND = 404 HTTP_405_METHOD_NOT_ALLOWED = 405 HTTP_500_INTERNAL_SERVER_ERROR = 500 HTTP_502_BAD_GATEWAY = 502 HTTP_503_SERVICE_UNAVAILABLE = 503 HTTP_504_GATEWAY_TIMEOUT = 504 全部的状态码标识符可以在rest_framework.status模块中看到。\n@api_view和APIView DRF对API视图做了2个封装：\n@api_view用于函数视图。 APIView用于类视图。 它们提供了一些新功能，比如：\n检查请求是Request对象 添加上下文到Response对象 返回请求错误如405 Method Not Allowed 当request.data格式有误时，抛出ParseError异常 改造views.py 接着就用上面这几个新实现对我们之前写的snippets/views.py进行改造：\nfrom rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from snippets.models import Snippet from snippets.serializers import SnippetSerializer @api_view([\u0026#39;GET\u0026#39;, \u0026#39;POST\u0026#39;]) def snippet_list(request): \u0026#34;\u0026#34;\u0026#34; List all code snippets, or create a new snippet. \u0026#34;\u0026#34;\u0026#34; if request.method == \u0026#39;GET\u0026#39;: snippets = Snippet.objects.all() serializer = SnippetSerializer(snippets, many=True) return Response(serializer.data) elif request.method == \u0026#39;POST\u0026#39;: serializer = SnippetSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view([\u0026#39;GET\u0026#39;, \u0026#39;PUT\u0026#39;, \u0026#39;DELETE\u0026#39;]) def snippet_detail(request, pk): \u0026#34;\u0026#34;\u0026#34; Retrieve, update or delete a code snippet. \u0026#34;\u0026#34;\u0026#34; try: snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == \u0026#39;GET\u0026#39;: serializer = SnippetSerializer(snippet) return Response(serializer.data) elif request.method == \u0026#39;PUT\u0026#39;: serializer = SnippetSerializer(snippet, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == \u0026#39;DELETE\u0026#39;: snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) 改动点有这些，添加了@api_view，如：\n@api_view([\u0026#39;GET\u0026#39;, \u0026#39;POST\u0026#39;]) 使用了状态码标识符，如：\nstatus.HTTP_404_NOT_FOUND 使用request.data替代了 data = JSONParser().parse(request)，如：\nserializer = SnippetSerializer(data=request.data) 使用Response()替代了JsonResponse()，如：\nreturn Response(serializer.data, status=status.HTTP_201_CREATED) request.data和Response()能根据请求的JSON自动处理content type。\n添加后缀格式（可选） 既然DRF能自动处理content type，那么也可以给URL指定具体的后缀格式，比如http://example.com/api/items/4.json。具体添加步骤是，先给view增加1个可选参数format：\ndef snippet_list(request, format=None): def snippet_detail(request, pk, format=None): 再更新snippets/urls.py，添加format_suffix_patterns：\nfrom django.urls import path from rest_framework.urlpatterns import format_suffix_patterns from snippets import views urlpatterns = [ path(\u0026#39;snippets/\u0026#39;, views.snippet_list), path(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;\u0026#39;, views.snippet_detail), ] urlpatterns = format_suffix_patterns(urlpatterns) 这并不是必须的，实际上也无需这么做。\n测试API http http://127.0.0.1:8000/snippets/ HTTP/1.1 200 OK ... [ { \u0026#34;id\u0026#34;: 1, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;foo = \\\u0026#34;bar\\\u0026#34;\\n\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34; }, { \u0026#34;id\u0026#34;: 2, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;print(\\\u0026#34;hello, world\\\u0026#34;)\\n\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34; } ] 跟之前的结果一样。再分别用form和json试试：\n## POST using form data http --form POST http://127.0.0.1:8000/snippets/ code=\u0026#34;print(123)\u0026#34; { \u0026#34;id\u0026#34;: 3, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;print(123)\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34; } ## POST using JSON http --json POST http://127.0.0.1:8000/snippets/ code=\u0026#34;print(456)\u0026#34; { \u0026#34;id\u0026#34;: 4, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;print(456)\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34; } API文档 DRF提供了可视化的API HTML文档，把API URL在浏览器中打开即可看到：\n小结 技术要依托于业务，让技术落地，在业务中发挥技术的价值，产生从业务到技术，从技术到业务的良好循环。当然，会技术是个大前提，对技术的学习不能停，比如Django REST framework。\n参考资料：\nhttps://www.django-rest-framework.org/tutorial/2-requests-and-responses/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000008-djangodrf%E5%AF%B9django%E8%AF%B7%E6%B1%82%E5%93%8D%E5%BA%94%E5%81%9A%E4%BA%86%E6%8A%80%E6%9C%AF%E5%8D%87%E7%BA%A7/","summary":"Django视图是用来处理请求和响应的，Django默认是按Form和Template来设计的，如果要处理以JSON格式为主的RESTful API，那么就需要对Django请求和响应的处理代码进行优化改造，本文就来介绍DRF在这一部分的技术升级。","title":"DRF对Django请求响应做了技术升级"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n事件和监听器 事件是指在程序运行过程中发生的特定动作或情况。比如：\n用户操作，如点击按钮、输入文本\n系统操作，如文件读取完成、网络连接断开\n应用程序内部的状态变化，如对象属性改变、方法调用\n监听器是一个接口或类，用于定义在特定事件发生时要执行的操作。监听器会“监听”特定的事件，当事件发生时，监听器中的相应方法会被调用，从而执行预定义的操作。\n事件和监听器是相辅相成的，它们的工作原理如下：\n事件源：触发事件的来源。例如，按钮是事件源，点击按钮会触发点击事件 事件对象：当事件发生时，事件源会创建一个事件对象，包含有关事件的信息 注册监听器：监听器需要注册到事件源，以便监听特定的事件。注册后，事件源会在事件发生时通知监听器 事件处理：当事件发生时，事件源会调用监听器中的相应方法，传递事件对象，从而执行预定义的操作 Servlet Event Class 事件（Event）是一个类，封装了与特定事件相关的信息。\n基本是构造方法和get方法：\nServletRequestEvent\npublic class ServletRequestEvent extends EventObject { private static final long serialVersionUID = 1L; private final ServletRequest request; public ServletRequestEvent(ServletContext sc, ServletRequest request) { super(sc); this.request = request; } public ServletRequest getServletRequest() { return this.request; } public ServletContext getServletContext() { return (ServletContext) super.getSource(); } } ServletRequestAttributeEvent\npublic class ServletRequestAttributeEvent extends ServletRequestEvent { private static final long serialVersionUID = 1L; private final String name; private final Object value; public ServletRequestAttributeEvent(ServletContext sc, ServletRequest request, String name, Object value) { super(sc, request); this.name = name; this.value = value; } public String getName() { return this.name; } public Object getValue() { return this.value; } } ServletContextEvent\npublic class ServletContextEvent extends EventObject { private static final long serialVersionUID = 1L; public ServletContextEvent(ServletContext source) { super(source); } public ServletContext getServletContext() { return (ServletContext) super.getSource(); } } ServletContextAttributeEvent\npublic class ServletContextAttributeEvent extends ServletContextEvent { private static final long serialVersionUID = 1L; private final String name; private final Object value; public ServletContextAttributeEvent(ServletContext source, String name, Object value) { super(source); this.name = name; this.value = value; } public String getName() { return this.name; } public Object getValue() { return this.value; } } HttpSessionEvent\npublic class HttpSessionEvent extends EventObject { private static final long serialVersionUID = 1L; public HttpSessionEvent(HttpSession source) { super(source); } public HttpSession getSession() { return (HttpSession) super.getSource(); } } HttpSessionBindingEvent\npublic class HttpSessionBindingEvent extends HttpSessionEvent { private static final long serialVersionUID = 1L; private final String name; private final Object value; public HttpSessionBindingEvent(HttpSession session, String name, Object value) { super(session); this.name = name; this.value = value; } public String getName() { return this.name; } public Object getValue() { return this.value; } } Servlet Listener Interface 监听器（Listener）是一个接口，定义了处理这些事件的方法。\nServletRequestListener\npublic interface ServletRequestListener extends EventListener { void requestDestroyed(ServletRequestEvent sre); void requestInitialized(ServletRequestEvent sre); } ServletRequestAttributeListener\npublic interface ServletRequestAttributeListener extends EventListener { void attributeAdded(ServletRequestAttributeEvent srae); void attributeRemoved(ServletRequestAttributeEvent srae); void attributeReplaced(ServletRequestAttributeEvent srae); } ServletContextListener\npublic interface ServletContextListener extends EventListener { void contextInitialized(ServletContextEvent sce); void contextDestroyed(ServletContextEvent sce); } ServletContextAttributeListener\npublic interface ServletContextAttributeListener extends EventListener { void attributeAdded(ServletContextAttributeEvent scae); void attributeRemoved(ServletContextAttributeEvent scae); void attributeReplaced(ServletContextAttributeEvent scae); } HttpSessionListener\npublic interface HttpSessionListener extends EventListener { void sessionCreated(HttpSessionEvent se); void sessionDestroyed(HttpSessionEvent se); } HttpSessionBindingListener\npublic interface HttpSessionBindingListener { void valueBound(HttpSessionBindingEvent event); void valueUnbound(HttpSessionBindingEvent event); } HttpSessionActivationListener\npublic interface HttpSessionActivationListener { void sessionWillPassivate(HttpSessionEvent se); void sessionDidActivate(HttpSessionEvent se); } HttpSessionAttributeListener\npublic interface HttpSessionAttributeListener extends EventListener { void attributeAdded(HttpSessionBindingEvent se); void attributeRemoved(HttpSessionBindingEvent se); void attributeReplaced(HttpSessionBindingEvent se); } Event与Listener 一个事件可以对应多个监听器，它们是怎么关联的？查看ServletRequestListener源码：\npackage javax.servlet; import java.util.EventListener; public interface ServletRequestListener extends EventListener { void requestDestroyed(ServletRequestEvent var1); // 入参是ServletRequestEvent void requestInitialized(ServletRequestEvent var1); } ServletRequestListener接口的方法入参是ServletRequestEvent类型，也就是说，在调用监听器ServletRequestListener的requestInitialized方法时，会传入事件对象ServletRequestEvent。\n每种类型的事件都有对应的监听器接口，Servlet容器会根据事件类型调用相应的监听器方法。以下是一个简化的示例，展示了Servlet容器如何根据事件类型调用相应的监听器方法：\n1.定义事件和监听器\nimport java.util.EventListener; import java.util.EventObject; // 定义ServletRequestEvent类 public class ServletRequestEvent extends EventObject { private static final long serialVersionUID = 1L; private final ServletRequest request; public ServletRequestEvent(ServletContext sc, ServletRequest request) { super(sc); this.request = request; } public ServletRequest getServletRequest() { return this.request; } public ServletContext getServletContext() { return (ServletContext) super.getSource(); } } // 定义ServletRequestListener接口 public interface ServletRequestListener extends EventListener { void requestDestroyed(ServletRequestEvent sre); void requestInitialized(ServletRequestEvent sre); } 2.定义ServletContext和ServletRequest\npublic class ServletContext { // 简化的ServletContext实现 } public class ServletRequest { // 简化的ServletRequest实现 } 3.定义Servlet容器\nimport java.util.ArrayList; import java.util.List; public class SimpleServletContainer { private List\u0026lt;ServletRequestListener\u0026gt; requestListeners = new ArrayList\u0026lt;\u0026gt;(); // 注册监听器 public void addServletRequestListener(ServletRequestListener listener) { requestListeners.add(listener); } // 模拟请求初始化 public void initializeRequest(ServletRequest request, ServletContext context) { // 创建事件对象 ServletRequestEvent event = new ServletRequestEvent(context, request); // 遍历监听器，调用方法，传入事件对象 for (ServletRequestListener listener : requestListeners) { listener.requestInitialized(event); } } // 模拟请求销毁 public void destroyRequest(ServletRequest request, ServletContext context) { ServletRequestEvent event = new ServletRequestEvent(context, request); for (ServletRequestListener listener : requestListeners) { listener.requestDestroyed(event); } } } 4.实现监听器\n@WebListener public class MyServletRequestListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println(\u0026#34;Request initialized: \u0026#34; + sre.getServletRequest()); } @Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println(\u0026#34;Request destroyed: \u0026#34; + sre.getServletRequest()); } } 监听器可以通过注解@WebListener或在web.xml中进行配置：\n使用注解：\n@WebListener public class MyServletRequestListener implements ServletRequestListener { // 实现接口方法 } 在web.xml中配置：\n\u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;com.example.MyServletRequestListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; 5.测试代码\npublic class Main { public static void main(String[] args) { // 创建Servlet容器 SimpleServletContainer container = new SimpleServletContainer(); // 创建ServletContext和ServletRequest ServletContext context = new ServletContext(); ServletRequest request = new ServletRequest(); // 注册监听器 container.addServletRequestListener(new MyServletRequestListener()); // 模拟请求初始化和销毁 container.initializeRequest(request, context); container.destroyRequest(request, context); } } 运行结果：\nRequest initialized: ServletRequest@1a2b3c4d Request destroyed: ServletRequest@1a2b3c4d 解释：\n事件创建：\n当请求初始化时，容器创建一个ServletRequestEvent对象，并将ServletContext和ServletRequest作为参数传递给事件对象的构造函数 同样，当请求销毁时，容器再次创建一个ServletRequestEvent对象 事件通知：\n容器遍历所有注册的监听器，检查它们是否实现了ServletRequestListener接口 对于实现了ServletRequestListener接口的监听器，容器调用它们的requestInitialized或requestDestroyed方法，并传递ServletRequestEvent对象 在实际的Servlet容器（如Tomcat、Jetty等）中，事件的触发和监听器的调用是由容器内部的代码实现的。容器会在特定事件发生时创建相应的事件对象，并调用注册的监听器方法。通过这种机制，开发者可以在特定事件发生时执行自定义逻辑，从而实现更灵活和强大的功能。换句话说，这些监听器的工作方式类似于钩子函数（Hook），通过实现这些监听器接口，我们可以在Servlet生命周期的不同阶段，自定义处理逻辑。\n参考资料：\nhttps://www.javatpoint.com/Event-and-Listener-in-Servlet\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000008-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servlet%E4%BA%8B%E4%BB%B6%E5%92%8C%E7%9B%91%E5%90%AC%E5%99%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"事件和监听器\"\u003e事件和监听器\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e事件\u003c/strong\u003e是指在程序运行过程中发生的特定动作或情况。比如：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e用户操作，如点击按钮、输入文本\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e系统操作，如文件读取完成、网络连接断开\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e应用程序内部的状态变化，如对象属性改变、方法调用\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e监听器\u003c/strong\u003e是一个接口或类，用于定义在特定事件发生时要执行的操作。监听器会“监听”特定的事件，当事件发生时，监听器中的相应方法会被调用，从而执行预定义的操作。\u003c/p\u003e\n\u003cp\u003e事件和监听器是相辅相成的，它们的工作原理如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/02ea64d9e6d741fe9f105fbf8cf9a112.png\"\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e事件源：触发事件的来源。例如，按钮是事件源，点击按钮会触发点击事件\u003c/li\u003e\n\u003cli\u003e事件对象：当事件发生时，事件源会创建一个事件对象，包含有关事件的信息\u003c/li\u003e\n\u003cli\u003e注册监听器：监听器需要注册到事件源，以便监听特定的事件。注册后，事件源会在事件发生时通知监听器\u003c/li\u003e\n\u003cli\u003e事件处理：当事件发生时，事件源会调用监听器中的相应方法，传递事件对象，从而执行预定义的操作\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"servlet-event-class\"\u003eServlet Event Class\u003c/h1\u003e\n\u003cp\u003e事件（Event）是一个类，封装了与特定事件相关的信息。\u003c/p\u003e\n\u003cp\u003e基本是构造方法和get方法：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eServletRequestEvent\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eServletRequestEvent\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eEventObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eserialVersionUID\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1L\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eServletRequestEvent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletContext\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esc\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esc\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003erequest\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetServletRequest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003erequest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletContext\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetServletContext\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletContext\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetSource\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServletRequestAttributeEvent\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eServletRequestAttributeEvent\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletRequestEvent\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eserialVersionUID\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1L\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eServletRequestAttributeEvent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletContext\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esc\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esc\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetValue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServletContextEvent\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eServletContextEvent\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eEventObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eserialVersionUID\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1L\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eServletContextEvent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletContext\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esource\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esource\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletContext\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetServletContext\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletContext\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetSource\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServletContextAttributeEvent\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eServletContextAttributeEvent\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletContextEvent\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eserialVersionUID\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1L\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eServletContextAttributeEvent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletContext\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esource\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esource\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetValue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eHttpSessionEvent\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eHttpSessionEvent\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eEventObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eserialVersionUID\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1L\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eHttpSessionEvent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eHttpSession\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esource\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esource\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eHttpSession\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetSession\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eHttpSession\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetSource\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eHttpSessionBindingEvent\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eHttpSessionBindingEvent\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eHttpSessionEvent\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eserialVersionUID\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1L\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eHttpSessionBindingEvent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eHttpSession\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esession\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esession\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evalue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eObject\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetValue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ol\u003e","title":"JavaWeb开发基础Servlet事件和监听器"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n鸡尾酒 Mockito是Java的单元测试Mock框架。它的logo是一杯古巴最著名的鸡尾酒Mojito，Ｍojito鸡尾酒，源自古巴的哈瓦那，带有浓厚的加勒比海风情。并不浓烈，但是喝一杯下去，脸上会泛起红晕，象少女的羞涩。味道很清新，有一点青涩、有点甜蜜。\nStub \u0026amp; Mock Stub和Mock是Test Double类型中的2种。Test Double一共有5种类型，Dummy，Stub，Spy，Mock，Fake。\nTest Double是测试复制品，用来统称模拟真实对象的假对象。因使用场景有略微不同，而有这5种类型。\nDummy，通常只用来填充参数列表。有可能是null对象引用，或Object类实例等。 Fake，是简化版的实现，比如基于内存实现的数据库，不会真的去做数据库操作，用简单的HashMap来存放数据。 Stub，Stub用来替代SUT(System Under Test)依赖的组件，但是只模拟一个外部依赖，不做断言。 Spy，介于Stub和Mock之间。如果真实对象没有被打桩，当调用Spy对象时，真实对象也会被调用。 Mock，可以理解为Stub+Verification，既模拟外部依赖，也会定义预期结果。 看一个实例，发送邮件，\npublic interface MailService { public void send(Message msg); } 先写个Stub，\npublic class MailServiceStub implements MailService { private List\u0026lt;Message\u0026gt; messages = new ArrayList\u0026lt;Message\u0026gt;(); public void send(Message msg) { messages.add(msg); } public int numberSent() { return messages.size(); } } 实现Stub的状态验证，\nclass OrderStateTester\u0026hellip;\npublic void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); MailServiceStub mailer = new MailServiceStub(); order.setMailer(mailer); order.fill(warehouse); assertEquals(1, mailer.numberSent()); } 只做了简单的测试，断言发出了1封邮件。没有测试是否发送给了对的收件人，或者邮件正文是否正确。不过不影响跟Mock比较。\n如果用Mock，会怎么写呢？\nclass OrderInteractionTester\u0026hellip;\npublic void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); Mock warehouse = mock(Warehouse.class); Mock mailer = mock(MailService.class); order.setMailer((MailService) mailer.proxy()); mailer.expects(once()).method(\u0026#34;send\u0026#34;); warehouse.expects(once()).method(\u0026#34;hasInventory\u0026#34;) .withAnyArguments() .will(returnValue(false)); order.fill((Warehouse) warehouse.proxy()); } 结合一张图，就一下全明白了，\n怎么喝Mockito？ 添加Maven dependency，\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mockito\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mockito-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.3.3\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 现在可以开始Mock了，先Mock一个List Interface试试，（示例只是玩语法，实际应使用instance）\n//Let\u0026#39;s import Mockito statically so that the code looks clearer import static org.mockito.Mockito.*; // mock creation List mockedList = mock(List.class); // using mock object mockedList.add(\u0026#34;one\u0026#34;); mockedList.clear(); // verification verify(mockedList).add(\u0026#34;one\u0026#34;); verify(mockedList).clear(); 接着用Mockito来做Stub：\n// You can mock concrete classes, not just interfaces LinkedList mockedList = mock(LinkedList.class); // stubbing when(mockedList.get(0)).thenReturn(\u0026#34;first\u0026#34;); when(mockedList.get(1)).thenThrow(new RuntimeException()); // following prints \u0026#34;first\u0026#34; System.out.println(mockedList.get(0)); // following throws runtime exception System.out.println(mockedList.get(1)); // following prints \u0026#34;null\u0026#34; because get(999) was not stubbed System.out.println(mockedList.get(999)); // Although it is possible to verify a stubbed invocation, usually it\u0026#39;s just redundant // If your code cares what get(0) returns, then something else breaks // (often even before verify() gets executed). // If your code doesn\u0026#39;t care what get(0) returns, then it should not be stubbed. verify(mockedList).get(0); mock默认会返回null，或原始值，或空集合。如int/Integer返回0，boolean/Boolean返回false。\n小结 本文首先介绍了Mockito是Java的单元测试Mock框架，然后比较了单元测试中Stub和Mock的区别，最后演示了Mockito如何使用。\n参考资料： https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html\nhttps://martinfowler.com/articles/mocksArentStubs.html\nhttp://xunitpatterns.com/Test%20Double.html\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000008-mockitojava%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95mock%E6%A1%86%E6%9E%B6mockito%E5%85%A5%E9%97%A8%E4%BB%8B%E7%BB%8D/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"鸡尾酒\"\u003e鸡尾酒\u003c/h1\u003e\n\u003cp\u003eMockito是Java的单元测试Mock框架。它的logo是一杯古巴最著名的鸡尾酒Mojito，Ｍojito鸡尾酒，源自古巴的哈瓦那，带有浓厚的加勒比海风情。并不浓烈，但是喝一杯下去，脸上会泛起红晕，象少女的羞涩。味道很清新，有一点青涩、有点甜蜜。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/72142d20108948cfa0db66a588a95d8d.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"stub--mock\"\u003eStub \u0026amp; Mock\u003c/h1\u003e\n\u003cp\u003eStub和Mock是Test Double类型中的2种。Test Double一共有5种类型，Dummy，Stub，Spy，Mock，Fake。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8b7d2c986e6845fb81ab9383fba03b16.gif\"\u003e\u003c/p\u003e\n\u003cp\u003eTest Double是测试复制品，用来统称模拟真实对象的假对象。因使用场景有略微不同，而有这5种类型。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDummy，通常只用来填充参数列表。有可能是null对象引用，或Object类实例等。\u003c/li\u003e\n\u003cli\u003eFake，是简化版的实现，比如基于内存实现的数据库，不会真的去做数据库操作，用简单的HashMap来存放数据。\u003c/li\u003e\n\u003cli\u003eStub，Stub用来替代SUT(System Under Test)依赖的组件，但是只模拟一个外部依赖，不做断言。\u003c/li\u003e\n\u003cli\u003eSpy，介于Stub和Mock之间。如果真实对象没有被打桩，当调用Spy对象时，真实对象也会被调用。\u003c/li\u003e\n\u003cli\u003eMock，可以理解为Stub+Verification，既模拟外部依赖，也会定义预期结果。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e看一个实例，发送邮件，\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMailService\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esend\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMessage\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emsg\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e先写个Stub，\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMailServiceStub\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMailService\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eMessage\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emessages\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eArrayList\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eMessage\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esend\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMessage\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emsg\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003emessages\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eadd\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emsg\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003enumberSent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emessages\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esize\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e     \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e实现Stub的状态验证，\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eclass OrderStateTester\u0026hellip;\u003c/em\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etestOrderSendsMailIfUnfilled\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eOrder\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eorder\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOrder\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eTALISKER\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e51\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eMailServiceStub\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emailer\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMailServiceStub\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eorder\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esetMailer\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emailer\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eorder\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003efill\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ewarehouse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eassertEquals\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emailer\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enumberSent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e只做了简单的测试，断言发出了1封邮件。没有测试是否发送给了对的收件人，或者邮件正文是否正确。不过不影响跟Mock比较。\u003c/p\u003e","title":"Java单元测试Mock框架Mockito入门介绍"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nNumbers Number的子类：\n另外还有BigDecimal和BigInteger，用于高精度计算，AtomicInteger和AtomicLong用于多线程应用。\n我们有时候需要用包装类而非基本数据类型，理由如下：\n方法入参类型为Object，只能传入对象\n使用包装类提供的常量，比如MIN_VALUE和MAX_VALUE\n使用包装类的方法来做类型转换\nformat\nimport java.util.Calendar; import java.util.Locale; public class TestFormat { public static void main(String[] args) { long n = 461012; System.out.format(\u0026#34;%d%n\u0026#34;, n); // --\u0026gt; \u0026#34;461012\u0026#34; System.out.format(\u0026#34;%08d%n\u0026#34;, n); // --\u0026gt; \u0026#34;00461012\u0026#34; System.out.format(\u0026#34;%+8d%n\u0026#34;, n); // --\u0026gt; \u0026#34; +461012\u0026#34; System.out.format(\u0026#34;%,8d%n\u0026#34;, n); // --\u0026gt; \u0026#34; 461,012\u0026#34; System.out.format(\u0026#34;%+,8d%n%n\u0026#34;, n); // --\u0026gt; \u0026#34;+461,012\u0026#34; double pi = Math.PI; System.out.format(\u0026#34;%f%n\u0026#34;, pi); // --\u0026gt; \u0026#34;3.141593\u0026#34; System.out.format(\u0026#34;%.3f%n\u0026#34;, pi); // --\u0026gt; \u0026#34;3.142\u0026#34; System.out.format(\u0026#34;%10.3f%n\u0026#34;, pi); // --\u0026gt; \u0026#34; 3.142\u0026#34; System.out.format(\u0026#34;%-10.3f%n\u0026#34;, pi); // --\u0026gt; \u0026#34;3.142\u0026#34; System.out.format(Locale.FRANCE, \u0026#34;%-10.4f%n%n\u0026#34;, pi); // --\u0026gt; \u0026#34;3,1416\u0026#34; Calendar c = Calendar.getInstance(); System.out.format(\u0026#34;%tB %te, %tY%n\u0026#34;, c, c, c); // --\u0026gt; \u0026#34;May 29, 2006\u0026#34; System.out.format(\u0026#34;%tl:%tM %tp%n\u0026#34;, c, c, c); // --\u0026gt; \u0026#34;2:34 am\u0026#34; System.out.format(\u0026#34;%tD%n\u0026#34;, c); // --\u0026gt; \u0026#34;05/29/06\u0026#34; } } DecimalFormat\nimport java.text.*; public class DecimalFormatDemo { static public void customFormat(String pattern, double value ) { DecimalFormat myFormatter = new DecimalFormat(pattern); String output = myFormatter.format(value); System.out.println(value + \u0026#34; \u0026#34; + pattern + \u0026#34; \u0026#34; + output); } static public void main(String[] args) { customFormat(\u0026#34;###,###.###\u0026#34;, 123456.789); customFormat(\u0026#34;###.##\u0026#34;, 123456.789); customFormat(\u0026#34;000000.000\u0026#34;, 123.78); customFormat(\u0026#34;$###,###.###\u0026#34;, 12345.67); } } Math\npublic class BasicMathDemo { public static void main(String[] args) { double a = -191.635; double b = 43.74; int c = 16, d = 45; System.out.printf(\u0026#34;The absolute value \u0026#34; + \u0026#34;of %.3f is %.3f%n\u0026#34;, a, Math.abs(a)); System.out.printf(\u0026#34;The ceiling of \u0026#34; + \u0026#34;%.2f is %.0f%n\u0026#34;, b, Math.ceil(b)); System.out.printf(\u0026#34;The floor of \u0026#34; + \u0026#34;%.2f is %.0f%n\u0026#34;, b, Math.floor(b)); System.out.printf(\u0026#34;The rint of %.2f \u0026#34; + \u0026#34;is %.0f%n\u0026#34;, b, Math.rint(b)); System.out.printf(\u0026#34;The max of %d and \u0026#34; + \u0026#34;%d is %d%n\u0026#34;, c, d, Math.max(c, d)); System.out.printf(\u0026#34;The min of of %d \u0026#34; + \u0026#34;and %d is %d%n\u0026#34;, c, d, Math.min(c, d)); } } Charaters 基本类型char\nchar ch = \u0026#39;a\u0026#39;; // Unicode for uppercase Greek omega character char uniChar = \u0026#39;\\u03A9\u0026#39;; // an array of chars char[] charArray = { \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;, \u0026#39;e\u0026#39; }; Character\nCharacter ch = new Character(\u0026#39;a\u0026#39;); Strings 定义：\nString greeting = \u0026#34;Hello world!\u0026#34;; char[] helloArray = { \u0026#39;h\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;o\u0026#39;, \u0026#39;.\u0026#39; }; String helloString = new String(helloArray); System.out.println(helloString); 回文字符串实现：\npublic class StringDemo { public static void main(String[] args) { String palindrome = \u0026#34;Dot saw I was Tod\u0026#34;; int len = palindrome.length(); char[] tempCharArray = new char[len]; char[] charArray = new char[len]; // put original string in an // array of chars for (int i = 0; i \u0026lt; len; i++) { tempCharArray[i] = palindrome.charAt(i); } // reverse array of chars for (int j = 0; j \u0026lt; len; j++) { charArray[j] = tempCharArray[len - 1 - j]; } String reversePalindrome = new String(charArray); System.out.println(reversePalindrome); } } String转换为基本数据类型，parseXXX()比valueOf更好用：\nfloat a = (Float.valueOf(args[0])).floatValue(); float b = (Float.valueOf(args[1])).floatValue(); float a = Float.parseFloat(args[0]); float b = Float.parseFloat(args[1]); 基本数据类型转换为String：\nint i; // Concatenate \u0026#34;i\u0026#34; with an empty string; conversion is handled for you. String s1 = \u0026#34;\u0026#34; + i; // The valueOf class method. String s2 = String.valueOf(i); int i; double d; String s3 = Integer.toString(i); String s4 = Double.toString(d); 根据字符查找对应索引：\nString anotherPalindrome = \u0026#34;Niagara. O roar again!\u0026#34;; char aChar = anotherPalindrome.charAt(9); 子串：\nString anotherPalindrome = \u0026#34;Niagara. O roar again!\u0026#34;; String roar = anotherPalindrome.substring(11, 15); String Builders String不可变，StringBuilder可变。StringBuilder除了length()，还有个capacity()，返回分配的字符数量，大于等于length，并且会自动扩充。\n// creates empty builder, capacity 16 StringBuilder sb = new StringBuilder(); // adds 9 character string at beginning sb.append(\u0026#34;Greetings\u0026#34;); StringBuffer用的少，只在需要保证线程安全时使用。\n自动装箱和拆箱 装箱，基本数据类型→包装类：\nList\u0026lt;Integer\u0026gt; ints = new ArrayList\u0026lt;\u0026gt;(); for (int i = 1; i \u0026lt; 50; i += 2) ints.add(i); 拆箱，包装类→基本数据类型：\npublic static int sumEven(List\u0026lt;Integer\u0026gt; ints) { int sum = 0; for (Integer i: ints) { if (i % 2 == 0) { sum+=i; } } return sum; } 参考资料：\nNumbers and Strings https://dev.java/learn/numbers-strings/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000008-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B05%E6%95%B0%E5%AD%97%E5%92%8C%E5%AD%97%E7%AC%A6%E4%B8%B2/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"numbers\"\u003eNumbers\u003c/h2\u003e\n\u003cp\u003eNumber的子类：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/dcbfcf05bdbe4805a767acf39b45e777.png\"\u003e\u003c/p\u003e\n\u003cp\u003e另外还有BigDecimal和BigInteger，用于高精度计算，AtomicInteger和AtomicLong用于多线程应用。\u003c/p\u003e\n\u003cp\u003e我们有时候需要用包装类而非基本数据类型，理由如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e方法入参类型为Object，只能传入对象\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e使用包装类提供的常量，比如MIN_VALUE和MAX_VALUE\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e使用包装类的方法来做类型转换\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eformat\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejava.util.Calendar\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejava.util.Locale\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eTestFormat\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003emain\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#0550ae\"\u003e[]\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eargs\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e461012\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%d%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e//  --\u0026gt;  \u0026#34;461012\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%08d%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e//  --\u0026gt;  \u0026#34;00461012\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%+8d%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e//  --\u0026gt;  \u0026#34; +461012\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%,8d%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34; 461,012\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%+,8d%n%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e//  --\u0026gt;  \u0026#34;+461,012\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003edouble\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epi\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMath\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ePI\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%f%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epi\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e       \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;3.141593\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%.3f%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epi\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e     \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;3.142\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%10.3f%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epi\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;     3.142\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%-10.3f%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epi\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;3.142\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eLocale\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eFRANCE\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                        \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%-10.4f%n%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epi\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;3,1416\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eCalendar\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCalendar\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetInstance\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%tB %te, %tY%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;May 29, 2006\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%tl:%tM %tp%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;2:34 am\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%tD%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ec\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// --\u0026gt;  \u0026#34;05/29/06\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eDecimalFormat\u003c/p\u003e","title":"Java官方笔记5数字和字符串"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter参数化是指把固定的数据动态化，这样更贴合实际的模拟用户请求，比如模拟多个不同账号。JMeter一共有四种参数化方式，分别是：\nCSV Data Set Config Function Helper User Defined Variables User Parameters CSV Data Set Config 所属组件：Config Element。\nName 带业务含义的名字。\nComments 可以为空的注释。\nConfigure the CSV Data Source 【Filename】文件名，可以是相对路径也可以是绝对路径。推荐使用相对路径，好处是脚本迁移或者分发到多台远程负载机，不改脚本就能直接复用。相对路径的根节点是%JMETER_HOME%\\bin，把CSV文件放到这个目录或其子目录下。\n注意，打开方式选择bin目录下jmeter.bat或快捷方式才会把bin作为相对路径的根节点。如果把bin目录加入系统环境变量后，用cmd执行jmeter命令打开，那么相对路径的根节点是cmd的工作目录，比如C:\\Users\\dongfanger\\Desktop。\n【File encoding】文件编码，推荐用UTF-8。\n【Variable Names (comma-delimited)】定义的变量名，用逗号隔开，与CSV文件的列一一对应。变量比列多，多余的变量取不到值；变量比列少，多余的列不会被取值。此选项可以为空。\n【Ignore first line (only used if Variable Names is not empty)】**上一个选项的变量名为空时，此选项生效。**它会把CSV文件的第一行的列名作为变量名，并在取值时忽略第一行。\n【Delimiter (use '\\t' for tab)】CSV文件分隔符，默认为逗号，也可以用制表符。\n【Allow quoted data】是否允许引用数据，比如双引号\u0026quot;dongfanger,pass\u0026quot;,1111，允许后能拆分出dongfanger,pass和1111。\n【Recycle on EOF】设置为True时，循环遍历CSV文件。\n【Stop thread on EOF】设置为True时，遍历完CSV文件就结束线程。\nEOF是End Of File缩写，文件结束符。JMeter在测试执行过程中每个线程每次迭代会从参数文件中新取一行数据，从头遍历到尾。\n【Sharing mode】有三种共享模式：默认为All threads，对所有线程共享；Current thread group只对当前线程组中的线程共享；Current thread仅当前线程获取。\n比如2个线程组，每个线程组2个线程，共享同一个csv文件，那么迭代1次这4个线程会分别取前4条数据，互不重复；迭代2次这4个线程会分别取前8条数据，互不重复。\nUser Defined Variables 所属组件：Config Element。\nJMeter有两个User Defined Variables，它们的区别是：Test Plan的User Defined Variables作用域是全局的，配置元件的User Defined Variables作用域可以是全局的也可以是局部的。\n表格共三列，第一列是Name变量名，第二列是Value变量值，第三列是Description描述。表格下方的功能按钮，用来对表格进行增删改查和上下移动，还能直接从剪贴板添加。\n定义好的变量通过${}格式来使用，比如变量a值为1，使用时写作${a}。这个地方定义的实际上是字符串常量，比如刚才已经定义了一个变量名a变量值1，再定义变量名b变量值${a} + 1，b的实际值不是整型2而是字符串\u0026quot;${a} + 1\u0026quot;。\nUser Parameters 所属组件：Pre Processors。由于User Parameters属于前置处理器，所以它只会对当前Sampler生效。\nName名字 带业务含义的名字。\nComments注释 可以为空的注释。\nUpdate Once Per Iteration 经过反复多次试验，勾选与否没看出区别。官方解释如下：\nA flag to indicate whether the User Parameters element should update its variables only once per iteration. if you embed functions into the UP, then you may need greater control over how often the values of the variables are updated. Keep this box checked to ensure the values are updated each time through the UP\u0026rsquo;s parent controller. Uncheck the box, and the UP will update the parameters for every sample request made within its scope.\n从各文章截图总结，一般会勾选上这个选项。\nParameters 表格列Name指变量名，表格列User_x指某个用户的变量值。下方有6个按钮，分别是“添加变量”、“删除变量”、“添加用户”、“删除用户”、“上移”、“下移”。每个线程分配一个变量值；如果线程数大于User_x，那么会循环取值。\nUser Parameters在参数较少的情况下使用。\nFunction Helper 函数除了参数化，还可以用来做运算，字符编码格式转换，获取运行时参数等，下篇文章专门来写。\n小结 本文介绍了配置组件里面的CSV Data Set Config和User Defined Variables，作用域可全局可局部。接着介绍了前置处理器User Parameters，只对当前Sampler生效。但都有局限性，为了帮助我们更好地进行参数化，JMeter提供了一组函数来帮我们生成需要的数据，这些函数在有个地方能够进行编辑，它就是：Function Helper。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\nhttp://tutorialjmeter.blogspot.com/2017/05/pre-processor-in-jmeter.html\nhttps://jmeter.apache.org/usermanual/component_reference.html#User_Parameters\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000008-jmeterjmeter%E5%9B%9B%E7%A7%8D%E5%8F%82%E6%95%B0%E5%8C%96%E6%96%B9%E5%BC%8F/","summary":"JMeter参数化是指\u003cstrong\u003e把固定的数据动态化\u003c/strong\u003e，这样更贴合实际的模拟用户请求，比如模拟多个不同账号。JMeter一共有四种参数化方式，分别是：","title":"JMeter四种参数化方式"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nSpring Boot integrates with a number of data technologies, both SQL and NoSQL.\nSQL Databases Spring Data provides an additional level of functionality: creating Repository implementations directly from interfaces and using conventions to generate queries from your method names.\nThe spring-boot-starter-data-jpa POM provides a quick way to get started. It provides the following key dependencies:\nHibernate: One of the most popular JPA implementations.\nSpring Data JPA: Helps you to implement JPA-based repositories.\nSpring ORM: Core ORM support from the Spring Framework.\nA typical entity class resembles the following example:\nimport java.io.Serializable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @Entity public class City implements Serializable { @Id @GeneratedValue private Long id; @Column(nullable = false) private String name; @Column(nullable = false) private String state; // ... additional members, often include @OneToMany mappings protected City() { // no-args constructor required by JPA spec // this one is protected since it should not be used directly } public City(String name, String state) { this.name = name; this.state = state; } public String getName() { return this.name; } public String getState() { return this.state; } // ... etc } Working with NoSQL Technologies Redis\nRedis is a cache, message broker, and richly-featured key-value store. Spring Boot offers basic auto-configuration for the Lettuceand Jedis client libraries and the abstractions on top of them provided by Spring Data Redis.\nBy default, the instance tries to connect to a Redis server at localhost:6379.\nspring.data.redis.host=localhost spring.data.redis.port=6379 spring.data.redis.database=0 spring.data.redis.username=user spring.data.redis.password=secret MongoDB\nThe auto-configured MongoClient is created using a MongoClientSettings bean.\nspring.data.mongodb.host=mongoserver1.example.com spring.data.mongodb.port=27017 spring.data.mongodb.additional-hosts[0]=mongoserver2.example.com:23456 spring.data.mongodb.database=test spring.data.mongodb.username=user spring.data.mongodb.password=secret Elasticsearch\nSpring Boot supports several clients:\nThe official low-level REST client\nThe official Java API client\nThe ReactiveElasticsearchClient provided by Spring Data Elasticsearch\nspring.elasticsearch.uris=https://search.example.com:9200 spring.elasticsearch.socket-timeout=10s spring.elasticsearch.username=user spring.elasticsearch.password=secret InfluxDB\nInfluxDB is an open-source time series database optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet-of-Things sensor data, and real-time analytics.\nspring.influx.url=https://172.0.0.1:8086 参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#data\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000008-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B05data/","summary":"Spring Boot integrates with a number of data technologies, both SQL and NoSQL.","title":"SpringBoot官方笔记5Data"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文开发内容 在免费开源的测试平台中，teprunner的UI界面应该算得上第一梯度了，BUT，美中不足，在不同分辨率显示器（比如13.3寸小屏笔记本）下布局大的弹出窗口，表现不尽如人意。而这又是核心功能代码编辑的界面，比如用例编写、fixture编写，很有必要优化升级。除此之外还有滚动条的问题：竖向双滚动条和横向滚动条，也对体验造成影响。本文将对这些内容进行改造，具体如下：\n编写用例页面，从弹窗改为子页面。 编写fixture页面，从弹窗改为子页面。 代码编辑区域高度自适应。 缩小左侧菜单，移动项目环境和新增按钮同一行，扩大编辑区面积。 消除所有双向滚动条。 消除所有横向滚动条。 语法说明添加环境变量和fixtures示例代码。 其他细节优化。 响应式是指同一套前端代码在不同浏览器（PC端、移动端等）都能适配，Element UI已经是响应式设计了，本文的响应式体现在代码编辑区行高根据window.innerHeight自动计算设定。\n升级效果展示 左侧菜单栏变窄；添加环境变量和fixtures示例代码。\n项目环境和新增按钮同一行显示，扩大有效内容可视面积。\nfixtures的新增和编辑从弹窗改为子路由页面，代码编辑区高度自适应，纵向滚动条只出现一个。\n消除横向滚动条；缩小了用例描述列以外其他列宽；替换操作按钮文字为图标。\n用例的新增和编辑从弹窗改为子路由页面，代码编辑区高度自适应，纵向滚动条只出现一个。\n用例运行结果的新增和编辑从弹窗改为子路由页面，代码编辑区高度自适应，纵向滚动条只出现一个。\n测试计划的新增，编辑，运行结果也做了相同调整。\n整体而言，有效信息的可视面积和可操作面积都尽可能扩大了！\n关键技术点 前端代码修改文件如下：\n比较繁琐，没有新知识，这里就不一一讲解了，而是选择用到的关键技术点来进行知识回顾：\ntemplate添加\u0026lt;router-view\u0026gt;\u0026lt;/router-view\u0026gt; 主页面div添加v-if=\u0026quot;$route.name === 'name'\u0026quot; 在router/index.js中添加children子路由 跳转页面前，存localStorage 新页面create()时，取localStorage this.$router.push跳转路由 div同行显示用float: left，紧跟的div换行用clear: both window.innerHeight获取屏幕高度 小结 本文完成了对teprunner测试平台前端UI界面的体验升级，已经适配13.3寸到27寸显示器，如果还在担心不好用的话，那么现在可以好好试试了。定时任务从目前优化效果来看，已经趋于稳定，不久就会和大家见面。\n参考资料：\nElement UI图标按钮样式 https://www.jianshu.com/p/a81cd965943c\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000008-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E7%94%A8%E4%BE%8B%E7%BC%96%E5%86%99%E7%AD%89%E4%BD%93%E9%AA%8C%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8D%87%E7%BA%A7/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"本文开发内容\"\u003e本文开发内容\u003c/h1\u003e\n\u003cp\u003e在免费开源的测试平台中，teprunner的UI界面应该算得上第一梯度了，BUT，美中不足，在不同分辨率显示器（比如13.3寸小屏笔记本）下布局大的弹出窗口，表现不尽如人意。而这又是核心功能代码编辑的界面，比如用例编写、fixture编写，很有必要优化升级。除此之外还有滚动条的问题：竖向双滚动条和横向滚动条，也对体验造成影响。本文将对这些内容进行改造，具体如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e编写用例页面，从弹窗改为子页面。\u003c/li\u003e\n\u003cli\u003e编写fixture页面，从弹窗改为子页面。\u003c/li\u003e\n\u003cli\u003e代码编辑区域高度自适应。\u003c/li\u003e\n\u003cli\u003e缩小左侧菜单，移动项目环境和新增按钮同一行，\u003cstrong\u003e扩大编辑区面积。\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e消除所有双向滚动条。\u003c/li\u003e\n\u003cli\u003e消除所有横向滚动条。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e语法说明添加环境变量和fixtures示例代码。\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e其他细节优化。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e响应式是指同一套前端代码在不同浏览器（PC端、移动端等）都能适配，\u003cstrong\u003eElement UI已经是响应式设计了，本文的响应式体现在代码编辑区行高根据window.innerHeight自动计算设定。\u003c/strong\u003e\u003c/p\u003e\n\u003ch1 id=\"升级效果展示\"\u003e升级效果展示\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/abb289b0c2ad40d0b876edfd0078712a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e左侧菜单栏变窄；添加环境变量和fixtures示例代码。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8f2785c1c9bd4d5ead1ab11ab700afe6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e项目环境和新增按钮同一行显示，扩大有效内容可视面积。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/21e3c8c2725b414d841f5aa8166eb133.png\"\u003e\u003c/p\u003e\n\u003cp\u003efixtures的新增和编辑从弹窗改为子路由页面，代码编辑区高度自适应，纵向滚动条只出现一个。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/978e0307dd804de58c101d3f8d908fe0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e消除横向滚动条；缩小了用例描述列以外其他列宽；替换操作按钮文字为图标。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a0ddb7fee2e74834b416755ead80e364.png\"\u003e\u003c/p\u003e\n\u003cp\u003e用例的新增和编辑从弹窗改为子路由页面，代码编辑区高度自适应，纵向滚动条只出现一个。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/98b3d62d1d144be595d485264f5f0a11.png\"\u003e\u003c/p\u003e\n\u003cp\u003e用例运行结果的新增和编辑从弹窗改为子路由页面，代码编辑区高度自适应，纵向滚动条只出现一个。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ac3e7fdec3304676a6c32a6815e55d8b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e测试计划的新增，编辑，运行结果也做了相同调整。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e整体而言，有效信息的可视面积和可操作面积都尽可能扩大了！\u003c/strong\u003e\u003c/p\u003e\n\u003ch1 id=\"关键技术点\"\u003e关键技术点\u003c/h1\u003e\n\u003cp\u003e前端代码修改文件如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4181f693f5a74f7d8e3b474ffc40e44a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e比较繁琐，\u003cstrong\u003e没有新知识\u003c/strong\u003e，这里就不一一讲解了，而是选择用到的关键技术点来进行\u003cstrong\u003e知识回顾\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003etemplate添加\u003ccode\u003e\u0026lt;router-view\u0026gt;\u0026lt;/router-view\u0026gt;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e主页面div添加\u003ccode\u003ev-if=\u0026quot;$route.name === 'name'\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e在router/index.js中添加children子路由\u003c/li\u003e\n\u003cli\u003e跳转页面前，存localStorage\u003c/li\u003e\n\u003cli\u003e新页面create()时，取localStorage\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ethis.$router.push\u003c/code\u003e跳转路由\u003c/li\u003e\n\u003cli\u003ediv同行显示用\u003ccode\u003efloat: left\u003c/code\u003e，紧跟的div换行用\u003ccode\u003eclear: both\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003ewindow.innerHeight获取屏幕高度\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"小结\"\u003e小结\u003c/h1\u003e\n\u003cp\u003e本文完成了对teprunner测试平台前端UI界面的体验升级，\u003cstrong\u003e已经适配13.3寸到27寸显示器\u003c/strong\u003e，如果还在担心不好用的话，那么现在可以好好试试了。定时任务从目前优化效果来看，已经趋于稳定，不久就会和大家见面。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003eElement UI图标按钮样式 \u003ca href=\"https://www.jianshu.com/p/a81cd965943c\"\u003ehttps://www.jianshu.com/p/a81cd965943c\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"teprunner测试平台用例编写等体验响应式升级"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n大家好，我是刚哥。\n趁着元旦假期最后一天，有着大把时间，奔着把tep做大做强的目标，好好学习了一波。在开始正文之前，先回答可能会问到的两个问题。第一个问题是为什么要集成HttpRunner？因为我最近在思考如何给tep做分层设计，参考了我司现有的接口自动化平台，它的设计是每个用例有很多测试步骤，可以针对用例设置预设变量，然后在测试步骤中引用。正当我准备自己开发类似功能时，想到了HttpRunner，我记得HttpRunner第3版是建议直接编写pytest代码而非以前的ymal或json文件了。大有所获，HttpRunner正是以这种方式编写的代码，而且和pytest有很好的结合，很符合tep要集成的第三方包的希望。第二个问题是为什么要集成Flask？刚开始只是我用来调试代码的，等到把Mock写完以后，想到可能大家也需要调试代码，就把它做到tep里面了，并且附带了测试用例的示例代码，安装完以后就能一键运行，开箱即用，美滋滋。归根结底，都是为了把tep做大做强。\ntep0.9.3正式发布 要体验HttpRunner和Flask，需要先安装或升级到tep0.9.3。\n安装：\npip install tep 升级：\npip install -U tep 或者指定版本：\npip install tep==0.9.3 安装tep时会顺带安装HttpRunner和Flask，安装完以后就可以执行命令初始化项目：\ntep startproject demo093 输出：\nD:\\PycharmProjects\u0026gt;tep startproject demo093 2022-01-03 16:07:31.929 | INFO | tep.scaffold:create_scaffold:53 - Create new project: demo093 Project root dir: D:\\PycharmProjects\\demo093 Created folder: demo093 Created folder: demo093\\fixtures Created folder: demo093\\tests Created folder: demo093\\files Created folder: demo093\\reports Created folder: demo093\\utils Created file: demo093\\.gitignore Created file: demo093\\conf.yaml Created file: demo093\\conftest.py Created file: demo093\\pytest.ini Created file: demo093\\fixtures\\__init__.py Created file: demo093\\fixtures\\fixture_admin.py Created file: demo093\\fixtures\\fixture_env_vars.py Created file: demo093\\fixtures\\fixture_login.py Created file: demo093\\fixtures\\fixture_your_name.py Created file: demo093\\tests\\__init__.py Created file: demo093\\tests\\test_login.py Created file: demo093\\tests\\test_post.py Created file: demo093\\tests\\test_mysql.py Created file: demo093\\tests\\test_request.py Created file: demo093\\tests\\test_login_pay.py Created file: demo093\\tests\\test_login_pay_httprunner.py Created file: demo093\\utils\\__init__.py Created file: demo093\\utils\\flask_mock_api.py 修改了fixture_env_vars.py里面的domain为http://127.0.0.1:5000，这是Flask启动后的默认地址。 修改了fixture_login.py里面的登录url和username，跟Flask的Mock对应。 新增了utils\\flask_mock_api.py，直接启动Mock服务。 新增了tests\\test_login_pay.py，用例数据一体开发模式，登录到下单流程的示例代码，可以一键运行成功。 新增了tests\\test_login_pay_httprunner.py，HttpRunner开发模式，登录到下单流程的示例代码，可以一键运行成功。 新版README.md 之前tep的README是全英文的，这次我也决定不装了，改成中文 ，丰富了内容，大家可以对tep有个全新和全面的了解啦。以下是全文：\ntep tep是Try Easy Pytest的首字母缩写，是一款基于pytest测试框架的测试工具，集成了各种实用的第三方包和优秀的自动化测试设计思想，帮你快速实现自动化项目落地。\n安装 支持Python3.6以上，推荐Python3.8以上。\n标准安装：\n$ pip install tep 国内镜像：\n$ pip --default-timeout=600 install -i https://pypi.tuna.tsinghua.edu.cn/simple tep 检查安装成功：\n$ tep -V # 或者 tep --version 0.2.3 快速创建项目 tep提供了脚手架，预置了项目结构和代码，打开cmd，使用startproject命令快速创建项目：\ntep startproject project_name 并且提供了-venv参数，在项目初始化时，可以同时创建一个虚拟环境（推荐）：\ntep startproject project_name -venv 输出测试报告 tep提供了--tep-reports参数来生成allure测试报告：\npytest --tep-reports 报告文件存放在根目录的reports/中。\nMock服务 tep自带了一个Flask应用（utils/flask_mock_api.py），提供了登录到下单流程的5个接口，启动后即可一键运行示例中的测试用例。\n三种开发模式 tep兼容三种开发模式：用例数据一体（适合新手）、用例数据分离（适合老手）、HttpRunner（新老皆宜）。\n①用例数据一体，示例代码如下所示：\nimport jmespath from tep.client import request def test(env_vars, login): # 搜索商品 response = request( \u0026#34;get\u0026#34;, url=env_vars.domain + \u0026#34;/searchSku\u0026#34;, headers={\u0026#34;token\u0026#34;: login.token}, params={\u0026#34;skuName\u0026#34;: \u0026#34;电子书\u0026#34;} ) sku_id = jmespath.search(\u0026#34;skuId\u0026#34;, response.json()) sku_price = jmespath.search(\u0026#34;price\u0026#34;, response.json()) assert response.status_code \u0026lt; 400 # 添加购物车 sku_num = 3 response = request( \u0026#34;post\u0026#34;, url=env_vars.domain + \u0026#34;/addCart\u0026#34;, headers={\u0026#34;token\u0026#34;: login.token}, json={\u0026#34;skuId\u0026#34;: sku_id, \u0026#34;skuNum\u0026#34;: sku_num} ) total_price = jmespath.search(\u0026#34;totalPrice\u0026#34;, response.json()) assert response.status_code \u0026lt; 400 # 下单 response = request( \u0026#34;post\u0026#34;, url=env_vars.domain + \u0026#34;/order\u0026#34;, headers={\u0026#34;token\u0026#34;: login.token}, json={\u0026#34;skuId\u0026#34;: sku_id, \u0026#34;price\u0026#34;: sku_price, \u0026#34;skuNum\u0026#34;: sku_num, \u0026#34;totalPrice\u0026#34;: total_price} ) order_id = jmespath.search(\u0026#34;orderId\u0026#34;, response.json()) assert response.status_code \u0026lt; 400 # 支付 response = request( \u0026#34;post\u0026#34;, url=env_vars.domain + \u0026#34;/pay\u0026#34;, headers={\u0026#34;token\u0026#34;: login.token}, json={\u0026#34;orderId\u0026#34;: order_id, \u0026#34;payAmount\u0026#34;: \u0026#34;6.9\u0026#34;} ) assert response.status_code \u0026lt; 400 assert response.json()[\u0026#34;success\u0026#34;] == \u0026#34;true\u0026#34; 更多内容请参考《如何使用teprunner测试平台编写从登录到下单的大流程接口自动化用例》\n②用例数据分离\n开发中，敬请期待\u0026hellip; ③HttpRunner，示例代码如下所示：\nfrom httprunner import HttpRunner, Config, Step, RunRequest class TestLoginPay(HttpRunner): config = ( Config(\u0026#34;登录到下单流程\u0026#34;) .variables( **{ \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34; } ) .base_url(\u0026#34;http://127.0.0.1:5000\u0026#34;) ) teststeps = [ Step( RunRequest(\u0026#34;登录\u0026#34;) .post(\u0026#34;/login\u0026#34;) .with_headers(**{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}) .with_json({\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.token\u0026#34;, \u0026#34;token\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), Step( RunRequest(\u0026#34;搜索商品\u0026#34;) .get(\u0026#34;searchSku?skuName=电子书\u0026#34;) .with_headers(**{\u0026#34;token\u0026#34;: \u0026#34;$token\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.skuId\u0026#34;, \u0026#34;skuId\u0026#34;) .with_jmespath(\u0026#34;body.price\u0026#34;, \u0026#34;skuPrice\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), Step( RunRequest(\u0026#34;添加购物车\u0026#34;) .post(\u0026#34;/addCart\u0026#34;) .with_headers(**{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;$token\u0026#34;}) .with_json({\u0026#34;skuId\u0026#34;: \u0026#34;$skuId\u0026#34;, \u0026#34;skuNum\u0026#34;: \u0026#34;$skuNum\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.totalPrice\u0026#34;, \u0026#34;totalPrice\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), Step( RunRequest(\u0026#34;下单\u0026#34;) .post(\u0026#34;/order\u0026#34;) .with_headers(**{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;$token\u0026#34;}) .with_json({\u0026#34;skuId\u0026#34;: \u0026#34;$skuId\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;$skuPrice\u0026#34;, \u0026#34;skuNum\u0026#34;: \u0026#34;$skuNum\u0026#34;, \u0026#34;totalPrice\u0026#34;: \u0026#34;$totalPrice\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.orderId\u0026#34;, \u0026#34;orderId\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), Step( RunRequest(\u0026#34;支付\u0026#34;) .post(\u0026#34;/pay\u0026#34;) .with_headers(**{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;$token\u0026#34;}) .with_json({\u0026#34;orderId\u0026#34;: \u0026#34;$orderId\u0026#34;, \u0026#34;payAmount\u0026#34;: \u0026#34;6.9\u0026#34;}) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) .assert_equal(\u0026#34;body.success\u0026#34;, \u0026#34;true\u0026#34;) ), ] 用户手册 https://dongfanger.gitee.io/blog/chapters/tep.html\n联系我 https://dongfanger.gitee.io/blog/more.html\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000008-tep%E9%9B%86%E6%88%90httprunner%E4%B8%8Eflask%E5%AE%9E%E7%8E%B0%E5%BC%80%E7%AE%B1%E5%8D%B3%E7%94%A8/","summary":"大家好，我是刚哥。","title":"tep集成HttpRunner与Flask实现开箱即用"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n热爱Python Python是Guido van Rossum设计出来的让使用者觉得如沐春风的一门编程语言。2020年11月12日，64岁的Python之父宣布由于退休生活太无聊，自己决定加入Microsoft的DevDiv Team，致力于“确保更好地使用Python”。尽管在国内有些声音在Diss着Python，认为它太简单，只是个脚本语言，但是它的发明者对Python的热情，仍然激励着我们坚持对Python的热爱。\n龟叔是所有编程语言发明者当中头发最多的这位。\n奇迹时刻 collection.len()是面向对象语言的写法，len(collection)是Python语言的写法，这种风格叫做Pythonic。从前者到后者，就像变魔术一样，一瞬间让人眼前一亮。这个魔术就是Python魔法方法，或者叫双下方法，它是用双下划线开头和双下划线结尾的特殊方法，比如obj[key]，Python解释器实际上会转换成obj.__getitem__(key)来运行，但是使用者并无感知。\n__getitem__和__len__ __getitem__用来获取数据，__len__用来返回长度，这2个魔法方法是Python基础，我们通过一副扑克牌来了解：\nimport collections # 定义一副牌 Card = collections.namedtuple(\u0026#39;Card\u0026#39;, [\u0026#39;rank\u0026#39;, \u0026#39;suit\u0026#39;]) class FrenchDeck: # 大小 ranks = [str(n) for n in range(2, 11)] + list(\u0026#39;JQKA\u0026#39;) # 花色 suits = \u0026#39;spades diamonds clubs hearts\u0026#39;.split() def __init__(self): # 生成一副牌 self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks] def __len__(self): return len(self._cards) def __getitem__(self, position): return self._cards[position] 本来我们对这副牌什么都不能做，但是由于实现了__len__，可以使用len()函数查看有多少张牌：\n\u0026gt;\u0026gt;\u0026gt; len(deck) 52 由于实现了__getitem__，可以使用中括号索引取值：\n\u0026gt;\u0026gt;\u0026gt; deck[0] Card(rank=\u0026#39;2\u0026#39;, suit=\u0026#39;spades\u0026#39;) 能进行切片：\n\u0026gt;\u0026gt;\u0026gt; deck[:3] [Card(rank=\u0026#39;2\u0026#39;, suit=\u0026#39;spades\u0026#39;), Card(rank=\u0026#39;3\u0026#39;, suit=\u0026#39;spades\u0026#39;), Card(rank=\u0026#39;4\u0026#39;, suit=\u0026#39;spades\u0026#39;)] \u0026gt;\u0026gt;\u0026gt; deck[12::13] [Card(rank=\u0026#39;A\u0026#39;, suit=\u0026#39;spades\u0026#39;), Card(rank=\u0026#39;A\u0026#39;, suit=\u0026#39;diamonds\u0026#39;), Card(rank=\u0026#39;A\u0026#39;, suit=\u0026#39;clubs\u0026#39;), Card(rank=\u0026#39;A\u0026#39;, suit=\u0026#39;hearts\u0026#39;)] 能迭代：\n\u0026gt;\u0026gt;\u0026gt; for card in deck: # doctest: +ELLIPSIS ... print(card) Card(rank=\u0026#39;2\u0026#39;, suit=\u0026#39;spades\u0026#39;) Card(rank=\u0026#39;3\u0026#39;, suit=\u0026#39;spades\u0026#39;) Card(rank=\u0026#39;4\u0026#39;, suit=\u0026#39;spades\u0026#39;) ... 发现没有，魔法方法是可以用来装B的！别人写个类只能get、set，你写个类还能花式炫技，666。\nPython魔法方法是给Python解释器使用的，一般不需要直接调用，Python会自己去调，比如把len(my_object)写成my_object.__len__()，就弄巧成拙了。\n魔法方法实现运算符 前面例子实现了取值和长度，接着再看一个例子，使用__repr__、__abs__、__bool__、__add__、__mul__，实现运算符：\nfrom math import hypot # 二维向量 class Vector: def __init__(self, x=0, y=0): self.x = x self.y = y # 表达式 def __repr__(self): return \u0026#39;Vector(%r, %r)\u0026#39; % (self.x, self.y) # 绝对值 def __abs__(self): return hypot(self.x, self.y) # 布尔值 def __bool__(self): return bool(abs(self)) # 加法 def __add__(self, other): x = self.x + other.x y = self.y + other.y return Vector(x, y) #乘法 def __mul__(self, scalar): return Vector(self.x * scalar, self.y * scalar) __add__实现了加法：\n\u0026gt;\u0026gt;\u0026gt; v1 = Vector(2, 4) \u0026gt;\u0026gt;\u0026gt; v2 = Vector(2, 1) \u0026gt;\u0026gt;\u0026gt; v1 + v2 Vector(4, 5) __abs__实现了绝对值：\n\u0026gt;\u0026gt;\u0026gt; v = Vector(3, 4) \u0026gt;\u0026gt;\u0026gt; abs(v) 5.0 __mul__实现了乘法：\n\u0026gt;\u0026gt;\u0026gt; v * 3 Vector(9, 12) __repr__实现了对象的字符串表示：\nVector(4, 5) 否则得到的字符串可能是\u0026lt;Vector object at 0x10e100070\u0026gt;。\n__bool__实现了布尔值：\nif Vector(4, 5): return True 其他魔法方法 一篇文章是讲不完魔法方法的，我们会在后续文章中，继续探讨如何使用和实现它们。\nTips 本小节内容是我看《流畅的Python》第一遍时记录的知识点：\ncollections.namedtuple可以用来创建只有少数属性但没有方法的对象，比如\nbeer_card = Card(\u0026#39;7\u0026#39;, \u0026#39;diamonds\u0026#39;) random.choice和random.sample不一样的地方在于，sample是返回序列，choice是返回元素，当使用sample(list, 1)[0]的时候，不如直接使用choice(list)。\n特殊方法的存在是为了被Python解释器调用的。\nPyVarObject是表示内存中长度可变的内置对象的C语言结构体。list或str或bytearray的__len__实际上返回的PyVarObject.ob_size属性，这个比调用一个方法要快的多。\nlen之所以不是一个普通方法，是为了让python自带的数据结构可以走后门，abs也是同理。\n很多时候调用__init__方法的目的是，在你自己的子类的__init__方法中调用超类的构造器。\nabs，如果输入是整数或者浮点数，它返回的是输入值的绝对值；如果输入是复数，那么返回这个复数的模。\n__repr__和__str__二选一的话，__repr__更好，因为如果一个对象没有__str__函数，解释器会用__repr__作为替代。\npython对象的一个基本要求就是它得有合理的字符串表示形式，这就是数据模型中存在特殊方法__repr__和__str__的原因。\n为了判定一个值x为真还是为假，python会调用bool(x)，它的背后是调用x.__bool__()。如果不存在，就会调用x.__len__()，返回0为Flase，非0为True。\npython通过运算符重载这一模式提供了丰富的数值类型，除了内置那些，还有decimal.Decimal和fractions.Fraction。\n小结 本文是Python进阶系列开篇，参考《流畅的Python》序章改写而成。原书内容有深度有广度，我选择了其中的魔法方法知识点，作为切入，循序渐进学习。其实书中这一章节的副标题是“数据模型”，它是个什么概念呢？\n系列文章会不定期同步到在线电子书中，欢迎访问查看：\nhttps://dongfanger.gitee.io/blog/\n参考资料：\n《流畅的Python》\nhttps://docs.python.org/3/reference/datamodel.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000008-python%E9%AD%94%E6%B3%95%E6%96%B9%E6%B3%95%E6%8E%A8%E5%BC%80python%E8%BF%9B%E9%98%B6%E5%AD%A6%E4%B9%A0%E5%A4%A7%E9%97%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"热爱python\"\u003e热爱Python\u003c/h1\u003e\n\u003cp\u003ePython是Guido van Rossum设计出来的让使用者觉得如沐春风的一门编程语言。2020年11月12日，64岁的Python之父宣布由于退休生活太无聊，自己决定加入Microsoft的DevDiv Team，致力于“确保更好地使用Python”。尽管在国内有些声音在Diss着Python，认为它太简单，只是个脚本语言，但是它的发明者对Python的热情，仍然激励着我们坚持对Python的热爱。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e龟叔是所有编程语言发明者当中头发最多的这位。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1e010527d9ca4eb9b82b300e155f3f3a.jpg\"\u003e\u003c/p\u003e\n\u003ch1 id=\"奇迹时刻\"\u003e奇迹时刻\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003ecollection.len()\u003c/code\u003e是面向对象语言的写法，\u003ccode\u003elen(collection)\u003c/code\u003e是Python语言的写法，这种风格叫做\u003ccode\u003ePythonic\u003c/code\u003e。从前者到后者，就像变魔术一样，一瞬间让人眼前一亮。这个魔术就是Python魔法方法，或者叫双下方法，它是用双下划线开头和双下划线结尾的特殊方法，比如\u003ccode\u003eobj[key]\u003c/code\u003e，Python解释器实际上会转换成\u003ccode\u003eobj.__getitem__(key)\u003c/code\u003e来运行，但是使用者并无感知。\u003c/p\u003e\n\u003ch1 id=\"__getitem__和__len__\"\u003e\u003ccode\u003e__getitem__\u003c/code\u003e和\u003ccode\u003e__len__\u003c/code\u003e\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003e__getitem__\u003c/code\u003e用来获取数据，\u003ccode\u003e__len__\u003c/code\u003e用来返回长度，这2个魔法方法是Python基础，我们通过一副扑克牌来了解：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003ecollections\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e# 定义一副牌\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCard \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e collections\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003enamedtuple\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Card\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;rank\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;suit\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eFrenchDeck\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 大小\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ranks \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003estr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e n \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003erange\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e11\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elist\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;JQKA\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 花色\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    suits \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;spades diamonds clubs hearts\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003esplit\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__init__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#57606a\"\u003e# 生成一副牌\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cards \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eCard\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e suit \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003esuits\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                        \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e rank \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eranks\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__len__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cards\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__getitem__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e position\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003e_cards\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eposition\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e本来我们对这副牌什么都不能做，但是由于实现了\u003ccode\u003e__len__\u003c/code\u003e，可以使用\u003ccode\u003elen()\u003c/code\u003e函数查看有多少张牌：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edeck\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e52\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e由于实现了\u003ccode\u003e__getitem__\u003c/code\u003e，可以使用中括号索引取值：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e deck\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCard\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;2\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;spades\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e能进行切片：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e deck\u003cspan style=\"color:#1f2328\"\u003e[:\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eCard\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;2\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;spades\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e Card\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;3\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;spades\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e Card\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;4\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;spades\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/span\u003e deck\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e12\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e::\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e13\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eCard\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;spades\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e Card\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;diamonds\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e Card\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;clubs\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e Card\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erank\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e suit\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;hearts\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e能迭代：\u003c/p\u003e","title":"魔法方法推开Python进阶学习大门"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n构建知识体系，首先需要具备写作习惯和写作能力。笔记的重点是记录，需要养成记笔记的习惯，把知识保存下来。良好的写作能力，可以将复杂的概念用文字清晰表达，具有逻辑性和条理性。知识体系的展现形式，不只是脑图，还包括文档、幻灯片、流程图等，既要会写又要会画，通过多种形式将知识呈现出来。\n其次知识体系需要一定的设计方法，组织和系统化知识，以便于理解、记忆和应用。怎么设计？岗位能力树，从素质、能力、技能、知识4个方面来梳理。素质是指个人的内在品质，如性格、态度、价值观、道德观等，有人说自己细心、耐心、有责任心，适合做测试，就是具备测试岗位要求的基本素质。能力是指个人完成某项任务或解决问题的才能，如逻辑思维能力、学习能力、创新能力，“有能力”是一种综合评价。技能是指个人通过学习和练习的特定操作方法和技巧，如测试技能、开发技能等，技能可以通过学习和实践不断提高。知识是指个人对于事物的了解和认识，如测试基础知识、计算机知识等。\n最后知识体系需要使用工具来管理。第一种方案是在线文档，比如飞书、语雀等。第二种方案，也是我在使用的方案是本地文件和Git，本地文件通过Obsidian软件管理，既能创建Markdown文字笔记，也可以链接XMind、PPT、Excel等资源文件。\n构建知识体系是一个动态的、持续的过程，需要不断地学习、思考和实践。构建知识体系，不是做知识的搬运工，把知识存放在电脑上就此作罢。我们要把知识记忆到大脑里，成为我们思想的一部分。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000008-%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91%E4%BD%93%E7%B3%BB/","summary":"构建知识体系，首先需要具备写作习惯和写作能力。笔记的重点是记录，需要养成记笔记的习惯，把知识保存下来。良好的写作能力，可以将复杂的概念用文字清晰表达，具有逻辑性和条理性。知识体系的展现形式，不只是脑图，还包括文档、幻灯片、流程图等，既要会写又要会画，通过多种形式将知识呈现出来。","title":"如何构建“测试开发体系”"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n4.1 提示词工程 提示词工程，不是简单的提问，而是一套涵盖指令设计、上下文注入、角色设定和格式控制的综合性技术。\n4.1.1 优秀提示词的核心实践 明确角色与目标\n范例：“假设你是一位拥有10年经验的市场营销总监，请为一款新型智能手表起草一份面向年轻专业人士的产品介绍文案。”\n提供清晰指令与完整上下文\n范例：“请总结以下技术文章的核心观点（不超过200字），并列出其中提到的三个关键数据。不要添加个人评论。文章内容如下：[在此处粘贴文章]”\n运用范例进行引导\n范例：“请分析以下句子的情感及关键对象。范例：输入：\u0026lsquo;我喜欢这部电影的剧情。\u0026lsquo;输出：{\u0026lsquo;sentiment\u0026rsquo;: \u0026lsquo;positive\u0026rsquo;, \u0026lsquo;aspect\u0026rsquo;: \u0026lsquo;plot\u0026rsquo;}。现在，请分析这个句子：\u0026lsquo;这台相机的画质很棒，但电池续航太短。\u0026rsquo;”\n定义结构化输出格式\n范例：“请将你的回答组织成JSON格式，包含\u0026rsquo;product_name\u0026rsquo;和\u0026rsquo;key_features\u0026rsquo;两个字段，其中key_features是一个包含至少三项功能的数组。”\n4.1.2 提示词工程的固有局限性 应对动态及长流程任务的局限\n应对模型固有的无状态特性\n无法突破模型固有的知识边界\n长上下文中的信息利用不均衡\n正是因为提示词工程有诸多局限性，上下文工程（Context Engineering）应运而生。\n4.2 上下文工程 上下文工程的核心理念，在于将AI应用开发的焦点从优化单次交互的指令，转向为模型的每一次推理动态构建一个完整、准确且高效的认知环境。它不再局限于设计静态的提示词模板，而是致力于创建一个系统，确保模型在执行任务时，能够实时获取并利用完成该任务所需的所有相关信息，包括外部知识、历史记忆、可用工具及执行环境。\n4.2.1 上下文工程与提示词工程的核心区别 4.2.2 上下文工程的关键组成部分 外部知识库的动态供给\n长期与短期记忆系统\n工具与能力的扩展\n运行时的上下文管理\n4.3 RAG技术原理与挑战 4.3.1 RAG的基础范式：三阶段工作流 1、第一阶段：索引（Indexing）\n将原始的外部知识转化为机器可高效检索的格式。文档解析与提取，文本分块（Chunking），语义向量化（Embedding），构建向量索引库。\n2、第二阶段：检索（Retrieval）\n根据用户的自然语言查询，从已构建好的向量索引库中，快速、准确地找出最相关的N个知识块（Top-K）。\n3、第三阶段：生成（Generation）\n将前一阶段检索到的多个相关知识块与用户的原始查询进行整合，构建一个增强的提示词（Augmented Prompt）。比如：\n4.3.2 RAG在工程化落地中面临的挑战 知识单元的完整性与信息密度的抉择\n难以精准捕捉模糊、多样的用户意图\n召回匹配时难以兼顾语义相关性与关键词精确性\n需要探索如何在检索精度与完整性之间取得平衡\n应对需要多知识点综合推理的查询实现\n4.4 RAG系统优化实践：索引构建 4.4.1 基础文本分块策略 1、固定大小分块\n2、基于句子或段落分块\n3、语义分块：引入Embedding技术计算相邻文本单元间的语义相似度，在相似度得分显著下降的位置进行切分，从而识别出文本中的语义边界。\n4.4.2 高级分块优化策略 1、滑动窗口（Sliding Window）\n2、小块检索，大块生成（Small-to-Big）\n3、元数据标注与过滤\n4.4.3 处理复杂文档的索引策略 文档除了纯文本，还包括表格、图片、代码、脚注等复杂结构。\n1、层次化索引\n为文档的顶层机构（如章节标题或摘要）创建摘要索引，为底层的详细内容创建区块索引。\n2、多模态内容的处理与表示\n文本：采用语义分块策略。\n表格：提取结构化数据（如转化为Markdown或JSON格式），或对表格进行摘要描述，并单独索引。\n图像：使用OCR技术提取图中文字，并结合图像描述模型（Image Captioning）生成对图像内容的语义描述，共同作为该图像的索引内容。\n综上所述，索引构建是一个需要根据数据源的特性、结构和应用场景，进行策略选择与组合优化的过程。\n4.5 RAG系统优化实践：检索流程 4.5.1 查询理解与增强 将用户原始、模糊的查询，转化为机器更容易理解、更适合下游检索的结构化或增强型查询。\n1、查询改写与扩充\n采用改写-检索-阅读（Rewrite-Retrieve-Read）框架，在检索前增加一个“改写”步骤。比如将“AI原生应用咋回事”改写为“AI原生应用的核心特征有哪些”。\n2、复杂问题分解\n利用LLM识别出原始问题中隐藏的、需要独立解答的子任务。比如“对比A产品和B产品在性能和成本上的差异”分解为四个子查询：“A产品的性能是什么”、“B产品的性能是什么”、“A产品的成本是多少”、“B产品的成本是多少”。\n3、假设性文档嵌入（HyDE）\n不直接用用户的简短查询去匹配冗长的文档，而是先利用LLM生成一个“假设性的”理想答案文档，然后用这个内容更丰富、与真实答案在形式和语义上更接近的假设性文档去进行向量检索。\n4.5.2 多路召回与混合检索 从索引库中找出所有相关的候选文档。\n1、向量检索与关键词检索的协同\n系统并行执行这两种检索，各自生成一个候选文档列表。\n2、倒数排名融合（RRF）\n一个文档如果在多个不同的检索结果列表里都排在靠前的位置，那么它很可能是一个非常相关的文档。\n4.5.3 结果精炼与上下文重组 针对候选文档池，进行结果精炼和上下文重组，提高生成答案质量。\n1、重排序（Rerank）\n使用一个更强大、更复杂的模型（如交叉编码器）来对查询与每个候选文档进行深度交互，从而计算出一个比向量相似度更精准的相关性分数。\n2、自动合并检索\n在数据处理阶段就建立起文档块之间的层级关系，并在检索后智能地将离散的子块合并回更完整的父块。\n4.6 RAG的未来方向 从一个被动的静态检索工具，发展为一个主动的、多维的、具备推理能力的智能知识基础设施。\n4.6.1 Agentic RAG 在传统RAG中，检索是一个被动、前置的步骤；而在Agentic RAG中，检索被封装成一个智能体可以自主调用、评估和迭代的工具（Tool）。智能体基于LLM决定是否检索、何时检索、检索什么以及如何利用检索结果。\n4.6.2 多模态RAG 通过统一的嵌入与检索机制，将文本、图像、语音等不同类型的内容纳入同一个语义空间。比如以文搜图、文搜视频等。\n4.6.3 知识图谱与RAG的融合 通过将实体、关系和属性建模为图结构，与传统的向量、关键词检索协同，实现了双规驱动的范式。在法律、医疗、金融等强调逻辑一致性与合规性的场景中特别有价值。\n总结，Agentic RAG提供了任务导向的自主规划与自我校验能力，多模态RAG扩展了外部大脑的感知边界，Graph RAG增强了结构化推理与可追溯合规的能力。\n4.7 上下文管理与记忆系统 4.7.1 核心挑战：有限的工作记忆 LLM的上下文窗口是智能体唯一的工作记忆，这个空间有限、有成本且存在性能瓶颈。如果将所有历史交互、工具调用结果、中间思考步骤全部塞入上下文，会导致成本与性能下降、中间遗忘、信息噪声等问题。\n4.7.2 运行时上下文处理的四大策略 1、写入：将信息保存到外部记忆\n2、选择：将相关信息拉入上下文\n3、压缩：为上下文瘦身\n4、隔离：拆分与保护上下文\n4.7.3 构建智能体的多级记忆系统 1、短期记忆：管理当前对话\n2、长期记忆：沉淀持久化知识\n3、记忆转换时机，包括对话自然结束时、识别重要特征时、定期摘要\n总结，运行时上下文是战术层面的操作，它决定了智能体在此时此刻如何最高效地利用其工作记忆；而多级记忆系统则是战略层面的支撑，它为智能体提供了历史感和个性化的基础。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005004-ai%E7%99%BD%E7%9A%AE%E4%B9%A6%E4%B8%8A%E4%B8%8B%E6%96%87%E5%B7%A5%E7%A8%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"41-提示词工程\"\u003e4.1 提示词工程\u003c/h1\u003e\n\u003cp\u003e提示词工程，不是简单的提问，而是一套涵盖指令设计、上下文注入、角色设定和格式控制的综合性技术。\u003c/p\u003e\n\u003ch2 id=\"411-优秀提示词的核心实践\"\u003e4.1.1 优秀提示词的核心实践\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e明确角色与目标\u003c/p\u003e\n\u003cp\u003e范例：“假设你是一位拥有10年经验的市场营销总监，请为一款新型智能手表起草一份面向年轻专业人士的产品介绍文案。”\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e提供清晰指令与完整上下文\u003c/p\u003e\n\u003cp\u003e范例：“请总结以下技术文章的核心观点（不超过200字），并列出其中提到的三个关键数据。不要添加个人评论。文章内容如下：[在此处粘贴文章]”\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e运用范例进行引导\u003c/p\u003e\n\u003cp\u003e范例：“请分析以下句子的情感及关键对象。范例：输入：\u0026lsquo;我喜欢这部电影的剧情。\u0026lsquo;输出：{\u0026lsquo;sentiment\u0026rsquo;: \u0026lsquo;positive\u0026rsquo;, \u0026lsquo;aspect\u0026rsquo;: \u0026lsquo;plot\u0026rsquo;}。现在，请分析这个句子：\u0026lsquo;这台相机的画质很棒，但电池续航太短。\u0026rsquo;”\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e定义结构化输出格式\u003c/p\u003e\n\u003cp\u003e范例：“请将你的回答组织成JSON格式，包含\u0026rsquo;product_name\u0026rsquo;和\u0026rsquo;key_features\u0026rsquo;两个字段，其中key_features是一个包含至少三项功能的数组。”\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"412-提示词工程的固有局限性\"\u003e4.1.2 提示词工程的固有局限性\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e应对动态及长流程任务的局限\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e应对模型固有的无状态特性\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e无法突破模型固有的知识边界\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e长上下文中的信息利用不均衡\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e正是因为提示词工程有诸多局限性，上下文工程（Context Engineering）应运而生。\u003c/p\u003e\n\u003ch1 id=\"42-上下文工程\"\u003e4.2 上下文工程\u003c/h1\u003e\n\u003cp\u003e上下文工程的核心理念，在于将AI应用开发的焦点从优化单次交互的指令，转向为模型的每一次推理动态构建一个完整、准确且高效的认知环境。它不再局限于设计静态的提示词模板，而是致力于创建一个系统，确保模型在执行任务时，能够实时获取并利用完成该任务所需的所有相关信息，包括外部知识、历史记忆、可用工具及执行环境。\u003c/p\u003e\n\u003ch2 id=\"421-上下文工程与提示词工程的核心区别\"\u003e4.2.1 上下文工程与提示词工程的核心区别\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c0347935ea124e78a6c12dddb359207f.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"422-上下文工程的关键组成部分\"\u003e4.2.2 上下文工程的关键组成部分\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e外部知识库的动态供给\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e长期与短期记忆系统\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e工具与能力的扩展\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e运行时的上下文管理\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"43-rag技术原理与挑战\"\u003e4.3 RAG技术原理与挑战\u003c/h1\u003e\n\u003ch2 id=\"431-rag的基础范式三阶段工作流\"\u003e4.3.1 RAG的基础范式：三阶段工作流\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e1、第一阶段：索引（Indexing）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e将原始的外部知识转化为机器可高效检索的格式。文档解析与提取，文本分块（Chunking），语义向量化（Embedding），构建向量索引库。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2、第二阶段：检索（Retrieval）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e根据用户的自然语言查询，从已构建好的向量索引库中，快速、准确地找出最相关的N个知识块（Top-K）。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3、第三阶段：生成（Generation）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e将前一阶段检索到的多个相关知识块与用户的原始查询进行整合，构建一个增强的提示词（Augmented Prompt）。比如：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/2011be172cd348e6af3e22fd7d2a5e25.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"432-rag在工程化落地中面临的挑战\"\u003e4.3.2 RAG在工程化落地中面临的挑战\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e知识单元的完整性与信息密度的抉择\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e难以精准捕捉模糊、多样的用户意图\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e召回匹配时难以兼顾语义相关性与关键词精确性\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e需要探索如何在检索精度与完整性之间取得平衡\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e应对需要多知识点综合推理的查询实现\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"44-rag系统优化实践索引构建\"\u003e4.4 RAG系统优化实践：索引构建\u003c/h1\u003e\n\u003ch2 id=\"441-基础文本分块策略\"\u003e4.4.1 基础文本分块策略\u003c/h2\u003e\n\u003cp\u003e1、固定大小分块\u003c/p\u003e\n\u003cp\u003e2、基于句子或段落分块\u003c/p\u003e\n\u003cp\u003e3、语义分块：引入Embedding技术计算相邻文本单元间的语义相似度，在相似度得分显著下降的位置进行切分，从而识别出文本中的语义边界。\u003c/p\u003e\n\u003ch2 id=\"442-高级分块优化策略\"\u003e4.4.2 高级分块优化策略\u003c/h2\u003e\n\u003cp\u003e1、滑动窗口（Sliding Window）\u003c/p\u003e\n\u003cp\u003e2、小块检索，大块生成（Small-to-Big）\u003c/p\u003e\n\u003cp\u003e3、元数据标注与过滤\u003c/p\u003e\n\u003ch2 id=\"443-处理复杂文档的索引策略\"\u003e4.4.3 处理复杂文档的索引策略\u003c/h2\u003e\n\u003cp\u003e文档除了纯文本，还包括表格、图片、代码、脚注等复杂结构。\u003c/p\u003e","title":"上下文工程"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n3.1 智能体的定义与主流开发范式 3.1.1 什么是智能体 智能体（Agent）可以理解为一个具备自主理解、规划、记忆和工具使用能力的数字化实体。\n我们在前面了解到LLM、MCP，它们和Agent是啥关系呢？LLM是“大脑”，Agent是“身体”，Agent通过MCP协议访问外部能力（Tools / Resources）。\n【纯LLM交互】 用户：帮我分析main.py文件 LLM：我无法直接访问文件，请您将文件内容粘贴给我 【Agent交互】 用户：帮我分析main.py文件 Agent：我来读取并分析这个文件 → 调用文件读取工具 → 分析代码结构 → 提供详细分析报告 智能体让AI从一个只会内容生成的语言模型，进化成一个具备自主规划能力的行动者。\n3.1.2 智能体的主流开发范式 简单LLM应用：直接调用模型API实现内容生成\n单智能体：增加RAG、Tool、Memory等，让模型具备与特定环境交互的能力\n工作流：将应用拆分，形成多个独立子智能体，再按照预定义的流程编排起来\n多智能体系统：相比于工作流，多智能体系统采用的是模型驱动的、对话式的流程编排，各个子智能体具备更多的自主决策权\n3.1.3 单智能体 典型的单智能体架构如下：\n单智能体非常依赖底层模型能力。\n3.1.4 工作流 1、Chain - 链式工作流\n每一步由一个独立的LLM调用完成，并将上一阶段的输出作为下一阶段的输入。\n2、Routing - 路由工作流\n对输入进行分类，将它们分派到专门设计的下游任务或处理路径。\n3.1.5 多智能体系统 由多个具备一定自治能力的Agent组成，每个Agent有自己的角色、能力或工具使用范围。它们之间可以通过消息或上下文进行交互，协同完成复杂目标。\n3.2 开发一个简单的智能体 3.2.1 智能体框架选型 Python：LangChain、LlamaIndex、AutoGen、CrewAI，开源的Google ADK、Strands等，阿里云开源的AgentScope等\nJava：Spring AI Alibaba等\n这本白皮书是阿里出的，后面就用Spring AI Alibaba为例进行介绍。\n3.2.2 Spring AI Alibaba中的Agent定义 React Agent，采用ReAct模式，将推理（Reasoning）和行动（Action）交错进行，形成一个动态的“思考-行动-观察”循环。\n思考（Thought）：LLM分析当前的任务目标、已有的信息和可用的工具，然后生成一个关于下一步应该做什么的推理过程或计划。这个思路过程通常会以自然语言文本展示，解释了它的决策逻辑。\n行动（Action）：智能体决定调用的外部工具（如搜索引擎、数据库查询API或自定义函数）。它会生成结构化的指令，明确指出要调用的工具名称和所需的参数。\n观察（Observation）：应用程序执行工具调用，并将返回的结果（如搜索结果、API响应或函数返回值）作为观察信息反馈给智能体。\n循环迭代：智能体收到观察结果，和之前的“思考”、任务目标结合，进入新一轮的思考阶段。它会评估上一步行动的结果是否使其更接近最终目标，并据此规划下一步的行动。\n3.2.3 示例代码与Agentic API 1、快速定义一个简单的Agent\n2、引导Agent的行为（设置Prompt提示词）\ninstruction包括以下信息：\n核心任务或目标\n性格或角色\n行为约束\n如何及何时使用工具\n期望的输出格式\n第四章上下文工程将介绍更多instruction编写技巧。\n3、更多高级设置：为Agent配置工具、模型参数等\nLLM会根据当前的对话内容和自身的指令，利用工具的函数/工具名称、描述（来自文档字符串或description字段）以及参数结构（Schemas）来决定调用哪一个工具。\n3.3 工作流与多智能体 3.3.1 Spring AI Alibab中的多智能体类型 总体上分为三种Agent类型：\nReactAgent：基本智能体\nFlowAgent：包含多个ReactAgent，按照特定流程协作\nSequentialAgent 串行\nParallelAgent 并行\nLoopAgent 循环\nLlmRoutingAgent 大模型决策\nA2ARemoteAgent：分布式Agent\n3.3.2 工作流 - SequentialAgent Agent依次执行，上一个Agent的输出会作为下一个Agent的输入。\n示例，开发一个文章协作助手的智能体：\n先定义了2个ReactAgent，然后定义了1个SequentialAgent将它们串联起来。\n3.3.3 工作流 - ParallelAgent 并行执行的工作流模式。\n示例，开发一个智能搜索助手的智能体：\n3.3.4 工作流 - LoopAgent 在指定的迭代次数内重复运行一系列代理，或者直到满足终止条件。\n3.3.5 多智能体系统 - LlmRoutingAgent 通过模型决策下一个子智能体走向。\n内置prompt：\n具体示例代码：\n3.4 从单进程到分布式部署 通过将Sub-Agent进行独立部署和维护，同时多个Sub-Agent间通过远程调用代替内存调用的方式，实现各个Sub-Agent独立迭代和维护。\n3.4.1 什么是A2A协议 A2A协议，Agent2Agent，是Google在开源社区中推出的分布式智能体之间的统一通信协议。\n1、A2A协议中的角色\n用户（User）\nA2A客户端（Client Agent）\nA2A服务端（Remote Agent）\n2、A2A协议中的元素\n智能体卡片（Agent Card）\n任务（Task）\n消息（Message）\n产出物（Artifact）\n内容块（Part）\n3、A2A协议中的交互机制\n轮询（Polling）\n流式传输（Streaming）\n推送通知（Push Notification）\n3.4.2 A2A协议的工作流 1、发现A2A Server的AgentCard\n2、A2A授权和权限认证\n3、发起A2A请求\n同步请求\n流式请求\n3.4.3 基于Nacos A2A Registry的自动注册与发现 A2A工作流程的首个步骤就是进行远端Agent的AgentCard获取和发现。\n1、AgentCard的作用\nAgentCard是A2A Server的数字名片，为发现与交互提供必要信息，包括以下内容：\n身份信息：名称、描述、服务提供方。\n服务端点：远端Agent的访问URL，包含完整的域名/IP，端口，URI等。\n协议能力：支持的协议功能（如流式传输streaming或推送通知pushNotifications）。\n认证方式：交互所需的鉴权机制（如Bearer令牌、OAuth2）。\n技能列表：智能体可执行的任务或功能（AgentSkill对象），含技能ID、名称、描述、输入/输出模式及示例。\nA2A Client通过AgentCard判断远端Agent是否适配当前任务、如何构建请求以及安全通信方式。\n2、AgentCard的获取方式\n直接配置\n固定URI（Well-Known URI）\n另外还有第三种方式，注册中心。\n3.4.4 Nacos A2A注册中心 由注册中心维护AgentCard集合，客户端通过查询条件（如技能、标签、提供方）检索目标。\nNacos还支持对Agent的版本进行管理和快速回滚。Nacos除了作为A2A注册中心，还可以作为MCP服务的注册中心，以及Prompt的动态管理中心。\n3.5 消息驱动的智能体开发模式 AI云原生架构下的消息队列必须具备以下特点：\n支持长会话与大消息体的消息中枢。\n实现削峰填谷、定速消费的智能调度能力。\n提供优先级、权重控制的分级事件驱动机制。\n构建高可靠、可恢复的Agent编排引擎。\n3.5.1 消息模型提升AI通信效率 RocketMQ提出了一种创新的轻量化架构：\n核心理念是：为每个会话或问题，动态创建一个独立的轻量级主题（Lite-Topic）。\n以客户端与AI服务建立会话为例，系统自动创建一个以SessionID命名的专属队列，所有会话历史、上下文和中间结果均以消息形式在该主题中有序流转。\n这一架构提供了四大核心能力：\n百万级Lite-Topic支持：单集群管理百万级Lite-Topic，每个会话独立Topic，高并发下会话隔离，性能无损。\n全自动轻量管理：Lite-Topic按需动态创建，连接断开后自动回收。\n大消息体传输能力：支持数十MB以上消息，可承载长Prompt、图像、文档等。\n严格顺序消息保障：在单队列内保证消息有序，确保LLM流式输出的token顺序不乱。\n轻量级发送\nQueue的消息是某个Broker专属的，一个Lite-Topic的发送只会到一台Broker，而不是轮询发送。\n轻量级订阅\n以client_ID维度维护订阅关系，管理消费进度。新增互斥消费模式。TTL到期后自动删除订阅关系。\n消息分发策略\n客户端发起读请求不再指定Topic，而是Broker根据client_ID识别订阅关系，并返回多个Topic的多条消息。\n引入Topic ready set，在POP请求处理时直接访问就绪的topic。\n当订阅上线、新消息发送、消息ACK后仍有消息、order Lock释放时往Topic ready set进行add操作。\n3.5.2 基于消息驱动的智能化资源调度 核心能力：\n天然削峰填谷，保护AI算力：消息队列可缓存突发请求。\n定速消费，精准控制算力使用：支持为消费者组设置消费配额。\n优先级调度，实现智能资源分配：抢占式优先级，权重动态分配。\n3.6 基于统一元数据的AI协同开发模式 AI时代，可以基于统一元数据，所有交付物都使用HTML格式，每个角色的产出可以直接被下游角色使用，并且在开发全流程的每个环节都基于AI提效。\n统一元数据需要长期建设，包括设计系统、组件库、API规范、状态管理等。从建立需求即代码的团队协作基准开始，从组件契约标准化入手，逐步构建跨工具链的智能开发环境。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005003-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"31-智能体的定义与主流开发范式\"\u003e3.1 智能体的定义与主流开发范式\u003c/h1\u003e\n\u003ch2 id=\"311-什么是智能体\"\u003e3.1.1 什么是智能体\u003c/h2\u003e\n\u003cp\u003e智能体（Agent）可以理解为一个具备自主理解、规划、记忆和工具使用能力的数字化实体。\u003c/p\u003e\n\u003cp\u003e我们在前面了解到LLM、MCP，它们和Agent是啥关系呢？LLM是“大脑”，Agent是“身体”，Agent通过MCP协议访问外部能力（Tools / Resources）。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e【纯LLM交互】\n用户：帮我分析main.py文件\nLLM：我无法直接访问文件，请您将文件内容粘贴给我\n\n\n【Agent交互】\n用户：帮我分析main.py文件\nAgent：我来读取并分析这个文件\n      → 调用文件读取工具\n      → 分析代码结构\n      → 提供详细分析报告\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e智能体让AI从一个只会内容生成的语言模型，进化成一个具备自主规划能力的行动者。\u003c/p\u003e\n\u003ch2 id=\"312-智能体的主流开发范式\"\u003e3.1.2 智能体的主流开发范式\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e简单LLM应用：直接调用模型API实现内容生成\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e单智能体：增加RAG、Tool、Memory等，让模型具备与特定环境交互的能力\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e工作流：将应用拆分，形成多个独立子智能体，再按照预定义的流程编排起来\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e多智能体系统：相比于工作流，多智能体系统采用的是模型驱动的、对话式的流程编排，各个子智能体具备更多的自主决策权\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"313-单智能体\"\u003e3.1.3 单智能体\u003c/h2\u003e\n\u003cp\u003e典型的单智能体架构如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/dfdbcb60c81c415ca4763d131989544d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e单智能体非常依赖底层模型能力。\u003c/p\u003e\n\u003ch2 id=\"314-工作流\"\u003e3.1.4 工作流\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e1、Chain - 链式工作流\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/349d721c73e24cccb1c6c9a580b50483.png\"\u003e\u003c/p\u003e\n\u003cp\u003e每一步由一个独立的LLM调用完成，并将上一阶段的输出作为下一阶段的输入。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2、Routing - 路由工作流\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4d806716b58b47e192a3c249d4c8c721.png\"\u003e\u003c/p\u003e\n\u003cp\u003e对输入进行分类，将它们分派到专门设计的下游任务或处理路径。\u003c/p\u003e\n\u003ch2 id=\"315-多智能体系统\"\u003e3.1.5 多智能体系统\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/09efbc8c281c4735ba94b14d6fcc05a7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e由多个具备一定自治能力的Agent组成，每个Agent有自己的角色、能力或工具使用范围。它们之间可以通过消息或上下文进行交互，协同完成复杂目标。\u003c/p\u003e\n\u003ch1 id=\"32-开发一个简单的智能体\"\u003e3.2 开发一个简单的智能体\u003c/h1\u003e\n\u003ch2 id=\"321-智能体框架选型\"\u003e3.2.1 智能体框架选型\u003c/h2\u003e\n\u003cp\u003ePython：LangChain、LlamaIndex、AutoGen、CrewAI，开源的Google ADK、Strands等，阿里云开源的AgentScope等\u003c/p\u003e\n\u003cp\u003eJava：Spring AI Alibaba等\u003c/p\u003e\n\u003cp\u003e这本白皮书是阿里出的，后面就用Spring AI Alibaba为例进行介绍。\u003c/p\u003e\n\u003ch2 id=\"322-spring-ai-alibaba中的agent定义\"\u003e3.2.2 Spring AI Alibaba中的Agent定义\u003c/h2\u003e\n\u003cp\u003eReact Agent，采用ReAct模式，将推理（Reasoning）和行动（Action）交错进行，形成一个动态的“思考-行动-观察”循环。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e1ff37e35ad04f3690aa56cafc0f3cee.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e思考（Thought）\u003c/strong\u003e：LLM分析当前的任务目标、已有的信息和可用的工具，然后生成一个关于下一步应该做什么的推理过程或计划。这个思路过程通常会以自然语言文本展示，解释了它的决策逻辑。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e行动（Action）\u003c/strong\u003e：智能体决定调用的外部工具（如搜索引擎、数据库查询API或自定义函数）。它会生成结构化的指令，明确指出要调用的工具名称和所需的参数。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e观察（Observation）\u003c/strong\u003e：应用程序执行工具调用，并将返回的结果（如搜索结果、API响应或函数返回值）作为观察信息反馈给智能体。\u003c/p\u003e","title":"AI应用开发框架"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n目录 简介 安装Allure Allure测试报告的结构 Java TestNG集成Allure Report Python Pytest集成Allure Report 简介 假如你想让测试报告变得漂亮一点，那你一定会在搜索引擎中找到Allure测试报告的。Allure官方给出了个在线网站，访问以下链接就能一睹为快：\nhttps://demo.qameta.io/allure/\n可以在左下角切换为中文报告：\n如此好看的测试报告，哪个老板不喜欢呢？\n安装Allure 1.在GitHub releases：\nhttps://github.com/allure-framework/allure2/releases\n或者Maven Central：\nhttps://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/\n下载安装包并解压。\n2.找到bin目录，Windows使用allure.bat，Unix使用allure：\n3.为了后续方便使用，可以把bin完整路径添加到系统环境变量PATH中，然后在命令行直接输入allure命令。\n官方也给出了通过Linux的ppa、Mac的brew、Windows的scoop的安装方式，不过对于咱们老百姓来说，不如手动下载包来安装实在。\n安装后就可以检查是否安装成功了：\n$ allure --version 2.17.2 因为Allure是用Java写的，所以如果无法运行，那么要检查下有没有安装Java。\nAllure测试报告的结构 Overview Categories Suites Graphs Timeline Behaviors Packages Java TestNG集成Allure Report 集成 在Maven的pom.xml中添加：\n\u0026lt;properties\u0026gt; \u0026lt;aspectj.version\u0026gt;1.8.10\u0026lt;/aspectj.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.qameta.allure\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;allure-testng\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;LAST_VERSION\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-surefire-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.20\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;argLine\u0026gt; -javaagent:\u0026#34;${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar\u0026#34; \u0026lt;/argLine\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.aspectj\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aspectjweaver\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${aspectj.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 然后执行命令：\n$ mvn clean test 在target/allure-results目录就会生成allure测试报告，再执行命令：\n$ allure serve target/allure-results Allure会创建一个Jetty服务器，拉起默认浏览器打开测试报告。\n@Description 添加测试描述：\npackage my.company.tests; import org.junit.Test; import io.qameta.allure.Description; @Test public class MyTests { @Test @Description(\u0026#34;Some detailed test description\u0026#34;) public void testSomething() throws Exception { ... } } @Step 添加测试步骤描述，除了文字，Allure2通过反射机制，能够在@Step中获取参数，比如：\npublic class User { private String name; private String password; ... } import io.qameta.allure.Step; ... @Step(\u0026#34;Type {user.name} / {user.password}.\u0026#34;) public void loginWith(User user) { ... } @Attachment 添加测试报告的附件，可以是String或byte[]类型，不是显式的话Allure会调用toString()尝试隐式转换：\nimport io.qameta.allure.Attachment; ... @Attachment public String performedActions(ActionSequence actionSequence) { return actionSequence.toString(); } @Attachment(value = \u0026#34;Page screenshot\u0026#34;, type = \u0026#34;image/png\u0026#34;) public byte[] saveScreenshot(byte[] screenShot) { return screenShot; } type用于指定MIME类型，不是必须的，Allure会根据内容自动适配。\n除了注解，也可以在代码中添加：\nimport io.qameta.allure.Allure; ... Allure.addAttachment(\u0026#34;My attachment\u0026#34;, \u0026#34;My attachment content\u0026#34;); Path content = Paths.get(\u0026#34;path-to-my-attachment-contnet\u0026#34;); try (InputStream is = Files.newInputStream(content)) { Allure.addAttachment(\u0026#34;My attachment\u0026#34;, is); } Links 添加超链接，有3个注解@Link、@Issue、@TmsLink（test management system）：\nimport io.qameta.allure.Link; import io.qameta.allure.Issue; import io.qameta.allure.TmsLink; @Link(\u0026#34;https://example.org\u0026#34;) @Link(name = \u0026#34;allure\u0026#34;, type = \u0026#34;mylink\u0026#34;) public void testSomething() { ... } @Issue(\u0026#34;123\u0026#34;) @Issue(\u0026#34;432\u0026#34;) public void testSomething() { ... } @TmsLink(\u0026#34;test-1\u0026#34;) @TmsLink(\u0026#34;test-2\u0026#34;) public void testSomething() { ... } 链接类型可以在properties中定义，Allure会用注解中的值替换{}：\nallure.link.mylink.pattern=https://example.org/mylink/{} allure.link.issue.pattern=https://example.org/issue/{} allure.link.tms.pattern=https://example.org/tms/{} @Severity 添加严重程度标识：\npackage org.example.tests; import org.junit.Test; import io.qameta.allure.Severity; import io.qameta.allure.SeverityLevel; public class MyTest { @Test @Severity(SeverityLevel.CRITICAL) public void testSomething() throws Exception { // ... } } 敏捷标识 在敏捷开发中有Epic、Feature和Stories三个概念，用于对需求进行分层管理，同理可以运用到测试分层管理：\npackage org.example.tests; import org.junit.Test; import io.qameta.allure.Epic; import io.qameta.allure.Feature; import io.qameta.allure.Story; @Epic(\u0026#34;Allure examples\u0026#34;) @Feature(\u0026#34;Junit 4 support\u0026#34;) public class MyTest { @Test @Story(\u0026#34;Base support for bdd annotations\u0026#34;) @Story(\u0026#34;Advanced support for bdd annotations\u0026#34;) public void testSomething() throws Exception { // ... } } Python Pytest集成Allure Report 集成 执行以下命令安装：\n$ pip install allure-pytest 它会同时安装allure-pytest和allure-python-commons两个包。\n然后指定Allure测试报告目录：\n$ pytest --alluredir=/tmp/my_allure_results 最后执行命令打开报告：\n$ allure serve /tmp/my_allure_results Allure会创建一个Jetty服务器，拉起默认浏览器打开测试报告。\npytest结果状态 Allure会根据pytest的结果状态进行标记：\nimport pytest def test_success(): \u0026#34;\u0026#34;\u0026#34;this test succeeds\u0026#34;\u0026#34;\u0026#34; assert True def test_failure(): \u0026#34;\u0026#34;\u0026#34;this test fails\u0026#34;\u0026#34;\u0026#34; assert False def test_skip(): \u0026#34;\u0026#34;\u0026#34;this test is skipped\u0026#34;\u0026#34;\u0026#34; pytest.skip(\u0026#39;for a reason!\u0026#39;) def test_broken(): raise Exception(\u0026#39;oops\u0026#39;) pytest特性 xfail @pytest.mark.xfail(condition=lambda: True, reason=\u0026#39;this test is expecting failure\u0026#39;) def test_xfail_expected_failure(): \u0026#34;\u0026#34;\u0026#34;this test is an xfail that will be marked as expected failure\u0026#34;\u0026#34;\u0026#34; assert False @pytest.mark.xfail(condition=lambda: True, reason=\u0026#39;this test is expecting failure\u0026#39;) def test_xfail_unexpected_pass(): \u0026#34;\u0026#34;\u0026#34;this test is an xfail that will be marked as unexpected success\u0026#34;\u0026#34;\u0026#34; assert True 会在Allure报告中展示：\nskipif @pytest.mark.skipif(\u0026#39;2 + 2 != 5\u0026#39;, reason=\u0026#39;This test is skipped by a triggered condition in @pytest.mark.skipif\u0026#39;) def test_skip_by_triggered_condition(): pass 会在Allure报告中展示：\nfixtures @pytest.fixture(params=[True, False], ids=[\u0026#39;param_true\u0026#39;, \u0026#39;param_false\u0026#39;]) def function_scope_fixture_with_finalizer(request): if request.param: print(\u0026#39;True\u0026#39;) else: print(\u0026#39;False\u0026#39;) def function_scope_finalizer(): function_scope_step() request.addfinalizer(function_scope_finalizer) @pytest.fixture(scope=\u0026#39;class\u0026#39;) def class_scope_fixture_with_finalizer(request): def class_finalizer_fixture(): class_scope_step() request.addfinalizer(class_finalizer_fixture) @pytest.fixture(scope=\u0026#39;module\u0026#39;) def module_scope_fixture_with_finalizer(request): def module_finalizer_fixture(): module_scope_step() request.addfinalizer(module_finalizer_fixture) @pytest.fixture(scope=\u0026#39;session\u0026#39;) def session_scope_fixture_with_finalizer(request): def session_finalizer_fixture(): session_scope_step() request.addfinalizer(session_finalizer_fixture) class TestClass(object): def test_with_scoped_finalizers(self, function_scope_fixture_with_finalizer, class_scope_fixture_with_finalizer, module_scope_fixture_with_finalizer, session_scope_fixture_with_finalizer): step_inside_test_body() 会在Allure报告中展示（fixtures会展示在Set up和Tear down中）：\n而对于fixture中的结果状态，Allure也会进行标记：\nimport pytest @pytest.fixture def skip_fixture(): pytest.skip() @pytest.fixture def fail_fixture(): assert False @pytest.fixture def broken_fixture(): raise Exception(\u0026#34;Sorry, it\u0026#39;s broken.\u0026#34;) def test_with_pytest_skip_in_the_fixture(skip_fixture): pass def test_with_failure_in_the_fixture(fail_fixture): pass def test_with_broken_fixture(broken_fixture): pass parametrize import allure import pytest @allure.step def simple_step(step_param1, step_param2 = None): pass @pytest.mark.parametrize(\u0026#39;param1\u0026#39;, [True, False], ids=[\u0026#39;id explaining value 1\u0026#39;, \u0026#39;id explaining value 2\u0026#39;]) def test_parameterize_with_id(param1): simple_step(param1) @pytest.mark.parametrize(\u0026#39;param1\u0026#39;, [True, False]) @pytest.mark.parametrize(\u0026#39;param2\u0026#39;, [\u0026#39;value 1\u0026#39;, \u0026#39;value 2\u0026#39;]) def test_parametrize_with_two_parameters(param1, param2): simple_step(param1, param2) @pytest.mark.parametrize(\u0026#39;param1\u0026#39;, [True], ids=[\u0026#39;boolean parameter id\u0026#39;]) @pytest.mark.parametrize(\u0026#39;param2\u0026#39;, [\u0026#39;value 1\u0026#39;, \u0026#39;value 2\u0026#39;]) @pytest.mark.parametrize(\u0026#39;param3\u0026#39;, [1]) def test_parameterize_with_uneven_value_sets(param1, param2, param3): simple_step(param1, param3) simple_step(param2) Allure会展示每个test和id：\n以及运行细节：\nAllure特性 @allure.step import allure import pytest from .steps import imported_step @allure.step def passing_step(): pass @allure.step def step_with_nested_steps(): nested_step() @allure.step def nested_step(): nested_step_with_arguments(1, \u0026#39;abc\u0026#39;) @allure.step def nested_step_with_arguments(arg1, arg2): pass def test_with_imported_step(): passing_step() imported_step() def test_with_nested_steps(): passing_step() step_with_nested_steps() step能从入参中读取值：\nimport allure @allure.step(\u0026#39;Step with placeholders in the title, positional: \u0026#34;{0}\u0026#34;, keyword: \u0026#34;{key}\u0026#34;\u0026#39;) def step_with_title_placeholders(arg1, key=None): pass def test_steps_with_placeholders(): step_with_title_placeholders(1, key=\u0026#39;something\u0026#39;) step_with_title_placeholders(2) step_with_title_placeholders(3, \u0026#39;anything\u0026#39;) step也能在conftest.py中用到fixtures上：\nimport allure import pytest @allure.step(\u0026#39;step in conftest.py\u0026#39;) def conftest_step(): pass @pytest.fixture def fixture_with_conftest_step(): conftest_step() import allure from .steps import imported_step @allure.step def passing_step(): pass def test_with_step_in_fixture_from_conftest(fixture_with_conftest_step): passing_step() allure.attach 给Allure测试报告添加附件，allure.attach(body, name, attachment_type, extension)：\nbody 文件内容（或者source指定文件路径） name 文件名称 attachment_type 附件类型（allure.attachment_type中的某个值） extension 文件扩展 import allure import pytest @pytest.fixture def attach_file_in_module_scope_fixture_with_finalizer(request): allure.attach(\u0026#39;A text attacment in module scope fixture\u0026#39;, \u0026#39;blah blah blah\u0026#39;, allure.attachment_type.TEXT) def finalizer_module_scope_fixture(): allure.attach(\u0026#39;A text attacment in module scope finalizer\u0026#39;, \u0026#39;blah blah blah blah\u0026#39;, allure.attachment_type.TEXT) request.addfinalizer(finalizer_module_scope_fixture) def test_with_attacments_in_fixture_and_finalizer(attach_file_in_module_scope_finalizer): pass def test_multiple_attachments(): allure.attach.file(\u0026#39;./data/totally_open_source_kitten.png\u0026#39;, attachment_type=allure.attachment_type.PNG) allure.attach(\u0026#39;\u0026lt;head\u0026gt;\u0026lt;/head\u0026gt;\u0026lt;body\u0026gt; a page \u0026lt;/body\u0026gt;\u0026#39;, \u0026#39;Attach with HTML type\u0026#39;, allure.attachment_type.HTML) Descriptions 既可以使用装饰器@allure.description或@allure.description_html添加描述：\nimport allure @allure.description_html(\u0026#34;\u0026#34;\u0026#34; \u0026lt;h1\u0026gt;Test with some complicated html description\u0026lt;/h1\u0026gt; \u0026lt;table style=\u0026#34;width:100%\u0026#34;\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;Firstname\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Lastname\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Age\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr align=\u0026#34;center\u0026#34;\u0026gt; \u0026lt;td\u0026gt;William\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Smith\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;50\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr align=\u0026#34;center\u0026#34;\u0026gt; \u0026lt;td\u0026gt;Vasya\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Jackson\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;94\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026#34;\u0026#34;\u0026#34;) def test_html_description(): assert True @allure.description(\u0026#34;\u0026#34;\u0026#34; Multiline test description. That comes from the allure.description decorator. Nothing special about it. \u0026#34;\u0026#34;\u0026#34;) def test_description_from_decorator(): assert 42 == int(6 * 7) def test_unicode_in_docstring_description(): \u0026#34;\u0026#34;\u0026#34;Unicode in description. Этот тест проверяет юникод. 你好伙计. \u0026#34;\u0026#34;\u0026#34; assert 42 == int(6 * 7) 也可以在代码中使用allure.dynamic.description动态添加描述：\nimport allure @allure.description(\u0026#34;\u0026#34;\u0026#34; This description will be replaced at the end of the test. \u0026#34;\u0026#34;\u0026#34;) def test_dynamic_description(): assert 42 == int(6 * 7) allure.dynamic.description(\u0026#39;A final description.\u0026#39;) Titles 既可以使用装饰器@allure.title添加标题：\nimport allure import pytest @allure.title(\u0026#34;This test has a custom title\u0026#34;) def test_with_a_title(): assert 2 + 2 == 4 @allure.title(\u0026#34;This test has a custom title with unicode: Привет!\u0026#34;) def test_with_unicode_title(): assert 3 + 3 == 6 并且可以获取参数值：\n@allure.title(\u0026#34;Parameterized test title: adding {param1} with {param2}\u0026#34;) @pytest.mark.parametrize(\u0026#39;param1,param2,expected\u0026#39;, [ (2, 2, 4), (1, 2, 5) ]) def test_with_parameterized_title(param1, param2, expected): assert param1 + param2 == expected 也可以在代码中使用allure.dynamic.title动态添加：\n@allure.title(\u0026#34;This title will be replaced in a test body\u0026#34;) def test_with_dynamic_title(): assert 2 + 2 == 4 allure.dynamic.title(\u0026#39;After a successful test finish, the title was replaced with this line.\u0026#39;) Links @allure.link、 @allure.issue 和@allure.testcase：\nimport allure TEST_CASE_LINK = \u0026#39;https://github.com/qameta/allure-integrations/issues/8#issuecomment-268313637\u0026#39; @allure.link(\u0026#39;https://www.youtube.com/watch?v=4YYzUTYZRMU\u0026#39;) def test_with_link(): pass @allure.link(\u0026#39;https://www.youtube.com/watch?v=Su5p2TqZxKU\u0026#39;, name=\u0026#39;Click me\u0026#39;) def test_with_named_link(): pass @allure.issue(\u0026#39;140\u0026#39;, \u0026#39;Pytest-flaky test retries shows like test steps\u0026#39;) def test_with_issue_link(): pass @allure.testcase(TEST_CASE_LINK, \u0026#39;Test case title\u0026#39;) def test_with_testcase_link(): pass @allure.link会提供可点击的超链接。 @allure.issue会有个可供点击的图标。\nissure链接模板通过--allure-link-pattern定义：\n$ pytest directory_with_tests/ --alluredir=/tmp/my_allure_report \\ --allure-link-pattern=issue:http://www.mytesttracker.com/issue/{} Retries Allure有个Retries标签页用来展示重试的测试：\nimport allure import random import time @allure.step def passing_step(): pass @allure.step def flaky_broken_step(): if random.randint(1, 5) != 1: raise Exception(\u0026#39;Broken!\u0026#39;) def test_broken_with_randomized_time(): passing_step() time.sleep(random.randint(1, 3)) flaky_broken_step() 敏捷标识 在敏捷开发中有Epic、Feature和Stories三个概念，用于对需求进行分层管理，同理可以运用到测试分层管理：\nimport allure def test_without_any_annotations_that_wont_be_executed(): pass @allure.story(\u0026#39;epic_1\u0026#39;) def test_with_epic_1(): pass @allure.story(\u0026#39;story_1\u0026#39;) def test_with_story_1(): pass @allure.story(\u0026#39;story_2\u0026#39;) def test_with_story_2(): pass @allure.feature(\u0026#39;feature_2\u0026#39;) @allure.story(\u0026#39;story_2\u0026#39;) def test_with_story_2_and_feature_2(): pass 也可以使用--allure-epics、--allure-features、--allure-stories指定运行测试：\n$ pytest tests.py --allure-stories story_1,story_2 collected 5 items tests.py ... [100%] ============================== 3 passed in 0.01 seconds ============================== $ pytest tests.py --allure-features feature2 --allure-stories story2 collected 5 items tests.py ... [100%] =============================== 2 passed in 0.01 seconds ============================== 严重程度 使用@allure.severity或allure.severity_level：\nimport allure def test_with_no_severity_label(): pass @allure.severity(allure.severity_level.TRIVIAL) def test_with_trivial_severity(): pass @allure.severity(allure.severity_level.NORMAL) def test_with_normal_severity(): pass @allure.severity(allure.severity_level.NORMAL) class TestClassWithNormalSeverity(object): def test_inside_the_normal_severity_test_class(self): pass @allure.severity(allure.severity_level.CRITICAL) def test_inside_the_normal_severity_test_class_with_overriding_critical_severity(self): pass --allure-severities用来指定运行哪些严重程度的测试：\n$ pytest tests.py --allure-severities normal,critical collected 5 items bdd_annotations_demo/test_severity_labels.py ... [100%] ================================ 3 passed in 0.01 seconds ============================ 参考资料：\nhttps://docs.qameta.io/allure-report/\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E8%87%AA%E5%8A%A8%E5%8C%96/000007-allure%E6%B5%8B%E8%AF%95%E6%8A%A5%E5%91%8A%E5%AE%8C%E6%95%B4%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"目录\"\u003e目录\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e简介\u003c/li\u003e\n\u003cli\u003e安装Allure\u003c/li\u003e\n\u003cli\u003eAllure测试报告的结构\u003c/li\u003e\n\u003cli\u003eJava TestNG集成Allure Report\u003c/li\u003e\n\u003cli\u003ePython Pytest集成Allure Report\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"简介\"\u003e简介\u003c/h1\u003e\n\u003cp\u003e假如你想让测试报告变得漂亮一点，那你一定会在搜索引擎中找到Allure测试报告的。Allure官方给出了个在线网站，访问以下链接就能一睹为快：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://demo.qameta.io/allure/\"\u003ehttps://demo.qameta.io/allure/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/12513ca6699c49569b795699f0d97579.png\"\u003e\u003c/p\u003e\n\u003cp\u003e可以在左下角切换为中文报告：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0e022a1416a444058f2241cbf2a00d8c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如此好看的测试报告，哪个老板不喜欢呢？\u003c/p\u003e\n\u003ch1 id=\"安装allure\"\u003e安装Allure\u003c/h1\u003e\n\u003cp\u003e1.在GitHub releases：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/allure-framework/allure2/releases\"\u003ehttps://github.com/allure-framework/allure2/releases\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/97e74735b6524fb687af5c5ec894811c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e或者Maven Central：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/\"\u003ehttps://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6d5dc013fc9a48bf91c95fbd41be42cb.png\"\u003e\u003c/p\u003e\n\u003cp\u003e下载安装包并解压。\u003c/p\u003e\n\u003cp\u003e2.找到bin目录，Windows使用\u003ccode\u003eallure.bat\u003c/code\u003e，Unix使用\u003ccode\u003eallure\u003c/code\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d3ba1cb635c846ecbd32e9377284c68a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e3.为了后续方便使用，可以把bin完整路径添加到\u003cstrong\u003e系统环境变量PATH\u003c/strong\u003e中，然后在命令行直接输入\u003cstrong\u003eallure\u003c/strong\u003e命令。\u003c/p\u003e\n\u003cp\u003e官方也给出了通过Linux的ppa、Mac的brew、Windows的scoop的安装方式，不过对于咱们老百姓来说，不如手动下载包来安装实在。\u003c/p\u003e\n\u003cp\u003e安装后就可以检查是否安装成功了：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e$ allure --version\n2.17.2\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003e因为Allure是用Java写的，所以如果无法运行，那么要检查下有没有安装Java。\u003c/strong\u003e\u003c/p\u003e\n\u003ch1 id=\"allure测试报告的结构\"\u003eAllure测试报告的结构\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ce7256011e964d36a061220166d12c1d.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"categories\"\u003eCategories\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5d641f9211064c399c724b7ca8deb3ee.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"suites\"\u003eSuites\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fcc7165248104aa5a74a703717652d1e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e446c3a5ebcd490fae30080c02c131ab.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"graphs\"\u003eGraphs\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9a50ab9021a6444d82a66095320feb83.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"timeline\"\u003eTimeline\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0b7039a102cd4f418955be77a28e33a4.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"behaviors\"\u003eBehaviors\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bec7ea1565e0460d932a4d68ed82691a.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"packages\"\u003ePackages\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fb8f324550224cbb848a9546255caaf0.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"java-testng集成allure-report\"\u003eJava TestNG集成Allure Report\u003c/h1\u003e\n\u003ch2 id=\"集成\"\u003e集成\u003c/h2\u003e\n\u003cp\u003e在Maven的pom.xml中添加：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;properties\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;aspectj.version\u0026gt;\u003c/span\u003e1.8.10\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/aspectj.version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/properties\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eio.qameta.allure\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003eallure-testng\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003eLAST_VERSION\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003etest\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;build\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;plugins\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.apache.maven.plugins\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003emaven-surefire-plugin\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2.20\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;argLine\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    -javaagent:\u0026#34;${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/argLine\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.aspectj\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003easpectjweaver\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e${aspectj.version}\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/plugins\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/build\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e然后执行命令：\u003c/p\u003e","title":"Allure测试报告完整学习笔记"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文继续对Flask官方教程进行学习，我就直接跳过Templates、Static Files、Blog Blueprint三小节了，因为基本不会在实际项目中用到这些技术，有时间多学习下前端才是。这篇文章把Make the Project Installable、Test Coverage、Deploy to Production这三小节汇总来学习。我觉得这是官方给出的一个Flask项目发布流程，如下图所示：\n这跟我在大型项目中接触到的发布流程大同小异。尤其是对于我们测试来说，跑单元测试这个环节还是有必要好好了解一下的，幸运的是，得益于Python的简单，理解起来会更容易些。所谓一通百通，Flask的单测懂了，其他语言的单测也通了。\n项目打包 创建setup.py文件：\nfrom setuptools import find_packages, setup setup( name=\u0026#39;flaskr\u0026#39;, version=\u0026#39;1.0.0\u0026#39;, packages=find_packages(), include_package_data=True, zip_safe=False, install_requires=[ \u0026#39;flask\u0026#39;, ], ) packages指定Python包，find_packages()函数会自动查找。 include_package_data表示包括其他文件比如静态（static）文件和模板（templates）文件。 如果还需要包括其他数据，那么就创建MANIFEST.in文件：\ninclude flaskr/schema.sql graft flaskr/static graft flaskr/templates global-exclude *.pyc 其中global-exclude排除了*.pyc文件。\n接着就可以使用pip命令安装了：\n$ pip install -e . 安装以后就能用pip list命令查看：\n$ pip list Package Version Location -------------- --------- ---------------------------------- click 6.7 Flask 1.0 flaskr 1.0.0 /home/user/Projects/flask-tutorial itsdangerous 0.24 Jinja2 2.10 MarkupSafe 1.0 pip 9.0.3 setuptools 39.0.1 Werkzeug 0.14.1 wheel 0.30.0 这个过程是这样的：pip在当前目录找到setup.py文件，然后根据文件描述把项目文件打包后安装到本地。安装以后就能在任何位置使用flask run来启动应用了，而不仅仅是在flask-turorial目录下。\n跑单元测试 单元测试不能保证程序没有Bug，但却是在开发阶段保障代码质量的有效手段。拿我们公司举例来说，开发提测和上线，都会把单元测试作为卡点，单测覆盖率没有达到45%是不能提测和上线的。\nFlask项目的单元测试要用到两个工具，一个是我们非常熟悉的pytest，还有一个是coverage，先安装它们：\n$ pip install pytest coverage 新建tests/data.sql文件，插入一些测试数据：\nINSERT INTO user (username, password) VALUES (\u0026#39;test\u0026#39;, \u0026#39;pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f\u0026#39;), (\u0026#39;other\u0026#39;, \u0026#39;pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79\u0026#39;); INSERT INTO post (title, body, author_id, created) VALUES (\u0026#39;test title\u0026#39;, \u0026#39;test\u0026#39; || x\u0026#39;0a\u0026#39; || \u0026#39;body\u0026#39;, 1, \u0026#39;2018-01-01 00:00:00\u0026#39;); pytest的fixture相当于setup，可以做一些测试前的初始化工作，新建tests/conftest.py，编写fixture：\nimport os import tempfile import pytest from flaskr import create_app from flaskr.db import get_db, init_db with open(os.path.join(os.path.dirname(__file__), \u0026#39;data.sql\u0026#39;), \u0026#39;rb\u0026#39;) as f: _data_sql = f.read().decode(\u0026#39;utf8\u0026#39;) @pytest.fixture def app(): db_fd, db_path = tempfile.mkstemp() app = create_app({ \u0026#39;TESTING\u0026#39;: True, \u0026#39;DATABASE\u0026#39;: db_path, }) with app.app_context(): init_db() get_db().executescript(_data_sql) yield app os.close(db_fd) os.unlink(db_path) @pytest.fixture def client(app): return app.test_client() @pytest.fixture def runner(app): return app.test_cli_runner() app\n创建应用，初始化数据库，使用的是测试配置和测试数据。\ntempfile.mkstemp()创建了一个临时文件，返回文件描述符和文件路径。并且把临时文件路径传入了DATABASE，接着插入测试数据。测试结束后关闭和移除临时文件。\nfixture的yield前面的代码相当于setup，yield后面的代码相当于teardown。\nTESTING: True将Flask置为测试模式，Flask内部会进行一些调整以便于进行测试。\nclient\n调用app.test_client返回一个测试客户端，可以用这个客户端给应用发送请求。\nrunner\n调用app.test_cli_runner()返回一个可以执行应用已注册命令的runner。\n测试一下Factory：\n# tests/test_factory.py from flaskr import create_app def test_config(): assert not create_app().testing assert create_app({\u0026#39;TESTING\u0026#39;: True}).testing def test_hello(client): response = client.get(\u0026#39;/hello\u0026#39;) assert response.data == b\u0026#39;Hello, World!\u0026#39; 测试一下Database：\n# tests/test_db.py import sqlite3 import pytest from flaskr.db import get_db def test_get_close_db(app): with app.app_context(): db = get_db() assert db is get_db() with pytest.raises(sqlite3.ProgrammingError) as e: db.execute(\u0026#39;SELECT 1\u0026#39;) assert \u0026#39;closed\u0026#39; in str(e.value) def test_init_db_command(runner, monkeypatch): class Recorder(object): called = False def fake_init_db(): Recorder.called = True # monkeypatch是pytest内置的一个fixture，也就是猴子补丁。 monkeypatch.setattr(\u0026#39;flaskr.db.init_db\u0026#39;, fake_init_db) result = runner.invoke(args=[\u0026#39;init-db\u0026#39;]) assert \u0026#39;Initialized\u0026#39; in result.output assert Recorder.called 测试一下Authentication：\n# tests/conftest.py class AuthActions(object): def __init__(self, client): self._client = client def login(self, username=\u0026#39;test\u0026#39;, password=\u0026#39;test\u0026#39;): return self._client.post( \u0026#39;/auth/login\u0026#39;, data={\u0026#39;username\u0026#39;: username, \u0026#39;password\u0026#39;: password} ) def logout(self): return self._client.get(\u0026#39;/auth/logout\u0026#39;) # 这样就可以使用auth.login()进行用户登录 @pytest.fixture def auth(client): return AuthActions(client) # tests/test_auth.py import pytest from flask import g, session from flaskr.db import get_db def test_register(client, app): assert client.get(\u0026#39;/auth/register\u0026#39;).status_code == 200 response = client.post( \u0026#39;/auth/register\u0026#39;, data={\u0026#39;username\u0026#39;: \u0026#39;a\u0026#39;, \u0026#39;password\u0026#39;: \u0026#39;a\u0026#39;} ) assert \u0026#39;http://localhost/auth/login\u0026#39; == response.headers[\u0026#39;Location\u0026#39;] with app.app_context(): assert get_db().execute( \u0026#34;SELECT * FROM user WHERE username = \u0026#39;a\u0026#39;\u0026#34;, ).fetchone() is not None @pytest.mark.parametrize((\u0026#39;username\u0026#39;, \u0026#39;password\u0026#39;, \u0026#39;message\u0026#39;), ( (\u0026#39;\u0026#39;, \u0026#39;\u0026#39;, b\u0026#39;Username is required.\u0026#39;), (\u0026#39;a\u0026#39;, \u0026#39;\u0026#39;, b\u0026#39;Password is required.\u0026#39;), (\u0026#39;test\u0026#39;, \u0026#39;test\u0026#39;, b\u0026#39;already registered\u0026#39;), )) def test_register_validate_input(client, username, password, message): response = client.post( \u0026#39;/auth/register\u0026#39;, data={\u0026#39;username\u0026#39;: username, \u0026#39;password\u0026#39;: password} ) assert message in response.data def test_login(client, auth): assert client.get(\u0026#39;/auth/login\u0026#39;).status_code == 200 response = auth.login() assert response.headers[\u0026#39;Location\u0026#39;] == \u0026#39;http://localhost/\u0026#39; # 使用with后就能在上下文中访问session with client: client.get(\u0026#39;/\u0026#39;) assert session[\u0026#39;user_id\u0026#39;] == 1 assert g.user[\u0026#39;username\u0026#39;] == \u0026#39;test\u0026#39; @pytest.mark.parametrize((\u0026#39;username\u0026#39;, \u0026#39;password\u0026#39;, \u0026#39;message\u0026#39;), ( (\u0026#39;a\u0026#39;, \u0026#39;test\u0026#39;, b\u0026#39;Incorrect username.\u0026#39;), (\u0026#39;test\u0026#39;, \u0026#39;a\u0026#39;, b\u0026#39;Incorrect password.\u0026#39;), )) def test_login_validate_input(auth, username, password, message): response = auth.login(username, password) assert message in response.data def test_logout(client, auth): auth.login() # 使用with后就能在上下文中访问session with client: auth.logout() assert \u0026#39;user_id\u0026#39; not in session 更多关于Blog的测试用例就不在此赘述了，感兴趣的同学可以点击文章尾部链接到官网查看。\n最后用例写完了，就该运行了。在setup.cfg文件中添加一些配置，可以适当减少单测冗余：\n[tool:pytest] testpaths = tests [coverage:run] branch = True source = flaskr 然后就可以执行pytest了：\n$ pytest ========================= test session starts ========================== platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg collected 23 items tests/test_auth.py ........ [ 34%] tests/test_blog.py ............ [ 86%] tests/test_db.py .. [ 95%] tests/test_factory.py .. [100%] ====================== 24 passed in 0.64 seconds ======================= pytest -v可以显示每个测试函数。\n单测覆盖率才是灵魂，所以建议这样来跑单测：\n$ coverage run -m pytest 然后查看报告：\n$ coverage report Name Stmts Miss Branch BrPart Cover ------------------------------------------------------ flaskr/__init__.py 21 0 2 0 100% flaskr/auth.py 54 0 22 0 100% flaskr/blog.py 54 0 16 0 100% flaskr/db.py 24 0 4 0 100% ------------------------------------------------------ TOTAL 153 0 44 0 100% 也可以生成html报告：\n$ coverage html 发布上线 先安装wheel库：\n$ pip install wheel 然后创建.whl文件：\n$ python setup.py bdist_wheel 命令执行后会生成一个dist/flaskr-1.0.0-py3-none-any.whl文件，文件格式是{project name}-{version}-{python tag} -{abi tag}-{platform tag}。\n在服务器上就可以安装了：\n$ pip install flaskr-1.0.0-py3-none-any.whl 因为是新机器，所以需要初始化数据库：\n$ export FLASK_APP=flaskr $ flask init-db 如果是Python虚拟环境，那么可以在venv/var/flaskr-instance找到Flask实例。\n最后设置下SECRET_KEY，Flask官网给出一种生成随机SECRET_KEY的方法：\n$ python -c \u0026#39;import secrets; print(secrets.token_hex())\u0026#39; \u0026#39;192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf\u0026#39; 生成后新建venv/var/flaskr-instance/config.py文件粘贴即可：\nSECRET_KEY = \u0026#39;192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf\u0026#39; 至于生产服务器的选取，建议不要使用flask run，因为这是Werkzeug提供的开发服务器，既不稳定，也不安全。\n可以使用WSGI服务器，比如Waitress：\n$ pip install waitress $ waitress-serve --call \u0026#39;flaskr:create_app\u0026#39; Serving on http://0.0.0.0:8080 标准的WSGI服务器如下：\nGunicorn\nuWSGI\nGevent，我们组就用的这个：\nfrom gevent.pywsgi import WSGIServer from yourapplication import app http_server = WSGIServer((\u0026#39;\u0026#39;, 5000), app) http_server.serve_forever() Twisted Web\nProxy Setups\n参考资料：\nhttps://flask.palletsprojects.com/en/2.0.x/tutorial/install/\nhttps://flask.palletsprojects.com/en/2.0.x/tutorial/tests/\nhttps://flask.palletsprojects.com/en/2.0.x/tutorial/deploy/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/flask/000007-flaskflask%E9%A1%B9%E7%9B%AE%E5%8F%91%E5%B8%83%E6%B5%81%E7%A8%8B/","summary":"本文继续对Flask官方教程进行学习，我就直接跳过Templates、Static Files、Blog Blueprint三小节了，因为基本不会在实际项目中用到这些技术，有时间多学习下前端才是。这篇文章把Make the Project Installable、Test Coverage、Deploy to Production这三小节汇总来学习。我觉得这是官方给出的一个Flask项目发布流程，","title":"Flask项目发布流程"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nSession是什么 在Web应用程序中，Session（会话）是用于在多个请求之间保持用户状态的一种机制。由于HTTP协议是无状态的，每个请求都是独立的，服务器无法直接知道两个请求是否来自同一个用户。Session通过在服务器端存储用户状态信息，并在客户端使用一个唯一的Session ID来标识用户，从而实现状态保持。\nSession工作原理如下：\n创建Session：当用户第一次访问Web应用程序时，服务器创建一个新的Session，并生成一个唯一的Session ID 发送Session ID：服务器将Session ID发送给客户端，通常通过HTTP响应头中的Set-Cookie字段 存储Session ID：客户端浏览器将Session ID存储在Cookie中，并在后续请求中将其发送回服务器 识别Session：服务器通过接收到的Session ID识别用户的会话，并从服务器端存储中获取相应的Session数据 四种主要技术 Cookies Hidden Form Field URL Rewriting HttpSession 1. Cookies（Cookie） Cookie是存储在客户端浏览器中的小数据片段。它们会随着每个HTTP请求一起发送到服务器，可以用来存储会话标识符。\n工作原理\n当用户第一次访问网站时，服务器生成一个唯一的会话ID，并将其作为Cookie发送到客户端浏览器 浏览器存储这个Cookie，并在后续请求中将其发送回服务器 服务器使用Cookie中的会话ID来检索用户的会话数据 优点\n对用户透明 由浏览器自动管理 可以存储少量数据 缺点\n用户可以在浏览器中禁用Cookie 存储容量有限（通常每个Cookie大约4KB） 如果处理不当，可能存在安全问题（例如Cookie盗窃） 示例\nimport javax.servlet.http.Cookie; // 在Servlet中设置Cookie Cookie sessionCookie = new Cookie(\u0026#34;JSESSIONID\u0026#34;, session.getId()); sessionCookie.setMaxAge(30 * 60); // 30分钟 response.addCookie(sessionCookie); // 在Servlet中检索Cookie Cookie[] cookies = request.getCookies(); // 从请求获取Cookie if (cookies != null) { for (Cookie cookie : cookies) { if (\u0026#34;JSESSIONID\u0026#34;.equals(cookie.getName())) { String sessionId = cookie.getValue(); // 使用会话ID检索会话数据 } } } Cookie有两种类型：\n非持久性Cookie（Non-persistent cookie），也称为会话Cookie（Session cookie），仅在浏览器会话期间有效。当用户关闭浏览器时，这些Cookie会被自动删除。适用于需要在用户会话期间保持状态的信息，如购物车内容、临时会话ID等。\n持久性Cookie（Persistent cookie），在浏览器会话结束后仍然有效，直到达到指定的过期时间或被用户手动删除。它们存储在客户端的硬盘上。适用于需要在多个会话之间保持状态的信息，如记住用户登录状态、用户偏好设置等。\n2. 隐藏表单字段（Hidden Form Field） 隐藏表单字段用于在HTML表单中存储会话数据。当表单提交时，这些数据会发送到服务器。\n工作原理\n服务器生成一个唯一的会话ID，并将其作为隐藏字段包含在HTML表单中 当用户提交表单时，隐藏字段会随表单数据一起发送到服务器 服务器从隐藏字段中检索会话ID，并使用它来访问用户的会话数据 优点\n实现简单 不依赖于浏览器设置（例如Cookie） 缺点\n仅适用于表单提交 需要修改所有表单以包含隐藏字段 不适用于跨多个页面跟踪会话（没有表单的页面） 示例\n\u0026lt;form action=\u0026#34;submitForm\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;sessionId\u0026#34; value=\u0026#34;\u0026lt;%= session.getId() %\u0026gt;\u0026#34;\u0026gt; \u0026lt;!-- 其他表单字段 --\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;提交\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 3. URL重写（URL Rewriting） URL重写涉及将会话ID附加到每个请求的URL中。这种技术确保会话ID随每个请求一起发送到服务器。\n工作原理\n服务器生成一个唯一的会话ID，并将其作为查询参数附加到URL中 会话ID包含在服务器生成的所有后续URL中 服务器从URL中检索会话ID，并使用它来访问用户的会话数据 优点\n即使禁用Cookie也能工作 可以跨多个页面跟踪会话 缺点\nURL变得更长且不易读 需要修改所有URL以包含会话ID 如果会话ID暴露在URL中，可能存在安全风险 示例\n// 生成带有会话ID的URL // http://localhost:8080/myapp/nextPage.jsp;jsessionid=1234567890ABCDEF1234567890ABCDEF String url = response.encodeURL(\u0026#34;nextPage.jsp\u0026#34;); // 从URL中检索会话ID String sessionId = request.getParameter(\u0026#34;jsessionid\u0026#34;); 4. HttpSession HttpSession接口提供了一种内置的会话跟踪机制。它抽象了底层的会话跟踪技术（例如Cookie、URL重写），并提供了一个简单的API来管理会话数据。\n工作原理\n服务器为每个用户会话创建一个HttpSession对象 会话ID由服务器管理，可以存储在Cookie或URL中 HttpSession对象提供了存储和检索会话属性的方法 优点\n简单且方便的API 抽象了底层的会话跟踪机制 自动处理会话的创建、过期和失效 缺点\n依赖于服务器的会话管理实现 可能需要额外配置以实现会话持久化和集群 示例\n// 创建或检索会话 HttpSession session = request.getSession(); // 在会话中存储属性 session.setAttribute(\u0026#34;username\u0026#34;, \u0026#34;JohnDoe\u0026#34;); // 从会话中检索属性 String username = (String) session.getAttribute(\u0026#34;username\u0026#34;); // 使会话失效 session.invalidate(); 在多数情况下，使用HttpSession是最方便和有效的方法，因为它抽象了底层的会话跟踪机制，并提供了一个简单的API来管理会话数据。\n为什么request.getSession()能拿到session？\nHttpSession抽象了底层的会话跟踪技术，实际上请求的sessionid还是通过cookie或url，从客户端发给服务器的，比如cookie，服务器响应（第一次请求时创建会话）：\nHTTP/1.1 200 OK Set-Cookie: JSESSIONID=1234567890ABCDEF1234567890ABCDEF; Path=/; HttpOnly Content-Type: text/html 客户端请求（后续请求中包含sessionid）：\nGET /myapp/urlRewriting HTTP/1.1 Host: example.com Cookie: JSESSIONID=1234567890ABCDEF1234567890ABCDEF HttpSession是一个封装好的接口，提供了一组方法，简化了Session管理。\n参考资料：\nhttps://www.javatpoint.com/session-tracking-in-servlets\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000007-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servletsession/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"session是什么\"\u003eSession是什么\u003c/h1\u003e\n\u003cp\u003e在Web应用程序中，Session（会话）是用于在多个请求之间保持用户状态的一种机制。由于HTTP协议是无状态的，每个请求都是独立的，服务器无法直接知道两个请求是否来自同一个用户。Session通过在服务器端存储用户状态信息，并在客户端使用一个唯一的Session ID来标识用户，从而实现状态保持。\u003c/p\u003e\n\u003cp\u003eSession工作原理如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e创建Session：当用户第一次访问Web应用程序时，服务器创建一个新的Session，并生成一个唯一的Session ID\u003c/li\u003e\n\u003cli\u003e发送Session ID：服务器将Session ID发送给客户端，通常通过HTTP响应头中的Set-Cookie字段\u003c/li\u003e\n\u003cli\u003e存储Session ID：客户端浏览器将Session ID存储在Cookie中，并在后续请求中将其发送回服务器\u003c/li\u003e\n\u003cli\u003e识别Session：服务器通过接收到的Session ID识别用户的会话，并从服务器端存储中获取相应的Session数据\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"四种主要技术\"\u003e四种主要技术\u003c/h1\u003e\n\u003col\u003e\n\u003cli\u003eCookies\u003c/li\u003e\n\u003cli\u003eHidden Form Field\u003c/li\u003e\n\u003cli\u003eURL Rewriting\u003c/li\u003e\n\u003cli\u003eHttpSession\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"1-cookiescookie\"\u003e1. Cookies（Cookie）\u003c/h2\u003e\n\u003cp\u003eCookie是存储在客户端浏览器中的小数据片段。它们会随着每个HTTP请求一起发送到服务器，可以用来存储会话标识符。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e工作原理\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e当用户第一次访问网站时，服务器生成一个唯一的会话ID，并将其作为Cookie发送到客户端浏览器\u003c/li\u003e\n\u003cli\u003e浏览器存储这个Cookie，并在后续请求中将其发送回服务器\u003c/li\u003e\n\u003cli\u003e服务器使用Cookie中的会话ID来检索用户的会话数据\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3e75a2f1a0ff4b6a8c74edfe28560fc8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e优点\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e对用户透明\u003c/li\u003e\n\u003cli\u003e由浏览器自动管理\u003c/li\u003e\n\u003cli\u003e可以存储少量数据\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e缺点\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e用户可以在浏览器中禁用Cookie\u003c/li\u003e\n\u003cli\u003e存储容量有限（通常每个Cookie大约4KB）\u003c/li\u003e\n\u003cli\u003e如果处理不当，可能存在安全问题（例如Cookie盗窃）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e示例\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.http.Cookie\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e// 在Servlet中设置Cookie\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCookie\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esessionCookie\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCookie\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;JSESSIONID\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esession\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetId\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esessionCookie\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esetMaxAge\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e30\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e60\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 30分钟\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eaddCookie\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esessionCookie\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e// 在Servlet中检索Cookie\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCookie\u003cspan style=\"color:#0550ae\"\u003e[]\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecookies\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetCookies\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 从请求获取Cookie\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecookies\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enull\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eCookie\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecookie\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecookies\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;JSESSIONID\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eequals\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecookie\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()))\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esessionId\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecookie\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetValue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 使用会话ID检索会话数据\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCookie有两种类型：\u003c/p\u003e","title":"JavaWeb开发基础ServletSession"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n创建类 定义类Bicycle：\npublic class Bicycle { // the Bicycle class has // three fields public int cadence; public int gear; public int speed; // the Bicycle class has // one constructor public Bicycle(int startCadence, int startSpeed, int startGear) { gear = startGear; cadence = startCadence; speed = startSpeed; } // the Bicycle class has // four methods public void setCadence(int newValue) { cadence = newValue; } public void setGear(int newValue) { gear = newValue; } public void applyBrake(int decrement) { speed -= decrement; } public void speedUp(int increment) { speed += increment; } } 创建子类MountainBike继承类Bicycle：\npublic class MountainBike extends Bicycle { // the MountainBike subclass has // one field public int seatHeight; // the MountainBike subclass has // one constructor public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear) { super(startCadence, startSpeed, startGear); seatHeight = startHeight; } // the MountainBike subclass has // one method public void setHeight(int newValue) { seatHeight = newValue; } } 可以看出定义类的语法如下：\nclass MyClass { // field, constructor, and // method declarations } 复杂点的：\nclass MyClass extends MySuperClass implements YourInterface { // field, constructor, and // method declarations } 定义了类MyClass继承类MySuperClass，并实现接口YourInterface。类只能有一个父类，但是能实现多个接口。\n定义方法 方法定义示例：\npublic double calculateAnswer(double wingSpan, int numberOfEngines, double length, double grossTons) { //do the calculation here } 方法签名，方法名+参数类型，比如上面的方法签名是：\ncalculateAnswer(double, int, double, double) 1个类中可以有多个同名的，但是不同参数的方法，也就是方法重载：\npublic class DataArtist { ... public void draw(String s) { ... } public void draw(int i) { ... } public void draw(double f) { ... } public void draw(int i, double f) { ... } } 但是方法重载应谨慎使用，它会降低代码可读性。\n类构造器 类有个默认的无参数的构造器，也可以自定义：\npublic Bicycle(int startCadence, int startSpeed, int startGear) { gear = startGear; cadence = startCadence; speed = startSpeed; } 构造器跟类同名，没有return。类可以有多个不同参数列表的构造器。\n调用 参数定义的叫做Parameters ，实际传入的叫做Arguments 。\n基本数据类型\npublic double computePayment( double loanAmt, double rate, double futureValue, int numPeriods) { double interest = rate / 100.0; double partial1 = Math.pow((1 + interest), - numPeriods); double denominator = (1 - partial1) / interest; double answer = (-loanAmt / denominator) - ((futureValue * partial1) / denominator); return answer; } 类对象\npublic Polygon polygonFrom(Point[] corners) { // method body goes here } 可变参数\npublic Polygon polygonFrom(Point... corners) { int numberOfSides = corners.length; double squareOfSide1, lengthOfSide1; squareOfSide1 = (corners[1].x - corners[0].x) * (corners[1].x - corners[0].x) + (corners[1].y - corners[0].y) * (corners[1].y - corners[0].y); lengthOfSide1 = Math.sqrt(squareOfSide1); // more method body code follows that creates and returns a // polygon connecting the Points } 使用...来表示可变参数。\n方法中的基本数据类型，return后消失：\npublic class PassPrimitiveByValue { public static void main(String[] args) { int x = 3; // invoke passMethod() with // x as argument passMethod(x); // print x to see if its // value has changed System.out.println(\u0026#34;After invoking passMethod, x = \u0026#34; + x); } // change parameter in passMethod() public static void passMethod(int p) { p = 10; } } 方法中的引用类型，return后还有作用到原来的对象：\npublic class RefType { public void moveCircle(Circle circle, int deltaX, int deltaY) { // code to move origin of circle to x+deltaX, y+deltaY circle.setX(circle.getX() + deltaX); circle.setY(circle.getY() + deltaY); // code to assign a new reference to circle circle = new Circle(0, 0); } public static void main(String[] args) { Circle circle = new Circle(1, 1); RefType refType = new RefType(); refType.moveCircle(circle, 2, 2); System.out.println(circle.getX()); } } 运行结果为3。moveCircle里面的circle，set方法调用会影响到引用的对象。虽然最后new了一个新对象，但是赋值给的是方法内部的这个circle，return后消失，外面的circle不受影响。可以这么理解，外面和里面的两个circle，引用的都是同一个对象。\n对象 创建对象：\nPoint originOne = new Point(23, 94); Rectangle rectOne = new Rectangle(originOne, 100, 200); Rectangle rectTwo = new Rectangle(50, 100); 变量声明：\ntype name; 如果是基本数据类型，则会先分配内存空间。如果是类引用，则不会，在new的时候才会分配内存空间。\n字段：\nobjectReference.fieldName 方法：\nobjectReference.methodName(argumentList); 请记住，一个对象能有多个引用。\n类的高级用法 方法退出有3种情况：\n代码执行完\nthrow异常\nreturn\n返回类型如果是类，那么可以return子类。返回类型如果是接口，那么可以return接口实现类。\nthis关键字表示当前类，或用来调用其他构造方法：\npublic class Rectangle { private int x, y; private int width, height; public Rectangle() { this(0, 0, 1, 1); } public Rectangle(int width, int height) { this(0, 0, width, height); } public Rectangle(int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; } ... } 调用其他构造方法必须放在第一行。\n范围控制\ntop级别：public、package-private\nmember级别：public、private、protected、package-private\n其中注意protected，等于package-private + 其他包中的子类（继承了当前类，但是在其他包里面）\n怎么理解？在自己的地盘（package）随便玩，但是到了其他地盘，必须有父类保护（protected）\n第3列的Subclass指的是其他包的子类。\n访问控制有2个用途：\n在用别人的类时，看哪些可以使用\n定义自己的类时，决定哪些允许别人用\nstatic关键字，创建类字段：\npublic class Bicycle { private int cadence; private int gear; private int speed; private int id; private static int numberOfBicycles = 0; public Bicycle(int startCadence, int startSpeed, int startGear){ gear = startGear; cadence = startCadence; speed = startSpeed; // increment number of Bicycles // and assign ID number id = ++numberOfBicycles; } // new method to return the ID instance variable public int getID() { return id; } ... } 可以直接用类名访问：\nBicycle.numberOfBicycles static关键字\n创建类方法：\npublic static int getNumberOfBicycles() { return numberOfBicycles; } 创建常量：\nstatic final double PI = 3.141592653589793; 注意，基本数据类型或string的常量，会在编译的时候直接替换。如果依赖外部包的常量值变化了，比如PI变成了3.9，那么当前代码需要重新编译。\n关于static，有一个很重要的点是：static只能访问static，也就是class级别只能访问class级别，如果想访问member级别，必须实例化对象后通过引用来访问。\nstatic块\n对于字段初始化来说，如果想写多行代码来初始化（比如写个for循环来填充复杂数组），instance级别可以在构造函数来做，而对于class级别呢？就可以使用static块：\npublic class MyClass { public static int x; public static int y; static { x = 10; y = 20; System.out.println(\u0026#34;静态块执行\u0026#34;); } public static void main(String[] args) { System.out.println(\u0026#34;x=\u0026#34; + x); System.out.println(\u0026#34;y=\u0026#34; + y); } } static块是用来给class级别字段做值初始化的，它们会按照在类中出现的顺序依次执行，并且它们在构造函数之前执行。\n也可以定义static方法后赋值：\nclass Whatever { public static varType myVar = initializeClassVariable(); private static varType initializeClassVariable() { // initialization code goes here } } member级别字段初始化块，也就是不带static的：\n{ // whatever code is needed for initialization goes here } 会在每个constructor中执行。也可以使用final方法：\nclass Whatever { private varType myVar = initializeInstanceVariable(); protected final varType initializeInstanceVariable() { // initialization code goes here } } 为什么这里必须要用final？因为在实例初始化期间调用非final方法会报错，这是为了提高代码健壮性、可读性和可维护性，初始化本来就是一个确定的事情，那么就用final限定清楚。\n嵌套类 class OuterClass { ... class InnerClass { ... } static class StaticNestedClass { ... } } InnerClass能访问OutClass所有成员（可以理解为跟method类似），StaticNestedClass则不能。\n什么情况需要用嵌套类？\n这个类只会被另外1个类使用，那么可以定义为嵌套类（helper classes），优化包结构\n封装，嵌套类可以访问内部private成员\n可读性，小的嵌套类，方便阅读\n必须先实例化OuterClass再实例化InnerClass：\nOuterClass outerObject = new OuterClass(); OuterClass.InnerClass innerObject = outerObject.new InnerClass(); 而static嵌套类跟普通类一样：\nStaticNestedClass staticNestedObject = new StaticNestedClass(); 注意，static嵌套类，跟其他顶层类是一样的，意味着它不能直接访问所在类的字段，而必须通过实例化对象引用才能访问。（也许是为了方便在一个文件里面写多个类，才设计了static嵌套类）\n以下是示例：\nOuterClass.java\npublic class OuterClass { String outerField = \u0026#34;Outer field\u0026#34;; static String staticOuterField = \u0026#34;Static outer field\u0026#34;; class InnerClass { void accessMembers() { System.out.println(outerField); System.out.println(staticOuterField); } } static class StaticNestedClass { void accessMembers(OuterClass outer) { // Compiler error: Cannot make a static reference to the non-static // field outerField // System.out.println(outerField); System.out.println(outer.outerField); System.out.println(staticOuterField); } } public static void main(String[] args) { System.out.println(\u0026#34;Inner class:\u0026#34;); System.out.println(\u0026#34;------------\u0026#34;); OuterClass outerObject = new OuterClass(); OuterClass.InnerClass innerObject = outerObject.new InnerClass(); innerObject.accessMembers(); System.out.println(\u0026#34;\\nStatic nested class:\u0026#34;); System.out.println(\u0026#34;--------------------\u0026#34;); StaticNestedClass staticNestedObject = new StaticNestedClass(); staticNestedObject.accessMembers(outerObject); System.out.println(\u0026#34;\\nTop-level class:\u0026#34;); System.out.println(\u0026#34;--------------------\u0026#34;); TopLevelClass topLevelObject = new TopLevelClass(); topLevelObject.accessMembers(outerObject); } } TopLevelClass.java\npublic class TopLevelClass { void accessMembers(OuterClass outer) { // Compiler error: Cannot make a static reference to the non-static // field OuterClass.outerField // System.out.println(OuterClass.outerField); System.out.println(outer.outerField); System.out.println(OuterClass.staticOuterField); } } 一个变量引用的例子，这个例子展示了不同级别的同名变量，是如何取值的：\npublic class ShadowTest { public int x = 0; class FirstLevel { public int x = 1; void methodInFirstLevel(int x) { System.out.println(\u0026#34;x = \u0026#34; + x); System.out.println(\u0026#34;this.x = \u0026#34; + this.x); System.out.println(\u0026#34;ShadowTest.this.x = \u0026#34; + ShadowTest.this.x); } } public static void main(String... args) { ShadowTest st = new ShadowTest(); ShadowTest.FirstLevel fl = st.new FirstLevel(); fl.methodInFirstLevel(23); } } x = 23 this.x = 1 ShadowTest.this.x = 0 // 注意这种在嵌套类取OuterClass同名字段的方式 Local Class，定义在method里面的嵌套类：\npublic class LocalClassExample { static String regularExpression = \u0026#34;[^0-9]\u0026#34;; public static void validatePhoneNumber( String phoneNumber1, String phoneNumber2) { final int numberLength = 10; // Valid in JDK 8 and later: // int numberLength = 10; class PhoneNumber { String formattedPhoneNumber = null; PhoneNumber(String phoneNumber){ // numberLength = 7; String currentNumber = phoneNumber.replaceAll( regularExpression, \u0026#34;\u0026#34;); if (currentNumber.length() == numberLength) formattedPhoneNumber = currentNumber; else formattedPhoneNumber = null; } public String getNumber() { return formattedPhoneNumber; } // Valid in JDK 8 and later: // public void printOriginalNumbers() { // System.out.println(\u0026#34;Original numbers are \u0026#34; + phoneNumber1 + // \u0026#34; and \u0026#34; + phoneNumber2); // } } PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1); PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2); // Valid in JDK 8 and later: // myNumber1.printOriginalNumbers(); if (myNumber1.getNumber() == null) System.out.println(\u0026#34;First number is invalid\u0026#34;); else System.out.println(\u0026#34;First number is \u0026#34; + myNumber1.getNumber()); if (myNumber2.getNumber() == null) System.out.println(\u0026#34;Second number is invalid\u0026#34;); else System.out.println(\u0026#34;Second number is \u0026#34; + myNumber2.getNumber()); } public static void main(String... args) { validatePhoneNumber(\u0026#34;123-456-7890\u0026#34;, \u0026#34;456-7890\u0026#34;); } } Local Class只能访问方法里面的final变量，或者“看似final”的变量（值不会发生变化）。否则会报错：local variables referenced from an inner class must be final or effectively final。但是能直接方法参数列表的parameters。\nAnonymous Class，定义在method里面的没有名字的嵌套类：\npublic class HelloWorldAnonymousClasses { interface HelloWorld { public void greet(); public void greetSomeone(String someone); } public void sayHello() { class EnglishGreeting implements HelloWorld { String name = \u0026#34;world\u0026#34;; public void greet() { greetSomeone(\u0026#34;world\u0026#34;); } public void greetSomeone(String someone) { name = someone; System.out.println(\u0026#34;Hello \u0026#34; + name); } } HelloWorld englishGreeting = new EnglishGreeting(); HelloWorld frenchGreeting = new HelloWorld() { // 匿名类 String name = \u0026#34;tout le monde\u0026#34;; public void greet() { greetSomeone(\u0026#34;tout le monde\u0026#34;); } public void greetSomeone(String someone) { name = someone; System.out.println(\u0026#34;Salut \u0026#34; + name); } }; HelloWorld spanishGreeting = new HelloWorld() { // 匿名类 String name = \u0026#34;mundo\u0026#34;; public void greet() { greetSomeone(\u0026#34;mundo\u0026#34;); } public void greetSomeone(String someone) { name = someone; System.out.println(\u0026#34;Hola, \u0026#34; + name); } }; englishGreeting.greet(); frenchGreeting.greetSomeone(\u0026#34;Fred\u0026#34;); spanishGreeting.greet(); } public static void main(String... args) { HelloWorldAnonymousClasses myApp = new HelloWorldAnonymousClasses(); myApp.sayHello(); } } 它的语法跟constructor类似，new后面跟上implements接口或extends类的名字，但它是一个表达式，所以最后要加上分号。\n请记住，Local Class和Anonymous Class，都是应以在method里面的。\n参考资料：\nClasses and Objects https://dev.java/learn/classes-objects/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000007-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B04%E7%B1%BB%E5%92%8C%E5%AF%B9%E8%B1%A1/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"创建类\"\u003e创建类\u003c/h2\u003e\n\u003cp\u003e定义类Bicycle：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eBicycle\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// the Bicycle class has\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// three fields\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecadence\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003egear\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003espeed\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// the Bicycle class has\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// one constructor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eBicycle\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartCadence\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartSpeed\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartGear\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003egear\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartGear\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003ecadence\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartCadence\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartSpeed\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// the Bicycle class has\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// four methods\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esetCadence\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003ecadence\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esetGear\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003egear\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eapplyBrake\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edecrement\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edecrement\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003espeedUp\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eincrement\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eincrement\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e创建子类MountainBike继承类Bicycle：\u003c/p\u003e","title":"Java官方笔记4类和对象"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter关联是什么 JMeter关联，这几个字看着可能会有点陌生，实际上却是工作中经常会做的一件事情，尤其是接口自动化，它指的是把一个接口的响应作为另一个接口的参数，从而把接口关联起来。\nJMeter关联是通过后置处理器来实现的，本文将介绍两个主流提取器：正则表达式提取器和JSON JMESPath提取器。\n正则表达式提取器 Name 最好有业务含义的名称。\nComments 可以为空的注释。\nApply to 匹配范围。\nMain sample and sub-samples\n匹配当前父取样器及其子取样器。\nMain sample only\n仅匹配当前父取样器。\nSub-samples only\n仅匹配子取样器。\nJMeter Variable Name to use\n匹配JMeter变量。\nField to check 匹配响应的字段，共七个选项。\nBody：主体，最常用。\nBody(unescaped) ：替换了所有的html转义符的响应主体内容，注意html转义符处理时不考虑上下文，因此可能有不正确的转换，不太建议使用。\nBody as a Document：从不同类型的文件中提取文本，注意这个选项比较影响性能。\nResponse Headers：响应Headers。\nRequest Headers：请求Headers。\nURL：请求URL。\nResponse Code：响应代码，比如200、404返回码。\nResponse Message：响应信息，比如“成功”、“OK”字样。\nName of created variable 创建的变量名，比如变量名为token，通过${token}进行访问。\nRegular Expression 正则表达式。\n正则表达式可阅读这篇文章进一步了解：https://dongfanger.gitee.io/blog/未分类/001-5分钟轻松学正则表达式.html\nTemplate($i$ where i is capturing group number, starts at 1) 用$$引用起来，如果在正则表达式中有多个正则表达式（多个括号括起来的），则可以是$1$、$2$等等，$1$指第一个模板，$2$指第二个模板，依次类推，$0$指全文匹配，$1$$2$指同时取第一个和第二个模板。\nMatch No.(0 for Random) 在匹配时往往会出现多个值匹配的情况，如果匹配数为0则代表随机取匹配值；可以用匹配数字来确定多个值中的哪一个；负数取所有值，可以与For Each Controller一起使用来遍历。\nDefault Value 默认值，如果没有匹配到可以指定一个默认值。\nUse empty default value勾选后把空作为默认值，跟不填写效果一样。如果填写了默认值同时勾选此选项，默认值仍然为空。\nJSON JMESPath提取器 Name 最好有业务含义的名称。\nComments 可以为空的注释。\nApply to 匹配范围。\nMain sample and sub-samples\n匹配当前父取样器及其子取样器。\nMain sample only\n仅匹配当前父取样器。\nSub-samples only\n仅匹配子取样器。\nJMeter Variable Name to use\n匹配JMeter变量。\nName of created variable 创建的变量名，比如变量名为token，通过${token}进行访问。\nJMESPath expressions JMESPath表达式。\n可以访问官网进一步了解：https://jmespath.org/\nMatch No.(0 for Random) 在匹配时往往会出现多个值匹配的情况，如果匹配数为0则代表随机取匹配值；可以用匹配数字来确定多个值中的哪一个；负数取所有值，可以与For Each Controller一起使用来遍历。\nDefault Value 默认值，如果没有匹配到可以指定一个默认值。\n小结 本文首先介绍了JMeter关联的概念，实际上指的就是把一个接口的响应作为另一个接口的参数，从而把接口关联起来。然后分别以元件编辑区为内容，讲解了JMeter关联常用的两个后置处理器：正则表达式提取器和JSON JMESPath提取器。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\nhttps://blog.csdn.net/weixin_43507959/article/details/104068508\nhttps://blog.csdn.net/doubledog1112/article/details/93480458\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000007-jmeterjmeter%E5%85%B3%E8%81%94%E9%99%8C%E7%94%9F%E5%8F%88%E7%86%9F%E6%82%89/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"jmeter关联是什么\"\u003eJMeter关联是什么\u003c/h1\u003e\n\u003cp\u003eJMeter关联，这几个字看着可能会有点陌生，实际上却是工作中经常会做的一件事情，尤其是接口自动化，它指的是把一个接口的响应作为另一个接口的参数，从而\u003cstrong\u003e把接口关联起来\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eJMeter关联是通过后置处理器来实现的，本文将介绍两个主流提取器：正则表达式提取器和JSON JMESPath提取器。\u003c/p\u003e\n\u003ch1 id=\"正则表达式提取器\"\u003e正则表达式提取器\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ce535876d05748b98a16512d1e2f536e.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"name\"\u003eName\u003c/h2\u003e\n\u003cp\u003e最好有业务含义的名称。\u003c/p\u003e\n\u003ch2 id=\"comments\"\u003eComments\u003c/h2\u003e\n\u003cp\u003e可以为空的注释。\u003c/p\u003e\n\u003ch2 id=\"apply-to\"\u003eApply to\u003c/h2\u003e\n\u003cp\u003e匹配范围。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMain sample and sub-samples\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e匹配当前父取样器及其子取样器。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMain sample only\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e仅匹配当前父取样器。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSub-samples only\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e仅匹配子取样器。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJMeter Variable Name to use\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e匹配JMeter变量。\u003c/p\u003e\n\u003ch2 id=\"field-to-check\"\u003eField to check\u003c/h2\u003e\n\u003cp\u003e匹配响应的字段，共七个选项。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBody\u003c/strong\u003e：主体，最常用。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBody(unescaped)\u003c/strong\u003e ：替换了所有的html转义符的响应主体内容，注意html转义符处理时不考虑上下文，因此可能有不正确的转换，不太建议使用。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBody as a Document\u003c/strong\u003e：从不同类型的文件中提取文本，注意这个选项比较影响性能。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResponse Headers\u003c/strong\u003e：响应Headers。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRequest Headers\u003c/strong\u003e：请求Headers。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eURL\u003c/strong\u003e：请求URL。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResponse Code\u003c/strong\u003e：响应代码，比如200、404返回码。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResponse Message\u003c/strong\u003e：响应信息，比如“成功”、“OK”字样。\u003c/p\u003e\n\u003ch2 id=\"name-of-created-variable\"\u003eName of created variable\u003c/h2\u003e\n\u003cp\u003e创建的变量名，比如变量名为token，通过\u003ccode\u003e${token}\u003c/code\u003e进行访问。\u003c/p\u003e\n\u003ch2 id=\"regular-expression\"\u003eRegular Expression\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e正则表达式。\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e正则表达式可阅读这篇文章进一步了解：\u003ca href=\"https://dongfanger.gitee.io/blog/%E6%9C%AA%E5%88%86%E7%B1%BB/001-5%E5%88%86%E9%92%9F%E8%BD%BB%E6%9D%BE%E5%AD%A6%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F.html\"\u003ehttps://dongfanger.gitee.io/blog/未分类/001-5分钟轻松学正则表达式.html\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"templatei-where-i-is-capturing-group-number-starts-at-1\"\u003eTemplate(\u003ccode\u003e$i$\u003c/code\u003e where i is capturing group number, starts at 1)\u003c/h2\u003e\n\u003cp\u003e用\u003ccode\u003e$$\u003c/code\u003e引用起来，如果在正则表达式中有多个正则表达式（多个括号括起来的），则可以是\u003ccode\u003e$1$\u003c/code\u003e、\u003ccode\u003e$2$\u003c/code\u003e等等，\u003ccode\u003e$1$\u003c/code\u003e指第一个模板，\u003ccode\u003e$2$\u003c/code\u003e指第二个模板，依次类推，\u003ccode\u003e$0$\u003c/code\u003e指全文匹配，\u003ccode\u003e$1$$2$\u003c/code\u003e指同时取第一个和第二个模板。\u003c/p\u003e","title":"JMeter关联陌生又熟悉"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n依赖注入 以前的JUnit的类构造方法和测试方法都是不能有参数的，JUnit Jupiter有一个颠覆性的改进，就是允许它们有入参，这样就能做依赖注入了。\n如果你对pytest的fixture有了解的话，就知道这个技术是多么的强大。\nParameterResolver是一个接口类，类构造方法和测试方法在运行时，必须由被注册的ParameterResolver进行解析。JUnit Jupiter有三个自动注册的内置解析器：\nTestInfoParameterResolver 参数类型为TestInfo RepetitionInfoParameterResolver 参数类型为RepetitionInfo TestReporterParameterResolver 参数类型为TestReporter TestInfo\nTestInfo包含the display name, the test class, the test method, and associated tags等信息。\n示例：\nimport static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @DisplayName(\u0026#34;TestInfo Demo\u0026#34;) class TestInfoDemo { TestInfoDemo(TestInfo testInfo) { assertEquals(\u0026#34;TestInfo Demo\u0026#34;, testInfo.getDisplayName()); } @BeforeEach void init(TestInfo testInfo) { String displayName = testInfo.getDisplayName(); assertTrue(displayName.equals(\u0026#34;TEST 1\u0026#34;) || displayName.equals(\u0026#34;test2()\u0026#34;)); } @Test @DisplayName(\u0026#34;TEST 1\u0026#34;) @Tag(\u0026#34;my-tag\u0026#34;) void test1(TestInfo testInfo) { assertEquals(\u0026#34;TEST 1\u0026#34;, testInfo.getDisplayName()); assertTrue(testInfo.getTags().contains(\u0026#34;my-tag\u0026#34;)); } @Test void test2() { } } RepetitionInfo\n主要是@RepeatedTest会用到，包含当前重复以及总重复次数等信息。\nTestReporter\nTestReporter能用来输出额外的信息。\n示例：\nclass TestReporterDemo { @Test void reportSingleValue(TestReporter testReporter) { testReporter.publishEntry(\u0026#34;a status message\u0026#34;); } @Test void reportKeyValuePair(TestReporter testReporter) { testReporter.publishEntry(\u0026#34;a key\u0026#34;, \u0026#34;a value\u0026#34;); } @Test void reportMultipleKeyValuePairs(TestReporter testReporter) { Map\u0026lt;String, String\u0026gt; values = new HashMap\u0026lt;\u0026gt;(); values.put(\u0026#34;user name\u0026#34;, \u0026#34;dk38\u0026#34;); values.put(\u0026#34;award year\u0026#34;, \u0026#34;1974\u0026#34;); testReporter.publishEntry(values); } } 传自定义参数\n除了内置解析器，如果想传自定义参数，那么需要使用@ExtendWith注册扩展，比如：\n@ExtendWith(RandomParametersExtension.class) class MyRandomParametersTest { @Test void injectsInteger(@Random int i, @Random int j) { assertNotEquals(i, j); } @Test void injectsDouble(@Random double d) { assertEquals(0.0, d, 1.0); } } 有点插件的意思，更常见的是MockitoExtension和SpringExtension。\n测试接口 JUnit Jupiter除了测试类和测试方法，其实也有测试接口，比如：\n@TestInstance(Lifecycle.PER_CLASS) interface TestLifecycleLogger { static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName()); @BeforeAll default void beforeAllTests() { logger.info(\u0026#34;Before all tests\u0026#34;); } @AfterAll default void afterAllTests() { logger.info(\u0026#34;After all tests\u0026#34;); } @BeforeEach default void beforeEachTest(TestInfo testInfo) { logger.info(() -\u0026gt; String.format(\u0026#34;About to execute [%s]\u0026#34;, testInfo.getDisplayName())); } @AfterEach default void afterEachTest(TestInfo testInfo) { logger.info(() -\u0026gt; String.format(\u0026#34;Finished executing [%s]\u0026#34;, testInfo.getDisplayName())); } } interface TestInterfaceDynamicTestsDemo { @TestFactory default Stream\u0026lt;DynamicTest\u0026gt; dynamicTestsForPalindromes() { return Stream.of(\u0026#34;racecar\u0026#34;, \u0026#34;radar\u0026#34;, \u0026#34;mom\u0026#34;, \u0026#34;dad\u0026#34;) .map(text -\u0026gt; dynamicTest(text, () -\u0026gt; assertTrue(isPalindrome(text)))); } } @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @BeforeEach, and @AfterEach能作用到接口的default方法上。\ndefault方法是接口已经实现好了的方法，接口的实现类不需要再编写实现代码，就能直接使用。\n如果测试类是@TestInstance(Lifecycle.PER_CLASS)注解，那么可以使用@BeforeAll and @AfterAll。\n测试接口可以作为模版。如果测试接口有@ExtendWith and @Tag注解，那么它的实现类也会继承tags and extensions。比如：\n@Tag(\u0026#34;timed\u0026#34;) @ExtendWith(TimingExtension.class) interface TimeExecutionLogger { } class TestInterfaceDemo implements TestLifecycleLogger, TimeExecutionLogger, TestInterfaceDynamicTestsDemo { @Test void isEqualValue() { assertEquals(1, \u0026#34;a\u0026#34;.length(), \u0026#34;is always equal\u0026#34;); } } 结果：\nINFO example.TestLifecycleLogger - Before all tests INFO example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()] INFO example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms. INFO example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()] INFO example.TestLifecycleLogger - About to execute [isEqualValue()] INFO example.TimingExtension - Method [isEqualValue] took 1 ms. INFO example.TestLifecycleLogger - Finished executing [isEqualValue()] INFO example.TestLifecycleLogger - After all tests **测试接口也可以作为契约。**比如：\npublic interface Testable\u0026lt;T\u0026gt; { T createValue(); } public interface EqualsContract\u0026lt;T\u0026gt; extends Testable\u0026lt;T\u0026gt; { T createNotEqualValue(); @Test default void valueEqualsItself() { T value = createValue(); assertEquals(value, value); } @Test default void valueDoesNotEqualNull() { T value = createValue(); assertFalse(value.equals(null)); } @Test default void valueDoesNotEqualDifferentValue() { T value = createValue(); T differentValue = createNotEqualValue(); assertNotEquals(value, differentValue); assertNotEquals(differentValue, value); } } public interface ComparableContract\u0026lt;T extends Comparable\u0026lt;T\u0026gt;\u0026gt; extends Testable\u0026lt;T\u0026gt; { T createSmallerValue(); @Test default void returnsZeroWhenComparedToItself() { T value = createValue(); assertEquals(0, value.compareTo(value)); } @Test default void returnsPositiveNumberWhenComparedToSmallerValue() { T value = createValue(); T smallerValue = createSmallerValue(); assertTrue(value.compareTo(smallerValue) \u0026gt; 0); } @Test default void returnsNegativeNumberWhenComparedToLargerValue() { T value = createValue(); T smallerValue = createSmallerValue(); assertTrue(smallerValue.compareTo(value) \u0026lt; 0); } } 实现类：\nclass StringTests implements ComparableContract\u0026lt;String\u0026gt;, EqualsContract\u0026lt;String\u0026gt; { @Override public String createValue() { return \u0026#34;banana\u0026#34;; } @Override public String createSmallerValue() { return \u0026#34;apple\u0026#34;; // \u0026#39;a\u0026#39; \u0026lt; \u0026#39;b\u0026#39; in \u0026#34;banana\u0026#34; } @Override public String createNotEqualValue() { return \u0026#34;cherry\u0026#34;; } } 小结 本文先介绍了JUnit Jupiter的颠覆性技术，允许传参以实现依赖注入，然后介绍了除了测试类和测试方法以外的测试接口，它既可以作为测试模板，也可以作为测试契约。\n参考资料：\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-dependency-injection\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-test-interfaces-and-default-methods\nhttps://blog.csdn.net/qq_35387940/article/details/104767746\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000007-junitjunit5%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E4%B8%8E%E6%B5%8B%E8%AF%95%E6%8E%A5%E5%8F%A3/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"依赖注入\"\u003e依赖注入\u003c/h1\u003e\n\u003cp\u003e以前的JUnit的类构造方法和测试方法都是不能有参数的，\u003cstrong\u003eJUnit Jupiter有一个颠覆性的改进，就是允许它们有入参，这样就能做依赖注入了。\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如果你对pytest的fixture有了解的话，就知道这个技术是多么的强大。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eParameterResolver是一个接口类，类构造方法和测试方法在运行时，必须由被注册的ParameterResolver进行解析。JUnit Jupiter有三个自动注册的内置解析器：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eTestInfoParameterResolver 参数类型为TestInfo\u003c/li\u003e\n\u003cli\u003eRepetitionInfoParameterResolver 参数类型为RepetitionInfo\u003c/li\u003e\n\u003cli\u003eTestReporterParameterResolver 参数类型为TestReporter\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eTestInfo\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eTestInfo包含the display name, the test class, the test method, and associated tags等信息。\u003c/p\u003e\n\u003cp\u003e示例：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport static\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Assertions.assertEquals\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport static\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Assertions.assertTrue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.BeforeEach\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.DisplayName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Tag\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Test\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.TestInfo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@DisplayName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;TestInfo Demo\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eTestInfoDemo\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eTestInfoDemo\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eTestInfo\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etestInfo\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eassertEquals\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;TestInfo Demo\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etestInfo\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetDisplayName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@BeforeEach\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003einit\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eTestInfo\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etestInfo\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edisplayName\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etestInfo\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetDisplayName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eassertTrue\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edisplayName\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eequals\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;TEST 1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e||\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edisplayName\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eequals\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;test2()\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@DisplayName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;TEST 1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Tag\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;my-tag\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etest1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eTestInfo\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etestInfo\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eassertEquals\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;TEST 1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003etestInfo\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetDisplayName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eassertTrue\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003etestInfo\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetTags\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003econtains\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;my-tag\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etest2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eRepetitionInfo\u003c/strong\u003e\u003c/p\u003e","title":"JUnit5依赖注入与测试接口"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n当执行pytest命令时，pytest会从project中查找test来执行。本文先从几个路径相关的概念讲起，这样便于理解pytest在遍历路径时的走向。\n几个路径相关的概念 PYTHONHOME 定义了Python标准库的路径。\nPYTHONPATH 定义了Python import模块搜索的路径。\nbasedir ①如果是module，目录中不包括__init__.py，basedir的值等于a\na |--b_test.py ②如果是package，目录中包括__init__.py，basedir的值等于y\ny |--a | |--b_test.py | |--__init__.py // 表明a是package ③如果是package的package，目录中都包括__init__.py，basedir的值等于x\nx |--y | |--a | |--__init__.py // 表明y是package | | |--b_test.py | | |--__init__.py // 表明a是package sys.path 模块搜索路径集，包括以上3个目录。它决定了import能否找到模块。\ncurrent working directory 当前工作目录，缩写cwd，等于执行pytest命令的目录。\n如果用python -m pytest，以模块的方式来执行，会把cwd也加入sys.path中。\n例如，在a目录下执行pytest，cwd是a，basedir是y，sys.path中只包含y。如果执行python -m pytest，sys.path中既包含y也包含a\ny |--a | |--b_test.py | |--__init__.py // 表明a是package pytest查找原理 第一种情况 pytest命令是可以加参数的，如果加了文件夹/文件参数，那么就只在参数指定的文件夹/文件中查找，可以指定多个。例如\npytest a_dir b_dir c_test.py d_test.py 特殊的，如果a_dir中包含了c_test.py，那么会收集成2次，pytest也会执行2次。\n也可以使用::来指定函数/类/方法，例如\npytest test_mod.py::test_func pytest test_mod.py::TestClass::test_method 第二种情况 pytest不带参数。\n从“当前工作目录”开始找，递归查找子目录。匹配 test_*.py 或 *_test.py 的文件。\n找到这些模块（Python中1个.py文件就是1个模块）以后，进一步根据上节所述找basedir。\n调用sys.path.insert(0, basedir) ，把basedir加入sys.path中。这些模块就可以被pytest import了。\nimport之后，查找test开头的函数或方法。如果是类中的方法，类必须以Test开头，并且没有__init__方法。\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/pytest/000007-pytestpytest%E7%94%A8%E4%BE%8B%E6%9F%A5%E6%89%BE%E5%8E%9F%E7%90%86/","summary":"当执行pytest命令时，pytest会从project中查找test来执行。本文先从几个路径相关的概念讲起，这样便于理解pytest在遍历路径时的走向。","title":"pytest用例查找原理"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nMost web applications use the spring-boot-starter-web module to get up and running quickly. You can also choose to build reactive web applications by using the spring-boot-starter-webflux module.\nServlet Web Applications Spring Web MVC Framework\nimport java.util.List; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(\u0026#34;/users\u0026#34;) public class MyRestController { private final UserRepository userRepository; private final CustomerRepository customerRepository; public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) { this.userRepository = userRepository; this.customerRepository = customerRepository; } @GetMapping(\u0026#34;/{userId}\u0026#34;) public User getUser(@PathVariable Long userId) { return this.userRepository.findById(userId).get(); } @GetMapping(\u0026#34;/{userId}/customers\u0026#34;) public List\u0026lt;Customer\u0026gt; getUserCustomers(@PathVariable Long userId) { return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get(); } @DeleteMapping(\u0026#34;/{userId}\u0026#34;) public void deleteUser(@PathVariable Long userId) { this.userRepository.deleteById(userId); } } Static Content\nBy default, Spring Boot serves static content from a directory called /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext.\nError Handling\nBy default, Spring Boot provides an /error mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container.\nCORS Support\nimport org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration(proxyBeanMethods = false) public class MyCorsConfiguration { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(\u0026#34;/api/**\u0026#34;); } }; } } By default, the embedded server listens for HTTP requests on port 8080.\nReactive Web Applications Spring WebFlux is the new reactive web framework introduced in Spring Framework 5.0. Unlike Spring MVC, it does not require the servlet API, is fully asynchronous and non-blocking, and implements the Reactive Streams specification through the Reactor project.\nSpring Security Spring Boot relies on Spring Security’s content-negotiation strategy to determine whether to use httpBasic or formLogin.\nThe basic features you get by default in a web application are:\nA UserDetailsService (or ReactiveUserDetailsService in case of a WebFlux application) bean with in-memory store and a single user with a generated password (see SecurityProperties.User for the properties of the user).\nForm-based login or HTTP Basic security (depending on the Accept header in the request) for the entire application (including actuator endpoints if actuator is on the classpath).\nA DefaultAuthenticationEventPublisher for publishing authentication events.\nOAuth2 is a widely used authorization framework that is supported by Spring.\nSpring Session When building a servlet web application, the following stores can be auto-configured:\nRedis\nJDBC\nHazelcast\nMongoDB\n参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#web\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000007-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B04web/","summary":"Most web applications use the \u003ccode\u003espring-boot-starter-web\u003c/code\u003e module to get up and running quickly. You can also choose to build reactive web applications by using the \u003ccode\u003espring-boot-starter-webflux\u003c/code\u003e module.","title":"SpringBoot官方笔记4Web"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文开发内容 上一篇文章已经把pytest引入到测试平台中，通过多线程和多进程的方式，运行测试用例。有了这个基础，做批量运行用例的功能就很简单了，只需要前端传入一个CaseList即可。本文的后端代码是增删改查和复用run_case相关代码做个run_plan。前端代码将学习如何通过LocalStorage在非父子组件之间传递数据。具体开发内容如下：\n测试计划增删改查 测试计划动态添加移除测试用例 运行测试计划，批量运行用例 计划运行结果、用例运行结果 编写后端代码 编辑teprunner/urls.py文件，添加测试计划的路由：\n编辑teprunner/models.py文件，添加3张表的model：\nPlan存放测试计划，PlanCase存放计划和用例关联关系，PlanResult和CaseResult类似，这里之所以把单独出来一张结果表，是为了和用例结果CaseResult相互独立，防止计划结果覆盖用例结果，毕竟CaseResult一般是手工跑出来的，PlanResult一般是定时任务跑出来的，功能数据解耦后处理起来也更方便。\n编辑teprunner/serializers.py文件，添加序列化器：\n为了前端展示需要，这里额外关联了一些字段进行展示。尤其是：\ncaseNum用例总条数，passedNum成功条数，failedNum失败条数，errorNum错误条数。成功定义为：只有passed，没有failed和error的用例。失败定义为：只有failed，没有error的用例。错误定义为：只要有error的用例。\n本文还展示了，如果序列化器的某个字段只在存库时使用，返回响应时不展示，该怎么处理：\n重写to_representation方法，把不需要返回的字段pop移出去。这里output在计划运行结果不需要，在用例运行结果才需要，为了减少传输数据，就在计划运行结果的接口响应中移出去了。\n最后来写View，新增teprunner/views/plan.py文件：\n重写list方法，添加查询条件。重写destroy方法，判断必须先删除关联测试用例，才能删除测试计划。\nPlanCaseView类似：\n添加add方法：\n在添加用例时写入plan_case表。添加remove方法：\n在移除用例时从plan_case表删除数据。继续：\nresult视图用来返回计划运行结果，根据searchType来分别查询用例总条数、成功条数、失败条数、错误条数。\n继续：\nplan.case_result和case.case_result类似，返回测试计划的每条用例的运行结果。\n在测试计划添加用例的时候，有个用例列表，需要增加2个过滤条件，编辑teprunner/views/case.py文件：\n第一个过滤条件是排除当前计划已经添加过的用例：exclude_plan_id由前端传入，根据PlanCase关联关系找到已添加的用例进行过滤；第二个查询条件是关键字：用例ID或用例描述，跟PlanCaseView.list()里面一样。\n最后，编辑teprunner/views/run.py文件，增加批量运行用例的功能：\n添加delete_plan_result方法，在运行用例前，先把已有结果删掉，注意这里的model是PlanResult。继续：\n修改pytest_subprocess函数，添加plan_id字段。继续：\n修改save_case_result函数，判断没有plan_id就存case_result表，有plan_id就存plan_result表。\n复制run_case的代码为run_plan：\n修改case相关代码为plan。值得注意的是中间那段代码，根据plan_id从PlanCase中找到关联用例，生成了case_list，这样就复用run_case的多线程和多进程代码，批量运行测试用例了。\n编写前端代码 编辑src/router/index.js文件，添加测试计划相关路由：\n{ path: \u0026#34;plan\u0026#34;, name: \u0026#34;plan\u0026#34;, meta: { title: \u0026#34;测试计划\u0026#34;, }, component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanManagement.vue\u0026#34;), children: [ { path: \u0026#34;addPlan\u0026#34;, name: \u0026#34;addPlan\u0026#34;, meta: { title: \u0026#34;添加计划\u0026#34;, }, component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanEditor\u0026#34;), }, { path: \u0026#34;editPlan\u0026#34;, name: \u0026#34;editPlan\u0026#34;, meta: { title: \u0026#34;编辑计划\u0026#34;, }, component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanEditor\u0026#34;), }, { path: \u0026#34;caseList\u0026#34;, name: \u0026#34;caseList\u0026#34;, meta: { title: \u0026#34;用例列表\u0026#34;, }, component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/CaseList\u0026#34;), }, { path: \u0026#34;planResult\u0026#34;, name: \u0026#34;planResult\u0026#34;, meta: { title: \u0026#34;计划运行结果\u0026#34;, }, component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanResult\u0026#34;), children: [ { path: \u0026#34;caseResult\u0026#34;, name: \u0026#34;caseResult\u0026#34;, meta: { title: \u0026#34;用例运行结果\u0026#34;, }, component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/CaseResult\u0026#34;), }, ], }, ], }, 跟之前做增删改查不一样的地方是：**在父组件中，不会import子组件，而是通过LocalStorage进行组件之间传值。**因此这里增加了添加计划、编辑计划等路由。\n编辑src/views/teprunner/index.vue文件：\n添加测试计划左侧菜单。\n新建src/views/teprunner/plan目录，新建src/views/teprunner/plan/PlanManagement.vue文件：\n项目环境根据路由不同，分别展示，路由plan是测试计划主页面，同时展示项目和环境。路由addPlan新增计划不需要展示环境。路由editPlan编辑计划，不需要展示环境，且项目不能修改。接着添加了\u0026lt;div\u0026gt;主路由界面，通过v-if来判断是否为主路由。\u0026lt;router-view\u0026gt;是用来展示路由的页面容器，addPlan和editPlan的组件经过主路由的this.$router.push()后，会加载到这个容器里。\n就像用例结果一样，我给不同计划结果区分了颜色和链接：\n继续：\n运行计划和删除计划跟之前写的代码相差不大。\n继续：\n**这就是LocalStorage给组件传数据的关键代码。**先存数据，再跳转路由，这个路由会加载到前面添加的\u0026lt;router-view\u0026gt;容器中。\n新建src/views/teprunner/plan/PlanEditor.vue文件：\n这是新增和修改计划的组件，在created()时读取localStorage中主页面存入的数据。在这个页面继续添加代码：\n点击取消按钮后，返回上一级路由：测试计划主页面。\n新增或修改后，返回测试计划主页面，需要立即展示新数据：\n通过watch来监测，每次路由跳转到plan时，就查询一次表格数据。\n新建src/views/teprunner/plan/CaseList.vue文件：\n在created()读取数据。这个地方有个弹出框，用到了selection-panel：\n这在用户管理，新增用户选择角色时已经介绍过了。此时需要加点东西，添加一个查询条件用来过滤测试用例，编辑src/components/SelectionPanel.vue文件：\n添加了总项数，和查询输入框。继续：\n这几个组件的函数很多都添加了async关键字，表示异步。这么做是为了页面操作看起来更流畅，防止添加移除频繁操作导致页面卡住。\n新建src/views/teprunner/plan/PlanResult.vue文件：\n主路由用来展示计划运行结果，子路由用来展示用例运行结果（包含了日志输出）。跳转代码是：\n和测试计划主页面方式一样，用到了this.$router.push()。\n新建src/views/teprunner/plan/CaseResult.vue文件，内容和src/views/teprunner/case/CaseResult.vue文件基本一样，区别在于现在不是用的弹出框，而是路由到子页面了，可视区域更佳，体验更棒。\n小结 本文进一步完善了测试平台的功能，除了单条用例运行，还能批量用例运行（催更的小伙伴可以拉代码尝试下哦）。其中有个新技术，前端通过localStorage来给不同组件之间传递数据，这样就可以把弹出框做成了子路由页面 ，然后很方便地点击链接进入子页面，点击面包屑导航返回上级页面。这为fixtures和用例管理中的弹出框优化提供了参考。\n参考资料：\n前端代码仓库 https://github.com/dongfanger/teprunner-frontend\n后端代码仓库 https://github.com/dongfanger/teprunner-backend\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000007-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E6%B5%8B%E8%AF%95%E8%AE%A1%E5%88%92%E6%89%B9%E9%87%8F%E8%BF%90%E8%A1%8C%E7%94%A8%E4%BE%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"本文开发内容\"\u003e本文开发内容\u003c/h1\u003e\n\u003cp\u003e上一篇文章已经把pytest引入到测试平台中，通过多线程和多进程的方式，运行测试用例。有了这个基础，做批量运行用例的功能就很简单了，只需要前端传入一个CaseList即可。本文的后端代码是增删改查和复用run_case相关代码做个run_plan。前端代码将学习如何通过LocalStorage在非父子组件之间传递数据。具体开发内容如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e测试计划增删改查\u003c/li\u003e\n\u003cli\u003e测试计划动态添加移除测试用例\u003c/li\u003e\n\u003cli\u003e运行测试计划，批量运行用例\u003c/li\u003e\n\u003cli\u003e计划运行结果、用例运行结果\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"编写后端代码\"\u003e编写后端代码\u003c/h1\u003e\n\u003cp\u003e编辑teprunner/urls.py文件，添加测试计划的路由：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1c96969043d24bd0b87c84d657795fa5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e编辑teprunner/models.py文件，添加3张表的model：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fcd47aad64ed43e4a1d72f29dfa16568.png\"\u003e\u003c/p\u003e\n\u003cp\u003ePlan存放测试计划，PlanCase存放计划和用例关联关系，PlanResult和CaseResult类似，这里之所以把单独出来一张结果表，是为了和用例结果CaseResult相互独立，防止计划结果覆盖用例结果，毕竟CaseResult一般是手工跑出来的，PlanResult一般是定时任务跑出来的，功能数据解耦后处理起来也更方便。\u003c/p\u003e\n\u003cp\u003e编辑teprunner/serializers.py文件，添加序列化器：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b2608ab6d83845e2a7c26f365a6ee6e3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/49b760af5b804974a9d5d8aa06a87402.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5992c4f23daf478f9984ca7186da9881.png\"\u003e\u003c/p\u003e\n\u003cp\u003e为了前端展示需要，这里额外关联了一些字段进行展示。尤其是：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/97d5f6ac98354ce5b2d1a501f45f3d94.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1fdbb3cd5aef45e89a27b0fc3d3cb3ed.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9fd4fcda762342339a84b37707c177be.png\"\u003e\u003c/p\u003e\n\u003cp\u003ecaseNum用例总条数，passedNum成功条数，failedNum失败条数，errorNum错误条数。成功定义为：只有passed，没有failed和error的用例。失败定义为：只有failed，没有error的用例。错误定义为：只要有error的用例。\u003c/p\u003e\n\u003cp\u003e本文还展示了，\u003cstrong\u003e如果序列化器的某个字段只在存库时使用，返回响应时不展示，该怎么处理\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ebdfaf64af22487994b1f0bf89b93140.png\"\u003e\u003c/p\u003e\n\u003cp\u003e重写to_representation方法，把不需要返回的字段pop移出去。这里output在计划运行结果不需要，在用例运行结果才需要，为了减少传输数据，就在计划运行结果的接口响应中移出去了。\u003c/p\u003e\n\u003cp\u003e最后来写View，新增teprunner/views/plan.py文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4353e6c7d27c4744bee01d379a0ffe9a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e重写list方法，添加查询条件。重写destroy方法，判断必须先删除关联测试用例，才能删除测试计划。\u003c/p\u003e\n\u003cp\u003ePlanCaseView类似：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e501d7506ae84627800008bd846012cf.png\"\u003e\u003c/p\u003e\n\u003cp\u003e添加add方法：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/486959963d0c41ce976ec387550e1748.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在添加用例时写入plan_case表。添加remove方法：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d26ec36f6aea4ff4aee737fc4165d453.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在移除用例时从plan_case表删除数据。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/55b6f7f5f39642fa9cb85bf810f7d579.png\"\u003e\u003c/p\u003e\n\u003cp\u003eresult视图用来返回计划运行结果，根据searchType来分别查询用例总条数、成功条数、失败条数、错误条数。\u003c/p\u003e\n\u003cp\u003e继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5b57a7743f314605bfe41c7b1667a75e.png\"\u003e\u003c/p\u003e\n\u003cp\u003eplan.case_result和case.case_result类似，返回测试计划的每条用例的运行结果。\u003c/p\u003e\n\u003cp\u003e在测试计划添加用例的时候，有个用例列表，需要增加2个过滤条件，编辑teprunner/views/case.py文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ab6d81b288cf41929ebb13657ff87609.png\"\u003e\u003c/p\u003e\n\u003cp\u003e第一个过滤条件是排除当前计划已经添加过的用例：exclude_plan_id由前端传入，根据PlanCase关联关系找到已添加的用例进行过滤；第二个查询条件是关键字：用例ID或用例描述，跟PlanCaseView.list()里面一样。\u003c/p\u003e\n\u003cp\u003e最后，编辑teprunner/views/run.py文件，增加批量运行用例的功能：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/84b451fcbfb5456d896dcf14c8e279e9.png\"\u003e\u003c/p\u003e\n\u003cp\u003e添加delete_plan_result方法，在运行用例前，先把已有结果删掉，注意这里的model是PlanResult。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a5ecc1634f3b4434887225e16c606fb1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e修改pytest_subprocess函数，添加plan_id字段。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9732bd147aa9445eb23ae6af75c02f7d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e修改save_case_result函数，判断没有plan_id就存case_result表，有plan_id就存plan_result表。\u003c/p\u003e\n\u003cp\u003e复制run_case的代码为run_plan：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c97b32984c5044e798ed7c68e26c1951.png\"\u003e\u003c/p\u003e\n\u003cp\u003e修改case相关代码为plan。值得注意的是中间那段代码，根据plan_id从PlanCase中找到关联用例，生成了case_list，这样就复用run_case的多线程和多进程代码，批量运行测试用例了。\u003c/p\u003e\n\u003ch1 id=\"编写前端代码\"\u003e编写前端代码\u003c/h1\u003e\n\u003cp\u003e编辑src/router/index.js文件，添加测试计划相关路由：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\t\t{\n            path: \u0026#34;plan\u0026#34;,\n            name: \u0026#34;plan\u0026#34;,\n            meta: {\n              title: \u0026#34;测试计划\u0026#34;,\n            },\n            component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanManagement.vue\u0026#34;),\n            children: [\n              {\n                path: \u0026#34;addPlan\u0026#34;,\n                name: \u0026#34;addPlan\u0026#34;,\n                meta: {\n                  title: \u0026#34;添加计划\u0026#34;,\n                },\n                component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanEditor\u0026#34;),\n              },\n              {\n                path: \u0026#34;editPlan\u0026#34;,\n                name: \u0026#34;editPlan\u0026#34;,\n                meta: {\n                  title: \u0026#34;编辑计划\u0026#34;,\n                },\n                component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanEditor\u0026#34;),\n              },\n              {\n                path: \u0026#34;caseList\u0026#34;,\n                name: \u0026#34;caseList\u0026#34;,\n                meta: {\n                  title: \u0026#34;用例列表\u0026#34;,\n                },\n                component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/CaseList\u0026#34;),\n              },\n              {\n                path: \u0026#34;planResult\u0026#34;,\n                name: \u0026#34;planResult\u0026#34;,\n                meta: {\n                  title: \u0026#34;计划运行结果\u0026#34;,\n                },\n                component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/PlanResult\u0026#34;),\n                children: [\n                  {\n                    path: \u0026#34;caseResult\u0026#34;,\n                    name: \u0026#34;caseResult\u0026#34;,\n                    meta: {\n                      title: \u0026#34;用例运行结果\u0026#34;,\n                    },\n                    component: () =\u0026gt; import(\u0026#34;@/views/teprunner/plan/CaseResult\u0026#34;),\n                  },\n                ],\n              },\n            ],\n          },\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e跟之前做增删改查不一样的地方是：**在父组件中，不会import子组件，而是通过LocalStorage进行组件之间传值。**因此这里增加了添加计划、编辑计划等路由。\u003c/p\u003e","title":"teprunner测试平台测试计划批量运行用例"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n“道”说起来有点抽象，它相当于我们常说的“软技能”，包括价值观、认知能力、学习能力、创新能力、沟通协作等。不同层次的测试人员，道的差异也很大。初级测试人员，可能只知道测试是为了找出问题，能把需求上线即可。高级测试人员，也许会更加注重质量保障，从全局思考，既要保证线上不出问题，又要提高测试效率。他们的行为准则也不一样。有些人不注重资产沉淀，虽然能完成任务，但给人散乱的印象。有些人敬畏流程，严格按照标准操作，能够记录各种结果和数据，经得起审查。只有专注工作和注重细节，持续思考和改进，才能提升测试的道。\n什么是测试的术？术可以理解为实践，指测试的具体技术、方法和工具。\n“术”就很好理解了，简单来说就是技术，“硬技能”，包括功能测试、自动化测试、性能测试、测试工具、平台开发等。术体现出了测试的专业能力。术的高低，可以通过结果来度量，比如测试用例质量、缺陷数量、漏测率、自动化用例条数、代码覆盖度等。提升技术没有捷径，除了完成基本工作，还需要我们去尝试一些具有挑战性的任务，解决工作中的痛点和难点，日积月累，日新月异，久久为功。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000007-%E6%B5%8B%E8%AF%95%E7%9A%84%E9%81%93%E4%B8%8E%E6%9C%AF/","summary":"“道”说起来有点抽象，它相当于我们常说的“软技能”，包括价值观、认知能力、学习能力、创新能力、沟通协作等。不同层次的测试人员，道的差异也很大。初级测试人员，可能只知道测试是为了找出问题，能把需求上线即可。高级测试人员，也许会更加注重质量保障，从全局思考，既要保证线上不出问题，又要提高测试效率。他们的行为准则也不一样。有些人不注重资产沉淀，虽然能完成任务，但给人散乱的印象。有些人敬畏流程，严格按照标","title":"测试的道与术"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n代码层次 Python是一门脚本语言，新建一个.py文件，写点代码，就可以跑起来了，无论放哪都可以。比如where.py文件：\nprint(\u0026#34;Where am I?\u0026#34;) 那么问题来了，这是写在哪里的呢？为了一目了然，我们用“导游图”的视角来看看代码层次：\n红色箭头指出了，是写在模块中的，原来一个.py文件就是一个模块。模块中可以写函数和类，模块可以放在包中。\n函数 Python中最出名的函数一定是print()了，毕竟全世界都在用它say Hello World。Python函数其实和数学中的函数很像，比如y = f(x)。有函数名字、输入和输出。Python的函数结构如下：\n函数通过def关键字来定义：\ndef 函数名（参数列表）: 函数体 参数列表有就有，无则无，多个参数用逗号分隔。例如：\ndef hello() : print(\u0026#34;Hello World!\u0026#34;) hello() # 调用函数 def max(a, b): if a \u0026gt; b: return a else: return b a = 4 b = 5 print(max(a, b)) 调用函数，不需要再加def，直接函数名（参数列表）即可。参数既可以是变量，也可以是其他函数，只要能一一对应。return关键字用来返回值。return不是必需的，如果没有，那么函数会把内部代码全部都执行完再退出，如果有，函数会在return语句立刻退出，同时返回return语句的值，例如：\n# 可写函数说明 def sum( arg1, arg2 ): # 返回2个参数的和.\u0026#34; total = arg1 + arg2 print (\u0026#34;函数内 : \u0026#34;, total) return total print(\u0026#34;这里不会执行！\u0026#34;) # 调用sum函数 total = sum( 10, 20 ) print (\u0026#34;函数外 : \u0026#34;, total) 类 为什么还要写类，函数它不香么？这个问题有点大，我只能简单解释一下，那就是因为，类是包括了函数的，如果有一天你发现函数不够用了，那么可以用类试试，哈哈哈。\n类是面向对象编程中的概念，把对象中共性的东西抽离出来而成。\n类中的函数叫做方法，除了方法还有属性（也就是变量），我写个不严谨的公式：类 = 属性 + 方法，例如：\nclass People: #定义属性 name = \u0026#39;\u0026#39; age = 0 #定义方法 def speak(self): print(\u0026#34;%s 说: 我 %d 岁。\u0026#34; %(self.name, self.age)) 类的使用跟函数一样，需要调用，例如：\ndongfanger = People() # 这叫做实例化对象 dongfanger.speak() # 调用方法 类的一大好处是，可以通过继承来进一步复用代码。\n模块 模块中可以包含模块级代码、函数和类。模块与模块之间是不能直接调用的，必须使用import关键字来导入。导入时，模块级代码一定会被执行，如果我们不想让某些代码执行，那么可以添加一句if __name__ == '__main__':，例如\nif __name__ == \u0026#39;__main__\u0026#39;: print(\u0026#39;这里的代码，仅在该模块自身运行时执行\u0026#39;) else: print(\u0026#39;模块被导入时执行\u0026#39;) 函数和类需要调用才会运行，所以不存在这个问题。\n包 包是一个目录，特殊的地方在于需要包含一个__init__.py文件（内容可以为空），这是为什么呢？设想一下import hello这条语句，Python从哪去找hello这个包，C盘D盘E盘，成千上万个文件，范围太大了。所以需要把有Python模块的目录标出来，只查找这些目录就可以了。示例：\nsound/ 顶层包 __init__.py 初始化 sound 包 formats/ 文件格式转换子包 __init__.py wavread.py wavwrite.py aiffread.py aiffwrite.py auread.py auwrite.py ... effects/ 声音效果子包 __init__.py echo.py surround.py reverse.py ... filters/ filters 子包 __init__.py equalizer.py vocoder.py karaoke.py ... 命名空间 命名冲突是个头疼的问题，Python提供了命名空间这个方法，把代码块划分为不同的命名空间，同一个命名空间不能重名，不同命名空间可以重名，如图所示：\n命名空间一般有三种：\n内置：Python内置的名字。 全局：模块中定义的名字，包括模块的函数、类、其他导入的模块、模块级的变量和常量。 局部：函数中定义的名字，包括函数的参数和局部定义的变量。（类中定义的也是） 包里面是文件，文件名重复与否由操作系统判断。\n作用域 命名空间决定了变量的作用域，小的作用域只在内部才有作用，比如函数内的变量，模块是不能用的：\ndef func(): a = 1 print(a) # 报错NameError: name \u0026#39;a\u0026#39; is not defined 反之，大的作用域能作用到小的作用域：\na = 1 def func(): print(a) func() # a = 1 如果不同作用域有相同名字的变量，Python的匹配顺序为：局部 -\u0026gt; 全局 -\u0026gt; 内置，例如：\na = 1 def func(): a = 2 # 不会作用到模块的a func() # 调用函数修改a的值 print(a) # a的值仍为1 函数内部的a并不能影响到模块级别的a，因为Python在找a时，函数内部已经找到了，就不会再找了。\n可以使用global关键字，把局部变量定义为全局变量，这样模块级别的变量也可以在函数内修改了：\na = 1 def func(): global a # global声明为全局 a = 2 func() # 调用函数修改a的值 print(a) # a的值变为2 另外，Python中只有模块、类和函数，才会产生作用域。其他代码块如if、while、for等是不会产生作用域的，也就是说这些语句内定义的变量，外部也可以访问，例如：\nif True: a = 1 print(a) 小结 本文是Python入门系列这道前菜的最后一篇了，正餐Python进阶系列计划在2021年1月开始推送，具体计划我会写在元旦的一篇文章中。Python入门系列并不算完整的教程，它的定位是进阶篇的铺垫，做一些知识储备，降低阅读门槛。如果想学习完整教程，可以找菜鸟教程，也可以上B站看视频（个人更推荐）。最后，为了知识共享和传递，我把入门的7篇文章都导出成pdf上传了，可以在公众号后台回复“入门”下载哦。\n参考资料：\nhttps://www.runoob.com/python3/python3-function.html\nhttps://www.runoob.com/python3/python3-class.html\nhttps://www.runoob.com/python3/python3-module.html\nhttps://www.runoob.com/python3/python3-namespace-scope.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000007-python%E4%BB%8E%E5%87%BD%E6%95%B0%E5%88%B0%E5%8C%85%E7%9A%84python%E4%BB%A3%E7%A0%81%E5%B1%82%E6%AC%A1/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"代码层次\"\u003e代码层次\u003c/h1\u003e\n\u003cp\u003ePython是一门脚本语言，新建一个\u003ccode\u003e.py\u003c/code\u003e文件，写点代码，就可以跑起来了，无论放哪都可以。比如\u003ccode\u003ewhere.py\u003c/code\u003e文件：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Where am I?\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e那么问题来了，这是写在哪里的呢？为了一目了然，我们用“导游图”的视角来看看代码层次：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/99521a29e66547c4b5370d5e836811e1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e红色箭头指出了，是写在模块中的，原来一个\u003ccode\u003e.py\u003c/code\u003e文件就是一个模块。模块中可以写函数和类，模块可以放在包中。\u003c/p\u003e\n\u003ch1 id=\"函数\"\u003e函数\u003c/h1\u003e\n\u003cp\u003ePython中最出名的函数一定是\u003ccode\u003eprint()\u003c/code\u003e了，毕竟全世界都在用它say Hello World。Python函数其实和数学中的函数很像，比如\u003ccode\u003ey = f(x)\u003c/code\u003e。有函数名字、输入和输出。Python的函数结构如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/68259600343b403fa0d3a5b74a2065b1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e函数通过\u003ccode\u003edef\u003c/code\u003e关键字来定义：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edef 函数名（参数列表）:\n    函数体\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e参数列表有就有，无则无，多个参数用逗号分隔。例如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehello\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Hello World!\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ehello\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e  \u003cspan style=\"color:#57606a\"\u003e# 调用函数\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003emax\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ea\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e a \u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e a\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e b\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ea \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eb \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e5\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003emax\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ea\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e b\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e调用函数，不需要再加\u003ccode\u003edef\u003c/code\u003e，直接\u003ccode\u003e函数名（参数列表）\u003c/code\u003e即可。参数既可以是变量，也可以是其他函数，只要能一一对应。\u003ccode\u003ereturn\u003c/code\u003e关键字用来返回值。return不是必需的，如果没有，那么函数会把内部代码全部都执行完再退出，如果有，函数会在return语句立刻退出，同时返回return语句的值，例如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e# 可写函数说明\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003esum\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e arg1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e arg2 \u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \u003cspan style=\"color:#57606a\"\u003e# 返回2个参数的和.\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   total \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e arg1 \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e arg2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;函数内 : \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e total\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e total\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;这里不会执行！\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e# 调用sum函数\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etotal \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003esum\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e10\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e20\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eprint\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;函数外 : \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e total\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"类\"\u003e类\u003c/h1\u003e\n\u003cp\u003e为什么还要写类，函数它不香么？这个问题有点大，我只能简单解释一下，那就是因为，类是包括了函数的，如果有一天你发现函数不够用了，那么可以用类试试，哈哈哈。\u003c/p\u003e","title":"从函数到包的Python代码层次"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在RESTful API中，接口返回的是JSON，JSON的内容对应的是数据库中的数据，DRF是通过序列化（Serialization）的技术，把数据模型转换为JSON的，反之，叫做反序列化（deserialization）。本文就来揭开2揭开DRF序列化技术的神秘面纱技术的神秘面纱。\n创建虚拟环境 虚拟环境是独立的Python环境，可以和系统环境分离，只安装需要的包即可，使用以下命令创建并激活：\n# 创建Python虚拟环境 python -m venv env # 激活虚拟环境 env\\Scripts\\activate.bat # Mac中使用`source env/bin/activate` 退出虚拟环境用deactivate。\n接着在虚拟环境中安装本文用到的包：\npip install django pip install djangorestframework pip install pygments # 代码高亮 创建项目 创建project：\ndjango-admin startproject tutorial 创建app：\ncd tutorial python manage.py startapp snippets 在tutorial/settings.py中的INSTALLED_APPS添加snippets和rest_framework：\nINSTALLED_APPS = [ ... \u0026#39;rest_framework\u0026#39;, \u0026#39;snippets.apps.SnippetsConfig\u0026#39;, ] 只有添加后，app才会生效哦。\n创建model 编辑snippets/models.py，创建Snippet模型：\nfrom django.db import models from pygments.lexers import get_all_lexers from pygments.styles import get_all_styles LEXERS = [item for item in get_all_lexers() if item[1]] LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS]) STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()]) class Snippet(models.Model): created = models.DateTimeField(auto_now_add=True) title = models.CharField(max_length=100, blank=True, default=\u0026#39;\u0026#39;) code = models.TextField() linenos = models.BooleanField(default=False) language = models.CharField(choices=LANGUAGE_CHOICES, default=\u0026#39;python\u0026#39;, max_length=100) style = models.CharField(choices=STYLE_CHOICES, default=\u0026#39;friendly\u0026#39;, max_length=100) class Meta: ordering = [\u0026#39;created\u0026#39;] Snippet有6个字段，created、title、code、linenos、language、style。\n接着同步数据库：\npython manage.py makemigrations snippets python manage.py migrate 创建Serializer rest_framework.serializers的结构如下：\n查看Python模块中的成员，需要勾选：\n之前我们用过HyperlinkedModelSerializer，是封装后的符合RESTful良好设计的序列化器，这里直接使用更底层的Serializer。在snippets目录下创建serializers.py，添加以下代码：\nfrom rest_framework import serializers from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES class SnippetSerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) title = serializers.CharField(required=False, allow_blank=True, max_length=100) code = serializers.CharField(style={\u0026#39;base_template\u0026#39;: \u0026#39;textarea.html\u0026#39;}) linenos = serializers.BooleanField(required=False) language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default=\u0026#39;python\u0026#39;) style = serializers.ChoiceField(choices=STYLE_CHOICES, default=\u0026#39;friendly\u0026#39;) def create(self, validated_data): \u0026#34;\u0026#34;\u0026#34; Create and return a new `Snippet` instance, given the validated data. \u0026#34;\u0026#34;\u0026#34; return Snippet.objects.create(**validated_data) def update(self, instance, validated_data): \u0026#34;\u0026#34;\u0026#34; Update and return an existing `Snippet` instance, given the validated data. \u0026#34;\u0026#34;\u0026#34; instance.title = validated_data.get(\u0026#39;title\u0026#39;, instance.title) instance.code = validated_data.get(\u0026#39;code\u0026#39;, instance.code) instance.linenos = validated_data.get(\u0026#39;linenos\u0026#39;, instance.linenos) instance.language = validated_data.get(\u0026#39;language\u0026#39;, instance.language) instance.style = validated_data.get(\u0026#39;style\u0026#39;, instance.style) instance.save() return instance 代码第一部分定义了序列化/反序列化的字段，字段定义方式类似于Form。第二部分的create()和update()方法定义了调用serializer.save()时，创建和更新实例的处理方式。\n玩玩Serializer 既然已经创建好了Serializer，何不玩玩它？只有玩了才知道序列化的技术细节。这里用到了Django shell，可以在命令行实时看到输出结果：\npython manage.py shell 首先通过模型新增2条测试数据：\nfrom snippets.models import Snippet from snippets.serializers import SnippetSerializer from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser snippet = Snippet(code=\u0026#39;foo = \u0026#34;bar\u0026#34;\\n\u0026#39;) snippet.save() snippet = Snippet(code=\u0026#39;print(\u0026#34;hello, world\u0026#34;)\\n\u0026#39;) snippet.save() 接着把它序列化看看：\nserializer = SnippetSerializer(snippet) serializer.data # {\u0026#39;id\u0026#39;: 2, \u0026#39;title\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;code\u0026#39;: \u0026#39;print(\u0026#34;hello, world\u0026#34;)\\n\u0026#39;, \u0026#39;linenos\u0026#39;: False, \u0026#39;language\u0026#39;: \u0026#39;python\u0026#39;, \u0026#39;style\u0026#39;: \u0026#39;friendly\u0026#39;} 因为snippet变量最后引用的是第2个数据模型的实例，所以这里只显示第2条数据。\nserializer.data返回的是个字典，最后把字典转换为JSON：\ncontent = JSONRenderer().render(serializer.data) content # b\u0026#39;{\u0026#34;id\u0026#34;: 2, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;print(\\\\\u0026#34;hello, world\\\\\u0026#34;)\\\\n\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34;}\u0026#39; 这就是序列化的过程，模型实例→Python字典→JSON。\n反序列化的过程是类似的，先把JSON转换为Python字典：\nimport io stream = io.BytesIO(content) data = JSONParser().parse(stream) 再把字典转换为模型实例：\nserializer = SnippetSerializer(data=data) serializer.is_valid() # True serializer.validated_data # OrderedDict([(\u0026#39;title\u0026#39;, \u0026#39;\u0026#39;), (\u0026#39;code\u0026#39;, \u0026#39;print(\u0026#34;hello, world\u0026#34;)\\n\u0026#39;), (\u0026#39;linenos\u0026#39;, False), (\u0026#39;language\u0026#39;, \u0026#39;python\u0026#39;), (\u0026#39;style\u0026#39;, \u0026#39;friendly\u0026#39;)]) serializer.save() # \u0026lt;Snippet: Snippet object\u0026gt; 因为SnippetSerializer的create()方法代码是return Snippet.objects.create(**validated_data)，所以serializer.save()会在数据库中新增1条数据，并返回Snippet模型实例。\n另外，序列化对象不只有模型实例，也可以是数据集：\nserializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data # [OrderedDict([(\u0026#39;id\u0026#39;, 1), (\u0026#39;title\u0026#39;, \u0026#39;\u0026#39;), (\u0026#39;code\u0026#39;, \u0026#39;foo = \u0026#34;bar\u0026#34;\\n\u0026#39;), (\u0026#39;linenos\u0026#39;, False), (\u0026#39;language\u0026#39;, \u0026#39;python\u0026#39;), (\u0026#39;style\u0026#39;, \u0026#39;friendly\u0026#39;)]), OrderedDict([(\u0026#39;id\u0026#39;, 2), (\u0026#39;title\u0026#39;, \u0026#39;\u0026#39;), (\u0026#39;code\u0026#39;, \u0026#39;print(\u0026#34;hello, world\u0026#34;)\\n\u0026#39;), (\u0026#39;linenos\u0026#39;, False), (\u0026#39;language\u0026#39;, \u0026#39;python\u0026#39;), (\u0026#39;style\u0026#39;, \u0026#39;friendly\u0026#39;)]), OrderedDict([(\u0026#39;id\u0026#39;, 3), (\u0026#39;title\u0026#39;, \u0026#39;\u0026#39;), (\u0026#39;code\u0026#39;, \u0026#39;print(\u0026#34;hello, world\u0026#34;)\u0026#39;), (\u0026#39;linenos\u0026#39;, False), (\u0026#39;language\u0026#39;, \u0026#39;python\u0026#39;), (\u0026#39;style\u0026#39;, \u0026#39;friendly\u0026#39;)])] 使用ModelSerializers rest_framework.serializers.Serializers比较底层，需要重复定义model中的字段，DRF的作者当然也想到了这个问题，并且给出了解决办法：把Serializers封装成ModelSerializers。在snippets/serializers.py中替换SnippetSerializer为：\nclass SnippetSerializer(serializers.ModelSerializer): class Meta: model = Snippet fields = [\u0026#39;id\u0026#39;, \u0026#39;title\u0026#39;, \u0026#39;code\u0026#39;, \u0026#39;linenos\u0026#39;, \u0026#39;language\u0026#39;, \u0026#39;style\u0026#39;] 写法果然简洁了很多。使用Django shell打印出来看看：\nfrom snippets.serializers import SnippetSerializer serializer = SnippetSerializer() print(repr(serializer)) # SnippetSerializer(): # id = IntegerField(label=\u0026#39;ID\u0026#39;, read_only=True) # title = CharField(allow_blank=True, max_length=100, required=False) # code = CharField(style={\u0026#39;base_template\u0026#39;: \u0026#39;textarea.html\u0026#39;}) # linenos = BooleanField(required=False) # language = ChoiceField(choices=[(\u0026#39;Clipper\u0026#39;, \u0026#39;FoxPro\u0026#39;), (\u0026#39;Cucumber\u0026#39;, \u0026#39;Gherkin\u0026#39;), (\u0026#39;RobotFramework\u0026#39;, \u0026#39;RobotFramework\u0026#39;), (\u0026#39;abap\u0026#39;, \u0026#39;ABAP\u0026#39;), (\u0026#39;ada\u0026#39;, \u0026#39;Ada\u0026#39;)... # style = ChoiceField(choices=[(\u0026#39;autumn\u0026#39;, \u0026#39;autumn\u0026#39;), (\u0026#39;borland\u0026#39;, \u0026#39;borland\u0026#39;), (\u0026#39;bw\u0026#39;, \u0026#39;bw\u0026#39;), (\u0026#39;colorful\u0026#39;, \u0026#39;colorful\u0026#39;)... 和手动定义的字段一模一样。其实ModelSerializer并没有做额外的处理，我们可以从它的部分代码片段看到:\nserializer_field_mapping = { models.AutoField: IntegerField, models.BigIntegerField: IntegerField, models.BooleanField: BooleanField, # Default `create` and `update` behavior... def create(self, validated_data): def update(self, instance, validated_data): raise_errors_on_nested_writes(\u0026#39;update\u0026#39;, self, validated_data) 建立字段映射，定义create()和update()等方法，只做了最简单的封装。\n编写views 我们之前用的是已经封装好的rest_framework.viewsets，这里直接用Django原生的view。在snippets/views.py中添加代码：\nfrom django.http import HttpResponse, JsonResponse from django.views.decorators.csrf import csrf_exempt from rest_framework.parsers import JSONParser from snippets.models import Snippet from snippets.serializers import SnippetSerializer 先创建snippet_list视图，返回snippets列表或者新建1个snippet：\n@csrf_exempt def snippet_list(request): \u0026#34;\u0026#34;\u0026#34; List all code snippets, or create a new snippet. \u0026#34;\u0026#34;\u0026#34; if request.method == \u0026#39;GET\u0026#39;: snippets = Snippet.objects.all() serializer = SnippetSerializer(snippets, many=True) return JsonResponse(serializer.data, safe=False) elif request.method == \u0026#39;POST\u0026#39;: data = JSONParser().parse(request) serializer = SnippetSerializer(data=data) if serializer.is_valid(): serializer.save() return JsonResponse(serializer.data, status=201) return JsonResponse(serializer.errors, status=400) 本文示例中，客户端发起POST请求是不带CSRF token的，所以需要添加csrf_exempt（exempt翻译为免除）装饰器，允许跨域访问。这里只是演示，实际会使用django-cors-headers来解决跨域问题，而不是给每个view都加上@csrf_exempt。\nCSRF是指跨站请求伪造，攻击者盗用你的身份，以你的名义发送恶意请求。CSRF token是指服务器通过token来认证，如果请求中没有token或者token不匹配，那么就认为可能是CSRF而拒绝该请求。\n接着添加一个view，对单个snippet进行删（delete）、改（update）、查（retrieve）：\n@csrf_exempt def snippet_detail(request, pk): \u0026#34;\u0026#34;\u0026#34; Retrieve, update or delete a code snippet. \u0026#34;\u0026#34;\u0026#34; try: snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: return HttpResponse(status=404) if request.method == \u0026#39;GET\u0026#39;: serializer = SnippetSerializer(snippet) return JsonResponse(serializer.data) elif request.method == \u0026#39;PUT\u0026#39;: data = JSONParser().parse(request) serializer = SnippetSerializer(snippet, data=data) if serializer.is_valid(): serializer.save() return JsonResponse(serializer.data) return JsonResponse(serializer.errors, status=400) elif request.method == \u0026#39;DELETE\u0026#39;: snippet.delete() return HttpResponse(status=204) 注意，前面create返回的status为201，这里retrieve返回的默认的200，附上几个常见状态码：\n200 OK - GET/PUT/PATCH：服务器成功返回用户请求的数据，该操作是幂等的（Idempotent） 201 CREATED - POST：用户新建数据成功 204 NO content - DELETE：用户删除数据成功 400 Invalid request - POST/PUT/PATCH：用户发出的请求有错误，服务器没有进行新建或修改数据的操作，该操作是幂等的 401 Unauthorized - *：表示用户没有权限（令牌、用户名、密码错误） 403 Forbidden - *：表示用户得到授权（与401错误相对），但是访问是被禁止的 最后，添加路由，新建文件snippets/urls.py：\nfrom django.urls import path from snippets import views urlpatterns = [ path(\u0026#39;snippets/\u0026#39;, views.snippet_list), path(\u0026#39;snippets/\u0026lt;int:pk\u0026gt;/\u0026#39;, views.snippet_detail), ] 再配置到tutorial/urls.py：\nfrom django.urls import path, include urlpatterns = [ path(\u0026#39;\u0026#39;, include(\u0026#39;snippets.urls\u0026#39;)), ] 测试API 启动项目：\npython manage.py runserver 新开一个Terminal，安装HTTP命令行工具：\npip install httpie 请求snippets列表：\nhttp http://127.0.0.1:8000/snippets/ HTTP/1.1 200 OK ... [ { \u0026#34;id\u0026#34;: 1, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;foo = \\\u0026#34;bar\\\u0026#34;\\n\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34; }, { \u0026#34;id\u0026#34;: 2, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;print(\\\u0026#34;hello, world\\\u0026#34;)\\n\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34; } ] 请求id为2的snippet：\nhttp http://127.0.0.1:8000/snippets/2/ HTTP/1.1 200 OK ... { \u0026#34;id\u0026#34;: 2, \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;print(\\\u0026#34;hello, world\\\u0026#34;)\\n\u0026#34;, \u0026#34;linenos\u0026#34;: false, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;friendly\u0026#34; } 小结 不得不说DRF的Tutorial实践性很强，基本上照着做都能敲出来，体验比较好。序列化的过程，就是模型实例→Python字典→JSON的过程。通过本文也理解了在app中models.py→serializers.py→views.py→urls.py的代码编写顺序。\n参考资料：\nhttps://www.django-rest-framework.org/tutorial/1-serialization/\nhttps://blog.csdn.net/yexudengzhidao/article/details/93527586\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000007-django%E6%8F%AD%E5%BC%80drf%E5%BA%8F%E5%88%97%E5%8C%96%E6%8A%80%E6%9C%AF%E7%9A%84%E7%A5%9E%E7%A7%98%E9%9D%A2%E7%BA%B1/","summary":"在RESTful API中，接口返回的是JSON，JSON的内容对应的是数据库中的数据，DRF是通过序列化（Serialization）的技术，把数据模型转换为JSON的，反之，叫做反序列化（deserialization）。本文就来揭开2揭开DRF序列化技术的神秘面纱技术的神秘面纱。","title":"揭开DRF序列化技术的神秘面纱"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n最近几个月时间有些小伙伴问到为什么tep项目初始化文件没有了？\n造成这种困扰，实在是抱歉，确实在删掉项目初始化文件时欠考虑了。我当时的想法是让tep像flask那样，只保留最核心的能力，其他能力自由扩展。但是好像tep并没有flask那么强（这是一句正确的废话），大家在用tep时也是复用了项目初始化文件，在此基础上实践自己的自动化项目。\n恢复项目初始化文件 于是我决定在最新的0.9.1版本中恢复这些初始化文件：\n**原汁原味还原了《tep用户手册帮你从unittest过渡到pytest》这篇文章提到的内容。**相比于用户手册，最新的0.9.1版本还做了两点增强：\n把fixture_env_vars.py和fixture_login.py从fixture_admin.py中拆了出来，解耦后，文件层次更清晰。\n添加了test_request.py文件，request的常见用法可以从这里找到：\nfrom tep.client import request request(\u0026#34;get\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, json={}) request(\u0026#34;post\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, params={}) request(\u0026#34;put\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, json={}) request(\u0026#34;delete\u0026#34;, url=\u0026#34;\u0026#34;, headers={}) # upload excel file_name = \u0026#34;\u0026#34; file_path = \u0026#34;\u0026#34; request(\u0026#34;post\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, files={ \u0026#34;file\u0026#34;: ( file_name, open(file_path, \u0026#34;rb\u0026#34;), \u0026#34;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\u0026#34; ) }, verify=False ) 初始化时创建虚拟环境 还有就是提供了-venv参数，在项目初始化时，可以同时创建一个虚拟环境，像这样：\ntep startproject demo091venv -venv 并自动安装了最新版本的tep。\n最后感谢小伙伴们对tep工具的支持，我会继续完善它的，一起加油！\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000007-%E5%8E%9F%E5%88%9Bpytest%E8%BE%85%E5%8A%A9%E5%B7%A5%E5%85%B7tep0.9.1%E7%89%88%E6%9C%AC%E6%81%A2%E5%A4%8D%E9%A1%B9%E7%9B%AE%E5%88%9D%E5%A7%8B%E5%8C%96%E6%96%87%E4%BB%B6/","summary":"最近几个月时间有些小伙伴问到为什么tep项目初始化文件没有了？","title":"原创pytest辅助工具tep0.9.1版本恢复项目初始化文件"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n2.1 模型 大模型扮演着大脑的角色，负责核心的理解、推理与生成任务。\n2.1.1 模型分类 通用大模型：GPT、Claude、Qwen、DeepSeek、Gemini等通用大模型。\n垂直领域模型：情感分析、语言翻译、意图分类等垂直领域模型。\n通用大模型适合处理复杂、开放的任务，垂直领域模型适合处理简单、高频的任务。\n2.1.2 模型能力和微调 模型的知识是固化的，需要不断地“喂”它，把这些信息结构化地描述清楚并且提供给它，它会在每次的交互中动态地理解你提供的内容，并结合自身的通用知识来完成内容的生成。\n企业专属模型的训练和微调的成本是巨大的，它不仅包括高昂的计算资源费用，还需要海量的高质量标注数据、专业的算法团队以及漫长的开发周期。对于绝大多企业来说，投入与产出不成正比，很难持续。\n2.1.3 如何选择模型 基于企业的实际业务场景，权衡任务复杂度、性能要求、开发成本和响应延迟等多个因素来综合选择和组合最合适的模型。\n一个务实的策略是从顶配开始，逐步优化：先用能力最强的模型搭建原型以验证业务逻辑，再逐步将流程中的非核心、简单任务替换为更经济、更快速的小模型，最终找到成本与性能的最佳平衡点。\n2.2 框架 Spring或Dubbo开发一个微服务应用，代码确定，容易形成标准化的框架。而开发一个AI Agent应用，LLM存在高度不确定性，设计模式的落地方式差异很大。\n2.2.1 Agent设计模式 1、Chain of Thought（思维链）\n让模型在回答前，把推理过程一步步写出来。\n2、Self-Ask（自问自答）\n让模型在回答时学会反问自己，把大问题拆成多个小问题，然后逐个回答。\n3、ReAct（推理+行动）\n在推理和外部行动（比如调用搜索引擎或API）之间交替进行。\n4、Plan-and-Execute（计划与执行）\n把任务拆成两个阶段，先生成计划，再逐步执行。\n5、Tree of Thoughts（ToT，树状思维）\n不是单线思维，而是生成多条思路分支，像树一样展开，再通过评估机制选出最佳分支。\n6、Reflexion / Iterative Refinement（反思与迭代优化）\nAgent自我纠错，犯错后会总结失败原因，再带着反思尝试下一次。\n7、Role-playing Agents（角色扮演式智能体）\n把任务拆分给不同角色的Agent，通过对话协作完成任务。\n这些模式不是互斥的，可以混搭使用。\n其中ReAct已经被LangChain、LlamaIndex、Dify、Spring AI Alibaba等开发框架内置为基础框架。\n2.2.2 Agent开发框架 1、从低代码到高代码\n低代码平台是对于高代码的一层封装，其抽象层次很难满足所有场景，无法在性能、扩展性和复杂业务逻辑方面满足大规模生产的要求。\n高代码是当下AI原生应用生产落地的主流形态，ADK、LangGraph、AutoGen、AgentScope、Spring AI Alibaba等框架，为开发者提供了面向Agent的编程接口。\n2、高代码的演进\n高代码模式本身经历了从ChatClient→Workflow→Agentic的演进过程。\nChatClient，一次单一的LLM调用。Workflow，将传统工作流转化为LLM节点编排。Agentic，提供面向Agent的API，并内置多种通用的协作模式（Pattern）。\n3、零代码的愿景\n尝试让用户完全通过自然语言即可驱动开发。目前处于探索与验证阶段，难以承担大规模生产任务。\n2.3 提示词 在AI原生应用中，不再编写复杂的代码，而是使用更接近人类语言的方式与AI沟通，这就是Prompt（提示词）。提示词的质量很大程度上影响了AI应用的输出效果。\n2.3.1 提示词是什么 提示词可以是一个具体的问题、一段描述、一组关键词，或是相关的上下文，不仅限于自然语言文本，还可以包含代码片段、数据格式说明，甚至是图像与文字相结合的多模态输入。\n2.3.2 提示词质量=AI输出质量 大模型的输出质量，既要看大模型本身，也要看提示词是否清晰、完整、具体。比如，“写点关于人工智能的东西”，生成的内容是泛泛而谈。“以科技专栏作家的身份，撰写一篇800字左右的文章，探讨人工智能在医疗影像诊断中的最新应用、优势与挑战，并列举实例”，能生成一篇逻辑严谨、信息详实、专业度高的文章。\n2.3.3 如何优化提示词 开发者需要清晰、有效地与模型交流，并明确指导它如何处理各自情况，在第4章中将围绕上下文工程对提示词展开详细介绍。\n2.4 RAG RAG（Retrieval Augmented Generation，检索增强生成）用于构建大模型的知识库。RAG技术能够弥补大模型因知识截止而无法获取最新信息的问题，并有效降低其产生幻觉的风险，而且RAG技术相比于大模型后训练或微调方式，以更加成本低的方式与企业的专有数据作对接，以实现大模型快速技术验证和商业化尝试。\n2.4.1 RAG知识库的应用架构 1、离线向量过程：把用户上传的文档进行文档智能解析、切片，再进行向量化存储到向量数据库。\n2、在线过程：把用户的请求问题向量化之后与向量库中的切片向量进行相似度对比，从而召回最接近用户问题的相关切片。\n一个简单的RAG系统可能会遭遇准确率和召回率的挑战、信息冗余噪声导致的模型幻觉、知识库庞杂难以管理等问题。当前RAG系统的构建也逐步向模块化、Agentic RAG的高级架构演进。\n1、离线过程，文档解析技术除了OCR和电子解析技术，也利用大模型进行解析，比如图片类文档使用VLM视觉理解大模型。\n2、在线检索过程，检索前、检索中、检索后都发展出很多技术手段，比如检索前增加Query改写、知识库路由等模块，检索过程采用检索策略，检索后增加重排序、拒识模块等。\n2.4.2 RAG知识库的应用场景 客户服务、个性化推荐、AI陪伴、内容创作等。除了简单的问答，基于RAG的系统，叠加大模型分析客户对话数据等能力，能帮助企业优化服务策略和挖掘销售线索等。多模态RAG的兴起，可以在零售电商场景，让用户通过上传图片来检索商品；在媒体娱乐领域，从海量音视频中检索出特定片段，服务于内容分发以及AI视频创作场景。\n2.4.3 RAG知识库技术的未来发展 在当前Advanced RAG架构里，仍然有许多问题待解决。多模态RAG相关的技术，也还在快速发展当中。RAG系统中的知识库向量检索，也是上下文工程的核心技术实现，在第4章将围绕上下文工程对RAG展开详细的介绍。\n2.5 记忆 在使用DeepSeek时，查看历史对话，就是AI应用的记忆功能。\n2.5.1 记忆的核心作用 大模型本质上是无状态的，AI原生应用引入了记忆组件，从一个单轮问答的机器人，成长为能与我们长期协作、处理复杂任务的智能伙伴。\n1、跨越会话的连贯性\n长期保存关键信息（如对话历史、任务状态、决策依据），并通过高效的检索与上下文注入机制，在新的交互中动态地为模型提供相关背景。\n2、高度自适应的个性化\n构建和维护动态的用户画像来实现个性化。系统地记录用户的偏好，如格式要求、沟通风格等，也能记住历史行为模式与长期目标，使模型能够生成高度定制化的输出。\n3、基于历史信息的深度推理\n借助记忆提供的历史知识和经验充当推理的证据库，当模型在决策时，能关联并整合分散在历史中的相关数据、相似案例或既有结论。\n2.5.2 短期记忆和长期记忆 短期记忆保证了交互的即时性与上下文的完整性，长期记忆则提供跨会话的知识背景。\n短期记忆（Short-Term Memory，STM）\n模型在单次、连续的交互会话中所能直接访问的信息。\n长期记忆（Long-Term Memory，LTM）\n让AI能够记住跨越不同会话，甚至数天数月前的信息。系统将需要长期保存的信息，如对话摘要、用户画像或外部文档，进行向量化处理，并存入专门的向量数据库进行索引。当新的交互发生时，系统会根据当前输入的语义，在数据库中高效检索出最相关的历史记忆片段，并将其作为背景知识动态加入到模型的输入中。也就是外部数据库和语义检索的机制。\n2.6 工具 LLM的知识是静态的，其知识内容截止于其训练数据的最后更新日期。模型本身也无法直接与外部系统交互，执行如发送电子邮件、预定航班或执行数据库查询等实际操作。需要借助工具来实现，工具本质上是可以调用的外部接口。\n2.6.1 如何调用工具 大模型本身不能调用工具，它是根据用户的输入和可用工具的描述，智能地判断并决定调用哪个工具以及传递哪些参数。\nOpenAI率先在其推理API接口中支持了Function Calling（后升级为Tools），函数调用，核心是让大模型生成调用外部API的结构化入参。开发者预先把工具信息提供给大模型，通常是JSON Schema形式，包括函数名称、功能描述和参数列表。当用户请求时，大模型分析意图，判断是否需要调用工具以及调用哪个工具，生成一个包含tool_calls字段的结构化JSON对象。应用程序拿到后，就可以解析并调用相应的工具。\n2.6.2 MCP协议 MCP，Model Context Protocol，模型上下文协议，是由Anthropic公司2024年推出的，为大模型与外部数据、应用和服务之间的通信提供一种安全且标准化的语言。MCP协议迅速地成为当前大模型供应商的标准协议，包括OpenAI、Google、阿里云百炼在内的模型服务商，都对MCP做了兼容性支持。\nMCP服务有很多托管平台，MCP社区为了统一，定义了MCP Registry，标准化服务器的分发和发现方式，使Agent和工具更容易连接，近期发布了Preview版本，用于公开可用的MCP服务器的开放目录和API。Nacos也开源了Nacos MCP Registry，定位私有化，方便企业内部部署。\n2.6.3 工具调用的挑战和发展 当前工具调用存在调用时延、工具提取参数准确性、安全鉴权等问题，长远来看都将被有效解决，大模型将更智能地组合和串联多个工具完成更复杂的跨领域任务。\n2.7 网关 AI网关用来解决模型切换、Token经济、语义缓存和内容风控等AI原生的需求。\n2.7.1 什么是AI网关 AI网关是一个专为AI应用设计的、位于应用和大模型之间、应用和工具之间、模型和模型之间的中间件。其核心职责不再仅仅是路由和保护RESTful API，而是要理解并管理以Token为中心的、高延迟、流式传输的流量。\n（AI Token跟我们常说的登录token不是一个概念，Token翻译过来也可以叫符号，对AI来说是将文本分解后的基本单元，类似于将句子拆分成词汇或字符的过程）\n2.7.2 统一的模型接入与厂商解耦 不同供应商（如OpenAI、Anthropic、DeepSeek）的API标准各异，AI网关需要提供统一规范的API，将所有模型的接口都转换成一个标准的、统一的接口对外服务。开发者无需为新模型编写定制代码，方便进行模型切换、A/B测试和组合模型等。\n2.7.3 融合存量系统与AI AI网关能通过其协议抽象层扫描REST、GraphQL、gRPC等存量服务的API规范（如Swagger），自动生成符合大模型工具调用（如MCP规范）的描述文件，并借助MCP Registry注册到统一的服务目录中。这样无需改动存量业务接口的代码，就能将它们升级为“AI-Ready API”。\n2.7.4 智能路由与故障转移 智能路由：不仅可以根据请求内容或用户身份分发流量，还可以根据实时的Token单价、延迟、显存占用等权重进行动态推理流量调度，实现成本与性能的最佳平衡。\n故障转移：通过持续监控后端服务健康状态，一旦检测到模型响应缓慢或不可用，便会自动将流量无感切换到备用模型，保障应用的高可用性。\n2.7.5 精细化的成本控制与优化 语义缓存：对于内容相似但表述不同的重复问题，可直接返回缓存，避免对昂贵模型的重复调用。\n成本与流控：按组织、用户、应用等维度管理Token预算，当超出限额后，可以自动降级到成本更低的模型，从而有效防止资源滥用和成本超支。\n2.7.6 企业级安全与合规 安全合规能力：内置国密算法支持和敏感语料实时过滤等能力。\n审计与追溯：所有流经网关的Prompt、Response、Token消耗量等数据都会记录，以满足实时审计和追溯等合规要求。\n统一身份认证：与企业内部自有的身份授权基础设施对接，对AI生态提供统一的认证授权切面。\n2.7.7 数据、观测与优化 AI网关能够捕获每一次交互的完整数据：原始提示词、最终响应、模型选择、Token消耗、调用时延和业务成本等。\n统一采集：所有数据都通过一个标准化的方式汇集。\n全面观测：这些数据可以用于观测，形成对系统性能、成本和用户行为的统一视图。\n驱动优化：通过分析洞察，可以自动优化路由策略、更新缓存内容、或者筛选出有价值的数据用于模型微调，形成一个持续学习、自我完善的闭环。\n第6章将详细介绍如何快速构建AI网关，以及API和Agent的货币化。\n2.8 运行时 开发范式从传统的“标准库与框架”迁移到“AI模型与工具链”。\n2.8.1 什么是AI原生应用的运行时 运行时是AI原生应用的核心执行环境。运行时处理的不再是固定的代码，而是充满不确定性的执行计划。核心职责是将模型、工具和数据流有机地整合在一起，不仅要能理解和执行模型生成的动态任务，还要为整个过程提供稳定、高效和安全的保障。\n2.8.2 运行时的核心挑战 动态逻辑的可靠执行：运行时需要具备强大的容错和异常处理能力，确保模型生成的动态任务能顺利完成或优雅失败。同时，任务的每一步可能需要不同的依赖库或计算资源，运行时必须能按需、即时地准备执行环境。\n海量与实时数据的高效处理：在RAG等场景下，运行时需要在毫秒级内从海量知识库中检索、处理并传递数据，这对存储I/O和网络延迟提出了极致要求。对于在线学习、实时推荐等场景，运行时还必须具备高效的流式处理能力，确保模型能基于最新的数据进行推理。\n异构组件的复杂协同：AI应用是由模型、向量数据库、外部API工具、多智能体等多个组件构成的，运行时需要提供强大的服务编排和治理能力，并原生支持MCP、A2A等主流交互协议，充当这些组件间的消息总线。\n2.8.3 面向AI优化的Serverless架构 以Serverless架构为核心，注入状态管理和性能优化能力。\n为无状态的Serverless引入记忆：通过亲和性调度等机制，将同一会话的多次请求调度到同一个预热实例上，实现状态的就近缓存。\n兼顾极致弹性与低延迟：Serverless按需执行、自动伸缩，轻松应对流量洪峰，并在闲时将成本降至为零。针对延迟敏感的场景，通过预留实例和依赖预加载等技术，可以有效解决冷启动问题，实现毫秒级响应。\n让AI工具即插即用：Serverless函数是承载AI工具的天然载体。每个工具可以封装成一个独立的函数，由智能体按需、事件驱动地调用。\n第7章将详细介绍为AI应用和工具提供经济、安全算力的Serverless运行时。\n2.9 可观测 传统监控主要关注基础设施的性能指标与日志，而AI应用面临着行为不可预测、输出质量波动和成本结构复杂等挑战。一个完成的AI可观测体系应具备3大核心能力：\n端到端全链路追踪：端到端日志采集和链路追踪，可视化展示请求在AI应用中的执行路径，支持对历史对话的查询与筛选。\n全栈可观测：包含应用、AI网关、推理引擎3个维度，观测内容有实时追踪响应延迟、请求吞吐量、Token消耗，错误率和资源使用情况（如CPU、内存、API令牌）。\n自动化评估功能：引入评估Agent，对应用和模型的输入输出进行自动化的评估，检测幻觉、不一致性或答案质量下降等问题。\n技术集成方面，OpenTelemetry作为行业开放标准，定义了统一的采集规范，使可观测系统能通过自动埋点等方式，无缝接入主流AI框架。数据安全方面，系统必须内置数据脱敏和访问控制等安全机制，确保合规。\n第8章将详细介绍AI可观测应具备的3大能力和实现方式。\n2.10 评估 传统应用的特点是固定输入必然产生稳定且可复现的输出，而AI应用是非确定性的概率输出，即使输入相同，模型的输出也可能因上下文、训练数据分布或随机性而千差万别。\n2.10.1 构建高质量的数据集 第一种是人工构建，评估用例包括标准的问答详情、复杂的多步推理任务，以及那些专门用来测试模型能力边界的边缘场景。第二种是自动化采集，通过对线上系统的观测日志和业务记录进行分析，挖掘出海量的真实用户交互数据。第三种是探索使用AI算法构建数据集，通过大语言模型或其他生成式AI技术，高效创建多样化的测试例，包括复杂场景和对抗样本。\n2.10.2 明确评估目标 语义评估：从文本中提取实体、格式、抽象语义（如意图、情绪、主题）等多层次信息，并生成相关问题以深化理解。\nRag评估：针对从知识库中召回语料的质量进行多维度衡量，包括检索准确性（找到最相关的知识片段）、生成内容可靠（回答不偏离事实）、重复性及多样性等指标。\n工具调用评估：针对需要执行动作的AI原生应用，评估其工具调用的合理性与效率。\n端到端Agent评估：从宏观视角出发，不纠结于具体步骤的详情，只关注Agent是否最终、高效地解决了用户的原始问题，并带来满意的整体体验。\n2.10.3 搭建完整的自动化评估系统进行评估 自动化评估系统需要使用高质量的数据集，优秀的裁判模型，匹配的评估模版，对评估目标进行有效的评估。\n评估模版需与评估目标相匹配，比如，在Prompt评估中主要关注识别模糊、歧义或结构不良的输入；在RAG评估中主要衡量检索相关性与生成忠实度；在Tools评估中，则通过参数结构校验与执行结果语义解析，验证工具调用的准确性与理解能力。\n自动化评估系统还需要覆盖完整的AI原生应用生命周期。评估的执行应该建立离线与在线相结合的机制。\n离线评估面向应用迭代周期， 在开发测试阶段对Agent在典型任务、编译案例及对抗性输入下的表现进行全面测试，支持版本间的横向对比与回归分析，确保新版本满足生产上线的要求。\n在线评估则通过小流量灰度发布，在真实生产环境中结合自动化评估器实时生成性能指标。通过与A/B Test框架相结合，在响应时间、业务效果等关键指标上进行综合对比，为上线决策提供数据支撑。\n第9章将引入全新的评估范式LLM-as-a-Judge，详细阐述如何构建一个高效的自动化评估系统。\n2.11 安全 AI原生应用的安全防护是一项系统工程，涵盖应用、模型、数据、身份与基础设施五大维度。\nAI原生应用的安全：比如通过提示词注入等手段操控AI应用行为，越权访问导致数据泄露，若基础设施存在漏洞，还有可能引发注入、逆向攻击和算力被滥用等威胁。AI与生俱来的非预期行为以及输出的不可预测性风险，更加加剧了内部治理和合规的挑战。\n模型的安全：输入层对抗样本、提示词注入与恶意文件上传等攻击；推理层模型越狱、RAG知识库爬取与函数调用劫持等风险；输出层可能生成钓鱼信息、虚假建议或传递隐蔽指令。需部署大模型原生安全护栏，作为连接应用与模型之间的可信中间件，提供覆盖全链路的一站式防护，保障输出内容合法可控，并实现AIGC内容可追溯、责任可追，满足监管要求。\n数据的安全：数据收集阶段需完成分类分级与脱敏，防止敏感数据进入训练流程；传输过程采用HTTPS/TLS加密与私有网络隔离，确保通信安全；存储环节通过租户隔离与端到端加密保护数据主权；访问控制需要实现精细化权限管理；访问和使用过程中嵌入AI安全护栏进行实时过滤；删除阶段支持账号注销后的彻底清除与迁移能力，保障用户数据主权。\n身份的安全：事前通过异常访问监测识别风险；事中实施动态权限管理，采用即时授权与细粒度访问控制，遵循最小权限原则；事后通过自动化审计清理僵尸凭证。\n基础设施的安全：构建安全、可控的运行环境，从全局安全态势到计算、网络等细节层面，建立多层次防护体系。\n面对动态、自助、多模态的新一代智能体，传统的边界防御已不再适用，必须转向以“纵深防御、动态检测、全程可控”为核心的综合防护体系。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005002-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E5%8E%9F%E7%94%9F%E5%BA%94%E7%94%A8%E7%9A%84%E5%85%B3%E9%94%AE%E8%A6%81%E7%B4%A0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"21-模型\"\u003e2.1 模型\u003c/h1\u003e\n\u003cp\u003e大模型扮演着大脑的角色，负责核心的理解、推理与生成任务。\u003c/p\u003e\n\u003ch2 id=\"211-模型分类\"\u003e2.1.1 模型分类\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e通用大模型\u003c/strong\u003e：GPT、Claude、Qwen、DeepSeek、Gemini等通用大模型。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e垂直领域模型\u003c/strong\u003e：情感分析、语言翻译、意图分类等垂直领域模型。\u003c/p\u003e\n\u003cp\u003e通用大模型适合处理复杂、开放的任务，垂直领域模型适合处理简单、高频的任务。\u003c/p\u003e\n\u003ch2 id=\"212-模型能力和微调\"\u003e2.1.2 模型能力和微调\u003c/h2\u003e\n\u003cp\u003e模型的知识是固化的，需要不断地“喂”它，把这些信息结构化地描述清楚并且提供给它，它会在每次的交互中动态地理解你提供的内容，并结合自身的通用知识来完成内容的生成。\u003c/p\u003e\n\u003cp\u003e企业专属模型的训练和微调的成本是巨大的，它不仅包括高昂的计算资源费用，还需要海量的高质量标注数据、专业的算法团队以及漫长的开发周期。对于绝大多企业来说，投入与产出不成正比，很难持续。\u003c/p\u003e\n\u003ch2 id=\"213-如何选择模型\"\u003e2.1.3 如何选择模型\u003c/h2\u003e\n\u003cp\u003e基于企业的实际业务场景，权衡任务复杂度、性能要求、开发成本和响应延迟等多个因素来综合选择和组合最合适的模型。\u003c/p\u003e\n\u003cp\u003e一个务实的策略是从顶配开始，逐步优化：先用能力最强的模型搭建原型以验证业务逻辑，再逐步将流程中的非核心、简单任务替换为更经济、更快速的小模型，最终找到成本与性能的最佳平衡点。\u003c/p\u003e\n\u003ch1 id=\"22-框架\"\u003e2.2 框架\u003c/h1\u003e\n\u003cp\u003eSpring或Dubbo开发一个微服务应用，代码确定，容易形成标准化的框架。而开发一个AI Agent应用，LLM存在高度不确定性，设计模式的落地方式差异很大。\u003c/p\u003e\n\u003ch2 id=\"221-agent设计模式\"\u003e2.2.1 Agent设计模式\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e1、Chain of Thought（思维链）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e让模型在回答前，把推理过程一步步写出来。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2、Self-Ask（自问自答）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e让模型在回答时学会反问自己，把大问题拆成多个小问题，然后逐个回答。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3、ReAct（推理+行动）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e在推理和外部行动（比如调用搜索引擎或API）之间交替进行。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e4、Plan-and-Execute（计划与执行）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e把任务拆成两个阶段，先生成计划，再逐步执行。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e5、Tree of Thoughts（ToT，树状思维）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e不是单线思维，而是生成多条思路分支，像树一样展开，再通过评估机制选出最佳分支。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e6、Reflexion / Iterative Refinement（反思与迭代优化）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAgent自我纠错，犯错后会总结失败原因，再带着反思尝试下一次。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e7、Role-playing Agents（角色扮演式智能体）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e把任务拆分给不同角色的Agent，通过对话协作完成任务。\u003c/p\u003e\n\u003cp\u003e这些模式不是互斥的，可以混搭使用。\u003c/p\u003e\n\u003cp\u003e其中ReAct已经被LangChain、LlamaIndex、Dify、Spring AI Alibaba等开发框架内置为基础框架。\u003c/p\u003e\n\u003ch2 id=\"222-agent开发框架\"\u003e2.2.2 Agent开发框架\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e1、从低代码到高代码\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e低代码平台是对于高代码的一层封装，其抽象层次很难满足所有场景，无法在性能、扩展性和复杂业务逻辑方面满足大规模生产的要求。\u003c/p\u003e\n\u003cp\u003e高代码是当下AI原生应用生产落地的主流形态，ADK、LangGraph、AutoGen、AgentScope、Spring AI Alibaba等框架，为开发者提供了面向Agent的编程接口。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2、高代码的演进\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e高代码模式本身经历了从ChatClient→Workflow→Agentic的演进过程。\u003c/p\u003e\n\u003cp\u003eChatClient，一次单一的LLM调用。Workflow，将传统工作流转化为LLM节点编排。Agentic，提供面向Agent的API，并内置多种通用的协作模式（Pattern）。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3、零代码的愿景\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e尝试让用户完全通过自然语言即可驱动开发。目前处于探索与验证阶段，难以承担大规模生产任务。\u003c/p\u003e\n\u003ch1 id=\"23-提示词\"\u003e2.3 提示词\u003c/h1\u003e\n\u003cp\u003e在AI原生应用中，不再编写复杂的代码，而是使用更接近人类语言的方式与AI沟通，这就是Prompt（提示词）。提示词的质量很大程度上影响了AI应用的输出效果。\u003c/p\u003e\n\u003ch2 id=\"231-提示词是什么\"\u003e2.3.1 提示词是什么\u003c/h2\u003e\n\u003cp\u003e提示词可以是一个具体的问题、一段描述、一组关键词，或是相关的上下文，不仅限于自然语言文本，还可以包含代码片段、数据格式说明，甚至是图像与文字相结合的多模态输入。\u003c/p\u003e\n\u003ch2 id=\"232-提示词质量ai输出质量\"\u003e2.3.2 提示词质量=AI输出质量\u003c/h2\u003e\n\u003cp\u003e大模型的输出质量，既要看大模型本身，也要看提示词是否清晰、完整、具体。比如，“写点关于人工智能的东西”，生成的内容是泛泛而谈。“以科技专栏作家的身份，撰写一篇800字左右的文章，探讨人工智能在医疗影像诊断中的最新应用、优势与挑战，并列举实例”，能生成一篇逻辑严谨、信息详实、专业度高的文章。\u003c/p\u003e","title":"AI原生应用的关键要素"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nFlask的view函数是用来对请求作出响应的。单个URL能匹配到单个View，那么多个类似的URL，比如：\n/auth/register /auth/login /auth/logout 有没有什么比较优雅的写法呢？\nBlueprints Blueprints就是一个路由分组，可以把共同的路由前缀注册为一个Blueprint，比如：\n在flaskr/auth.py文件中先定义一个Blueprint：\nimport functools from flask import ( Blueprint, flash, g, redirect, render_template, request, session, url_for ) from werkzeug.security import check_password_hash, generate_password_hash from flaskr.db import get_db bp = Blueprint(\u0026#39;auth\u0026#39;, __name__, url_prefix=\u0026#39;/auth\u0026#39;) 'auth'是Blueprint的名字。 __name__用来告诉Blueprint它的定义位置。 url_prefix就是路由前缀。 接着在flaskr/__init__.py文件中注册：\ndef create_app(): app = ... # existing code omitted from . import auth app.register_blueprint(auth.bp) return app Views 定义和注册了Blueprints后就可以在view中使用了。比如：\n①在flaskr/auth.py文件中添加一个注册view：\n@bp.route(\u0026#39;/register\u0026#39;, methods=(\u0026#39;GET\u0026#39;, \u0026#39;POST\u0026#39;)) def register(): if request.method == \u0026#39;POST\u0026#39;: username = request.form[\u0026#39;username\u0026#39;] password = request.form[\u0026#39;password\u0026#39;] db = get_db() error = None if not username: error = \u0026#39;Username is required.\u0026#39; elif not password: error = \u0026#39;Password is required.\u0026#39; if error is None: try: db.execute( \u0026#34;INSERT INTO user (username, password) VALUES (?, ?)\u0026#34;, (username, generate_password_hash(password)), ) db.commit() except db.IntegrityError: error = f\u0026#34;User {username} is already registered.\u0026#34; else: return redirect(url_for(\u0026#34;auth.login\u0026#34;)) flash(error) return render_template(\u0026#39;auth/register.html\u0026#39;) @bp.route就会把路由前缀加到'/register'上拼成/auth/register。 request.form是一个字典，可以读取接口入参。 db.execute执行SQL语句。db.commit()提交。 redirect在注册成功后重定向到登录页面。 url_for根据view名字来获取url，因为这里用到了Blueprint，所以入参是\u0026quot;auth.login\u0026quot;。如果没有用Blueprint，url_for()函数入参就写view函数名即可。 ②在flaskr/auth.py文件中添加一个登录view：\n@bp.route(\u0026#39;/login\u0026#39;, methods=(\u0026#39;GET\u0026#39;, \u0026#39;POST\u0026#39;)) def login(): if request.method == \u0026#39;POST\u0026#39;: username = request.form[\u0026#39;username\u0026#39;] password = request.form[\u0026#39;password\u0026#39;] db = get_db() error = None user = db.execute( \u0026#39;SELECT * FROM user WHERE username = ?\u0026#39;, (username,) ).fetchone() if user is None: error = \u0026#39;Incorrect username.\u0026#39; elif not check_password_hash(user[\u0026#39;password\u0026#39;], password): error = \u0026#39;Incorrect password.\u0026#39; if error is None: session.clear() session[\u0026#39;user_id\u0026#39;] = user[\u0026#39;id\u0026#39;] return redirect(url_for(\u0026#39;index\u0026#39;)) flash(error) return render_template(\u0026#39;auth/login.html\u0026#39;) fetchone()取一行数据，fetchall()取多行数据。\n登录成功后会把user_id存入session中，session是一个字典，这样后续请求就可以用到这个数据。比如：\n@bp.before_app_request def load_logged_in_user(): user_id = session.get(\u0026#39;user_id\u0026#39;) if user_id is None: g.user = None else: g.user = get_db().execute( \u0026#39;SELECT * FROM user WHERE id = ?\u0026#39;, (user_id,) ).fetchone() 值得注意的是@bp.before_app_request有点像setup，就是在所有请求前先运行这一段代码。\n③在flaskr/auth.py文件中添加一个登出view：\nflaskr/auth.py @bp.route(\u0026#39;/logout\u0026#39;) def logout(): session.clear() return redirect(url_for(\u0026#39;index\u0026#39;)) session.clear()清除session。 ④最后可以在在flaskr/auth.py文件中顺手写一个装饰器，用来做认证鉴权：\ndef login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for(\u0026#39;auth.login\u0026#39;)) return view(**kwargs) return wrapped_view 在需要登录才能访问的view上，就可以加上这个login_required装饰器。\n参考资料：\nhttps://flask.palletsprojects.com/en/2.0.x/tutorial/views/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/flask/000006-flaskflask%E7%9A%84blueprints%E5%92%8Cviews/","summary":"Flask的view函数是用来对请求作出响应的。单个URL能匹配到单个View，那么多个类似的URL，比如：","title":"Flask的Blueprints和Views"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nServletConfig package javax.servlet; import java.util.Enumeration; public interface ServletConfig { String getServletName(); ServletContext getServletContext(); String getInitParameter(String var1); Enumeration\u0026lt;String\u0026gt; getInitParameterNames(); } ServletConfig用于Servlet初始化时从web.xml获取配置信息。主要有以下方法：\npublic String getInitParameter(String name): Returns the parameter value for the specified parameter name. public Enumeration getInitParameterNames(): Returns an enumeration of all the initialization parameter names. public String getServletName(): Returns the name of the servlet. public ServletContext getServletContext(): Returns an object of ServletContext. 示例，web.xml\n\u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;ExampleServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;com.example.ExampleServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;param1\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;value1\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;param2\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;value2\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;ExampleServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/example\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; ExampleServlet.java\npackage com.example; import java.io.IOException; import java.io.PrintWriter; import java.util.Enumeration; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ExampleServlet extends HttpServlet { private ServletConfig config; @Override public void init(ServletConfig config) throws ServletException { this.config = config; super.init(config); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = response.getWriter(); // 获取初始化参数 String param1 = config.getInitParameter(\u0026#34;param1\u0026#34;); String param2 = config.getInitParameter(\u0026#34;param2\u0026#34;); // 获取所有初始化参数 Enumeration\u0026lt;String\u0026gt; paramNames = config.getInitParameterNames(); while (paramNames.hasMoreElements()) { String paramName = paramNames.nextElement(); String paramValue = config.getInitParameter(paramName); out.println(\u0026#34;\u0026lt;p\u0026gt;\u0026#34; + paramName + \u0026#34;: \u0026#34; + paramValue + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); } // 获取 ServletContext ServletContext context = config.getServletContext(); out.println(\u0026#34;\u0026lt;p\u0026gt;ServletContext: \u0026#34; + context.getContextPath() + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); // 获取 Servlet 名称 String servletName = config.getServletName(); out.println(\u0026#34;\u0026lt;p\u0026gt;Servlet Name: \u0026#34; + servletName + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); } } ServletContext ServletContext用于在整个Web应用程序范围内共享信息和资源。每个Web应用程序都有一个唯一的ServletContext对象，该对象在应用程序启动时创建，并在应用程序关闭时销毁。\n通过ServletContext，Servlet可以获取上下文初始化参数、访问资源、记录日志、共享数据等。主要有以下方法：\npublic String getInitParameter(String name): Returns the parameter value for the specified parameter name. public Enumeration getInitParameterNames(): Returns the names of the context\u0026rsquo;s initialization parameters. public void setAttribute(String name,Object object): sets the given object in the application scope. public Object getAttribute(String name): Returns the attribute for the specified name. public Enumeration getInitParameterNames(): Returns the names of the context\u0026rsquo;s initialization parameters as an Enumeration of String objects. public void removeAttribute(String name): Removes the attribute with the given name from the servlet context. ServletContext不但能从web.xml文件get属性，还能set和remove属性。\n示例，web.xml\n\u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;contextParam1\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;value1\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; \u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;contextParam2\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;value2\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; ExampleServlet.java\npackage com.example; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ExampleServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = response.getWriter(); // 获取 ServletContext ServletContext context = getServletContext(); // 获取上下文初始化参数 String contextParam1 = context.getInitParameter(\u0026#34;contextParam1\u0026#34;); String contextParam2 = context.getInitParameter(\u0026#34;contextParam2\u0026#34;); // 获取所有上下文初始化参数 Enumeration\u0026lt;String\u0026gt; paramNames = context.getInitParameterNames(); while (paramNames.hasMoreElements()) { String paramName = paramNames.nextElement(); String paramValue = context.getInitParameter(paramName); out.println(\u0026#34;\u0026lt;p\u0026gt;\u0026#34; + paramName + \u0026#34;: \u0026#34; + paramValue + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); } // 设置和获取上下文属性 context.setAttribute(\u0026#34;attributeName\u0026#34;, \u0026#34;attributeValue\u0026#34;); String attributeValue = (String) context.getAttribute(\u0026#34;attributeName\u0026#34;); out.println(\u0026#34;\u0026lt;p\u0026gt;Attribute: \u0026#34; + attributeValue + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); // 获取资源 InputStream is = context.getResourceAsStream(\u0026#34;/WEB-INF/config.properties\u0026#34;); if (is != null) { out.println(\u0026#34;\u0026lt;p\u0026gt;Resource found\u0026lt;/p\u0026gt;\u0026#34;); } else { out.println(\u0026#34;\u0026lt;p\u0026gt;Resource not found\u0026lt;/p\u0026gt;\u0026#34;); } // 获取实际路径 String realPath = context.getRealPath(\u0026#34;/index.html\u0026#34;); out.println(\u0026#34;\u0026lt;p\u0026gt;Real Path: \u0026#34; + realPath + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); // 记录日志 context.log(\u0026#34;This is a log message.\u0026#34;); // 获取上下文路径 String contextPath = context.getContextPath(); out.println(\u0026#34;\u0026lt;p\u0026gt;Context Path: \u0026#34; + contextPath + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); } } ExampleServlet继承HttpServlet，getServletContext方法来自HttpServlet，HttpServlet继承GenericServlet，GenericServlet提供了getServletContext方法，内部调用的是ServletConfig的getServletContext方法：\npublic ServletContext getServletContext() { ServletConfig sc = this.getServletConfig(); if (sc == null) { throw new IllegalStateException(lStrings.getString(\u0026#34;err.servlet_config_not_initialized\u0026#34;)); } else { return sc.getServletContext(); } } 总结，ServletConfig每个Servlet都有，ServletContext整个web容器只有一个。ServletConfig用于在Servlet初始化时传递配置信息，包含初始化参数和ServletContext的引用，主要用于获取初始化参数和共享上下文。ServletContext表示整个Web应用程序的上下文，提供在不同Servlet之间共享数据和资源的机制，主要用于共享数据、访问资源、记录日志和获取应用程序信息。\n参考资料：\nhttps://www.javatpoint.com/servletconfig\nhttps://www.javatpoint.com/servletcontext\nhttps://www.javatpoint.com/attribute\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000006-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servletconfig%E4%B8%8Eservletcontext/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"servletconfig\"\u003eServletConfig\u003c/h1\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejava.util.Enumeration\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eServletConfig\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetServletName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eServletContext\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetServletContext\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetInitParameter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar1\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eEnumeration\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eString\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetInitParameterNames\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eServletConfig用于Servlet初始化时从web.xml获取配置信息。主要有以下方法：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003epublic String getInitParameter(String name):\u003c/strong\u003e Returns the parameter value for the specified parameter name.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003epublic Enumeration getInitParameterNames():\u003c/strong\u003e Returns an enumeration of all the initialization parameter names.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003epublic String getServletName():\u003c/strong\u003e Returns the name of the servlet.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003epublic ServletContext getServletContext():\u003c/strong\u003e Returns an object of ServletContext.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e示例，web.xml\u003c/p\u003e","title":"JavaWeb开发基础ServletConfig与ServletContext"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n变量 Instance Variables (Non-Static Fields) 实例变量（非静态变量）\n一个类可以创造多个实例，实例中的变量叫做实例变量，相互独立。\nClass Variables (Static Fields) 类变量（静态变量）\n对比来看，类变量就是类的变量，一个类只能有一份，不能复制，使用static关键字来定义类变量。\n代码：\npublic class MyVar { static int classVar = 1; // 类变量（静态变量） int instanceVar = 2; // 实例变量 } 对比Python来看就很清楚：\nclass MyVar: class_var = 1 def __int__(self): instance_var = 2 Local Variables 局部变量\n局部变量是由代码位置决定的，前面的实例变量和类变量，是放到Field位置，而局部变量是放到Method里面。也就是说，局部变量是放在方法的花括号里面的变量，并且只能在方法内部访问，不能被其他地方访问，这就是所谓的局部。\npublic class MyVar { static int classVar = 1; // 类变量（静态变量） int instanceVar = 2; // 实例变量 void method() { int localVar = 3; // 局部变量 } } Parameters 参数\n参数就是方法名后面小括号里面的变量。\npublic class MyVar { static int classVar = 1; // 类变量（静态变量） int instanceVar = 2; // 实例变量 void method(int parameter) { // 参数 int localVar = 3; // 局部变量 } } 变量命名规则\n驼峰命名法，可参考阿里规范： https://github.com/alibaba/p3c\n基本数据类型 Java有8个基本数据类型：byte、short、int、long、float、double、boolean、char。\n对比Python的6个基本数据类型：Number（数字）、String（字符串）、List（列表）、Tuple（元组）、Set（集合）、Dictionary（字典），你发现了什么吗？\nJava的String严格来说不算基本数据类型，因为它其实是一个类，java.lang.String。\n默认值\n只声明不赋值的字段（定义在类级别的field），编译器会给它赋予默认值：\nbyte：0\nshort：0\nint：0\nlong：0L\nfloat：0.0f\ndouble：0.0d\nboolean：false\nchar：\\u0000\nString或任何对象，会赋值为null。\n而对于局部变量（定义在方法里面的variable），编译器不会自动赋值，只声明不赋值，会报编译错误。\n类的field会赋默认值。方法的局部variable不会。\n字面量Literals\n基本数据类型的初始化可以不使用new，而直接使用字面量：\nbyte b = 100; short s = 10000; int i = 100000; int hexadecimalValue = 0x1a; // 十六进制 int binaryValue = 0b11010; // 二进制 long x = 10000000L; float f1 = 123.4f; double d1 = 123.4; double d2 = 1.234e2; // 科学计数法 boolean result = true; char capitalC = \u0026#39;C\u0026#39;; String也不需要new就能初始化：\nString s = \u0026#34;this is a string\u0026#34;; 另外还有一个字面量.class，比如String.class，用来表示自己的类型。\n数组 An array is a container object that holds a fixed number of values of a single type.\n注意这里说的是container object，数组不是基本数据类型，而是容器对象。这一点也可以从数组初始化来论证，数组初始化是需要new的：\nint[] anArray; anArray = new int[10]; 代码示例：\nclass ArrayDemo { public static void main(String[] args) { // declares an array of integers int[] anArray; // allocates memory for 10 integers anArray = new int[10]; // initialize first element anArray[0] = 100; // initialize second element anArray[1] = 200; // and so forth anArray[2] = 300; anArray[3] = 400; anArray[4] = 500; anArray[5] = 600; anArray[6] = 700; anArray[7] = 800; anArray[8] = 900; anArray[9] = 1000; System.out.println(\u0026#34;Element at index 0: \u0026#34; + anArray[0]); System.out.println(\u0026#34;Element at index 1: \u0026#34; + anArray[1]); System.out.println(\u0026#34;Element at index 2: \u0026#34; + anArray[2]); System.out.println(\u0026#34;Element at index 3: \u0026#34; + anArray[3]); System.out.println(\u0026#34;Element at index 4: \u0026#34; + anArray[4]); System.out.println(\u0026#34;Element at index 5: \u0026#34; + anArray[5]); System.out.println(\u0026#34;Element at index 6: \u0026#34; + anArray[6]); System.out.println(\u0026#34;Element at index 7: \u0026#34; + anArray[7]); System.out.println(\u0026#34;Element at index 8: \u0026#34; + anArray[8]); System.out.println(\u0026#34;Element at index 9: \u0026#34; + anArray[9]); } } 多维数组：\nclass MultiDimArrayDemo { public static void main(String[] args) { String[][] names = { {\u0026#34;Mr. \u0026#34;, \u0026#34;Mrs. \u0026#34;, \u0026#34;Ms. \u0026#34;}, {\u0026#34;Smith\u0026#34;, \u0026#34;Jones\u0026#34;} }; // Mr. Smith System.out.println(names[0][0] + names[1][0]); // Ms. Jones System.out.println(names[0][2] + names[1][1]); } } 使用System.arraycopy()复制数组：\nclass ArrayCopyDemo { public static void main(String[] args) { String[] copyFrom = { \u0026#34;Affogato\u0026#34;, \u0026#34;Americano\u0026#34;, \u0026#34;Cappuccino\u0026#34;, \u0026#34;Corretto\u0026#34;, \u0026#34;Cortado\u0026#34;, \u0026#34;Doppio\u0026#34;, \u0026#34;Espresso\u0026#34;, \u0026#34;Frappucino\u0026#34;, \u0026#34;Freddo\u0026#34;, \u0026#34;Lungo\u0026#34;, \u0026#34;Macchiato\u0026#34;, \u0026#34;Marocchino\u0026#34;, \u0026#34;Ristretto\u0026#34; }; String[] copyTo = new String[7]; System.arraycopy(copyFrom, 2, copyTo, 0, 7); for (String coffee : copyTo) { System.out.print(coffee + \u0026#34; \u0026#34;); } } } 使用java.util.Arrays.copyOfRange复制数组：\nclass ArrayCopyOfDemo { public static void main(String[] args) { String[] copyFrom = { \u0026#34;Affogato\u0026#34;, \u0026#34;Americano\u0026#34;, \u0026#34;Cappuccino\u0026#34;, \u0026#34;Corretto\u0026#34;, \u0026#34;Cortado\u0026#34;, \u0026#34;Doppio\u0026#34;, \u0026#34;Espresso\u0026#34;, \u0026#34;Frappucino\u0026#34;, \u0026#34;Freddo\u0026#34;, \u0026#34;Lungo\u0026#34;, \u0026#34;Macchiato\u0026#34;, \u0026#34;Marocchino\u0026#34;, \u0026#34;Ristretto\u0026#34; }; String[] copyTo = java.util.Arrays.copyOfRange(copyFrom, 2, 9); for (String coffee : copyTo) { System.out.print(coffee + \u0026#34; \u0026#34;); } } } 操作符 赋值：\nint cadence = 0; int speed = 0; int gear = 1; 数学运算：\nclass ArithmeticDemo { public static void main (String[] args) { int result = 1 + 2; // result is now 3 System.out.println(\u0026#34;1 + 2 = \u0026#34; + result); int original_result = result; result = result - 1; // result is now 2 System.out.println(original_result + \u0026#34; - 1 = \u0026#34; + result); original_result = result; result = result * 2; // result is now 4 System.out.println(original_result + \u0026#34; * 2 = \u0026#34; + result); original_result = result; result = result / 2; // result is now 2 System.out.println(original_result + \u0026#34; / 2 = \u0026#34; + result); original_result = result; result = result + 8; // result is now 10 System.out.println(original_result + \u0026#34; + 8 = \u0026#34; + result); original_result = result; result = result % 7; // result is now 3 System.out.println(original_result + \u0026#34; % 7 = \u0026#34; + result); } } x += 1;等同于x = x + 1;\n一元运算符：\nclass UnaryDemo { public static void main(String[] args) { int result = +1; // result is now 1 System.out.println(result); result--; // result is now 0 System.out.println(result); result++; // result is now 1 System.out.println(result); result = -result; // result is now -1 System.out.println(result); boolean success = false; // false System.out.println(success); // true System.out.println(!success); } } ++i和i++都会加1，区别是++i的结果是递增后的值，i++的结果是原来的值：\nclass PrePostDemo { public static void main(String[] args){ int i = 3; i++; // prints 4 System.out.println(i); ++i; // prints 5 System.out.println(i); // prints 6 System.out.println(++i); // prints 6 System.out.println(i++); // prints 7 System.out.println(i); } } 关系运算符：\nclass ComparisonDemo { public static void main(String[] args){ int value1 = 1; int value2 = 2; if(value1 == value2) System.out.println(\u0026#34;value1 == value2\u0026#34;); if(value1 != value2) System.out.println(\u0026#34;value1 != value2\u0026#34;); if(value1 \u0026gt; value2) System.out.println(\u0026#34;value1 \u0026gt; value2\u0026#34;); if(value1 \u0026lt; value2) System.out.println(\u0026#34;value1 \u0026lt; value2\u0026#34;); if(value1 \u0026lt;= value2) System.out.println(\u0026#34;value1 \u0026lt;= value2\u0026#34;); } } 条件运算符：\nclass ConditionalDemo1 { public static void main(String[] args){ int value1 = 1; int value2 = 2; if ((value1 == 1) \u0026amp;\u0026amp; (value2 == 2)) System.out.println(\u0026#34;value1 is 1 AND value2 is 2\u0026#34;); if ((value1 == 1) || (value2 == 1)) System.out.println(\u0026#34;value1 is 1 OR value2 is 1\u0026#34;); } } ?:等同于if-then-else\nclass ConditionalDemo2 { public static void main(String[] args){ int value1 = 1; int value2 = 2; int result; boolean someCondition = true; result = someCondition ? value1 : value2; System.out.println(result); } } Instanceof判断：an object is an instance of a class, an instance of a subclass, or an instance of a class that implements a particular interface\nclass InstanceofDemo { public static void main(String[] args) { Parent obj1 = new Parent(); Parent obj2 = new Child(); System.out.println(\u0026#34;obj1 instanceof Parent: \u0026#34; + (obj1 instanceof Parent)); System.out.println(\u0026#34;obj1 instanceof Child: \u0026#34; + (obj1 instanceof Child)); System.out.println(\u0026#34;obj1 instanceof MyInterface: \u0026#34; + (obj1 instanceof MyInterface)); System.out.println(\u0026#34;obj2 instanceof Parent: \u0026#34; + (obj2 instanceof Parent)); System.out.println(\u0026#34;obj2 instanceof Child: \u0026#34; + (obj2 instanceof Child)); System.out.println(\u0026#34;obj2 instanceof MyInterface: \u0026#34; + (obj2 instanceof MyInterface)); } } class Parent {} class Child extends Parent implements MyInterface {} interface MyInterface {} 位运算符：\nclass BitDemo { public static void main(String[] args) { int bitmask = 0x000F; int val = 0x2222; // prints \u0026#34;2\u0026#34; System.out.println(val \u0026amp; bitmask); } } 语句 表达式\nint cadence = 0; anArray[0] = 100; System.out.println(\u0026#34;Element 1 at index 0: \u0026#34; + anArray[0]); int result = 1 + 2; // result is now 3 if (value1 == value2) System.out.println(\u0026#34;value1 == value2\u0026#34;); 语句\n// assignment statement aValue = 8933.234; // increment statement aValue++; // method invocation statement System.out.println(\u0026#34;Hello World!\u0026#34;); // object creation statement Bicycle myBike = new Bicycle(); 块\nclass BlockDemo { public static void main(String[] args) { boolean condition = true; if (condition) { // begin block 1 System.out.println(\u0026#34;Condition is true.\u0026#34;); } // end block one else { // begin block 2 System.out.println(\u0026#34;Condition is false.\u0026#34;); } // end block 2 } } 控制语句 if-then\nvoid applyBrakes() { // the \u0026#34;if\u0026#34; clause: bicycle must be moving if (isMoving){ // the \u0026#34;then\u0026#34; clause: decrease current speed currentSpeed--; } } if-then-else\nclass IfElseDemo { public static void main(String[] args) { int testscore = 76; char grade; if (testscore \u0026gt;= 90) { grade = \u0026#39;A\u0026#39;; } else if (testscore \u0026gt;= 80) { grade = \u0026#39;B\u0026#39;; } else if (testscore \u0026gt;= 70) { grade = \u0026#39;C\u0026#39;; } else if (testscore \u0026gt;= 60) { grade = \u0026#39;D\u0026#39;; } else { grade = \u0026#39;F\u0026#39;; } System.out.println(\u0026#34;Grade = \u0026#34; + grade); } } while\nclass WhileDemo { public static void main(String[] args){ int count = 1; while (count \u0026lt; 11) { System.out.println(\u0026#34;Count is: \u0026#34; + count); count++; } } } do-while\nclass DoWhileDemo { public static void main(String[] args){ int count = 1; do { System.out.println(\u0026#34;Count is: \u0026#34; + count); count++; } while (count \u0026lt; 11); } } for\nclass ForDemo { public static void main(String[] args){ for(int i = 1; i \u0026lt; 11; i++){ System.out.println(\u0026#34;Count is: \u0026#34; + i); } } } enhanced for\nclass EnhancedForDemo { public static void main(String[] args){ int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; for (int item : numbers) { System.out.println(\u0026#34;Count is: \u0026#34; + item); } } } break\nclass BreakDemo { public static void main(String[] args) { int[] arrayOfInts = { 32, 87, 3, 589, 12, 1076, 2000, 8, 622, 127 }; int searchfor = 12; int i; boolean foundIt = false; for (i = 0; i \u0026lt; arrayOfInts.length; i++) { if (arrayOfInts[i] == searchfor) { foundIt = true; break; } } if (foundIt) { System.out.println(\u0026#34;Found \u0026#34; + searchfor + \u0026#34; at index \u0026#34; + i); } else { System.out.println(searchfor + \u0026#34; not in the array\u0026#34;); } } } continue\nclass ContinueDemo { public static void main(String[] args) { String searchMe = \u0026#34;peter piper picked a \u0026#34; + \u0026#34;peck of pickled peppers\u0026#34;; int max = searchMe.length(); int numPs = 0; for (int i = 0; i \u0026lt; max; i++) { // interested only in p\u0026#39;s if (searchMe.charAt(i) != \u0026#39;p\u0026#39;) continue; // process p\u0026#39;s numPs++; } System.out.println(\u0026#34;Found \u0026#34; + numPs + \u0026#34; p\u0026#39;s in the string.\u0026#34;); } } return\nreturn ++count; // 返回value return; // 返回void Switch语句 int quarter = ...; // any value String quarterLabel = null; switch (quarter) { case 0: quarterLabel = \u0026#34;Q1 - Winter\u0026#34;; break; case 1: quarterLabel = \u0026#34;Q2 - Spring\u0026#34;; break; case 2: quarterLabel = \u0026#34;Q3 - Summer\u0026#34;; break; case 3: quarterLabel = \u0026#34;Q3 - Summer\u0026#34;; break; default: quarterLabel = \u0026#34;Unknown quarter\u0026#34;; }; selector的类型只能是：\nbyte short char int 基本数据类型\nByte Short Character Integer 包装类型\n枚举类型\nString类型\n不能是：boolean long float double\n不写break会全部case执行一遍：\nint month = 8; List\u0026lt;String\u0026gt; futureMonths = new ArrayList\u0026lt;\u0026gt;(); switch (month) { case 1: futureMonths.add(\u0026#34;January\u0026#34;); case 2: futureMonths.add(\u0026#34;February\u0026#34;); case 3: futureMonths.add(\u0026#34;March\u0026#34;); case 4: futureMonths.add(\u0026#34;April\u0026#34;); case 5: futureMonths.add(\u0026#34;May\u0026#34;); case 6: futureMonths.add(\u0026#34;June\u0026#34;); case 7: futureMonths.add(\u0026#34;July\u0026#34;); case 8: futureMonths.add(\u0026#34;August\u0026#34;); case 9: futureMonths.add(\u0026#34;September\u0026#34;); case 10: futureMonths.add(\u0026#34;October\u0026#34;); case 11: futureMonths.add(\u0026#34;November\u0026#34;); case 12: futureMonths.add(\u0026#34;December\u0026#34;); break; default: break; } 多个case连写：\nint month = 2; int year = 2021; int numDays = 0; switch (month) { case 1: case 3: case 5: // January March May case 7: case 8: case 10: // July August October case 12: numDays = 31; break; case 4: case 6: // April June case 9: case 11: // September November numDays = 30; break; case 2: // February if (((year % 4 == 0) \u0026amp;\u0026amp; !(year % 100 == 0)) || (year % 400 == 0)) numDays = 29; else numDays = 28; break; default: System.out.println(\u0026#34;Invalid month.\u0026#34;); break; } 如果selector是null，会报空指针异常：NullPointerException\n参考资料：\nJava Language Basics https://dev.java/learn/language-basics/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000006-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B03java%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"变量\"\u003e变量\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eInstance Variables (Non-Static Fields)\u003c/strong\u003e 实例变量（非静态变量）\u003c/p\u003e\n\u003cp\u003e一个类可以创造多个实例，实例中的变量叫做实例变量，相互独立。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eClass Variables (Static Fields)\u003c/strong\u003e 类变量（静态变量）\u003c/p\u003e\n\u003cp\u003e对比来看，类变量就是类的变量，一个类只能有一份，不能复制，使用\u003ccode\u003estatic\u003c/code\u003e关键字来定义类变量。\u003c/p\u003e\n\u003cp\u003e代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyVar\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclassVar\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 类变量（静态变量）\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003einstanceVar\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e2\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 实例变量\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e对比Python来看就很清楚：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyVar\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eclass_var \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003e__int__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#6a737d\"\u003eself\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  instance_var \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003eLocal Variables\u003c/strong\u003e 局部变量\u003c/p\u003e\n\u003cp\u003e局部变量是由代码位置决定的，前面的实例变量和类变量，是放到Field位置，而局部变量是放到Method里面。也就是说，局部变量是放在方法的花括号里面的变量，并且只能在方法内部访问，不能被其他地方访问，这就是所谓的局部。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyVar\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclassVar\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 类变量（静态变量）\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003einstanceVar\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e2\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 实例变量\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003emethod\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003elocalVar\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e3\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 局部变量\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eParameters\u003c/strong\u003e 参数\u003c/p\u003e","title":"Java官方笔记3Java语言基础"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n线程组编辑区如下：\n有点复杂，但是慢慢看下来，还是比较容易理解。\nName 带有业务含义的名字。\nComments 线程组的备注说明。\nAction to be taken after a Sampler error 取样器报错后执行动作。有5个选项：Continue，Start Next Thread Loop，Stop Thread，Stop Test，Stop Test Now。为了搞懂这几个选项，我画了张时序图进行说明：\n图中有一个线程，在最左边。 右边有两个迭代：迭代1和迭代2。 每个迭代有两个请求，第一个请求失败。 Stop Thread是直接结束线程，没有画出来，一般不会设置此项，不然会导致运行线程越来越少，最后负载不够，对服务器的压力不够，测试结果不具参考性。 剩余4个选项用红色字体标注了出来。 线程在第一个迭代的第一个请求失败了。Continue表示继续执行第二个请求，再执行第二个迭代；Start Next Thread Loop表示忽略第二个请求，跳到第二个迭代执行；Stop Test表示把当前迭代的第二个请求执行完后，停止测试；Stop Test Now表示从第一个请求失败这里直接结束测试。\n**JMeter默认选项是Continue，保证足够的并发压力。**我们在大量用户并发时，服务器偶尔响应错误是正常现象，比如服务器由于性能问题500，此时出错我们正好要记录下来，作为有性能问题的依据。\n如果想减少关联请求报错，可以选择Start Next Thread Loop。\nThread Properties Number of Threads (users) 线程组的线程数量。\nRamp-up period (seconds) 启动时间，线程组在多少秒内启动完所有线程。\n比如设置线程数为50，设置启动时间为10秒，那么每秒就会启动50 / 10 = 5个线程；如果设置为0秒，则50个线程会立刻启动；如果设置为100秒，就会每隔100 / 50 = 2秒启动1个线程。\nRamp-up period如何设置？ 以下是5个线程依次从启动到执行到退出的示意图：\nJMeter线程组产生的并发压力，实际上是红色框起来的那部分，在这个时间段才是所有线程在并发着运行。\n先从Ramp-up period设置最小和最大来分析这个问题：\n假设有3000个线程，只迭代1次，如果设置为0秒，那么测试一开始就会产生3000个并发请求，说不定直接把服务器压崩了，还没开始就结束了。 假设有10个线程，只迭代1次，如果设置为100秒，那么每隔10秒启动1个线程，很可能前一个线程跑完了，下一个线程还没启起来，某一时刻最多只有1个线程在跑，没有并发压力。 接着看看该设置成多少？这个答案我找了很多资料，都没有明确的说法。结合实践经验来谈的话，既不能太小，也不能太大，可以根据业务场景、硬件配置、系统资源来进行设置。\nLoop Count 迭代次数。\n填写数字，指定迭代次数。 勾选Infinite，无限迭代，一直运行到测试停止或异常崩溃。 Same user on each iteration 每个迭代都用相同的user（线程）。\n**默认这个选项是勾选的。**因为销毁和创建线程本身就会占用资源，可能会影响性能测试结果。\n什么时候去掉勾选呢？比如登录，加了HTTP Cookie管理器以后，单个线程多次迭代（注意不是多个线程哦）登录用的都是相同的Cookie。去掉勾选后，同时在HTTP Cookie管理器选择清除旧Cookie：\n那么每次迭代就能用不同Cookie了。\nDelay Thread creation until needed 保持默认就好。跟JVM创建线程时机有关，实际运用勾不勾选都不影响测试结果。\nSpecify Thread lifetime Duration\n持续时间，单位秒。\n前面的Loop Count勾选了Infinite，Duration才会生效。\nStartup delay\n启动延迟，单位秒。\n延迟到多少秒后再启动线程。\n小结 本文对线程组编辑区进行了揭秘，看似复杂，实则简单，问题在于实际使用过程中如何结合业务来设置，这需要实践经验不断积累才能找到答案。需要注意的是，如果Ramp-up period设置不当，有可能100个线程只能产生1个并发请求。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\nhttps://www.cnblogs.com/hjhsysu/p/9189897.html\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000006-jmeterjmeter100%E4%B8%AA%E7%BA%BF%E7%A8%8B%E7%AB%9F%E7%84%B6%E5%8F%AA%E6%A8%A1%E6%8B%9F%E5%87%BA1%E4%B8%AA%E5%B9%B6%E5%8F%91/","summary":"线程组编辑区如下：","title":"JMeter100个线程竟然只模拟出1个并发"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n参数化测试一直是津津乐道的话题，我们都知道JMeter有四种参数化方式：用户自定义变量、用户参数、CSV文件、函数助手，那么JUnit5有哪些参数化测试的方式呢？\n依赖 JUnit5需要添加junit-jupiter-params依赖才能使用参数化：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.jupiter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-jupiter-params\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.7.2\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 简单示例 @ParameterizedTest用来定义参数化测试，@ValueSource用来定义参数值：\n@ParameterizedTest @ValueSource(strings = { \u0026#34;racecar\u0026#34;, \u0026#34;radar\u0026#34;, \u0026#34;able was I ere I saw elba\u0026#34; }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); } 执行结果：\npalindromes(String) ✔ ├─ [1] candidate=racecar ✔ ├─ [2] candidate=radar ✔ └─ [3] candidate=able was I ere I saw elba ✔ 参数值会匹配测试方法的参数列表，然后依次赋值，这里一共产生了3个测试。\n七种方式 1 @ValueSource @ValueSource是最简单的参数化方式，它是一个数组，支持以下数据类型：\nshort byte int long float double char boolean java.lang.String java.lang.Class 示例：\n@ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testWithValueSource(int argument) { assertTrue(argument \u0026gt; 0 \u0026amp;\u0026amp; argument \u0026lt; 4); } 2 Null and Empty Sources @NullSource 值为null\n不能用在基元类型的测试方法。\n@EmptySource 值为空，根据测试方法的参数类决定数据类型，支持java.lang.String, java.util.List, java.util.Set, java.util.Map, 基元类型数组 (int[], char[][]等), 对象数组 (String[], Integer[][]等)\n@NullAndEmptySource 结合了前面两个\n示例：\n@ParameterizedTest @NullSource @EmptySource @ValueSource(strings = { \u0026#34; \u0026#34;, \u0026#34; \u0026#34;, \u0026#34;\\t\u0026#34;, \u0026#34;\\n\u0026#34; }) void nullEmptyAndBlankStrings(String text) { assertTrue(text == null || text.trim().isEmpty()); } 等价于：\n@ParameterizedTest @NullAndEmptySource @ValueSource(strings = { \u0026#34; \u0026#34;, \u0026#34; \u0026#34;, \u0026#34;\\t\u0026#34;, \u0026#34;\\n\u0026#34; }) void nullEmptyAndBlankStrings(String text) { assertTrue(text == null || text.trim().isEmpty()); } 3 @EnumSource 参数化的值为枚举类型。\n示例：\n@ParameterizedTest @EnumSource void testWithEnumSourceWithAutoDetection(ChronoUnit unit) { assertNotNull(unit); } 其中的ChronoUnit是个日期枚举类。\nChronoUnit是接口TemporalUnit的实现类，如果测试方法的参数为TemporalUnit，那么需要给@EnumSource加上值：\n@ParameterizedTest @EnumSource(ChronoUnit.class) void testWithEnumSource(TemporalUnit unit) { assertNotNull(unit); } 因为JUnit5规定了@EnumSource的默认值的类型必须是枚举类型。\nnames属性用来指定使用哪些特定的枚举值：\n@ParameterizedTest @EnumSource(names = { \u0026#34;DAYS\u0026#34;, \u0026#34;HOURS\u0026#34; }) void testWithEnumSourceInclude(ChronoUnit unit) { assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit)); } mode属性用来指定使用模式，比如排除哪些枚举值：\n@ParameterizedTest @EnumSource(mode = EXCLUDE, names = { \u0026#34;ERAS\u0026#34;, \u0026#34;FOREVER\u0026#34; }) void testWithEnumSourceExclude(ChronoUnit unit) { assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit)); } 比如采用正则匹配：\n@ParameterizedTest @EnumSource(mode = MATCH_ALL, names = \u0026#34;^.*DAYS$\u0026#34;) void testWithEnumSourceRegex(ChronoUnit unit) { assertTrue(unit.name().endsWith(\u0026#34;DAYS\u0026#34;)); } 4 @MethodSource 参数值为factory方法，并且factory方法不能带参数。\n示例：\n@ParameterizedTest @MethodSource(\u0026#34;stringProvider\u0026#34;) void testWithExplicitLocalMethodSource(String argument) { assertNotNull(argument); } static Stream\u0026lt;String\u0026gt; stringProvider() { return Stream.of(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;); } 除非是@TestInstance(Lifecycle.PER_CLASS)生命周期，否则factory方法必须是static。factory方法的返回值是能转换为Stream的类型，比如Stream, DoubleStream, LongStream, IntStream, Collection, Iterator, Iterable, 对象数组, 或者基元类型数组，比如：\n@ParameterizedTest @MethodSource(\u0026#34;range\u0026#34;) void testWithRangeMethodSource(int argument) { assertNotEquals(9, argument); } static IntStream range() { return IntStream.range(0, 20).skip(10); } @MethodSource的属性如果省略了，那么JUnit Jupiter会找跟测试方法同名的factory方法，比如：\n@ParameterizedTest @MethodSource void testWithDefaultLocalMethodSource(String argument) { assertNotNull(argument); } static Stream\u0026lt;String\u0026gt; testWithDefaultLocalMethodSource() { return Stream.of(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;); } 如果测试方法有多个参数，那么factory方法也应该返回多个：\n@ParameterizedTest @MethodSource(\u0026#34;stringIntAndListProvider\u0026#34;) void testWithMultiArgMethodSource(String str, int num, List\u0026lt;String\u0026gt; list) { assertEquals(5, str.length()); assertTrue(num \u0026gt;=1 \u0026amp;\u0026amp; num \u0026lt;=2); assertEquals(2, list.size()); } static Stream\u0026lt;Arguments\u0026gt; stringIntAndListProvider() { return Stream.of( arguments(\u0026#34;apple\u0026#34;, 1, Arrays.asList(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;)), arguments(\u0026#34;lemon\u0026#34;, 2, Arrays.asList(\u0026#34;x\u0026#34;, \u0026#34;y\u0026#34;)) ); } 其中arguments(Object…)是Arguments接口的static factory method，也可以换成Arguments.of(Object…)。\nfactory方法也可以防止测试类外部：\npackage example; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; class ExternalMethodSourceDemo { @ParameterizedTest @MethodSource(\u0026#34;example.StringsProviders#tinyStrings\u0026#34;) void testWithExternalMethodSource(String tinyString) { // test with tiny string } } class StringsProviders { static Stream\u0026lt;String\u0026gt; tinyStrings() { return Stream.of(\u0026#34;.\u0026#34;, \u0026#34;oo\u0026#34;, \u0026#34;OOO\u0026#34;); } } 5 @CsvSource 参数化的值为csv格式的数据（默认逗号分隔），比如：\n@ParameterizedTest @CsvSource({ \u0026#34;apple, 1\u0026#34;, \u0026#34;banana, 2\u0026#34;, \u0026#34;\u0026#39;lemon, lime\u0026#39;, 0xF1\u0026#34; }) void testWithCsvSource(String fruit, int rank) { assertNotNull(fruit); assertNotEquals(0, rank); } delimiter属性可以设置分隔字符。delimiterString属性可以设置分隔字符串（String而非char）。\n更多输入输出示例如下：\n注意，如果null引用的目标类型是基元类型，那么会报异常ArgumentConversionException。\n6 @CsvFileSource 顾名思义，选择本地csv文件作为数据来源。\n示例：\n@ParameterizedTest @CsvFileSource(resources = \u0026#34;/two-column.csv\u0026#34;, numLinesToSkip = 1) void testWithCsvFileSourceFromClasspath(String country, int reference) { assertNotNull(country); assertNotEquals(0, reference); } @ParameterizedTest @CsvFileSource(files = \u0026#34;src/test/resources/two-column.csv\u0026#34;, numLinesToSkip = 1) void testWithCsvFileSourceFromFile(String country, int reference) { assertNotNull(country); assertNotEquals(0, reference); } delimiter属性可以设置分隔字符。delimiterString属性可以设置分隔字符串（String而非char）。需要特别注意的是，#开头的行会被认为是注释而略过。\n7 @ArgumentsSource 自定义ArgumentsProvider。\n示例：\n@ParameterizedTest @ArgumentsSource(MyArgumentsProvider.class) void testWithArgumentsSource(String argument) { assertNotNull(argument); } public class MyArgumentsProvider implements ArgumentsProvider { @Override public Stream\u0026lt;? extends Arguments\u0026gt; provideArguments(ExtensionContext context) { return Stream.of(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;).map(Arguments::of); } } MyArgumentsProvider必须是外部类或者static内部类。\n参数类型转换 隐式转换 JUnit Jupiter会对String类型进行隐式转换。比如：\n@ParameterizedTest @ValueSource(strings = \u0026#34;SECONDS\u0026#34;) void testWithImplicitArgumentConversion(ChronoUnit argument) { assertNotNull(argument.name()); } 更多转换示例：\n也可以把String转换为自定义对象：\n@ParameterizedTest @ValueSource(strings = \u0026#34;42 Cats\u0026#34;) void testWithImplicitFallbackArgumentConversion(Book book) { assertEquals(\u0026#34;42 Cats\u0026#34;, book.getTitle()); } public class Book { private final String title; private Book(String title) { this.title = title; } public static Book fromTitle(String title) { return new Book(title); } public String getTitle() { return this.title; } } JUnit Jupiter会找到Book.fromTitle(String)方法，然后把@ValueSource的值传入进去，进而把String类型转换为Book类型。转换的factory方法既可以是接受单个String参数的构造方法，也可以是接受单个String参数并返回目标类型的普通方法。详细规则如下（官方原文）：\n显式转换 显式转换需要使用@ConvertWith注解：\n@ParameterizedTest @EnumSource(ChronoUnit.class) void testWithExplicitArgumentConversion( @ConvertWith(ToStringArgumentConverter.class) String argument) { assertNotNull(ChronoUnit.valueOf(argument)); } 并实现ArgumentConverter：\npublic class ToStringArgumentConverter extends SimpleArgumentConverter { @Override protected Object convert(Object source, Class\u0026lt;?\u0026gt; targetType) { assertEquals(String.class, targetType, \u0026#34;Can only convert to String\u0026#34;); if (source instanceof Enum\u0026lt;?\u0026gt;) { return ((Enum\u0026lt;?\u0026gt;) source).name(); } return String.valueOf(source); } } 如果只是简单类型转换，实现TypedArgumentConverter即可：\npublic class ToLengthArgumentConverter extends TypedArgumentConverter\u0026lt;String, Integer\u0026gt; { protected ToLengthArgumentConverter() { super(String.class, Integer.class); } @Override protected Integer convert(String source) { return source.length(); } } JUnit Jupiter只内置了一个JavaTimeArgumentConverter，通过@JavaTimeConversionPattern使用：\n@ParameterizedTest @ValueSource(strings = { \u0026#34;01.01.2017\u0026#34;, \u0026#34;31.12.2017\u0026#34; }) void testWithExplicitJavaTimeConverter( @JavaTimeConversionPattern(\u0026#34;dd.MM.yyyy\u0026#34;) LocalDate argument) { assertEquals(2017, argument.getYear()); } 参数聚合 测试方法的多个参数可以聚合为一个ArgumentsAccessor参数，然后通过get来取值，示例：\n@ParameterizedTest @CsvSource({ \u0026#34;Jane, Doe, F, 1990-05-20\u0026#34;, \u0026#34;John, Doe, M, 1990-10-22\u0026#34; }) void testWithArgumentsAccessor(ArgumentsAccessor arguments) { Person person = new Person(arguments.getString(0), arguments.getString(1), arguments.get(2, Gender.class), arguments.get(3, LocalDate.class)); if (person.getFirstName().equals(\u0026#34;Jane\u0026#34;)) { assertEquals(Gender.F, person.getGender()); } else { assertEquals(Gender.M, person.getGender()); } assertEquals(\u0026#34;Doe\u0026#34;, person.getLastName()); assertEquals(1990, person.getDateOfBirth().getYear()); } 也可以自定义Aggregator：\npublic class PersonAggregator implements ArgumentsAggregator { @Override public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { return new Person(arguments.getString(0), arguments.getString(1), arguments.get(2, Gender.class), arguments.get(3, LocalDate.class)); } } 然后通过@AggregateWith来使用：\n@ParameterizedTest @CsvSource({ \u0026#34;Jane, Doe, F, 1990-05-20\u0026#34;, \u0026#34;John, Doe, M, 1990-10-22\u0026#34; }) void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) { // perform assertions against person } 借助于组合注解，我们可以进一步简化代码：\n@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @AggregateWith(PersonAggregator.class) public @interface CsvToPerson { } @ParameterizedTest @CsvSource({ \u0026#34;Jane, Doe, F, 1990-05-20\u0026#34;, \u0026#34;John, Doe, M, 1990-10-22\u0026#34; }) void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) { // perform assertions against person } 自定义显示名字 参数化测试生成的test，JUnit Jupiter给定了默认名字，我们可以通过name属性进行自定义。\n示例：\n@DisplayName(\u0026#34;Display name of container\u0026#34;) @ParameterizedTest(name = \u0026#34;{index} ==\u0026gt; the rank of \u0026#39;\u0026#39;{0}\u0026#39;\u0026#39; is {1}\u0026#34;) @CsvSource({ \u0026#34;apple, 1\u0026#34;, \u0026#34;banana, 2\u0026#34;, \u0026#34;\u0026#39;lemon, lime\u0026#39;, 3\u0026#34; }) void testWithCustomDisplayNames(String fruit, int rank) { } 结果：\nDisplay name of container ✔ ├─ 1 ==\u0026gt; the rank of \u0026#39;apple\u0026#39; is 1 ✔ ├─ 2 ==\u0026gt; the rank of \u0026#39;banana\u0026#39; is 2 ✔ └─ 3 ==\u0026gt; the rank of \u0026#39;lemon, lime\u0026#39; is 3 ✔ 注意如果要显示'apple'，需要使用两层''apple''，因为name是MessageFormat。\n占位符说明如下：\n小结 本文介绍了JUnit5参数化测试的7种方式，分别是@ValueSource，Null and Empty Sources，@EnumSource，@MethodSource，@CsvSource，@CsvFileSource，@ArgumentsSource，比较偏向于Java语法，符合JUnit单元测试框架的特征。另外还介绍了JUnit Jupiter的参数类型转换和参数聚合。最后，如果想要自定义参数化测试的名字，可以使用name属性实现。\n参考资料：\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000006-junitjunit5%E5%8F%82%E6%95%B0%E5%8C%96%E6%B5%8B%E8%AF%95%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E5%BC%8F/","summary":"参数化测试一直是津津乐道的话题，我们都知道JMeter有四种参数化方式：用户自定义变量、用户参数、CSV文件、函数助手，那么JUnit5有哪些参数化测试的方式呢？","title":"JUnit5参数化测试的几种方式"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\npytest的断言把Python语言简洁的优点发挥的淋漓尽致，因为它用的就是Python的标准断言assert。\nassert基础 assert用法\nassert_stmt ::= \u0026#34;assert\u0026#34; expression1 [\u0026#34;,\u0026#34; expression2] \u0026ldquo;assert\u0026rdquo; expression1，expression1和 if 的条件语句是一模一样的，if 能写什么这里就能写什么。\n[\u0026quot;,\u0026quot; expression2]，是可选的，不写错误信息也可以。\nassert断言等价于\nif __debug__: if not expression1: raise AssertionError(expression2) 示例，只考了59分，满分100分，不及格\ndef test(): assert 59 == 100, \u0026#34;不及格\u0026#34; 结果，\u0026gt;指出了断言失败的代码行，E指出了具体的错误\na.py::test FAILED [100%] AssertionError: 不及格 59 != 100 Expected :100 Actual :59 \u0026lt;Click to see difference\u0026gt; def test(): \u0026gt; assert 59 == 100, \u0026#34;不及格\u0026#34; E AssertionError: 不及格 E assert 59 == 100 a.py:2: AssertionError ================================== FAILURES =================================== ____________________________________ test _____________________________________ def test(): \u0026gt; assert 59 == 100, \u0026#34;不及格\u0026#34; E AssertionError: 不及格 E assert 59 == 100 a.py:2: AssertionError =========================== short test summary info =========================== FAILED a.py::test - AssertionError: 不及格 ============================== 1 failed in 0.30s ============================== 把这个例子稍微抽象一下，assert 实际 == 预期，错误信息。记住把预期放在右边。\npytest.raises pytest.raises用于断言异常。\n示例，断言会抛出ZeroDivisionError，如果抛出了测试结果是pass，如果没抛出，测试结果是failed\nimport pytest def test_zero_division(): with pytest.raises(ZeroDivisionError): 1 / 0 同时可以断言异常信息。示例，定义f()，递归循环抛异常，assert断言这个异常信息包括\u0026quot;maximum recursion\u0026quot;\ndef test_recursion_depth(): with pytest.raises(RuntimeError) as excinfo: def f(): f() f() assert \u0026#34;maximum recursion\u0026#34; in str(excinfo.value) 可以添加match参数来用正则过滤异常信息。示例，匹配包括123的异常\nimport pytest def myfunc(): raise ValueError(\u0026#34;Exception 123 raised\u0026#34;) def test_match(): with pytest.raises(ValueError, match=r\u0026#34;.* 123 .*\u0026#34;): myfunc() 如果是对整个test异常断言，也可以加marker xfail\n@pytest.mark.xfail(raises=IndexError) 44个failure例子 每个test用大横线做了分隔\nassertion $ pytest failure_demo.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/assertion collected 44 items failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%] ================================= FAILURES ================================= ___________________________ test_generative[3-6] ___________________________ param1 = 3, param2 = 6 @pytest.mark.parametrize(\u0026#34;param1, param2\u0026#34;, [(3, 6)]) def test_generative(param1, param2): \u0026gt; assert param1 * 2 \u0026lt; param2 E assert (3 * 2) \u0026lt; 6 failure_demo.py:19: AssertionError _________________________ TestFailing.test_simple __________________________ self = \u0026lt;failure_demo.TestFailing object at 0xdeadbeef\u0026gt; def test_simple(self): def f(): return 42 def g(): return 43 \u0026gt; assert f() == g() E assert 42 == 43 E + where 42 = \u0026lt;function TestFailing.test_simple.\u0026lt;locals\u0026gt;.f at 0xdeadbeef\u0026gt;() E + and 43 = \u0026lt;function TestFailing.test_simple.\u0026lt;locals\u0026gt;.g at 0xdeadbeef\u0026gt;() failure_demo.py:30: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ self = \u0026lt;failure_demo.TestFailing object at 0xdeadbeef\u0026gt; def test_simple_multiline(self): \u0026gt; otherfunc_multi(42, 6 * 9) failure_demo.py:33: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 42, b = 54 def otherfunc_multi(a, b): \u0026gt; assert a == b E assert 42 == 54 failure_demo.py:14: AssertionError ___________________________ TestFailing.test_not ___________________________ self = \u0026lt;failure_demo.TestFailing object at 0xdeadbeef\u0026gt; def test_not(self): def f(): return 42 \u0026gt; assert not f() E assert not 42 E + where 42 = \u0026lt;function TestFailing.test_not.\u0026lt;locals\u0026gt;.f at 0xdeadbeef\u0026gt;() failure_demo.py:39: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_text(self): \u0026gt; assert \u0026#34;spam\u0026#34; == \u0026#34;eggs\u0026#34; E AssertionError: assert \u0026#39;spam\u0026#39; == \u0026#39;eggs\u0026#39; E - eggs E + spam failure_demo.py:44: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_similar_text(self): \u0026gt; assert \u0026#34;foo 1 bar\u0026#34; == \u0026#34;foo 2 bar\u0026#34; E AssertionError: assert \u0026#39;foo 1 bar\u0026#39; == \u0026#39;foo 2 bar\u0026#39; E - foo 2 bar E ? ^ E + foo 1 bar E ? ^ failure_demo.py:47: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_multiline_text(self): \u0026gt; assert \u0026#34;foo\\nspam\\nbar\u0026#34; == \u0026#34;foo\\neggs\\nbar\u0026#34; E AssertionError: assert \u0026#39;foo\\nspam\\nbar\u0026#39; == \u0026#39;foo\\neggs\\nbar\u0026#39; E foo E - eggs E + spam E bar failure_demo.py:50: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_long_text(self): a = \u0026#34;1\u0026#34; * 100 + \u0026#34;a\u0026#34; + \u0026#34;2\u0026#34; * 100 b = \u0026#34;1\u0026#34; * 100 + \u0026#34;b\u0026#34; + \u0026#34;2\u0026#34; * 100 \u0026gt; assert a == b E AssertionError: assert \u0026#39;111111111111...2222222222222\u0026#39; == \u0026#39;111111111111...2222222222222\u0026#39; E Skipping 90 identical leading characters in diff, use -v to show E Skipping 91 identical trailing characters in diff, use -v to show E - 1111111111b222222222 E ? ^ E + 1111111111a222222222 E ? ^ failure_demo.py:55: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_long_text_multiline(self): a = \u0026#34;1\\n\u0026#34; * 100 + \u0026#34;a\u0026#34; + \u0026#34;2\\n\u0026#34; * 100 b = \u0026#34;1\\n\u0026#34; * 100 + \u0026#34;b\u0026#34; + \u0026#34;2\\n\u0026#34; * 100 \u0026gt; assert a == b E AssertionError: assert \u0026#39;1\\n1\\n1\\n1\\n...n2\\n2\\n2\\n2\\n\u0026#39; == \u0026#39;1\\n1\\n1\\n1\\n...n2\\n2\\n2\\n2\\n\u0026#39; E Skipping 190 identical leading characters in diff, use -v to show E Skipping 191 identical trailing characters in diff, use -v to show E 1 E 1 E 1 E 1 E 1... E E ...Full output truncated (7 lines hidden), use \u0026#39;-vv\u0026#39; to show failure_demo.py:60: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_list(self): \u0026gt; assert [0, 1, 2] == [0, 1, 3] E assert [0, 1, 2] == [0, 1, 3] E At index 2 diff: 2 != 3 E Use -v to get the full diff failure_demo.py:63: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_list_long(self): a = [0] * 100 + [1] + [3] * 100 b = [0] * 100 + [2] + [3] * 100 \u0026gt; assert a == b E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...] E At index 100 diff: 1 != 2 E Use -v to get the full diff failure_demo.py:68: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_dict(self): \u0026gt; assert {\u0026#34;a\u0026#34;: 0, \u0026#34;b\u0026#34;: 1, \u0026#34;c\u0026#34;: 0} == {\u0026#34;a\u0026#34;: 0, \u0026#34;b\u0026#34;: 2, \u0026#34;d\u0026#34;: 0} E AssertionError: assert {\u0026#39;a\u0026#39;: 0, \u0026#39;b\u0026#39;: 1, \u0026#39;c\u0026#39;: 0} == {\u0026#39;a\u0026#39;: 0, \u0026#39;b\u0026#39;: 2, \u0026#39;d\u0026#39;: 0} E Omitting 1 identical items, use -vv to show E Differing items: E {\u0026#39;b\u0026#39;: 1} != {\u0026#39;b\u0026#39;: 2} E Left contains 1 more item: E {\u0026#39;c\u0026#39;: 0} E Right contains 1 more item: E {\u0026#39;d\u0026#39;: 0}... E E ...Full output truncated (2 lines hidden), use \u0026#39;-vv\u0026#39; to show failure_demo.py:71: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_set(self): \u0026gt; assert {0, 10, 11, 12} == {0, 20, 21} E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21} E Extra items in the left set: E 10 E 11 E 12 E Extra items in the right set: E 20 E 21... E E ...Full output truncated (2 lines hidden), use \u0026#39;-vv\u0026#39; to show failure_demo.py:74: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_longer_list(self): \u0026gt; assert [1, 2] == [1, 2, 3] E assert [1, 2] == [1, 2, 3] E Right contains one more item: 3 E Use -v to get the full diff failure_demo.py:77: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_in_list(self): \u0026gt; assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5] failure_demo.py:80: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_not_in_text_multiline(self): text = \u0026#34;some multiline\\ntext\\nwhich\\nincludes foo\\nand a\\ntail\u0026#34; \u0026gt; assert \u0026#34;foo\u0026#34; not in text E AssertionError: assert \u0026#39;foo\u0026#39; not in \u0026#39;some multil...nand a\\ntail\u0026#39; E \u0026#39;foo\u0026#39; is contained here: E some multiline E text E which E includes foo E ? +++ E and a... E E ...Full output truncated (2 lines hidden), use \u0026#39;-vv\u0026#39; to show failure_demo.py:84: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_not_in_text_single(self): text = \u0026#34;single foo line\u0026#34; \u0026gt; assert \u0026#34;foo\u0026#34; not in text E AssertionError: assert \u0026#39;foo\u0026#39; not in \u0026#39;single foo line\u0026#39; E \u0026#39;foo\u0026#39; is contained here: E single foo line E ? +++ failure_demo.py:88: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_not_in_text_single_long(self): text = \u0026#34;head \u0026#34; * 50 + \u0026#34;foo \u0026#34; + \u0026#34;tail \u0026#34; * 20 \u0026gt; assert \u0026#34;foo\u0026#34; not in text E AssertionError: assert \u0026#39;foo\u0026#39; not in \u0026#39;head head h...l tail tail \u0026#39; E \u0026#39;foo\u0026#39; is contained here: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ failure_demo.py:92: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_not_in_text_single_long_term(self): text = \u0026#34;head \u0026#34; * 50 + \u0026#34;f\u0026#34; * 70 + \u0026#34;tail \u0026#34; * 20 \u0026gt; assert \u0026#34;f\u0026#34; * 70 not in text E AssertionError: assert \u0026#39;fffffffffff...ffffffffffff\u0026#39; not in \u0026#39;head head h...ltail tail \u0026#39; E \u0026#39;ffffffffffffffffff...fffffffffffffffffff\u0026#39; is contained here: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ failure_demo.py:96: AssertionError ______________ TestSpecialisedExplanations.test_eq_dataclass _______________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_dataclass(self): from dataclasses import dataclass @dataclass class Foo: a: int b: str left = Foo(1, \u0026#34;b\u0026#34;) right = Foo(1, \u0026#34;c\u0026#34;) \u0026gt; assert left == right E AssertionError: assert TestSpecialis...oo(a=1, b=\u0026#39;b\u0026#39;) == TestSpecialis...oo(a=1, b=\u0026#39;c\u0026#39;) E E Omitting 1 identical items, use -vv to show E Differing attributes: E [\u0026#39;b\u0026#39;] E E Drill down into differing attribute b: E b: \u0026#39;b\u0026#39; != \u0026#39;c\u0026#39;... E E ...Full output truncated (3 lines hidden), use \u0026#39;-vv\u0026#39; to show failure_demo.py:108: AssertionError ________________ TestSpecialisedExplanations.test_eq_attrs _________________ self = \u0026lt;failure_demo.TestSpecialisedExplanations object at 0xdeadbeef\u0026gt; def test_eq_attrs(self): import attr @attr.s class Foo: a = attr.ib() b = attr.ib() left = Foo(1, \u0026#34;b\u0026#34;) right = Foo(1, \u0026#34;c\u0026#34;) \u0026gt; assert left == right E AssertionError: assert Foo(a=1, b=\u0026#39;b\u0026#39;) == Foo(a=1, b=\u0026#39;c\u0026#39;) E E Omitting 1 identical items, use -vv to show E Differing attributes: E [\u0026#39;b\u0026#39;] E E Drill down into differing attribute b: E b: \u0026#39;b\u0026#39; != \u0026#39;c\u0026#39;... E E ...Full output truncated (3 lines hidden), use \u0026#39;-vv\u0026#39; to show failure_demo.py:120: AssertionError ______________________________ test_attribute ______________________________ def test_attribute(): class Foo: b = 1 i = Foo() \u0026gt; assert i.b == 2 E assert 1 == 2 E + where 1 = \u0026lt;failure_demo.test_attribute.\u0026lt;locals\u0026gt;.Foo object at 0xdeadbeef\u0026gt;.b failure_demo.py:128: AssertionError _________________________ test_attribute_instance __________________________ def test_attribute_instance(): class Foo: b = 1 \u0026gt; assert Foo().b == 2 E AssertionError: assert 1 == 2 E + where 1 = \u0026lt;failure_demo.test_attribute_instance.\u0026lt;locals\u0026gt;.Foo object at 0xdeadbeef\u0026gt;.b E + where \u0026lt;failure_demo.test_attribute_instance.\u0026lt;locals\u0026gt;.Foo object at 0xdeadbeef\u0026gt; = \u0026lt;class \u0026#39;failure_demo.test_attribute_instance.\u0026lt;locals\u0026gt;.Foo\u0026#39;\u0026gt;() failure_demo.py:135: AssertionError __________________________ test_attribute_failure __________________________ def test_attribute_failure(): class Foo: def _get_b(self): raise Exception(\u0026#34;Failed to get attrib\u0026#34;) b = property(_get_b) i = Foo() \u0026gt; assert i.b == 2 failure_demo.py:146: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = \u0026lt;failure_demo.test_attribute_failure.\u0026lt;locals\u0026gt;.Foo object at 0xdeadbeef\u0026gt; def _get_b(self): \u0026gt; raise Exception(\u0026#34;Failed to get attrib\u0026#34;) E Exception: Failed to get attrib failure_demo.py:141: Exception _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): class Foo: b = 1 class Bar: b = 2 \u0026gt; assert Foo().b == Bar().b E AssertionError: assert 1 == 2 E + where 1 = \u0026lt;failure_demo.test_attribute_multiple.\u0026lt;locals\u0026gt;.Foo object at 0xdeadbeef\u0026gt;.b E + where \u0026lt;failure_demo.test_attribute_multiple.\u0026lt;locals\u0026gt;.Foo object at 0xdeadbeef\u0026gt; = \u0026lt;class \u0026#39;failure_demo.test_attribute_multiple.\u0026lt;locals\u0026gt;.Foo\u0026#39;\u0026gt;() E + and 2 = \u0026lt;failure_demo.test_attribute_multiple.\u0026lt;locals\u0026gt;.Bar object at 0xdeadbeef\u0026gt;.b E + where \u0026lt;failure_demo.test_attribute_multiple.\u0026lt;locals\u0026gt;.Bar object at 0xdeadbeef\u0026gt; = \u0026lt;class \u0026#39;failure_demo.test_attribute_multiple.\u0026lt;locals\u0026gt;.Bar\u0026#39;\u0026gt;() failure_demo.py:156: AssertionError __________________________ TestRaises.test_raises __________________________ self = \u0026lt;failure_demo.TestRaises object at 0xdeadbeef\u0026gt; def test_raises(self): s = \u0026#34;qwe\u0026#34; \u0026gt; raises(TypeError, int, s) E ValueError: invalid literal for int() with base 10: \u0026#39;qwe\u0026#39; failure_demo.py:166: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ self = \u0026lt;failure_demo.TestRaises object at 0xdeadbeef\u0026gt; def test_raises_doesnt(self): \u0026gt; raises(OSError, int, \u0026#34;3\u0026#34;) E Failed: DID NOT RAISE \u0026lt;class \u0026#39;OSError\u0026#39;\u0026gt; failure_demo.py:169: Failed __________________________ TestRaises.test_raise ___________________________ self = \u0026lt;failure_demo.TestRaises object at 0xdeadbeef\u0026gt; def test_raise(self): \u0026gt; raise ValueError(\u0026#34;demo error\u0026#34;) E ValueError: demo error failure_demo.py:172: ValueError ________________________ TestRaises.test_tupleerror ________________________ self = \u0026lt;failure_demo.TestRaises object at 0xdeadbeef\u0026gt; def test_tupleerror(self): \u0026gt; a, b = [1] # NOQA E ValueError: not enough values to unpack (expected 2, got 1) failure_demo.py:175: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ self = \u0026lt;failure_demo.TestRaises object at 0xdeadbeef\u0026gt; def test_reinterpret_fails_with_print_for_the_fun_of_it(self): items = [1, 2, 3] print(\u0026#34;items is {!r}\u0026#34;.format(items)) \u0026gt; a, b = items.pop() E TypeError: cannot unpack non-iterable int object failure_demo.py:180: TypeError --------------------------- Captured stdout call --------------------------- items is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ self = \u0026lt;failure_demo.TestRaises object at 0xdeadbeef\u0026gt; def test_some_error(self): \u0026gt; if namenotexi: # NOQA E NameError: name \u0026#39;namenotexi\u0026#39; is not defined failure_demo.py:183: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): import importlib.util import sys src = \u0026#34;def foo():\\n assert 1 == 0\\n\u0026#34; name = \u0026#34;abc-123\u0026#34; spec = importlib.util.spec_from_loader(name, loader=None) module = importlib.util.module_from_spec(spec) code = compile(src, name, \u0026#34;exec\u0026#34;) exec(code, module.__dict__) sys.modules[name] = module \u0026gt; module.foo() failure_demo.py:202: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \u0026gt; ??? E AssertionError abc-123:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_complex_error(self): def f(): return 44 def g(): return 43 \u0026gt; somefunc(f(), g()) failure_demo.py:213: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ failure_demo.py:10: in somefunc otherfunc(x, y) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 44, b = 43 def otherfunc(a, b): \u0026gt; assert a == b E assert 44 == 43 failure_demo.py:6: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_z1_unpack_error(self): items = [] \u0026gt; a, b = items E ValueError: not enough values to unpack (expected 2, got 0) failure_demo.py:217: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_z2_type_error(self): items = 3 \u0026gt; a, b = items E TypeError: cannot unpack non-iterable int object failure_demo.py:221: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_startswith(self): s = \u0026#34;123\u0026#34; g = \u0026#34;456\u0026#34; \u0026gt; assert s.startswith(g) E AssertionError: assert False E + where False = \u0026lt;built-in method startswith of str object at 0xdeadbeef\u0026gt;(\u0026#39;456\u0026#39;) E + where \u0026lt;built-in method startswith of str object at 0xdeadbeef\u0026gt; = \u0026#39;123\u0026#39;.startswith failure_demo.py:226: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_startswith_nested(self): def f(): return \u0026#34;123\u0026#34; def g(): return \u0026#34;456\u0026#34; \u0026gt; assert f().startswith(g()) E AssertionError: assert False E + where False = \u0026lt;built-in method startswith of str object at 0xdeadbeef\u0026gt;(\u0026#39;456\u0026#39;) E + where \u0026lt;built-in method startswith of str object at 0xdeadbeef\u0026gt; = \u0026#39;123\u0026#39;.startswith E + where \u0026#39;123\u0026#39; = \u0026lt;function TestMoreErrors.test_startswith_nested.\u0026lt;locals\u0026gt;.f at 0xdeadbeef\u0026gt;() E + and \u0026#39;456\u0026#39; = \u0026lt;function TestMoreErrors.test_startswith_nested.\u0026lt;locals\u0026gt;.g at 0xdeadbeef\u0026gt;() failure_demo.py:235: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_global_func(self): \u0026gt; assert isinstance(globf(42), float) E assert False E + where False = isinstance(43, float) E + where 43 = globf(42) failure_demo.py:238: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_instance(self): self.x = 6 * 7 \u0026gt; assert self.x != 42 E assert 42 != 42 E + where 42 = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt;.x failure_demo.py:242: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_compare(self): \u0026gt; assert globf(10) \u0026lt; 5 E assert 11 \u0026lt; 5 E + where 11 = globf(10) failure_demo.py:245: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = \u0026lt;failure_demo.TestMoreErrors object at 0xdeadbeef\u0026gt; def test_try_finally(self): x = 1 try: \u0026gt; assert x == 0 E assert 1 == 0 failure_demo.py:250: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = \u0026lt;failure_demo.TestCustomAssertMsg object at 0xdeadbeef\u0026gt; def test_single_line(self): class A: a = 1 b = 2 \u0026gt; assert A.a == b, \u0026#34;A.a appears not to be b\u0026#34; E AssertionError: A.a appears not to be b E assert 1 == 2 E + where 1 = \u0026lt;class \u0026#39;failure_demo.TestCustomAssertMsg.test_single_line.\u0026lt;locals\u0026gt;.A\u0026#39;\u0026gt;.a failure_demo.py:261: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = \u0026lt;failure_demo.TestCustomAssertMsg object at 0xdeadbeef\u0026gt; def test_multiline(self): class A: a = 1 b = 2 \u0026gt; assert ( A.a == b ), \u0026#34;A.a appears not to be b\\nor does not appear to be b\\none of those\u0026#34; E AssertionError: A.a appears not to be b E or does not appear to be b E one of those E assert 1 == 2 E + where 1 = \u0026lt;class \u0026#39;failure_demo.TestCustomAssertMsg.test_multiline.\u0026lt;locals\u0026gt;.A\u0026#39;\u0026gt;.a failure_demo.py:268: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = \u0026lt;failure_demo.TestCustomAssertMsg object at 0xdeadbeef\u0026gt; def test_custom_repr(self): class JSON: a = 1 def __repr__(self): return \u0026#34;This is JSON\\n{\\n \u0026#39;foo\u0026#39;: \u0026#39;bar\u0026#39;\\n}\u0026#34; a = JSON() b = 2 \u0026gt; assert a.a == b, a E AssertionError: This is JSON E { E \u0026#39;foo\u0026#39;: \u0026#39;bar\u0026#39; E } E assert 1 == 2 E + where 1 = This is JSON\\n{\\n \u0026#39;foo\u0026#39;: \u0026#39;bar\u0026#39;\\n}.a failure_demo.py:281: AssertionError ============================ 44 failed in 0.12s ============================ ","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/pytest/000006-pytestpytest%E6%96%AD%E8%A8%80%E6%8A%80%E5%B7%A7/","summary":"pytest的断言把Python语言简洁的优点发挥的淋漓尽致，因为它用的就是Python的标准断言\u003cstrong\u003eassert\u003c/strong\u003e。","title":"pytest断言技巧"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nSpringApplication By default, INFO logging messages are shown, including some relevant startup details, such as the user that launched the application.\nLazy Initialization\nWhen lazy initialization is enabled, beans are created as they are needed rather than during application startup.\nspring.main.lazy-initialization=true @Lazy(false) annotation：disable lazy initialization for certain beans\nCustomizing the Banner\nadding a banner.txt file to your classpath or by setting the spring.banner.location property to the location of such a file. If the file has an encoding other than UTF-8, you can set spring.banner.charset.\nSpringApplication.setBanner(…​) method\nspring.main.banner-mode property System.out(console)、logger (log)、not produced at all (off)\nThe printed banner is registered as a singleton bean under the following name: springBootBanner.\nCustomizing SpringApplication\nimport org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication application = new SpringApplication(MyApplication.class); application.setBannerMode(Banner.Mode.OFF); application.run(args); } } The internal state of Spring Boot applications is mostly represented by the Spring ApplicationContext.\nInternally, Spring Boot uses events to handle a variety of tasks. Application events are sent in the following order, as your application runs:\nAn ApplicationStartingEvent is sent at the start of a run but before any processing, except for the registration of listeners and initializers.\nAn ApplicationEnvironmentPreparedEvent is sent when the Environment to be used in the context is known but before the context is created.\nAn ApplicationContextInitializedEvent is sent when the ApplicationContext is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded.\nAn ApplicationPreparedEvent is sent just before the refresh is started but after bean definitions have been loaded.\nAn ApplicationStartedEvent is sent after the context has been refreshed but before any application and command-line runners have been called.\nAn AvailabilityChangeEvent is sent right after with LivenessState.CORRECT to indicate that the application is considered as live.\nAn ApplicationReadyEvent is sent after any application and command-line runners have been called.\nAn AvailabilityChangeEvent is sent right after with ReadinessState.ACCEPTING_TRAFFIC to indicate that the application is ready to service requests.\nAn ApplicationFailedEvent is sent if there is an exception on startup.\nAccessing Application Arguments\nimport java.util.List; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @Component public class MyBean { public MyBean(ApplicationArguments args) { boolean debug = args.containsOption(\u0026#34;debug\u0026#34;); List\u0026lt;String\u0026gt; files = args.getNonOptionArgs(); if (debug) { System.out.println(files); } // if run with \u0026#34;--debug logfile.txt\u0026#34; prints [\u0026#34;logfile.txt\u0026#34;] } } If you need to run some specific code once the SpringApplication has started, you can implement the ApplicationRunner or CommandLineRunner interfaces.\nimport org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class MyCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) { // Do something... } } Application Exit\nEach SpringApplication registers a shutdown hook with the JVM to ensure that the ApplicationContext closes gracefully on exit.\nExternalized Configuration Sources are considered in the following order:\nDefault properties (specified by setting SpringApplication.setDefaultProperties).\n@PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.\nConfig data (such as application.properties files).\nA RandomValuePropertySource that has properties only in random.*.\nOS environment variables.\nJava System properties (System.getProperties()).\nJNDI attributes from java:comp/env.\nServletContext init parameters.\nServletConfig init parameters.\nProperties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).\nCommand line arguments.\nproperties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.\n@DynamicPropertySource annotations in your tests.\n@TestPropertySource annotations on your tests.\nDevtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.\nConfig data files are considered in the following order:\nApplication properties packaged inside your jar (application.properties and YAML variants).\nProfile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).\nApplication properties outside of your packaged jar (application.properties and YAML variants).\nProfile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).\nIf you have configuration files with both .propertiesand YAML format in the same location, .properties takes precedence.\n读取配置：\nProperty values can be injected directly into your beans by using the @Value annotation, accessed through Spring’s Environmentabstraction, or be bound to structured objects through @ConfigurationProperties.\nimport org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class MyBean { @Value(\u0026#34;${name}\u0026#34;) private String name; // ... } command line properties (that is, arguments starting with --, such as --server.port=9000) always take precedence over file-based property sources.\nSpring Boot will automatically find and load application.properties and application.yaml files from the following locations when your application starts:\nFrom the classpath\nThe classpath root\nThe classpath /config package\nFrom the current directory\nThe current directory\nThe config/ subdirectory in the current directory\nImmediate child directories of the config/ subdirectory\nWildcard locations only work with external directories. You cannot use a wildcard in a classpath: location.\nProfile Specific Files\napplication-{profile}. For example, if your application activates a profile named prod and uses YAML files, then both application.yaml and application-prod.yaml will be considered.\nconfigtree：\netc/ config/ myapp/ username password spring.config.import=optional:configtree:/etc/config/ You can then access or inject myapp.username and myapp.password properties from the Environment in the usual way.\nProperty placeholders can also specify a default value using a : to separate the default value from the property name, for example ${name:default}.\napp.name=MyApp app.description=${app.name} is a Spring Boot application written by ${username:Unknown} Working With YAML\nIf you use “Starters”, SnakeYAML is automatically provided by spring-boot-starter.\nYAML files cannot be loaded by using the @PropertySource or @TestPropertySource annotations. So, in the case that you need to load values that way, you need to use a properties file.\nThe YamlPropertiesFactoryBeanloads YAML as Properties and the YamlMapFactoryBean loads YAML as a Map. YamlPropertySourceLoader class if you want to load YAML as a Spring PropertySource.\nConfiguring Random Values\nRandomValuePropertySource\nmy.secret=${random.value} my.number=${random.int} my.bignumber=${random.long} my.uuid=${random.uuid} my.number-less-than-ten=${random.int(10)} my.number-in-range=${random.int[1024,65536]} Type-safe Configuration Properties\n@ConfigurationProperties\nRelaxed Binding\nAs an example, consider the following @ConfigurationProperties class:\nimport org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = \u0026#34;my.main-project.person\u0026#34;) public class MyPersonProperties { private String firstName; public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } } With the preceding code, the following properties names can all be used:\nmy.main-project.person.first-name Kebab case, which is recommended for use in .properties and YAML files.\nmy.main-project.person.firstName Standard camel case syntax.\nmy.main-project.person.first_name Underscore notation, which is an alternative format for use in .properties and YAML files.\nMY_MAINPROJECT_PERSON_FIRSTNAME Upper case format, which is recommended when using system environment variables.\nWe recommend that, when possible, properties are stored in lower-case kebab format, such as my.person.first-name=Rod.\nProfiles make it be available only in certain environments\nimport org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration(proxyBeanMethods = false) @Profile(\u0026#34;production\u0026#34;) public class ProductionConfiguration { // ... } Logging By default, if you use the “Starters”, Logback is used for logging.\nLog Format\n2023-06-22T12:08:05.861Z INFO 22768 --- [ main] o.s.b.d.f.s.MyApplication : Starting MyApplication using Java 17.0.7 with PID 22768 (/opt/apps/myapp.jar started by myuser in /opt/apps/) 2023-06-22T12:08:05.872Z INFO 22768 --- [ main] o.s.b.d.f.s.MyApplication : No active profile set, falling back to 1 default profile: \u0026#34;default\u0026#34; 2023-06-22T12:08:09.854Z INFO 22768 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2023-06-22T12:08:09.892Z INFO 22768 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-06-22T12:08:09.892Z INFO 22768 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.10] 2023-06-22T12:08:10.160Z INFO 22768 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-06-22T12:08:10.162Z INFO 22768 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4038 ms 2023-06-22T12:08:11.512Z INFO 22768 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path \u0026#39;\u0026#39; 2023-06-22T12:08:11.534Z INFO 22768 --- [ main] o.s.b.d.f.s.MyApplication : Started MyApplication in 7.251 seconds (process running for 8.584) Date and Time: Millisecond precision and easily sortable.\nLog Level: ERROR, WARN, INFO, DEBUG, or TRACE.\nProcess ID.\nA --- separator to distinguish the start of actual log messages.\nThread name: Enclosed in square brackets (may be truncated for console output).\nLogger name: This is usually the source class name (often abbreviated).\nThe log message.\nInternationalization spring.messages.basename=messages,config.i18n.messages spring.messages.fallback-to-system-locale=false JSON Jackson is the preferred and default library. Auto-configuration for Jackson is provided and Jackson is part of spring-boot-starter-json.\n@JsonComponent Custom Serializers and Deserializers\nimport java.io.IOException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.springframework.boot.jackson.JsonComponent; @JsonComponent public class MyJsonComponent { public static class Serializer extends JsonSerializer\u0026lt;MyObject\u0026gt; { @Override public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException { jgen.writeStartObject(); jgen.writeStringField(\u0026#34;name\u0026#34;, value.getName()); jgen.writeNumberField(\u0026#34;age\u0026#34;, value.getAge()); jgen.writeEndObject(); } } public static class Deserializer extends JsonDeserializer\u0026lt;MyObject\u0026gt; { @Override public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { ObjectCodec codec = jsonParser.getCodec(); JsonNode tree = codec.readTree(jsonParser); String name = tree.get(\u0026#34;name\u0026#34;).textValue(); int age = tree.get(\u0026#34;age\u0026#34;).intValue(); return new MyObject(name, age); } } } Task Execution and Scheduling In the absence of an Executor bean in the context, Spring Boot auto-configures a ThreadPoolTaskExecutor with sensible defaults that can be automatically associated to asynchronous task execution (@EnableAsync) and Spring MVC asynchronous request processing.\nA ThreadPoolTaskScheduler can also be auto-configured if need to be associated to scheduled task execution (using @EnableScheduling for instance).\nTesting Most developers use the spring-boot-starter-test “Starter”, which imports both Spring Boot test modules as well as JUnit Jupiter, AssertJ, Hamcrest, and a number of other useful libraries.\nJUnit 5: The de-facto standard for unit testing Java applications.\nSpring Test \u0026amp; Spring Boot Test: Utilities and integration test support for Spring Boot applications.\nAssertJ: A fluent assertion library.\nHamcrest: A library of matcher objects (also known as constraints or predicates).\nMockito: A Java mocking framework.\nJSONassert: An assertion library for JSON.\nJsonPath: XPath for JSON.\nOne of the major advantages of dependency injection is that it should make your code easier to unit test. You can instantiate objects by using the new operator without even involving Spring. You can also use mock objects instead of real dependencies.\nBy default, @SpringBootTest will not start a server. You can use the webEnvironment attribute of @SpringBootTest to further refine how your tests run:\nMOCK(Default) : Loads a web ApplicationContext and provides a mock web environment. Embedded servers are not started when using this annotation. If a web environment is not available on your classpath, this mode transparently falls back to creating a regular non-web ApplicationContext. It can be used in conjunction with @AutoConfigureMockMvc or @AutoConfigureWebTestClient for mock-based testing of your web application.\nRANDOM_PORT: Loads a WebServerApplicationContext and provides a real web environment. Embedded servers are started and listen on a random port.\nDEFINED_PORT: Loads a WebServerApplicationContext and provides a real web environment. Embedded servers are started and listen on a defined port (from your application.properties) or on the default port of 8080.\nNONE: Loads an ApplicationContext by using SpringApplication but does not provide any web environment (mock or otherwise).\nDocker Compose Support A compose.yml file is typically created next to your application which defines and configures service containers.\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-docker-compose\u0026lt;/artifactId\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Creating Your Own Auto-configuration Classes that implement auto-configuration are annotated with @AutoConfiguration.\nSSL spring.ssl.bundle.jks.mybundle.key.alias=application spring.ssl.bundle.jks.mybundle.keystore.location=classpath:application.p12 spring.ssl.bundle.jks.mybundle.keystore.password=secret spring.ssl.bundle.jks.mybundle.keystore.type=PKCS12 参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000006-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B03%E6%A0%B8%E5%BF%83/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"springapplication\"\u003eSpringApplication\u003c/h1\u003e\n\u003cp\u003eBy default, \u003ccode\u003eINFO\u003c/code\u003e logging messages are shown, including some relevant startup details, such as the user that launched the application.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLazy Initialization\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eWhen lazy initialization is enabled, beans are created as they are needed rather than during application startup.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003espring.main.lazy-initialization=true\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003ccode\u003e@Lazy(false)\u003c/code\u003e annotation：disable lazy initialization for certain beans\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCustomizing the Banner\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eadding a \u003ccode\u003ebanner.txt\u003c/code\u003e file to your classpath or by setting the \u003ccode\u003espring.banner.location\u003c/code\u003e property to the location of such a file. If the file has an encoding other than UTF-8, you can set \u003ccode\u003espring.banner.charset\u003c/code\u003e.\u003c/p\u003e","title":"SpringBoot官方笔记3核心"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文开发内容 pytest登场！本文将在Django中引入pytest，原理是先执行tep startproject命令创建pytest项目文件，然后从数据库中拉取代码写入文件，最后调用pytest命令运行用例。为了提高运行效率，用例运行是并行的，采用了多线程和多进程，两个都有，这在最后有个单独小结进行比较完整的说明。因为用例运行是异步的，所以前端并不知道什么时候执行完才能拿到运行结果，可以发多个HTTP请求轮询，但这种方式并不优雅，本文将采用WebSocket来实现用例结果查询。具体内容为：\ncases/\u0026lt;int:pk\u0026gt;/run运行用例接口 ws/teprunner/cases/\u0026lt;int:case_id\u0026gt;/result/用例结果查询接口 projects/\u0026lt;int:pk\u0026gt;/export下载项目环境接口 前端添加WebSocket请求 知识点涉及有点多：tep、pytest、同步、异步、多线程、多进程、回调函数、WebSocket、长连接、全双工、ASGI、WSGI、打ZIP包、文件字节流传输。\n编写后端代码 编辑requirements.txt，添加tep和channels：\ntep==0.6.9 channels==3.0.3 tep是用来创建pytest项目的，channels是用来实现WebSocket的。\n编辑teprunner/urls.py文件，添加HTTP路由：\n首先实现run接口，新建teprunner/views/run.py文件：\n这是运行用例的主体流程：\n第一步从请求中获取用例id、运行环境、运行人，这里演示了获取user数据的两种方式：接口传参和从token中解析。然后根据project_id，run_env，user_id定义了pytest项目的路径。 第二步使用tep startproject创建项目文件，清空fixtures和tests目录，目的有两个：一是清掉tep默认fixtures和示例cases，防止对平台产生干扰；二是保证每次运行目录都是干净的，就不用单独去处理前端手动删掉fixture/case后，文件残留的问题。然后从数据库中拉取环境变量、fixtures等数据更新文件。 起多个线程，分别执行用例，执行前先拉取用例代码写入文件，这里是单条用例运行，之所以要用for循环，是因为用例迟早是要批量执行，在设计时就考虑到，避免后面走弯路。然后删掉数据库运行结果，通过subprocess起子进程调用pytest命令，最后在线程的回调函数中根据pytest_result保存用例结果到数据库中。 注意！run_case接口不会直接返回结果，前端是用WebSocket来查询结果的。\n图中很多函数和类是我封装的，一个一个拆解来看：\n这里定义了Django中存放pytest项目的目录文件，project_temp_name是按照project_id、env_name、user_id来划分的，目的是让运行目录尽量隔离开来，不要相互影响，借鉴了Docker容器的思想，可以把这个目录视为用例运行容器。继续：\ntep startproject命令创建pytest项目，pytest文件有特定组织方式，比如conftest.py文件等，tep提供了脚手架一条命令创建项目结构。继续：\nfixture_env_vars.py里面存放了tep的环境变量，Django这里每次都从数据库的env_var表中获取数据，动态更新到文件里面。setdefault是个骚操作，这行代码等价于：\nif env_name in mapping.keys(): mapping[env_name][name] = value else: mapping[env_name] = {name: value} 继续：\n分别从数据库中获取代码写入fixture文件和case文件，把前端传参的运行环境写入conf文件。继续：\n清空fixtures目录，清空tests目录。继续：\npull_tep_files是写环境变量，写fixture文件，写conf文件三步的集合，复用代码。pull_case_files通过yield定义为了生成器，它和list的区别是不会一次把所有数据产生到内存中，而是每次用的时候产生一次，节约内存开销。delete_case_result用于运行用例前删除case_result表里面已经存在的这条用例的数据。case_result按照用例id和运行人存的多条，每个运行人都有一条属于自己的运行数据，避免数据相互干扰，返给前端的是运行时间最新的那一条！\n继续：\nsubprocess.getoutput()可以执行shell命令并返回执行结果，这里就拿到了pytest控制台日志，这个函数是在线程池中异步执行的，主线程不能一直等待它执行，所以需要有个回调函数，等它自己执行完了去调用这个回调函数。save_case_result就是个回调函数，它的入参pytest_result等于pytest_subprocess函数返回的元组，拆包后就能拿到outout、cmd、case_id、run_env、run_user_nickname，从中解析出result和elapsed后，就可以存库了，无则新增，有则更新。\nrun接口做好了，再接口做下载环境接口，编辑teprunner/views/project.py：\n打包的代码是从网上找的，把source_dir打包成zip_filename文件。继续：\nfile_iterator函数也是网上找的，把二进制文件读取为字节流，传输给前端，需要使用StreamingHttpResponse对象并添加Content-Type和Content-Disposition。红框的代码跟run接口类似，区别在于目录换成了export_temp_dir()，且不包含测试用例，生成zip文件后会把导出临时目录删掉，防止冲突。\n两个HTTP接口做完了，开始实现WebSocket。WSGI一种网关接口，是Python为了解决Web服务器端与客户端之间的通信问题而产生的，不支持WebSocket；ASGI是WSGI的扩展，意思是异步网关接口，支持WebSocket。编辑teprunnerbackend/urls.py文件：\n添加了WebSocket路由。编辑teprunnerbackend/asgi.py文件：\n添加websocket的URLRouter，http保持默认。编辑teprunnerbackend/settings.py文件：\nINSTALLED_APPS中添加channels，继续：\n添加ASGI应用配置和CHANNEL配置。CHANNEL_LAYERS是一种通信系统，允许多个Consumer实例之间互相通信，以及与外部Django程序实现互通。学习版这里使用的InMemory。\n生产中不建议使用InMemory，可能会有性能问题，而是应该使用Redis：\nCHANNEL_LAYERS = { \u0026#34;default\u0026#34;: { \u0026#34;BACKEND\u0026#34;: \u0026#34;channels_redis.core.RedisChannelLayer\u0026#34;, \u0026#34;CONFIG\u0026#34;: { \u0026#34;hosts\u0026#34;: [(\u0026#34;127.0.0.1\u0026#34;, 6379)], }, }, } 最后，编辑teprunner/views/case.py文件：\nCaseResultView是继承了JsonWebsocketConsumer，可以接受和发送JSON的WebSocket消费者。这里只是简单使用了channels来实现用例结果查询，connect()在建立连接时，从url中拿到case_id，作为房间名，在channel_layer中创建了房间。disconnect()在断开连接时，把房间从channel_layer中移除。继续：\nreceive_json是在后端收到前端消息时调用的。WebSocket是长连接，在建立连接后，不会断开，可以继续传递消息；WebSocket是全双工，不只是客户端向服务器发消息，服务器也能向客户端发消息。这里服务端会给客户端发4次消息：\n第1次，返回用例描述和用例创建人。 第2次，准确说会有多次，当查询数据库没有结果时，会返回计时，前端效果是计时从1s递增。 第3次，如果查询数据库有结果，返回用例结果。 第4次，60s后还没有结果，返回超时信息。 其中CaseResult是用order_by('-run_time')取的最新一条。最后的self.close()不是必须的，这里加上是因为频繁建立和关闭连接时，如果只是前端发起close()，后端可能会关闭不及时导致channels报错，后端也加上close()能一定程度上避免报错。\n编写前端代码 新建.env文件：\n添加HTTP和WebSocket后端地址，里面以键值对的形式写出环境变量，键名需要以VUE_APP_ 开头。vue-cli打包时会自动寻找这些环境变量，注入到编辑上下文环境中。编辑vue.config.js文件：\n把target替换成.env里面的环境变量。\n编辑views/teprunner/case/CaseResult.vue文件：\nsocketUrl用到了.env中的环境变量。通过new WebSocket创建socket对象，使用send()发送消息，传了token。onmessage接收后端发过来的消息。\n每次打开弹窗建立WebSocket连接，每次关闭弹窗断开WebSocket连接：\n前后端是在以用例id作为房间名的房间中，相互传递消息的。多个浏览器的数据不会互串，因为Django Server默认是多线程！\n多线程和多进程 每次浏览器发起请求到Django Server，Django都会新起一个线程来处理，这是异步的，意味着多个浏览器连续发多个请求，每个请求的上下文都是独立的，也不会阻塞等待。\n如果Server不是用的Django Server而是用的Nginx，需要结合WSGI才能实现多线程。\n在WebSocket通信时，每个房间都是单个线程自己创建的，数据不会互串，具体原理还没有研究，这个结论我是测试过的：修改后端代码返回随机值，多个浏览器打开同一个Case的结果，后打开的Case结果并不会影响已经打开的Case结果。\n同理，多个浏览器同时运行用例，默认它们就是并行不是串行的，不会存在等待执行的情况，从前面代码可以知道，pytest命令是用subprocess子进程方式调用的，为了看到效果，我找了一个比较慢的Case，用多个浏览器运行了一下：\n赤裸裸的多进程！pytest多进程靠谱么？靠谱，因为pytest-xdist就只支持多进程，以下是截取的官方Github的Issue：\nthreads是线程，processes是进程，pytest-xdist没有使用线程。\n如果想要多台机器分布式运行用例，就要用pytest-xdist。\n批量运行用例的情况略有不同，当批量运行用例时，前端只会有一个浏览器发起一次请求，让后端拿多个Case来运行，Django只会分配一个线程来处理这个请求！如果我们在这个View里面只是for循环去运行用例，那么这些用例一定是串行的：虽然是用的subprocess，但是启用subprocess的只有这一个线程，必须前一个执行完，才启动下一个。这就是为什么要再定义线程池的原因：\n本文还没有开发批量运行用例的模块，但后端已经实现了这个扩展，只需要再生成一个CaseList就能跑批量了。\n小结 本文把pytest引入到了测试平台中，已经可以跑Case了。文章涉及到的知识点有点繁杂，对我来说这一版也做了不少优化，反复实践和测试，参考资料加了很多。完整源码请到GitHub上获取，按照README命令就能直接把前后项目跑起来看效果。做到这里，teprunner测试平台已经不是个花架子了，而是有着pytest内核引擎驱动的真测试平台。它一定不是你做测试平台的终点，但也许能成为做测试平台的起点，也许能成为撬动地球的支点。\n参考资料：\n前端源码 https://github.com/dongfanger/teprunner-frontend\n后端源码 https://github.com/dongfanger/teprunner-backend\nhttps://github.com/pytest-dev/pytest-xdist/issues/409\nhttps://blog.csdn.net/weixin_42329277/article/details/80741589\nhttps://www.cnblogs.com/xiao987334176/p/14361893.html\nhttps://juejin.cn/post/6844904195758243848\nhttps://segmentfault.com/q/1010000022975655\nhttps://channels.readthedocs.io/en/stable/topics/channel_layers.html\nhttps://segmentfault.com/a/1190000018096988\nhttps://www.jianshu.com/p/65807220b44a\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000006-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0django%E5%BC%95%E5%85%A5pytest%E5%AE%8C%E6%95%B4%E6%BA%90%E7%A0%81/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"本文开发内容\"\u003e本文开发内容\u003c/h1\u003e\n\u003cp\u003epytest登场！本文将在Django中引入pytest，原理是先执行tep startproject命令创建pytest项目文件，然后从数据库中拉取代码写入文件，最后调用pytest命令运行用例。为了提高运行效率，用例运行是并行的，采用了多线程和多进程，两个都有，这在最后有个单独小结进行比较完整的说明。因为用例运行是异步的，所以前端并不知道什么时候执行完才能拿到运行结果，可以发多个HTTP请求轮询，但这种方式并不优雅，本文将采用WebSocket来实现用例结果查询。具体内容为：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003ecases/\u0026lt;int:pk\u0026gt;/run\u003c/code\u003e运行用例接口\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ews/teprunner/cases/\u0026lt;int:case_id\u0026gt;/result/\u003c/code\u003e用例结果查询接口\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eprojects/\u0026lt;int:pk\u0026gt;/export\u003c/code\u003e下载项目环境接口\u003c/li\u003e\n\u003cli\u003e前端添加WebSocket请求\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e知识点涉及有点多：tep、pytest、同步、异步、多线程、多进程、回调函数、WebSocket、长连接、全双工、ASGI、WSGI、打ZIP包、文件字节流传输。\u003c/p\u003e\n\u003ch1 id=\"编写后端代码\"\u003e编写后端代码\u003c/h1\u003e\n\u003cp\u003e编辑requirements.txt，添加tep和channels：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003etep==0.6.9\nchannels==3.0.3\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003etep是用来创建pytest项目的，channels是用来实现WebSocket的。\u003c/p\u003e\n\u003cp\u003e编辑teprunner/urls.py文件，添加HTTP路由：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/71e7cccd40254bb886c396048dadf6af.png\"\u003e\u003c/p\u003e\n\u003cp\u003e首先实现run接口，新建teprunner/views/run.py文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1482f5173eb94926b01b3ad07a6385d0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这是运行用例的主体流程：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e第一步从请求中获取用例id、运行环境、运行人，这里演示了获取user数据的两种方式：接口传参和从token中解析。然后根据project_id，run_env，user_id定义了pytest项目的路径。\u003c/li\u003e\n\u003cli\u003e第二步使用tep startproject创建项目文件，清空fixtures和tests目录，目的有两个：一是清掉tep默认fixtures和示例cases，防止对平台产生干扰；二是保证每次运行目录都是干净的，就不用单独去处理前端手动删掉fixture/case后，文件残留的问题。然后从数据库中拉取环境变量、fixtures等数据更新文件。\u003c/li\u003e\n\u003cli\u003e起多个线程，分别执行用例，执行前先拉取用例代码写入文件，这里是单条用例运行，之所以要用for循环，是因为用例迟早是要批量执行，在设计时就考虑到，避免后面走弯路。然后删掉数据库运行结果，通过subprocess起子进程调用pytest命令，最后在线程的回调函数中根据pytest_result保存用例结果到数据库中。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注意！run_case接口不会直接返回结果，前端是用WebSocket来查询结果的。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e图中很多函数和类是我封装的，一个一个拆解来看：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/87c46d8175da4b6bb0a65934443cfc15.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这里定义了Django中存放pytest项目的目录文件，project_temp_name是按照project_id、env_name、user_id来划分的，目的是让运行目录尽量隔离开来，不要相互影响，借鉴了Docker容器的思想，可以把这个目录视为用例运行容器。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/722bf303d1954b17acf44d5ffec77b9f.png\"\u003e\u003c/p\u003e\n\u003cp\u003etep startproject命令创建pytest项目，pytest文件有特定组织方式，比如conftest.py文件等，tep提供了脚手架一条命令创建项目结构。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/2d67ae2147504315972dea092501d3b0.png\"\u003e\u003c/p\u003e\n\u003cp\u003efixture_env_vars.py里面存放了tep的环境变量，Django这里每次都从数据库的env_var表中获取数据，动态更新到文件里面。setdefault是个骚操作，这行代码等价于：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e env_name \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e mapping\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ekeys\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    mapping\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eenv_name\u003cspan style=\"color:#1f2328\"\u003e][\u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e value\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    mapping\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003eenv_name\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e value\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f8475fdd82504e2d91977a43571c2eca.png\"\u003e\u003c/p\u003e\n\u003cp\u003e分别从数据库中获取代码写入fixture文件和case文件，把前端传参的运行环境写入conf文件。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/dd57e6fa2537457783ff455741aadd16.png\"\u003e\u003c/p\u003e\n\u003cp\u003e清空fixtures目录，清空tests目录。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/efcf49e2264f4fc7adcd548d1a684337.png\"\u003e\u003c/p\u003e\n\u003cp\u003epull_tep_files是写环境变量，写fixture文件，写conf文件三步的集合，复用代码。pull_case_files通过yield定义为了生成器，它和list的区别是不会一次把所有数据产生到内存中，而是每次用的时候产生一次，节约内存开销。delete_case_result用于运行用例前删除case_result表里面已经存在的这条用例的数据。case_result按照用例id和运行人存的多条，每个运行人都有一条属于自己的运行数据，避免数据相互干扰，返给前端的是运行时间最新的那一条！\u003c/p\u003e\n\u003cp\u003e继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/980539c2eed4477593219cebec3796da.png\"\u003e\u003c/p\u003e\n\u003cp\u003esubprocess.getoutput()可以执行shell命令并返回执行结果，这里就拿到了pytest控制台日志，这个函数是在线程池中异步执行的，主线程不能一直等待它执行，所以需要有个回调函数，等它自己执行完了去调用这个回调函数。save_case_result就是个回调函数，它的入参pytest_result等于pytest_subprocess函数返回的元组，拆包后就能拿到outout、cmd、case_id、run_env、run_user_nickname，从中解析出result和elapsed后，就可以存库了，无则新增，有则更新。\u003c/p\u003e\n\u003cp\u003erun接口做好了，再接口做下载环境接口，编辑teprunner/views/project.py：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/72bf0e5f7e674d8482921741ebb8f928.png\"\u003e\u003c/p\u003e\n\u003cp\u003e打包的代码是从网上找的，把source_dir打包成zip_filename文件。继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a50944e18d2345b88e7ccb728e47af84.png\"\u003e\u003c/p\u003e\n\u003cp\u003efile_iterator函数也是网上找的，把二进制文件读取为字节流，传输给前端，需要使用StreamingHttpResponse对象并添加\u003ccode\u003eContent-Type\u003c/code\u003e和\u003ccode\u003eContent-Disposition\u003c/code\u003e。红框的代码跟run接口类似，区别在于目录换成了export_temp_dir()，且不包含测试用例，生成zip文件后会把导出临时目录删掉，防止冲突。\u003c/p\u003e\n\u003cp\u003e两个HTTP接口做完了，开始实现WebSocket。WSGI一种网关接口，是Python为了解决Web服务器端与客户端之间的通信问题而产生的，不支持WebSocket；ASGI是WSGI的扩展，意思是异步网关接口，支持WebSocket。编辑teprunnerbackend/urls.py文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ec702894023e4a068e14f158dbffe5c0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e添加了WebSocket路由。编辑teprunnerbackend/asgi.py文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/48a80d492a1c42aeb7f9caed12be48dc.png\"\u003e\u003c/p\u003e\n\u003cp\u003e添加websocket的URLRouter，http保持默认。编辑teprunnerbackend/settings.py文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/84a0a9dcae3346829fa45e4ae0e60869.png\"\u003e\u003c/p\u003e\n\u003cp\u003eINSTALLED_APPS中添加channels，继续：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b9e11ff4a40d467b901fb8ddedc6e89f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e添加ASGI应用配置和CHANNEL配置。CHANNEL_LAYERS是一种通信系统，允许多个Consumer实例之间互相通信，以及与外部Django程序实现互通。学习版这里使用的InMemory。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e生产中不建议使用InMemory，可能会有性能问题，而是应该使用Redis：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCHANNEL_LAYERS \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;default\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;BACKEND\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;channels_redis.core.RedisChannelLayer\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;CONFIG\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;hosts\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;127.0.0.1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e6379\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)],\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cp\u003e最后，编辑teprunner/views/case.py文件：\u003c/p\u003e","title":"teprunner测试平台Django引入pytest完整源码"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\ntep是一款测试工具，在pytest测试框架基础上集成了第三方包，提供项目脚手架，帮助以写Python代码方式，快速实现自动化项目落地。\n在tep项目中，自动化测试用例都是放到tests目录下的，每个.py文件相互独立，没有依赖，1个文件即1条用例，彼此分离。\n虽然用例也能相互引用，但是除非万不得已，一般不建议这么做，牵一发动全身，后期维护困难。\n用例的代码编写，思路是从上往下的，和pytest/unittest/script常规写法无异，不会有学习成本，一般也不会有问题。有成本有问题的可能是环境变量和fixtures，因为tep做了封装，提供了依赖注入的共享方式，fixture又是pytest较难理解的知识点，所以有必要通过本文来讲讲tep环境变量、fixtures、用例三者之间的关系，帮助理解，以便更灵活顺手的借助tep实现pytest自动化项目。\n假如不用环境变量和fixtures 假如不用环境变量和fixtures，是完全可以的！比如，在tests下新建脚本login_test.py：\nfrom tep.client import request def test(): response = request(\u0026#34;post\u0026#34;, url=\u0026#34;https://qa.com/api/users/login\u0026#34;, headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 请求接口https://qa.com/api/users/login，断言响应状态码小于400。问题来了：url固定，假如需要切换两个环境qa和release，该怎么办？\n参数化 无论是做自动化测试还是性能测试，都会接触到参数化这个词。它是指把代码中的固定数据（硬编码）定义成变量，让每次运行时数据不一样，固定数据变为动态数据。动态数据的来源是变量、数据库、外部文件等。动态数据的类型一般是常量的字符串，也可以是函数，比如JMeter的函数助手，也可以是依赖注入，比如pytest的fixture。\n依赖注入的fixture “依赖注入是控制反转（IoC， Inversion of Control）的一种技术形式”，这句话出自维基百科，我也不知道什么意思，画个图简单表达下：\n意思是，给client一个injector，client不需要做什么，就能用到service。\npytest的fixture实现了依赖注入，允许我们在不修改测试代码的情况下，引入fixture来额外添加一些东东。\n对于url来说，域名是需要做参数化的，不同环境域名不同，所以tep把它做成了fixture，通过函数参数引入：\nfrom tep.client import request from tep.fixture import * def test(url): # 引入fixture response = request(\u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users/login\u0026#34;), headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 tep.fixture.url定义如下：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def url(env_vars): def domain_and_uri(uri): if not uri.startswith(\u0026#34;/\u0026#34;): uri = \u0026#34;/\u0026#34; + uri return env_vars.domain + uri return domain_and_uri 如果一眼就看懂了，恭喜你，如果一眼就看懵了，没关系。我会花功夫把它讲明白，它很关键！\n把fixture当变量看 虽然从定义上看，fixture是用def关键字定义的函数，但是理解上把它看做变量就可以了。比如：\nimport pytest @pytest.fixture def name(): return \u0026#34;dongfanger\u0026#34; 一般函数的用法是函数名加小括号，通过name()才能得到\u0026quot;dongfanger\u0026quot;。fixture不一样，以上定义可以理解为：\nname = \u0026#34;dongfanger\u0026#34; 把\u0026quot;dongfanger\u0026quot;赋值给name，fixture名 = return值。通过变量name就得到\u0026quot;dongfanger\u0026quot;了。\n既然是变量，那么就能随便赋值，str、function、class、object都行。比如在fixture内部定义个函数：\nimport pytest @pytest.fixture def who(): def get_name(): return \u0026#34;dongfanger\u0026#34; return get_name 理解为把函数名get_name赋值给fixture名变量：\nwho = get_name get_name是个函数名，需要加小括号get_name()才能得到\u0026quot;dongfanger\u0026quot;。who也必须通过who()才能得到\u0026quot;dongfanger\u0026quot;。再看tep.fixture.url是不是清楚些了：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def url(env_vars): def domain_and_uri(uri): if not uri.startswith(\u0026#34;/\u0026#34;): uri = \u0026#34;/\u0026#34; + uri return env_vars.domain + uri return domain_and_uri 理解为把函数名domain_and_uri赋值给fixture名变量：\nurl = domain_and_uri 使用时通过url(\u0026quot;/api\u0026quot;)得到域名和uri拼接后的结果。\n第2行的def url(env_vars):也有一个参数env_vars，接下来继续解释。\nfixture参数是其他fixture fixture的参数只能是其他fixture。比如：\nimport pytest @pytest.fixture def chinese_name(): return \u0026#34;东方er\u0026#34; @pytest.fixture def english_name(chinese_name): return \u0026#34;dongfanger\u0026#34; 调用english_name，pytest会先执行参数里的其他fixture chinese_name，然后执行自己english_name。\n如果把tep.fixture.url拆成两步来看，就很清晰了，第一步：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def url(env_vars): func = None return func 第二步：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def url(env_vars): func = None def domain_and_uri(uri): if not uri.startswith(\u0026#34;/\u0026#34;): uri = \u0026#34;/\u0026#34; + uri return env_vars.domain + uri func = domain_and_uri return func 环境变量 tep.fixture.url的参数是另外一个fixture env_vars 环境变量，它的定义如下：\nfrom tep.fixture import * @pytest.fixture(scope=\u0026#34;session\u0026#34;) def env_vars(config): class Clazz(TepVars): env = config[\u0026#34;env\u0026#34;] \u0026#34;\u0026#34;\u0026#34;Variables define start\u0026#34;\u0026#34;\u0026#34; # Environment and variables mapping = { \u0026#34;qa\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://qa.com\u0026#34;, }, \u0026#34;release\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://release.com\u0026#34;, } # Add your environment and variables } # Define properties for auto display domain = mapping[env][\u0026#34;domain\u0026#34;] \u0026#34;\u0026#34;\u0026#34;Variables define end\u0026#34;\u0026#34;\u0026#34; return Clazz() 只看中间注释\u0026quot;\u0026quot;\u0026quot;Variables define start\u0026quot;\u0026quot;\u0026quot;到\u0026quot;\u0026quot;\u0026quot;Variables define end\u0026quot;\u0026quot;\u0026quot;部分即可。url参数化的域名就在这里，mapping字典建立了环境和变量之间的映射，根据不同的环境key，获取不同的变量value。\nconfig fixture的作用是读取conf.yaml文件里面的配置。\n参数化的方式很多，JMeter提供了4种参数化方式，tep的fixture env_vars借鉴了JMeter的用户自定义变量：\nenv_vars.put()和env_vars.get()借鉴了JMeter BeanShell的vars.put()和vars.get()。\n实例：测试多个网址 讲到最后，形成了思路，通过实际的例子，看看环境变量、fixtures、用例是怎么用起来的，加深下印象。假如qa环境有2个网址，学校端和机构端，脚本都需要用到。\n第一步修改env_vars，编辑fixture_env_vars.py：\n\u0026#34;\u0026#34;\u0026#34;Variables define start\u0026#34;\u0026#34;\u0026#34; # Environment and variables mapping = { \u0026#34;qa\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://qa.com\u0026#34;, \u0026#34;domain_school\u0026#34;: \u0026#34;https://school.qa.com\u0026#34;, # 新增 \u0026#34;domain_org\u0026#34;: \u0026#34;https://org.qa.com\u0026#34; # 新增 }, \u0026#34;release\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://release.com\u0026#34;, \u0026#34;domain_school\u0026#34;: \u0026#34;https://school.release.com\u0026#34; # 新增 \u0026#34;domain_org\u0026#34;: \u0026#34;https://org.release.com\u0026#34; # 新增 } # Add your environment and variables } # Define properties for auto display domain = mapping[env][\u0026#34;domain\u0026#34;] domain_school = mapping[env][\u0026#34;domain_school\u0026#34;] # 新增 domain_org = mapping[env][\u0026#34;domain_org\u0026#34;] # 新增 \u0026#34;\u0026#34;\u0026#34;Variables define end\u0026#34;\u0026#34;\u0026#34; 添加了6行代码，定义了env_vars.domain_school和env_vars.domain_org。\n第二步定义fixtures，新建fixture_url.py：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def url_school(env_vars): def domain_and_uri(uri): if not uri.startswith(\u0026#34;/\u0026#34;): uri = \u0026#34;/\u0026#34; + uri return env_vars.domain_school + uri return domain_and_uri @pytest.fixture(scope=\u0026#34;session\u0026#34;) def url_org(env_vars): def domain_and_uri(uri): if not uri.startswith(\u0026#34;/\u0026#34;): uri = \u0026#34;/\u0026#34; + uri return env_vars.domain_org + uri return domain_and_uri 参照tep.fixture.url，修改env_vars.domain为env_vars.domain_school和env_vars.domain_org，新增了2个fixture url_school和url_org。\n更进一步，也许会定义fixture login_school和login_org，灵活选择。\n小结 本文循序渐进的讲解了tep环境变量、fixtures和用例之间的关系，重点对tep.fixture.url进行了解释，只要理解了它，整体关系就很清楚了。之所以要用fixture，原因一是多人协作共享，我们需要用别人写好的函数，复用返回值，有些同学习惯定义函数参数，参数不变还好，万一哪天改了，别人引用的用例会全部报错，fixture很好的限制了这一点，它默认是不能传参的，虽然可以通过定义内部函数来实现传参，但是并不推荐这么做，宁愿增加冗余代码，定义多个fixture，也比代码耦合度高好一些。原因二是import的问题，pytest会自动查找conftest.py里的fixture，tep会进一步自动查找fixtures下的fixture导入到conftest.py，不需要import就能使用，减少了import代码，避免了可能会出现的循环导入问题。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000006-tep%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8Ffixtures%E7%94%A8%E4%BE%8B%E4%B8%89%E8%80%85%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB/","summary":"tep是一款测试工具，在pytest测试框架基础上集成了第三方包，提供项目脚手架，帮助以写Python代码方式，快速实现自动化项目落地。","title":"tep环境变量、fixtures、用例三者之间的关系"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n小伙伴B，测试过程中时刻充满了学习呀，工作中对自己需求的部分搞清楚，就涉及到很多知识点的学习呀，比如去了解开发咋实现的，然后你如何去测试。\n学习是为了成长，对于测试工程师来说，功利点，成长最直接的体现，就在2个地方，一是简历上的经历含金量很高，二是面试时的回答专业性很强，进而在跳槽时涨薪。（内部晋升同理，有业绩和答辩好，实现升职加薪）\n这种成长只能通过我们的工作去提升。我在做简历优化，或者咨询时，发现有些同行认为别人会的我也会，为什么不给我面试机会，不给我发offer，我曾经也有过这样的想法。但是，我认为我能，不等于，我曾经做过。社招是看重经验的，会优先考虑有过实际经验的。\n这就要求我们必须知道自己工作的价值点在哪，有什么样的成果，可以包装成怎样的故事。推荐采用STAR法则来梳理。\n工作也会促使我们学习新知识，正如小伙伴所说那样，多理解，多思考，多总结。然而还是会有些东西，需要在工作之外去学习的，比如编程语言，这就必须花费额外时间了。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000006-%E6%B5%8B%E8%AF%95%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E6%88%90%E9%95%BF%E4%BD%93%E7%8E%B0%E5%9C%A82%E4%B8%AA%E5%9C%B0%E6%96%B9/","summary":"小伙伴B，测试过程中时刻充满了学习呀，工作中对自己需求的部分搞清楚，就涉及到很多知识点的学习呀，比如去了解开发咋实现的，然后你如何去测试。","title":"测试工程师的成长，体现在2个地方"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDjango默认是前后端绑定的，提供了Template和Form，现在流行前后端分离项目，Python大佬坐不住了，于是便有了Django REST framework：https://github.com/tomchristie\n官网：https://www.django-rest-framework.org/\nDjango REST framework（简称DRF）是个Python技术栈的后端框架，用来构建RESTful API。\nRESTful API REST，是指REpresentational State Transfer，有个精辟的解释什么是RESTful：\n看URL就知道要什么 看Method就知道干什么 看Status Code就知道结果如何 良好的RESTful API设计的基本原则是：\n返回JSON 严禁乱用状态码 处理好分页 返回具体的实体数据而不是返回通用的JSON数据 请求对象有默认值 创建项目 接下来我们使用DRF创建一个简单的API，允许管理员查看和编辑用户和组。\n先创建名为tutorial的project和名为quickstart的app：\n# 创建项目目录 mkdir tutorial cd tutorial # 创建Python虚拟环境 python -m venv env # 激活虚拟环境 env\\Scripts\\activate.bat # Mac中使用`source env/bin/activate` # 在虚拟环境中安装Django和Django REST framework pip install django pip install djangorestframework # 创建project，注意最后有个“.”，表示在当前目录创建 django-admin startproject tutorial . cd tutorial # 创建app django-admin startapp quickstart cd .. 创建好的目录结构如下：\n$ pwd \u0026lt;some path\u0026gt;/tutorial $ find . . ./manage.py ./tutorial ./tutorial/__init__.py ./tutorial/quickstart ./tutorial/quickstart/__init__.py ./tutorial/quickstart/admin.py ./tutorial/quickstart/apps.py ./tutorial/quickstart/migrations ./tutorial/quickstart/migrations/__init__.py ./tutorial/quickstart/models.py ./tutorial/quickstart/tests.py ./tutorial/quickstart/views.py ./tutorial/settings.py ./tutorial/urls.py ./tutorial/wsgi.py 一般不会把app放到project里面，这里是为了避免命名冲突。\n接着同步数据库：\npython manage.py migrate 然后创建一个超级管理员，密码password123：\npython manage.py createsuperuser --email admin@example.com --username admin Serializers 序列化是指把数据库模型转换为JSON。新建模块tutorial/quickstart/serializers.py：\nfrom django.contrib.auth.models import User, Group from rest_framework import serializers class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User fields = [\u0026#39;url\u0026#39;, \u0026#39;username\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;groups\u0026#39;] class GroupSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Group fields = [\u0026#39;url\u0026#39;, \u0026#39;name\u0026#39;] Views 视图用来接受Web请求并且返回Web响应。打开tutorial/quickstart/views.py，添加代码：\nfrom django.contrib.auth.models import User, Group from rest_framework import viewsets from rest_framework import permissions from tutorial.quickstart.serializers import UserSerializer, GroupSerializer class UserViewSet(viewsets.ModelViewSet): \u0026#34;\u0026#34;\u0026#34; API endpoint that allows users to be viewed or edited. \u0026#34;\u0026#34;\u0026#34; queryset = User.objects.all().order_by(\u0026#39;-date_joined\u0026#39;) serializer_class = UserSerializer permission_classes = [permissions.IsAuthenticated] class GroupViewSet(viewsets.ModelViewSet): \u0026#34;\u0026#34;\u0026#34; API endpoint that allows groups to be viewed or edited. \u0026#34;\u0026#34;\u0026#34; queryset = Group.objects.all() serializer_class = GroupSerializer permission_classes = [permissions.IsAuthenticated] URLs 配置路由，打开tutorial/urls.py，添加代码：\nfrom django.urls import include, path from rest_framework import routers from tutorial.quickstart import views router = routers.DefaultRouter() router.register(r\u0026#39;users\u0026#39;, views.UserViewSet) router.register(r\u0026#39;groups\u0026#39;, views.GroupViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ path(\u0026#39;\u0026#39;, include(router.urls)), path(\u0026#39;api-auth/\u0026#39;, include(\u0026#39;rest_framework.urls\u0026#39;, namespace=\u0026#39;rest_framework\u0026#39;)) ] 因为这里用的不是view而是viewsets，所以可以自动生成API的URLconf，只需要注册class即可。\n也可以不用viewsets，用view，再自定义API URL。\nPagination 分页用来控制每页返回多少数据，在tutorial/settings.py中添加：\nREST_FRAMEWORK = { \u0026#39;DEFAULT_PAGINATION_CLASS\u0026#39;: \u0026#39;rest_framework.pagination.PageNumberPagination\u0026#39;, \u0026#39;PAGE_SIZE\u0026#39;: 10 } Settings 在tutorial/settings.py中，把'rest_framework'添加到INSTALLED_APPS：\nINSTALLED_APPS = [ ... \u0026#39;rest_framework\u0026#39;, ] 测试API 启动项目：\npython manage.py runserver 访问http://127.0.0.1:8000/users/，点击右上角用超管登录，即可看到：\n小结 本文是Django REST framework系列的开篇，内容参考的是官网的Tutorial。学了Django再看DRF，思路清晰多了，虽然我代码能力不强，但总是在追求规范和标准，难道是因为做测试的职业病么？\n参考资料：\nhttps://www.django-rest-framework.org/tutorial/quickstart/\nhttp://www.ruanyifeng.com/blog/2014/05/restful_api.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000006-django%E5%BF%AB%E7%94%A8djangorestframework%E5%86%99%E5%86%99api%E5%90%A7/","summary":"Django默认是前后端绑定的，提供了Template和Form，现在流行前后端分离项目，Python大佬坐不住了，于是便有了Django REST framework：https://github.com/tomchristie","title":"快用DjangoRESTframework写写API吧"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n测试环境困局 测试环境治理专项是在我入职2个月接手的活，用“烂摊子”来形容一点也不为过。年久失修，需求基本没有在测试环境使用，想用也有很多问题。人员缺乏信心，之前负责测试环境的同学抱怨极多，外部推不动，内部又没有技术能力支撑，负能量已经蔓延到整个团队。当然，我也是其中一员，这事确实难搞。在技术部测试环境平台化的大方向下，需要每个组出一个接口人来牵头，考虑到我以前写过测试平台，乱七八糟的事都整过，老板就指定了我。\nWhat，测试环境是什么？ 测试环境是一套能够支撑业务系统运行的环境，是相对于预发环境和生产环境来说完全隔离的环境。测试环境=应用+数据+配置+依赖服务。应用包含系统应用和中间件等，数据就是测试物料，配置针对测试环境应该有单独的一套，依赖服务要跟外部联调好以后整个环境才是可用的。测试环境治理就是通过内部搭建和外部联调，保障业务系统能够在完全隔离的环境中运行。\nWhy，为什么要做测试环境？ 测试环境是为了隔离。如果只有一套生产环境，那么开发、测试、上线等活动，可能会对线上系统稳定性造成影响，可能会污染线上用户数据，可能引起客户投诉甚至资产损失，所以需要有一套完全隔离的测试环境。\nWho，谁来负责测试环境？ 每个测试人员都需要具备测试环境搭建能力。当然可以推动运维或研发来搭建测试环境，但是测试人员需要具备搭建能力，能够自主编译，构建，部署，遇到问题能够根据日志做初步定位，将问题抛出推动研发或外部依赖方解决。\nWhen，什么时间做测试环境？ 测试环境治理需要前置到研发自测阶段。在实践过程中，测试同学暴露出来最多的问题是，没有时间投入到测试环境搭建中，一方面是缺乏动力，不肯迈出艰难的第一步，另一方面也是跟项目节奏有关，研发就算用测试环境自测，也会在预发联调好以后再提测，到测试手上时，已经有可用的预发环境了，就不会再去用测试环境。“项目时间紧，测试环境不好用，直接用预发测吧”，顺理成章的就成为了很合理的借口。所以需要从提测，前置到，研发自测，做测试环境治理：\nWhere，从哪里入手测试环境？ 测试环境治理需要从上往下才能推动，先有文化，才有行动。假如只有某个部门想做测试环境治理，外部依赖基本上是推不动的，一个人呐喊得再愤怒其他人也可能无动于衷。所以得先把测试环境治理这件事，跟老板沟通，从高层入手，把它定义为一项政治任务，从上往下层层递进。\nHow，怎么做测试环境？ 工欲善其事，必先利其器，公司的基建比较完善，给测试人员搭建测试环境降低了很大难度。设想一下，假如只能折腾Docker和K8S，那可能需要专门的运维才能搞得定环境，对测试来说门槛就会比较高了，除了少数技术能力强的，其他同学基本没有什么可操作性。测试环境平台化就是武器。公司有个部署平台叫做JDOS，日常运维都是在这上面操作，没有运维人员，环境全靠开发人员和测试人员自己整。JDOS已经是页面化操作了，但它对测试环境没有特殊的应用场景，是个大而全的平台，所以二级部门开了一个测试环境平台，Env，\u0026ldquo;一款全流程自动化测试环境搭建的利器\u0026rdquo;。\nEnv的核心思想是1+N。1代表稳定分组（分组≈Pod），搭建稳定分组需要经过创建系统，创建应用，创建集群，申请机器，代码编译构建，部署镜像，修改配置等操作，需要选取master稳定版本，这样才能对外提供稳定服务。N是克隆1产生的多个分组，相互隔离，能让开发同学复用测试搭建好的环境，完成冒烟自测等过程；也能在多业务联调测试时，通过平台快速搭建一套全链路测试环境。\nEnv提供了环境监控的功能。对于搭建好的稳定环境，它可以每天监控机器是否可用，服务是否正常，镜像是否老旧，等等等，让你及时维护环境，保障稳定性。\nEnv提供了一键搭建的功能。新克隆的环境，只要点一下“一键搭建”，就会基于稳定分组，自动化搭建一套新环境，大大节约了时间成本。\nEnv提供了拓扑画图的功能。将多个应用拖到一张拓扑图里面，根据依赖关系连线，能够直观了解到应用之间的依赖关系。基于拓扑来克隆新环境，也能批量克隆多个应用。假如多项目需求并行迭代，想要多套联调的测试环境，就可以克隆多套新环境。\nEnv还解决了一个资源浪费的问题，以前搭建测试环境比较随意，申请资源也很容易，其实会导致很多机器用过后，一直空置在那里。Env收拢了申请资源入口，基于Env来创建稳定分组，只能有1个，想要更多必须走审批。而对于克隆的新环境，也会有期限，14天以后会自动释放，想要延期也必须走审批。\n我所负责的第一件事，就是用Env来搭建一张拓扑图，把组内所有应用都从JDOS迁移到Env上，维持稳定环境对外提供支撑对接，克隆环境对内支持多项目并行。\n搭建测试环境只是起步，测试环境治理的目标是需求能够使用测试环境。对于外部依赖少的业务模块来说，这相对容易。对于外部依赖很多的业务模块来说，这就很难了。那么该如何推动这件事呢？\n第1个策略是共建，让研发和测试一起来搭建和维护测试环境。\n第2个策略是做功能验证，梳理出链路用例，跟踪验证结果，识别卡点问题：\n对于卡点问题的处理，把干系人单独拉群对，响应超过3天就往上抛，一定会遇到推不动的问题，那么办法只能找自己的上游负责人，层层向上反馈，让上面的负责人去找对方负责人，然后推动对方的下游同学来协助解决。卡点问题记录后需要做问题分类，做好统计，这既是工作量的体现，也是每周进度汇报的依据：\n第3个策略就是制定kpi，强制大家使用测试环境完成绩效。\nHow much，测试环境做到什么程度？ 测试环境的kpi能反应出测试环境应该做到什么程度，可以使用需求使用测试环境占比和测试环境bug占比。需求使用测试环境占比的计算公式是打标了测试环境的提测卡片数/总的提测卡片数，测试在点击开始测试时，选择是否使用测试环境进行打标。测试环境bug占比的计算公式是测试环境bug/(测试环境bug+预发环境bug+生产环境bug)，在提交缺陷时选择环境类型进行打标。测试环境bug占比的意义更大，因为使用测试环境与否，最终的体现还是落在缺陷上面的，比较容易量化，需求使用测试环境占比不是很确定，需求多少功能在测试环境使用了，相对来说不是很好度量，但也可以作为辅助指标进行观察。\n测试环境破局 测试环境关联了很多东西，比如物料、自动化、精准、流水线等，都可以围绕测试环境来做文章。测试环境既是团队重点，也是是我个人的大案例，目前已经解决了部分困难，要想做出一鸣惊人的成绩，还需要站在更高的角度去看待整个事情，这也是一个全新的挑战，需要不断刷新自己，突破。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E8%87%AA%E5%8A%A8%E5%8C%96/000006-%E8%80%81%E6%9D%BF%E7%BB%99%E6%88%91%E5%AE%89%E6%8E%92%E4%BA%86%E4%B8%AA%E6%B5%8B%E8%AF%95%E7%8E%AF%E5%A2%83%E6%B2%BB%E7%90%86%E7%9A%84%E6%B4%BB/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"测试环境困局\"\u003e测试环境困局\u003c/h1\u003e\n\u003cp\u003e测试环境治理专项是在我入职2个月接手的活，用“烂摊子”来形容一点也不为过。年久失修，需求基本没有在测试环境使用，想用也有很多问题。人员缺乏信心，之前负责测试环境的同学抱怨极多，外部推不动，内部又没有技术能力支撑，负能量已经蔓延到整个团队。当然，我也是其中一员，这事确实难搞。在技术部\u003cstrong\u003e测试环境平台化\u003c/strong\u003e的大方向下，需要每个组出一个接口人来牵头，考虑到我以前写过测试平台，乱七八糟的事都整过，老板就指定了我。\u003c/p\u003e\n\u003ch1 id=\"what测试环境是什么\"\u003eWhat，测试环境是什么？\u003c/h1\u003e\n\u003cp\u003e测试环境是一套能够支撑业务系统运行的环境，是相对于预发环境和生产环境来说完全隔离的环境。\u003cstrong\u003e测试环境=应用+数据+配置+依赖服务\u003c/strong\u003e。应用包含系统应用和中间件等，数据就是测试物料，配置针对测试环境应该有单独的一套，依赖服务要跟外部联调好以后整个环境才是可用的。测试环境治理就是通过内部搭建和外部联调，保障业务系统能够在完全隔离的环境中运行。\u003c/p\u003e\n\u003ch1 id=\"why为什么要做测试环境\"\u003eWhy，为什么要做测试环境？\u003c/h1\u003e\n\u003cp\u003e测试环境是为了隔离。如果只有一套生产环境，那么开发、测试、上线等活动，可能会对线上系统稳定性造成影响，可能会污染线上用户数据，可能引起客户投诉甚至资产损失，所以\u003cstrong\u003e需要有一套完全隔离的测试环境\u003c/strong\u003e。\u003c/p\u003e\n\u003ch1 id=\"who谁来负责测试环境\"\u003eWho，谁来负责测试环境？\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e每个测试人员\u003c/strong\u003e都需要具备测试环境搭建能力。当然可以推动运维或研发来搭建测试环境，但是测试人员需要具备搭建能力，能够自主编译，构建，部署，遇到问题能够根据日志做初步定位，将问题抛出推动研发或外部依赖方解决。\u003c/p\u003e\n\u003ch1 id=\"when什么时间做测试环境\"\u003eWhen，什么时间做测试环境？\u003c/h1\u003e\n\u003cp\u003e测试环境治理需要\u003cstrong\u003e前置到研发自测阶段\u003c/strong\u003e。在实践过程中，测试同学暴露出来最多的问题是，没有时间投入到测试环境搭建中，一方面是缺乏动力，不肯迈出艰难的第一步，另一方面也是跟项目节奏有关，研发就算用测试环境自测，也会在预发联调好以后再提测，到测试手上时，已经有可用的预发环境了，就不会再去用测试环境。“项目时间紧，测试环境不好用，直接用预发测吧”，顺理成章的就成为了很合理的借口。所以需要从提测，前置到，研发自测，做测试环境治理：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/366febbd0a19441881cdffe1a55b1ea4.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"where从哪里入手测试环境\"\u003eWhere，从哪里入手测试环境？\u003c/h1\u003e\n\u003cp\u003e测试环境治理需要\u003cstrong\u003e从上往下\u003c/strong\u003e才能推动，先有文化，才有行动。假如只有某个部门想做测试环境治理，外部依赖基本上是推不动的，一个人呐喊得再愤怒其他人也可能无动于衷。所以得先把测试环境治理这件事，跟老板沟通，从高层入手，把它定义为一项政治任务，从上往下层层递进。\u003c/p\u003e\n\u003ch1 id=\"how怎么做测试环境\"\u003eHow，怎么做测试环境？\u003c/h1\u003e\n\u003cp\u003e工欲善其事，必先利其器，公司的基建比较完善，给测试人员搭建测试环境降低了很大难度。设想一下，假如只能折腾Docker和K8S，那可能需要专门的运维才能搞得定环境，对测试来说门槛就会比较高了，除了少数技术能力强的，其他同学基本没有什么可操作性。\u003cstrong\u003e测试环境平台化就是武器\u003c/strong\u003e。公司有个部署平台叫做JDOS，日常运维都是在这上面操作，没有运维人员，环境全靠开发人员和测试人员自己整。JDOS已经是页面化操作了，但它对测试环境没有特殊的应用场景，是个大而全的平台，所以二级部门开了一个测试环境平台，Env，\u0026ldquo;一款全流程自动化测试环境搭建的利器\u0026rdquo;。\u003c/p\u003e\n\u003cp\u003eEnv的核心思想是\u003ccode\u003e1+N\u003c/code\u003e。\u003ccode\u003e1\u003c/code\u003e代表稳定分组（分组≈Pod），搭建稳定分组需要经过创建系统，创建应用，创建集群，申请机器，代码编译构建，部署镜像，修改配置等操作，需要选取master稳定版本，这样才能对外提供稳定服务。\u003ccode\u003eN\u003c/code\u003e是克隆\u003ccode\u003e1\u003c/code\u003e产生的多个分组，相互隔离，能让开发同学复用测试搭建好的环境，完成冒烟自测等过程；也能在多业务联调测试时，通过平台快速搭建一套全链路测试环境。\u003c/p\u003e\n\u003cp\u003eEnv提供了环境监控的功能。对于搭建好的稳定环境，它可以每天监控机器是否可用，服务是否正常，镜像是否老旧，等等等，让你及时维护环境，保障稳定性。\u003c/p\u003e\n\u003cp\u003eEnv提供了一键搭建的功能。新克隆的环境，只要点一下“一键搭建”，就会基于稳定分组，自动化搭建一套新环境，大大节约了时间成本。\u003c/p\u003e\n\u003cp\u003eEnv提供了拓扑画图的功能。将多个应用拖到一张拓扑图里面，根据依赖关系连线，能够直观了解到应用之间的依赖关系。基于拓扑来克隆新环境，也能批量克隆多个应用。假如多项目需求并行迭代，想要多套联调的测试环境，就可以克隆多套新环境。\u003c/p\u003e\n\u003cp\u003eEnv还解决了一个资源浪费的问题，以前搭建测试环境比较随意，申请资源也很容易，其实会导致很多机器用过后，一直空置在那里。Env收拢了申请资源入口，基于Env来创建稳定分组，只能有1个，想要更多必须走审批。而对于克隆的新环境，也会有期限，14天以后会自动释放，想要延期也必须走审批。\u003c/p\u003e\n\u003cp\u003e我所负责的第一件事，就是用Env来搭建一张拓扑图，把组内所有应用都从JDOS迁移到Env上，\u003cstrong\u003e维持稳定环境对外提供支撑对接，克隆环境对内支持多项目并行\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e搭建测试环境只是起步，\u003cstrong\u003e测试环境治理的目标是需求能够使用测试环境\u003c/strong\u003e。对于外部依赖少的业务模块来说，这相对容易。对于外部依赖很多的业务模块来说，这就很难了。那么该如何推动这件事呢？\u003c/p\u003e\n\u003cp\u003e第1个策略是共建，让研发和测试一起来搭建和维护测试环境。\u003c/p\u003e\n\u003cp\u003e第2个策略是做功能验证，梳理出链路用例，跟踪验证结果，识别卡点问题：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/993f842d5d314b23a877f507494f3697.png\"\u003e\u003c/p\u003e\n\u003cp\u003e对于卡点问题的处理，把干系人单独拉群对，响应超过3天就往上抛，\u003cstrong\u003e一定会遇到推不动的问题，那么办法只能找自己的上游负责人，层层向上反馈，让上面的负责人去找对方负责人，然后推动对方的下游同学来协助解决\u003c/strong\u003e。卡点问题记录后需要做问题分类，做好统计，这既是工作量的体现，也是每周进度汇报的依据：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/546328b1b59744b9951e185dea9319fd.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/687bb904572e48f9ad64703e7e3a6f86.png\"\u003e\u003c/p\u003e\n\u003cp\u003e第3个策略就是制定kpi，强制大家使用测试环境完成绩效。\u003c/p\u003e\n\u003ch1 id=\"how-much测试环境做到什么程度\"\u003eHow much，测试环境做到什么程度？\u003c/h1\u003e\n\u003cp\u003e测试环境的kpi能反应出测试环境应该做到什么程度，可以使用\u003cstrong\u003e需求使用测试环境占比\u003c/strong\u003e和\u003cstrong\u003e测试环境bug占比\u003c/strong\u003e。需求使用测试环境占比的计算公式是\u003ccode\u003e打标了测试环境的提测卡片数/总的提测卡片数\u003c/code\u003e，测试在点击开始测试时，选择是否使用测试环境进行打标。测试环境bug占比的计算公式是\u003ccode\u003e测试环境bug/(测试环境bug+预发环境bug+生产环境bug)\u003c/code\u003e，在提交缺陷时选择环境类型进行打标。测试环境bug占比的意义更大，因为使用测试环境与否，最终的体现还是落在缺陷上面的，比较容易量化，需求使用测试环境占比不是很确定，需求多少功能在测试环境使用了，相对来说不是很好度量，但也可以作为辅助指标进行观察。\u003c/p\u003e\n\u003ch1 id=\"测试环境破局\"\u003e测试环境破局\u003c/h1\u003e\n\u003cp\u003e测试环境关联了很多东西，比如物料、自动化、精准、流水线等，都可以围绕测试环境来做文章。测试环境既是团队重点，也是是我个人的大案例，目前已经解决了部分困难，要想做出一鸣惊人的成绩，还需要站在更高的角度去看待整个事情，这也是一个全新的挑战，需要不断刷新自己，突破。\u003c/p\u003e","title":"老板给我安排了个测试环境治理的活"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n条件控制 我们都知道流程图是有多个分支的，程序中也是如此，在Python中是用if语句来判断程序该走哪个分支的。它的执行过程如下：\n代码执行过程如下\nif if语句的一般形式如下：\nif condition_1: statement_block_1 如果condition_1为True，执行statement_block_1。 condition_1是条件表达式，由各种运算符和数据类型组成，条件表达式的结果可以是True/False的布尔值，也可以是非布尔值。Python中的0、None、空列表、空字典等都会被认为是False，反之都会被认为是True。\nelse else语句在条件为False时执行：\nif condition_1: statement_block_1 else: statement_block_2 如果condition_1为True，执行statement_block_1。 如果condition_1为False，执行statement_block_2。 elif if\u0026hellip;else实现了“如果满足条件，那么xxx，否则xxx”，除此之外，还可以用elif实现多个连续判断，分别执行不同的语句：\nif condition_1: statement_block_1 elif condition_2: statement_block_2 else: statement_block_3 如果condition_1为True，执行statement_block_1。 如果condition_1为False，再判断，如果condition_2为True，执行statement_block_2。 否则，执行statement_block_3。 elif可以写多个。这种写法的执行顺序是从上往下，只要满足了某个if或elif的条件，就会在当前分支执行后跳出去，不再执行后面的判断，如果每个if或elif的条件都是False，最后就会执行else。\nif嵌套 假如想满足if或elif的条件后，继续做判断，那么就需要进行if语句嵌套，让statement_block_1等代码块也包含if语句：\nif 表达式1: 语句 if 表达式2: 语句 elif 表达式3: 语句 else: 语句 elif 表达式4: 语句 else: 语句 示例：\nnum=int(input(\u0026#34;输入一个数字：\u0026#34;)) if num%2==0: if num%3==0: print(\u0026#34;你输入的数字可以整除 2 和 3\u0026#34;) else: print(\u0026#34;你输入的数字可以整除 2，但不能整除 3\u0026#34;) else: if num%3==0: print(\u0026#34;你输入的数字可以整除 3，但不能整除 2\u0026#34;) else: print (\u0026#34;你输入的数字不能整除 2 和 3\u0026#34;) 循环语句 条件语句是从上往下流水式单次执行，循环语句和条件语句的不同点在于，循环是可以回到起始点，反复执行多次的，它的执行过程如下：\nwhile 循环语句的一种形式是while语句：\nwhile 判断条件(condition)： 执行语句(statements)…… 它的执行流程图如下：\n示例：\nn = 100 sum = 0 counter = 1 while counter \u0026lt;= n: sum = sum + counter counter += 1 print(\u0026#34;1 到 %d 之和为: %d\u0026#34; % (n,sum)) # 5050 for 循环语句的另一种形式是for语句：\nfor \u0026lt;variable\u0026gt; in \u0026lt;sequence\u0026gt;: \u0026lt;statements\u0026gt; else: \u0026lt;statements\u0026gt; 它的执行流程图如下：\n示例：\nlanguages = [\u0026#34;C\u0026#34;, \u0026#34;C++\u0026#34;, \u0026#34;Perl\u0026#34;, \u0026#34;Python\u0026#34;] for x in languages: print(x) break break用来跳出整个循环，它的执行流程图如下：\n示例：\nfor letter in \u0026#39;Runoob\u0026#39;: # 第一个实例 if letter == \u0026#39;b\u0026#39;: break print (\u0026#39;当前字母为 :\u0026#39;, letter) var = 10 # 第二个实例 while var \u0026gt; 0: print (\u0026#39;当期变量值为 :\u0026#39;, var) var = var -1 if var == 5: break print (\u0026#34;Good bye!\u0026#34;) continue break用来跳出整个循环，continue则是用来跳过当前循环，回到起点，继续下一次循环。它的执行流程图如下：\n示例：\nfor letter in \u0026#39;Runoob\u0026#39;: # 第一个实例 if letter == \u0026#39;o\u0026#39;: # 字母为 o 时跳过输出 continue print (\u0026#39;当前字母 :\u0026#39;, letter) var = 10 # 第二个实例 while var \u0026gt; 0: var = var -1 if var == 5: # 变量为 5 时跳过输出 continue print (\u0026#39;当前变量值 :\u0026#39;, var) print (\u0026#34;Good bye!\u0026#34;) break和continue在while循环和for循环中的代码执行过程如下：\n循环else 循环后面可以加else语句，while...else，for...else。咋看一下else好像并没有什么用，因为while循环条件为False或for循环结束后，一定会执行接下来的语句。但是如果循环中出现了break，当循环从break跳出来后，是不会执行else语句的。例如：\nfor n in range(2, 10): for x in range(2, n): if n % x == 0: print(n, \u0026#39;等于\u0026#39;, x, \u0026#39;*\u0026#39;, n//x) break else: # 循环中没有找到元素 print(n, \u0026#39; 是质数\u0026#39;) range()函数可以生成数字序列。\n无限循环 通过设置while循环条件始终为True，可以让循环一直进行下去，例如：\nwhile True: pass pass是占位语句，不执行任何操作。\n无限循环是很有用的，比如定时任务，每隔5分钟收1次邮件：\nimport time while True: receive_email() time.sleep(300) 还有比如客户端和服务器保持连接的心跳机制等。\n小结 本文介绍了条件控制和循环语句，也就是写Python常用的if、while、for语句，同时也讲解了附带语句elif、else、break、continue、pass的使用。同时也包括了if嵌套、循环else、无限循环的知识点。这一部分的知识非常重要，是代码的骨架，除了看文章，学代码最好的方式是实际敲代码。我们建了一个“Python互助讨论群”，每周日会有算法刷题打卡活动（照着答案敲一遍也ok），公众号后台回复“加群”即可加入，有兴趣的同学可以一起来相互学习交流呀。\n参考资料：\nhttps://www.runoob.com/python3/python3-conditional-statements.html\nhttps://www.runoob.com/python3/python3-loop.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000006-python%E6%9D%A1%E4%BB%B6%E5%BE%AA%E7%8E%AF%E8%AF%AD%E5%8F%A5%E7%BB%84%E6%88%90%E4%BA%86python%E4%BB%A3%E7%A0%81%E7%9A%84%E9%AA%A8%E6%9E%B6/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"条件控制\"\u003e条件控制\u003c/h1\u003e\n\u003cp\u003e我们都知道流程图是有多个分支的，程序中也是如此，在Python中是用if语句来判断程序该走哪个分支的。它的执行过程如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b968333b5cbb4e0ea99a905c7fbc2f7f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e代码执行过程如下\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bb0c70cd48844a2e86fd84d92f8a77bf.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"if\"\u003eif\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eif\u003c/code\u003e语句的一般形式如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e condition_1\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    statement_block_1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e如果condition_1为True，执行statement_block_1。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003econdition_1是条件表达式，由各种运算符和数据类型组成，条件表达式的结果可以是True/False的布尔值，也可以是非布尔值。Python中的0、None、空列表、空字典等都会被认为是False，反之都会被认为是True。\u003c/p\u003e\n\u003ch2 id=\"else\"\u003eelse\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eelse\u003c/code\u003e语句在条件为False时执行：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e condition_1\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    statement_block_1\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    statement_block_2\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e如果condition_1为True，执行statement_block_1。\u003c/li\u003e\n\u003cli\u003e如果condition_1为False，执行statement_block_2。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"elif\"\u003eelif\u003c/h2\u003e\n\u003cp\u003eif\u0026hellip;else实现了“如果满足条件，那么xxx，否则xxx”，除此之外，还可以用\u003ccode\u003eelif\u003c/code\u003e实现多个连续判断，分别执行不同的语句：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e condition_1\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    statement_block_1\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eelif\u003c/span\u003e condition_2\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    statement_block_2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    statement_block_3\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e如果condition_1为True，执行statement_block_1。\u003c/li\u003e\n\u003cli\u003e如果condition_1为False，再判断，如果condition_2为True，执行statement_block_2。\u003c/li\u003e\n\u003cli\u003e否则，执行statement_block_3。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ccode\u003eelif\u003c/code\u003e可以写多个。这种写法的执行顺序是从上往下，只要满足了某个\u003ccode\u003eif\u003c/code\u003e或\u003ccode\u003eelif\u003c/code\u003e的条件，就会在当前分支执行后跳出去，不再执行后面的判断，如果每个\u003ccode\u003eif\u003c/code\u003e或\u003ccode\u003eelif\u003c/code\u003e的条件都是False，最后就会执行\u003ccode\u003eelse\u003c/code\u003e。\u003c/p\u003e\n\u003ch2 id=\"if嵌套\"\u003eif嵌套\u003c/h2\u003e\n\u003cp\u003e假如想满足\u003ccode\u003eif\u003c/code\u003e或\u003ccode\u003eelif\u003c/code\u003e的条件后，继续做判断，那么就需要进行\u003ccode\u003eif\u003c/code\u003e语句嵌套，让statement_block_1等代码块也包含\u003ccode\u003eif\u003c/code\u003e语句：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e 表达式1\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    语句\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e 表达式2\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        语句\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eelif\u003c/span\u003e 表达式3\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        语句\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        语句\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eelif\u003c/span\u003e 表达式4\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    语句\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    语句\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e示例：\u003c/p\u003e","title":"条件循环语句组成了Python代码的骨架"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n1、mindsdb 加星数：26.4k\n技术栈：python，SDK (Golang, js, ruby, rust, C#, java, Cli)\nGitHub地址：https://github.com/mindsdb/mindsdb\n项目介绍：MindsDB是一个开源的机器学习平台，旨在简化AI模型的创建、训练和部署过程。它允许开发者使用熟悉的SQL查询语言来构建和操作机器学习模型，从而降低了AI开发的门槛。\n2、Mattermost 加星数：30.3k\n技术栈：TypeScript, Golang\nGitHub地址：https://github.com/mattermost/mattermost\n项目介绍：Mattermost是一个强大的开源协作平台，特别适合软件开发团队。它提供了丰富的功能和高度的可定制性，确保团队在整个软件开发生命周期中能够高效、安全地协作。通过自托管，用户可以完全控制他们的数据和平台的运行环境。\n3、Kong 加星数：39k\n技术栈：Lua, Perl, Raku, Starlark, Shell, Python\nGitHub地址：https://github.com/Kong/kong\n项目介绍：Kong API Gateway是一个功能强大且高度可扩展的API网关，适用于各种环境。它以高性能和丰富的功能集（如安全性、负载均衡和监控）而著称。通过插件系统，Kong可以轻松扩展其功能，满足不同用户的需求。此外，Kong还提供了高级的AI功能，支持多种大语言模型，帮助用户实现智能化的API管理和优化。\n4、Taipy 加星数：12.5k\n技术栈：python, typescript\nGitHub地址：https://github.com/avaiga/taipy\n项目介绍：Taipy是一个强大的工具，专为数据科学家和机器学习工程师设计，帮助他们构建数据和AI的Web应用程序。通过简化开发和部署流程，Taipy使用户能够专注于数据和AI算法的开发。其仅需Python语言的特点，使得用户无需学习新的编程语言，从而降低了使用门槛。Taipy还提供了强大的UI生成和场景/数据管理功能，进一步简化了项目的开发和管理。\n5、appwrite 加星数：44.1k\n技术栈：TypeScript, php\nGitHub地址：https://github.com/appwrite/appwrite\n项目介绍：Appwrite是一个强大的端到端后端服务器，专为Web、移动、原生或后端应用程序设计。通过抽象复杂性和提供一整套现成的API和工具，Appwrite使开发者能够更快地构建安全的应用程序。其Docker微服务架构还使得部署和扩展变得更加方便。\n参考资料：\nhttps://dev.to/rohan_sharma/top-open-source-reposprojects-to-contribute-hacktoberfest-edition--273o\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%9F%A5%E8%AF%86/000006-%E6%8E%A8%E8%8D%905%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE152k%E5%8A%A0%E6%98%9F/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"1mindsdb\"\u003e1、mindsdb\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/18a21b28ce8d46279001cd2602369199.png\"\u003e\u003c/p\u003e\n\u003cp\u003e加星数：26.4k\u003c/p\u003e\n\u003cp\u003e技术栈：python，SDK (Golang, js, ruby, rust, C#, java, Cli)\u003c/p\u003e\n\u003cp\u003eGitHub地址：https://github.com/mindsdb/mindsdb\u003c/p\u003e\n\u003cp\u003e项目介绍：MindsDB是一个开源的机器学习平台，旨在简化AI模型的创建、训练和部署过程。它允许开发者使用熟悉的SQL查询语言来构建和操作机器学习模型，从而降低了AI开发的门槛。\u003c/p\u003e\n\u003ch1 id=\"2mattermost\"\u003e2、Mattermost\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/227ddd7b56bf425f9f550edc49ad975e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e加星数：30.3k\u003c/p\u003e\n\u003cp\u003e技术栈：TypeScript, Golang\u003c/p\u003e\n\u003cp\u003eGitHub地址：https://github.com/mattermost/mattermost\u003c/p\u003e\n\u003cp\u003e项目介绍：Mattermost是一个强大的开源协作平台，特别适合软件开发团队。它提供了丰富的功能和高度的可定制性，确保团队在整个软件开发生命周期中能够高效、安全地协作。通过自托管，用户可以完全控制他们的数据和平台的运行环境。\u003c/p\u003e\n\u003ch1 id=\"3kong\"\u003e3、Kong\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0e12bc5c27d04827b5b670f02acc9d93.png\"\u003e\u003c/p\u003e\n\u003cp\u003e加星数：39k\u003c/p\u003e\n\u003cp\u003e技术栈：Lua, Perl, Raku, Starlark, Shell, Python\u003c/p\u003e\n\u003cp\u003eGitHub地址：https://github.com/Kong/kong\u003c/p\u003e\n\u003cp\u003e项目介绍：Kong API Gateway是一个功能强大且高度可扩展的API网关，适用于各种环境。它以高性能和丰富的功能集（如安全性、负载均衡和监控）而著称。通过插件系统，Kong可以轻松扩展其功能，满足不同用户的需求。此外，Kong还提供了高级的AI功能，支持多种大语言模型，帮助用户实现智能化的API管理和优化。\u003c/p\u003e\n\u003ch1 id=\"4taipy\"\u003e4、Taipy\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3d2ae3a1724f4b3fb9cb133144aad0cd.png\"\u003e\u003c/p\u003e\n\u003cp\u003e加星数：12.5k\u003c/p\u003e\n\u003cp\u003e技术栈：python, typescript\u003c/p\u003e\n\u003cp\u003eGitHub地址：https://github.com/avaiga/taipy\u003c/p\u003e\n\u003cp\u003e项目介绍：Taipy是一个强大的工具，专为数据科学家和机器学习工程师设计，帮助他们构建数据和AI的Web应用程序。通过简化开发和部署流程，Taipy使用户能够专注于数据和AI算法的开发。其仅需Python语言的特点，使得用户无需学习新的编程语言，从而降低了使用门槛。Taipy还提供了强大的UI生成和场景/数据管理功能，进一步简化了项目的开发和管理。\u003c/p\u003e\n\u003ch1 id=\"5appwrite\"\u003e5、appwrite\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3b94f9ed06774916a4114944363fcfd6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e加星数：44.1k\u003c/p\u003e\n\u003cp\u003e技术栈：TypeScript, php\u003c/p\u003e\n\u003cp\u003eGitHub地址：https://github.com/appwrite/appwrite\u003c/p\u003e\n\u003cp\u003e项目介绍：Appwrite是一个强大的端到端后端服务器，专为Web、移动、原生或后端应用程序设计。通过抽象复杂性和提供一整套现成的API和工具，Appwrite使开发者能够更快地构建安全的应用程序。其Docker微服务架构还使得部署和扩展变得更加方便。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://dev.to/rohan_sharma/top-open-source-reposprojects-to-contribute-hacktoberfest-edition--273o\"\u003ehttps://dev.to/rohan_sharma/top-open-source-reposprojects-to-contribute-hacktoberfest-edition--273o\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"推荐5个开源项目，152k加星"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n1.1 大模型技术发展回顾和产业价值 1.1.1 大模型发展回顾与展望 2022年11月，ChatGPT横空出世，模型即服务（MaaS）模式快速兴起，AI从实验室走向商业场景。2024年，OpenAI推出o1模型和4o模型，大模型技术迎来质的飞跃。2025年，多家大模型厂商支持模型上下文协议（MCP），Google推出A2A架构，AI从单点工具向系统级生产力工具转变。\n1.1.2 大模型的五大产业价值 效率新工具、服务新体验、产品新形态、决策新助手、科研新模式。\n1.2 AI时代应用架构的演进 1.2.1 IT应用架构的演进脉络 云原生架构是指，Kubernetes等技术通过容器化、集群化管理，实现按量使用、秒级弹性的极致资源调度。\n每一次架构升级，都是在满足业务规模更大、需求变化更快、资源成本更低的述求。\n1.2.2 云原生应用架构向AI原生应用架构的跃迁 如果说，云原生解决的是如何高效地运行，那么AI原生是在此基础上解决如何智能地运行。\n在云原生应用架构中，我们讨论的是容器如何管理、服务如何拆分、流量如何治理。而在AI原生应用架构下，其目标是在满足可扩展、可观测、安全合规的同时，最大化释放模型的智能潜力。\n在大语言模型（LLM）出现之前，AI只能以功能模块形态嵌入系统。LLM具备通用理解、推理和生成能力，并能通过函数调用、外部工具联动和知识库，形成可扩展的Agent体系。AI原生应用的运行逻辑不再完全由工程师编写的代码所决定，而是由大模型进行自出判断、行动和生成，并具备以下3个特征：\n以LLM为核心，用自然语言统一交互协议；\n以多模态感知扩展输入边界，以Agent框架编排工具链；\n以数据飞轮驱动模型持续进化，实现系统的自我优化。\n1.3 AI原生应用及其架构的定义 AI原生应用架构，涵盖了模型、应用开发框架、提示词、RAG、记忆、工具、网关、运行时、可观测、评估和安全等关键要素。\n1.3.1 大模型推理决策 传统应用使用编程语言（Java、C++等）进行编码，AI原生应用使用大语言模型（LLM），通过Prompt（提示词）等自然语言方式进行构建。\nLLM具有语义理解与推理方面的能力，能够在面对模糊和复杂的开放式任务时，自主生成和调整业务执行逻辑，并根据需要完成工具调用与流程编排。\n1.3.2 Agent编排和执行 传统应用是工具，Agent是助手。单Agent无法完成复杂任务时，可以协同多Agent编排完成复杂任务。自身能力受限时，可以扩展工具，甚至自己编写工具完成任务。\n1.3.3 数据优化决策 模型输出可能存在偏差，甚至在某些情况下完全不符合用户或业务方的预期。为了解决这个问题，AI原生应用必须具备基于数据驱动的持续进化能力。在多轮交互中，持续保留并利用历史信息，理解用户的偏好、行为习惯与目标。同时，应用还需要通过数据采集构建高质量的评测数据集，并结合行业数据、用户反馈数据和客户业务数据进行持续评估与优化。\n1.3.4 工具调用与环境连接 大语言模型运行机制的本质仍是基于“输入Token→输出Token”的序列生成过程。受限于这一机制，模型既无法直接感知外部环境，也无法获取实时更新的知识，更缺乏对物理世界的直接操控能力。\nAI原生应用通常通过工具调用的方式扩展模型的环境连接能力。一是支持语音、图像乃至动作等多模态输入；二是支持联网检索获取最新信息，并且通过API对接外部系统，或直接驱动企业内部系统的业务流程。“模型+工具”协同运行。\n1.4 AI原生应用架构成熟度 1.4.1 AI原生应用架构成熟度的定义 AI原生应用架构成熟度，是指用于综合衡量AI原生应用在技术实现、业务融合与安全可信等方面所达到的水平，客观反映其从简单功能集成到复杂智能决策的演进阶段与发展层次。\n1.4.2 AI原生应用架构成熟度的演进 1、概念验证级：单点功能辅助\n2、早期试用级：场景化初步闭环\n3、成熟应用级：核心业务深度集成\n4、完全成熟级：企业级自适应迭代\n1.4.3 AI原生应用架构成熟度的评估 以五大能力特征作为核心评估维度：自然语言交互能力、多模态理解与生成能力、动态推理与自主决策能力、持续学习与迭代能力、安全可信。\n本评估体系旨在为AI原生应用的规划、开发、部署与优化提供一套完整的诊断工具和行动指南，最终推动其从概念验证走向规模化、高价值的产业应用。\n参考资料：\nhttps://developer.aliyun.com/ebook/8479\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/005001-ai%E7%99%BD%E7%9A%AE%E4%B9%A6ai%E5%8E%9F%E7%94%9F%E5%BA%94%E7%94%A8%E5%8F%8A%E5%85%B6%E6%9E%B6%E6%9E%84/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"11-大模型技术发展回顾和产业价值\"\u003e1.1 大模型技术发展回顾和产业价值\u003c/h1\u003e\n\u003ch2 id=\"111-大模型发展回顾与展望\"\u003e1.1.1 大模型发展回顾与展望\u003c/h2\u003e\n\u003cp\u003e2022年11月，ChatGPT横空出世，模型即服务（MaaS）模式快速兴起，AI从实验室走向商业场景。2024年，OpenAI推出o1模型和4o模型，大模型技术迎来质的飞跃。2025年，多家大模型厂商支持模型上下文协议（MCP），Google推出A2A架构，AI从单点工具向系统级生产力工具转变。\u003c/p\u003e\n\u003ch2 id=\"112-大模型的五大产业价值\"\u003e1.1.2 大模型的五大产业价值\u003c/h2\u003e\n\u003cp\u003e效率新工具、服务新体验、产品新形态、决策新助手、科研新模式。\u003c/p\u003e\n\u003ch1 id=\"12-ai时代应用架构的演进\"\u003e1.2 AI时代应用架构的演进\u003c/h1\u003e\n\u003ch2 id=\"121-it应用架构的演进脉络\"\u003e1.2.1 IT应用架构的演进脉络\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/7efa463c67ae45da82088594212f5057.png\"\u003e\u003c/p\u003e\n\u003cp\u003e云原生架构是指，Kubernetes等技术通过容器化、集群化管理，实现按量使用、秒级弹性的极致资源调度。\u003c/p\u003e\n\u003cp\u003e每一次架构升级，都是在满足业务规模更大、需求变化更快、资源成本更低的述求。\u003c/p\u003e\n\u003ch2 id=\"122-云原生应用架构向ai原生应用架构的跃迁\"\u003e1.2.2 云原生应用架构向AI原生应用架构的跃迁\u003c/h2\u003e\n\u003cp\u003e如果说，云原生解决的是如何高效地运行，那么AI原生是在此基础上解决如何智能地运行。\u003c/p\u003e\n\u003cp\u003e在云原生应用架构中，我们讨论的是容器如何管理、服务如何拆分、流量如何治理。而在AI原生应用架构下，其目标是在满足可扩展、可观测、安全合规的同时，最大化释放模型的智能潜力。\u003c/p\u003e\n\u003cp\u003e在大语言模型（LLM）出现之前，AI只能以功能模块形态嵌入系统。LLM具备通用理解、推理和生成能力，并能通过函数调用、外部工具联动和知识库，形成可扩展的Agent体系。AI原生应用的运行逻辑不再完全由工程师编写的代码所决定，而是由大模型进行自出判断、行动和生成，并具备以下3个特征：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e以LLM为核心，用自然语言统一交互协议；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e以多模态感知扩展输入边界，以Agent框架编排工具链；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e以数据飞轮驱动模型持续进化，实现系统的自我优化。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"13-ai原生应用及其架构的定义\"\u003e1.3 AI原生应用及其架构的定义\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c0b3a9a5fdbc4fce8d82a8d32367b7a2.png\"\u003e\u003c/p\u003e\n\u003cp\u003eAI原生应用架构，涵盖了模型、应用开发框架、提示词、RAG、记忆、工具、网关、运行时、可观测、评估和安全等关键要素。\u003c/p\u003e\n\u003ch2 id=\"131-大模型推理决策\"\u003e1.3.1 大模型推理决策\u003c/h2\u003e\n\u003cp\u003e传统应用使用编程语言（Java、C++等）进行编码，AI原生应用使用大语言模型（LLM），通过Prompt（提示词）等自然语言方式进行构建。\u003c/p\u003e\n\u003cp\u003eLLM具有语义理解与推理方面的能力，能够在面对模糊和复杂的开放式任务时，自主生成和调整业务执行逻辑，并根据需要完成工具调用与流程编排。\u003c/p\u003e\n\u003ch2 id=\"132-agent编排和执行\"\u003e1.3.2 Agent编排和执行\u003c/h2\u003e\n\u003cp\u003e传统应用是工具，Agent是助手。单Agent无法完成复杂任务时，可以协同多Agent编排完成复杂任务。自身能力受限时，可以扩展工具，甚至自己编写工具完成任务。\u003c/p\u003e\n\u003ch2 id=\"133-数据优化决策\"\u003e1.3.3 数据优化决策\u003c/h2\u003e\n\u003cp\u003e模型输出可能存在偏差，甚至在某些情况下完全不符合用户或业务方的预期。为了解决这个问题，AI原生应用必须具备基于数据驱动的持续进化能力。在多轮交互中，持续保留并利用历史信息，理解用户的偏好、行为习惯与目标。同时，应用还需要通过数据采集构建高质量的评测数据集，并结合行业数据、用户反馈数据和客户业务数据进行持续评估与优化。\u003c/p\u003e\n\u003ch2 id=\"134-工具调用与环境连接\"\u003e1.3.4 工具调用与环境连接\u003c/h2\u003e\n\u003cp\u003e大语言模型运行机制的本质仍是基于“输入Token→输出Token”的序列生成过程。受限于这一机制，模型既无法直接感知外部环境，也无法获取实时更新的知识，更缺乏对物理世界的直接操控能力。\u003c/p\u003e\n\u003cp\u003eAI原生应用通常通过工具调用的方式扩展模型的环境连接能力。一是支持语音、图像乃至动作等多模态输入；二是支持联网检索获取最新信息，并且通过API对接外部系统，或直接驱动企业内部系统的业务流程。“模型+工具”协同运行。\u003c/p\u003e\n\u003ch1 id=\"14-ai原生应用架构成熟度\"\u003e1.4 AI原生应用架构成熟度\u003c/h1\u003e\n\u003ch2 id=\"141-ai原生应用架构成熟度的定义\"\u003e1.4.1 AI原生应用架构成熟度的定义\u003c/h2\u003e\n\u003cp\u003eAI原生应用架构成熟度，是指用于综合衡量AI原生应用在技术实现、业务融合与安全可信等方面所达到的水平，客观反映其从简单功能集成到复杂智能决策的演进阶段与发展层次。\u003c/p\u003e\n\u003ch2 id=\"142-ai原生应用架构成熟度的演进\"\u003e1.4.2 AI原生应用架构成熟度的演进\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/325ca6fa933f422b8c630b4b4956668e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1、概念验证级：单点功能辅助\u003c/p\u003e\n\u003cp\u003e2、早期试用级：场景化初步闭环\u003c/p\u003e\n\u003cp\u003e3、成熟应用级：核心业务深度集成\u003c/p\u003e\n\u003cp\u003e4、完全成熟级：企业级自适应迭代\u003c/p\u003e\n\u003ch2 id=\"143-ai原生应用架构成熟度的评估\"\u003e1.4.3 AI原生应用架构成熟度的评估\u003c/h2\u003e\n\u003cp\u003e以五大能力特征作为核心评估维度：自然语言交互能力、多模态理解与生成能力、动态推理与自主决策能力、持续学习与迭代能力、安全可信。\u003c/p\u003e\n\u003cp\u003e本评估体系旨在为AI原生应用的规划、开发、部署与优化提供一套完整的诊断工具和行动指南，最终推动其从概念验证走向规模化、高价值的产业应用。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://developer.aliyun.com/ebook/8479\"\u003ehttps://developer.aliyun.com/ebook/8479\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"AI原生应用及其架构"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nSQLite是一个小型的轻量数据库，特别适合个人学习使用。因为SQLite不需要额外的数据库服务器，同时它也是内嵌在Python中的。缺点就是如果有大量的写请求过来，它是串行处理的，速度很慢。\n连接数据库 新建flaskr/db.py文件：\nimport sqlite3 import click from flask import current_app, g from flask.cli import with_appcontext def get_db(): if \u0026#39;db\u0026#39; not in g: g.db = sqlite3.connect( current_app.config[\u0026#39;DATABASE\u0026#39;], detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db def close_db(e=None): db = g.pop(\u0026#39;db\u0026#39;, None) if db is not None: db.close() g是flask给每个请求创建的独立的对象，用来存储全局数据。通过g实现了同一个请求多次调用get_db时，不会创建新连接而是会复用已建立的连接。\nget_db会在flask应用创建后，处理数据库连接时被调用。\nsqlite3.connect()用来建立数据库连接，它指定了配置文件的Key DATABASE。\nsqlite3.Row让数据库以字典的形式返回行，这样就能通过列名进行取值。\nclose_db关闭数据库连接，它先检查g.db有没有设置，如果设置了就关闭db。\n创建表 新建flaskr/schema.sql文件：\nDROP TABLE IF EXISTS user; DROP TABLE IF EXISTS post; CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL ); CREATE TABLE post ( id INTEGER PRIMARY KEY AUTOINCREMENT, author_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL, body TEXT NOT NULL, FOREIGN KEY (author_id) REFERENCES user (id) ); 在flaskr/db.py文件中添加以下代码：\ndef init_db(): db = get_db() with current_app.open_resource(\u0026#39;schema.sql\u0026#39;) as f: db.executescript(f.read().decode(\u0026#39;utf8\u0026#39;)) @click.command(\u0026#39;init-db\u0026#39;) @with_appcontext def init_db_command(): \u0026#34;\u0026#34;\u0026#34;Clear the existing data and create new tables.\u0026#34;\u0026#34;\u0026#34; init_db() click.echo(\u0026#39;Initialized the database.\u0026#39;) open_resource()打开刚才创建的数据库脚本文件。\n@click.command()定义了命令行命令init-db。\n注册到应用 close_db和init_db_command函数Flask不会自动触发，需要手动注册到应用上。\n编辑flaskr/db.py文件：\ndef init_app(app): app.teardown_appcontext(close_db) app.cli.add_command(init_db_command) app.teardown_appcontext指定响应结束后清理时的函数。\napp.cli.add_command定义了可以被flask命令使用的命令。\n再把init_app手动添加到创建应用函数中，编辑flaskr/__init__.py文件：\ndef create_app(): app = ... # existing code omitted from . import db db.init_app(app) return app 执行命令 至此，准备工作已就绪，打开命令行执行：\n$ flask init-db Initialized the database. 在项目目录下，就会生成一个flaskr.sqlite，这就是SQLite数据库。\n参考资料：\nhttps://flask.palletsprojects.com/en/2.0.x/tutorial/database/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/flask/000005-flaskflask%E4%BD%BF%E7%94%A8sqlite%E6%95%B0%E6%8D%AE%E5%BA%93/","summary":"SQLite是一个小型的轻量数据库，特别适合个人学习使用。因为SQLite不需要额外的数据库服务器，同时它也是内嵌在Python中的。缺点就是如果有大量的写请求过来，它是串行处理的，速度很慢。","title":"Flask使用SQLite数据库"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n变量解析 $符号在很多测试框架中都会拿来用做变量解析，大名鼎鼎的JMeter就有这个用法。HttpRunner3也支持$符号，比如：\nStep( RunRequest(\u0026#34;登录\u0026#34;) .post(\u0026#34;/login\u0026#34;) .with_headers(**{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}) .with_json({\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.token\u0026#34;, \u0026#34;token\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), Step( RunRequest(\u0026#34;搜索商品\u0026#34;) .get(\u0026#34;searchSku?skuName=电子书\u0026#34;) .with_headers(**{\u0026#34;token\u0026#34;: \u0026#34;$token\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.skuId\u0026#34;, \u0026#34;skuId\u0026#34;) .with_jmespath(\u0026#34;body.price\u0026#34;, \u0026#34;skuPrice\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), 从登录提取的变量token，在搜索商品中使用$token来引用。\nparser.py HttpRunner对$符号的解析是在parser.py文件中实现的：\n这些方法可以分为四类：\nurl\nbuild_url()\n变量\nextract_variables() get_mapping_variable() parse_variables_mapping() regex_findall_variables() parse_data() parse_string()\n函数\nget_mapping_function() parse_function_params() regex_findall_functions() parse_string_value() parse_data() parse_string()\n参数化（\u0026quot;${parameterize(account.csv)}\u0026quot;）\nparse_parameters()\n本文专门针对变量这一块源码来进行剖析。\n全局变量池 HttpRunner3在运行时的所有变量都是存储在__session_variables这个字典中的：\n在脚本开始时会把config的预设变量加载进来，然后会在执行Step测试步骤时，把各个步骤的变量也放到全局变量池里面。\n解析过程 在runner.py的第350行代码能看到解析变量的调用：\n然后parse_variables_mapping()函数需要仔细看：\ndef parse_variables_mapping( variables_mapping: VariablesMapping, functions_mapping: FunctionsMapping = None ) -\u0026gt; VariablesMapping: parsed_variables: VariablesMapping = {} while len(parsed_variables) != len(variables_mapping): for var_name in variables_mapping: if var_name in parsed_variables: continue var_value = variables_mapping[var_name] variables = extract_variables(var_value) # check if reference variable itself if var_name in variables: # e.g. # variables_mapping = {\u0026#34;token\u0026#34;: \u0026#34;abc$token\u0026#34;} # variables_mapping = {\u0026#34;key\u0026#34;: [\u0026#34;$key\u0026#34;, 2]} raise exceptions.VariableNotFound(var_name) # check if reference variable not in variables_mapping not_defined_variables = [ v_name for v_name in variables if v_name not in variables_mapping ] if not_defined_variables: # e.g. {\u0026#34;varA\u0026#34;: \u0026#34;123$varB\u0026#34;, \u0026#34;varB\u0026#34;: \u0026#34;456$varC\u0026#34;} # e.g. {\u0026#34;varC\u0026#34;: \u0026#34;${sum_two($a, $b)}\u0026#34;} raise exceptions.VariableNotFound(not_defined_variables) try: parsed_value = parse_data( var_value, parsed_variables, functions_mapping ) except exceptions.VariableNotFound: continue parsed_variables[var_name] = parsed_value return parsed_variables tests/parser_test.py有一段测试代码：\n建议打个断点，调试一把，就知道代码是怎么个原理了。\n大概思路是：最外层的while循环会比较已解析和未解析的存储字典长度，只有当所有的变量都解析到值以后，才会正常退出。拿示例代码来说，第一次循环只会解析出varC、a、b，第二次循环才会解析出varB，第三次循环解析出varA。\n循环内部，先是调用extract_variables()方法解析出变量集合：\ndef extract_variables(content: Any) -\u0026gt; Set: \u0026#34;\u0026#34;\u0026#34; extract all variables in content recursively. \u0026#34;\u0026#34;\u0026#34; if isinstance(content, (list, set, tuple)): variables = set() for item in content: variables = variables | extract_variables(item) return variables elif isinstance(content, dict): variables = set() for key, value in content.items(): variables = variables | extract_variables(value) return variables elif isinstance(content, str): return set(regex_findall_variables(content)) return set() regex_findall_variables()函数就是用来解析字符串的，采用的是正则匹配：\ndef regex_findall_variables(raw_string: Text) -\u0026gt; List[Text]: \u0026#34;\u0026#34;\u0026#34; extract all variable names from content, which is in format $variable Args: raw_string (str): string content Returns: list: variables list extracted from string content Examples: \u0026gt;\u0026gt;\u0026gt; regex_findall_variables(\u0026#34;$variable\u0026#34;) [\u0026#34;variable\u0026#34;] \u0026gt;\u0026gt;\u0026gt; regex_findall_variables(\u0026#34;/blog/$postid\u0026#34;) [\u0026#34;postid\u0026#34;] \u0026gt;\u0026gt;\u0026gt; regex_findall_variables(\u0026#34;/$var1/$var2\u0026#34;) [\u0026#34;var1\u0026#34;, \u0026#34;var2\u0026#34;] \u0026gt;\u0026gt;\u0026gt; regex_findall_variables(\u0026#34;abc\u0026#34;) [] \u0026#34;\u0026#34;\u0026#34; try: match_start_position = raw_string.index(\u0026#34;$\u0026#34;, 0) except ValueError: return [] vars_list = [] while match_start_position \u0026lt; len(raw_string): # Notice: notation priority # $$ \u0026gt; $var # search $$ dollar_match = dolloar_regex_compile.match(raw_string, match_start_position) if dollar_match: match_start_position = dollar_match.end() continue # search variable like ${var} or $var var_match = variable_regex_compile.match(raw_string, match_start_position) if var_match: var_name = var_match.group(1) or var_match.group(2) vars_list.append(var_name) match_start_position = var_match.end() continue curr_position = match_start_position try: # find next $ location match_start_position = raw_string.index(\u0026#34;$\u0026#34;, curr_position + 1) except ValueError: # break while loop break return vars_list 调用extract_variables()方法解析出变量集合以后，就会进行异常校验：变量是否引用自己和变量未定义：\n如果校验通过就会调用parse_data()解析出变量值，存入已解析的字典parsed_variables中。\nparse_data()和parse_string()两个函数和主要流程无关了，它们的作用就是解析出变量值，感兴趣的读者朋友可以自行研究一下：\ndef parse_data( raw_data: Any, variables_mapping: VariablesMapping = None, functions_mapping: FunctionsMapping = None, ) -\u0026gt; Any: \u0026#34;\u0026#34;\u0026#34; parse raw data with evaluated variables mapping. Notice: variables_mapping should not contain any variable or function. \u0026#34;\u0026#34;\u0026#34; if isinstance(raw_data, str): # content in string format may contains variables and functions variables_mapping = variables_mapping or {} functions_mapping = functions_mapping or {} # only strip whitespaces and tabs, \\n\\r is left because they maybe used in changeset raw_data = raw_data.strip(\u0026#34; \\t\u0026#34;) return parse_string(raw_data, variables_mapping, functions_mapping) elif isinstance(raw_data, (list, set, tuple)): return [ parse_data(item, variables_mapping, functions_mapping) for item in raw_data ] elif isinstance(raw_data, dict): parsed_data = {} for key, value in raw_data.items(): parsed_key = parse_data(key, variables_mapping, functions_mapping) parsed_value = parse_data(value, variables_mapping, functions_mapping) parsed_data[parsed_key] = parsed_value return parsed_data else: # other types, e.g. None, int, float, bool return raw_data def parse_string( raw_string: Text, variables_mapping: VariablesMapping, functions_mapping: FunctionsMapping, ) -\u0026gt; Any: \u0026#34;\u0026#34;\u0026#34; parse string content with variables and functions mapping. Args: raw_string: raw string content to be parsed. variables_mapping: variables mapping. functions_mapping: functions mapping. Returns: str: parsed string content. Examples: \u0026gt;\u0026gt;\u0026gt; raw_string = \u0026#34;abc${add_one($num)}def\u0026#34; \u0026gt;\u0026gt;\u0026gt; variables_mapping = {\u0026#34;num\u0026#34;: 3} \u0026gt;\u0026gt;\u0026gt; functions_mapping = {\u0026#34;add_one\u0026#34;: lambda x: x + 1} \u0026gt;\u0026gt;\u0026gt; parse_string(raw_string, variables_mapping, functions_mapping) \u0026#34;abc4def\u0026#34; \u0026#34;\u0026#34;\u0026#34; try: match_start_position = raw_string.index(\u0026#34;$\u0026#34;, 0) parsed_string = raw_string[0:match_start_position] except ValueError: parsed_string = raw_string return parsed_string while match_start_position \u0026lt; len(raw_string): # Notice: notation priority # $$ \u0026gt; ${func($a, $b)} \u0026gt; $var # search $$ dollar_match = dolloar_regex_compile.match(raw_string, match_start_position) if dollar_match: match_start_position = dollar_match.end() parsed_string += \u0026#34;$\u0026#34; continue # search function like ${func($a, $b)} func_match = function_regex_compile.match(raw_string, match_start_position) if func_match: func_name = func_match.group(1) func = get_mapping_function(func_name, functions_mapping) func_params_str = func_match.group(2) function_meta = parse_function_params(func_params_str) args = function_meta[\u0026#34;args\u0026#34;] kwargs = function_meta[\u0026#34;kwargs\u0026#34;] parsed_args = parse_data(args, variables_mapping, functions_mapping) parsed_kwargs = parse_data(kwargs, variables_mapping, functions_mapping) try: func_eval_value = func(*parsed_args, **parsed_kwargs) except Exception as ex: logger.error( f\u0026#34;call function error:\\n\u0026#34; f\u0026#34;func_name: {func_name}\\n\u0026#34; f\u0026#34;args: {parsed_args}\\n\u0026#34; f\u0026#34;kwargs: {parsed_kwargs}\\n\u0026#34; f\u0026#34;{type(ex).__name__}: {ex}\u0026#34; ) raise func_raw_str = \u0026#34;${\u0026#34; + func_name + f\u0026#34;({func_params_str})\u0026#34; + \u0026#34;}\u0026#34; if func_raw_str == raw_string: # raw_string is a function, e.g. \u0026#34;${add_one(3)}\u0026#34;, return its eval value directly return func_eval_value # raw_string contains one or many functions, e.g. \u0026#34;abc${add_one(3)}def\u0026#34; parsed_string += str(func_eval_value) match_start_position = func_match.end() continue # search variable like ${var} or $var var_match = variable_regex_compile.match(raw_string, match_start_position) if var_match: var_name = var_match.group(1) or var_match.group(2) var_value = get_mapping_variable(var_name, variables_mapping) if f\u0026#34;${var_name}\u0026#34; == raw_string or \u0026#34;${\u0026#34; + var_name + \u0026#34;}\u0026#34; == raw_string: # raw_string is a variable, $var or ${var}, return its value directly return var_value # raw_string contains one or many variables, e.g. \u0026#34;abc${var}def\u0026#34; parsed_string += str(var_value) match_start_position = var_match.end() continue curr_position = match_start_position try: # find next $ location match_start_position = raw_string.index(\u0026#34;$\u0026#34;, curr_position + 1) remain_string = raw_string[curr_position:match_start_position] except ValueError: remain_string = raw_string[curr_position:] # break while loop match_start_position = len(raw_string) parsed_string += remain_string return parsed_string 技术都是相通的 通过阅读源码可以发现，技术都是相通的。对于HttpRunner来说，它之所以要费这么大周章设计$符号，就是因为它的定位是要让不怎么会写代码的同学，也能使用yaml轻松写自动化。但是V3版本出来以后，官方强烈建议采用直接编写Python代码，但仍然遵循yaml这种约定，是不是反而成为了一种约束呢？跳出这个框架，直接编写pytest，岂不是效率会更高一些？这些问题的答案需要我们根据自身特点和业务场景去把握了。就我个人而言，仍然且始终坚持，纯Python代码写自动化的方式。\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/httprunner/000005-httprunnerhttprunner3%E7%9A%84%E7%AC%A6%E5%8F%B7%E6%98%AF%E5%A6%82%E4%BD%95%E8%A7%A3%E6%9E%90%E7%9A%84/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"变量解析\"\u003e变量解析\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003e$\u003c/code\u003e符号在很多测试框架中都会拿来用做变量解析，大名鼎鼎的JMeter就有这个用法。HttpRunner3也支持\u003ccode\u003e$\u003c/code\u003e符号，比如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eStep\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    RunRequest\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;登录\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003epost\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/login\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewith_headers\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e**\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Content-Type\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;application/json\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewith_json\u003cspan style=\"color:#1f2328\"\u003e({\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;username\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dongfanger\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;123456\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eextract\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewith_jmespath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;body.token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003evalidate\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eassert_equal\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;status_code\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e200\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eStep\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    RunRequest\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;搜索商品\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;searchSku?skuName=电子书\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewith_headers\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e**\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$token\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eextract\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewith_jmespath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;body.skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ewith_jmespath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;body.price\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003evalidate\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eassert_equal\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;status_code\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e200\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e从\u003ccode\u003e登录\u003c/code\u003e提取的变量\u003ccode\u003etoken\u003c/code\u003e，在\u003ccode\u003e搜索商品\u003c/code\u003e中使用\u003ccode\u003e$token\u003c/code\u003e来引用。\u003c/p\u003e\n\u003ch1 id=\"parserpy\"\u003eparser.py\u003c/h1\u003e\n\u003cp\u003eHttpRunner对\u003ccode\u003e$\u003c/code\u003e符号的解析是在parser.py文件中实现的：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3881ed1c0d374c3da2d839e5d738265a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这些方法可以分为四类：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eurl\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ebuild_url()\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e变量\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eextract_variables()\u003c/code\u003e  \u003ccode\u003eget_mapping_variable()\u003c/code\u003e  \u003ccode\u003eparse_variables_mapping()\u003c/code\u003e  \u003ccode\u003eregex_findall_variables()\u003c/code\u003e  \u003ccode\u003eparse_data()\u003c/code\u003e  \u003ccode\u003eparse_string()\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e函数\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eget_mapping_function()\u003c/code\u003e  \u003ccode\u003eparse_function_params()\u003c/code\u003e  \u003ccode\u003eregex_findall_functions()\u003c/code\u003e  \u003ccode\u003eparse_string_value()\u003c/code\u003e  \u003ccode\u003eparse_data()\u003c/code\u003e  \u003ccode\u003eparse_string()\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e参数化（\u003ccode\u003e\u0026quot;${parameterize(account.csv)}\u0026quot;\u003c/code\u003e）\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eparse_parameters()\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e本文专门针对\u003cstrong\u003e变量\u003c/strong\u003e这一块源码来进行剖析。\u003c/p\u003e\n\u003ch1 id=\"全局变量池\"\u003e全局变量池\u003c/h1\u003e\n\u003cp\u003eHttpRunner3在运行时的所有变量都是存储在\u003ccode\u003e__session_variables\u003c/code\u003e这个字典中的：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d8ba076be0004516a4f07e77a353c1b1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在脚本开始时会把config的预设变量加载进来，然后会在执行Step测试步骤时，把各个步骤的变量也放到全局变量池里面。\u003c/p\u003e\n\u003ch1 id=\"解析过程\"\u003e解析过程\u003c/h1\u003e\n\u003cp\u003e在runner.py的第350行代码能看到解析变量的调用：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3f98196457354490be91adcc5a1e1265.png\"\u003e\u003c/p\u003e\n\u003cp\u003e然后parse_variables_mapping()函数需要仔细看：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eparse_variables_mapping\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    variables_mapping\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e VariablesMapping\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e functions_mapping\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e FunctionsMapping \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e-\u0026gt;\u003c/span\u003e VariablesMapping\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    parsed_variables\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e VariablesMapping \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ewhile\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eparsed_variables\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003evariables_mapping\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e var_name \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e variables_mapping\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e var_name \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e parsed_variables\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#cf222e\"\u003econtinue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            var_value \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e variables_mapping\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003evar_name\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            variables \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e extract_variables\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003evar_value\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# check if reference variable itself\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e var_name \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e variables\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#57606a\"\u003e# e.g.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#57606a\"\u003e# variables_mapping = {\u0026#34;token\u0026#34;: \u0026#34;abc$token\u0026#34;}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#57606a\"\u003e# variables_mapping = {\u0026#34;key\u0026#34;: [\u0026#34;$key\u0026#34;, 2]}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#cf222e\"\u003eraise\u003c/span\u003e exceptions\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eVariableNotFound\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003evar_name\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#57606a\"\u003e# check if reference variable not in variables_mapping\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            not_defined_variables \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                v_name \u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e v_name \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e variables \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e v_name \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003ein\u003c/span\u003e variables_mapping\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e not_defined_variables\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#57606a\"\u003e# e.g. {\u0026#34;varA\u0026#34;: \u0026#34;123$varB\u0026#34;, \u0026#34;varB\u0026#34;: \u0026#34;456$varC\u0026#34;}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#57606a\"\u003e# e.g. {\u0026#34;varC\u0026#34;: \u0026#34;${sum_two($a, $b)}\u0026#34;}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#cf222e\"\u003eraise\u003c/span\u003e exceptions\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eVariableNotFound\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003enot_defined_variables\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                parsed_value \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e parse_data\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    var_value\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e parsed_variables\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e functions_mapping\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#cf222e\"\u003eexcept\u003c/span\u003e exceptions\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eVariableNotFound\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#cf222e\"\u003econtinue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            parsed_variables\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003evar_name\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e parsed_value\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e parsed_variables\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003etests/parser_test.py\u003c/code\u003e有一段测试代码：\u003c/p\u003e","title":"HttpRunner3的$符号是如何解析的"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n什么是增量覆盖率 如图所示，在master分支提交了HelloController，然后从master拉了个新分支test；提交了第1次代码，增加了WorldController；提交了第2次代码，增加了DonController。增量的获取方式有两种：\n版本对比：在分支上，第2次提交和第1次提交版本对比的增量代码，就是DonController；第2次提交和刚从master拉取分支时版本对比的增量代码，除了DonController还有WorldController；\n分支对比：把test分支和master分支进行对比，增量代码就是DonController和WorldController；\n增量，就是通过版本对比或分支对比，多出来的新增加的那部分代码。覆盖率是指程序运行以后，有多少代码被执行到了，除以总的代码数算出来的，即覆盖率=执行代码行数/总代码行数。增量覆盖率是针对增量代码来计算的，也就是增量覆盖率=执行代码行数/增量代码行数。\n基本实现原理 使用JaCoCo的On-the-fly模式，以tcpserver方式启动，将远程机器的覆盖率数据通过TCP通信dump到本地jacoco.exec文件，二次开发JaCoCo源码，实现增量覆盖率分析和输出HTML报告。\nJaCoCo默认覆盖率 JaCoCo是一个开源的覆盖率工具，它的On-the-fly模式，无须侵入应用启动脚本，只需在JVM中通过-javaagent参数指定jar文件启动的代理程序，代理程序在ClassLoader装载一个class前判断是否需要注入class文件，将统计代码插入class，覆盖率分析就可以在JVM执行的过程中完成。\n启动脚本示例：\njava -javaagent:jacocoagent.jar -jar target/app.jar 默认情况下，JaCoCo会在JVM停掉以后，生成覆盖率数据，一个jacoco.exec文件。\n官方手册也有相关说明：\noutput默认为file：At VM termination execution data is written to the file specified in the destfile attribute。这种方式有2个局限，一是必须停掉应用；二是只能把覆盖率数据生成到本地文件。\noutput还有另外一个选项，tcpserver：\njava -javaagent:jacocoagent.jar=output=tcpserver,address=0.0.0.0,port=2014 -jar target/app.jar address：服务端地址；\nport：服务端端口；\n通过这种方式启动后，会同时启动一个TCP服务端，使用lsof -i :2014命令查看端口进程：\n然后就能通过TCP通信，来获取覆盖率数据，也就是所谓的”dump“。\n代码片段：\n@Override public void dump(String dumpPath, String ip, int port) throws IOException { dumpPath = FileUtil.getAbsolutePath(dumpPath); if (!FileUtil.exist(dumpPath)) { FileUtil.mkdir(dumpPath); } FileOutputStream localFile = new FileOutputStream(dumpPath + File.separator + \u0026#34;jacoco.exec\u0026#34;); ExecutionDataWriter localWriter = new ExecutionDataWriter(localFile); SocketAddress socketAddress = new InetSocketAddress(ip, port); Socket socket = new Socket(); try { socket.connect(socketAddress, 10000); RemoteControlWriter writer = new RemoteControlWriter(socket.getOutputStream()); RemoteControlReader reader = new RemoteControlReader(socket.getInputStream()); reader.setSessionInfoVisitor(localWriter); reader.setExecutionDataVisitor(localWriter); log.info(\u0026#34;开始dump:{} {}\u0026#34;, ip, port); writer.visitDumpCommand(true, false); if (!reader.read()) { log.error(\u0026#34;Socket closed unexpectedly\u0026#34;); throw new IOException(\u0026#34;Socket closed unexpectedly.\u0026#34;); } log.info(\u0026#34;dump完成:{} {}\u0026#34;, ip, port); } catch (Exception e) { log.info(\u0026#34;dump失败:{}\u0026#34;, e.getMessage()); } finally { socket.close(); localFile.close(); } } dump经过TCP通信，将远程机器的覆盖率数据取到本地后，生成jacoco.exec文件，然后可以使用jacococli.jar生成HTML报告：\njava -jar jacococli.jar report ./dump/jacoco.exec --classfiles ./target/classes/ --sourcefiles ./src/main/java/ --html ./report 第1个参数：jacoco.exec文件路径；\n第2个参数：class文件路径；\n第3个参数：src源码路径；\n第4个参数：报告存放地址；\nJaCoCo也提供了OpenApi来生成报告。\n代码片段：\ndumpPath = FileUtil.getAbsolutePath(dumpPath); classFilesPath = FileUtil.getAbsolutePath(classFilesPath); srcPath = FileUtil.getAbsolutePath(srcPath); reportPath = FileUtil.getAbsolutePath(reportPath); File execFile = new File(dumpPath + File.separator + \u0026#34;jacoco.exec\u0026#34;); ExecFileLoader execFileLoader = new ExecFileLoader(); execFileLoader.load(execFile); CoverageBuilder coverageBuilder = new CoverageBuilder(); Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder); analyzer.analyzeAll(new File(classFilesPath)); String reportTile = \u0026#34;报告标题\u0026#34;; IBundleCoverage bundleCoverage = coverageBuilder.getBundle(reportTile); HTMLFormatter htmlFormatter = new HTMLFormatter(); IReportVisitor iReportVisitor = htmlFormatter.createVisitor(new FileMultiReportOutput(new File(reportPath))); iReportVisitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(), execFileLoader.getExecutionDataStore().getContents()); DirectorySourceFileLocator directorySourceFileLocator = new DirectorySourceFileLocator(new File(srcPath), \u0026#34;utf-8\u0026#34;, 4); iReportVisitor.visitBundle(bundleCoverage, directorySourceFileLocator); iReportVisitor.visitEnd(); JaCoCo的org.jacoco.core和org.jacoco.report两个包提供了这些方法。\nJaCoCo默认只能分析全量覆盖率。\n改造JaCoCo支持增量 改造的底层逻辑是，在分析覆盖率数据时，class文件只选取git diff的文件，从而只统计差异的增量代码，实现增量覆盖率。\n在ClassProbesAdapter类的visitMethod方法里面，有一个对方法级别的探针计算逻辑，改造它，只对提取出的每个类的新增或变更方法做解析。\n代码片段：\nCoverageBuilder.classInfos是git diff出来的差异类。\n改造CoverageBuilder支持分支对比和版本对比：\n获取差异代码使用jgit和jdt切割到了方法粒度：\n完整源码可以参考开源项目JacocoPlus：\nhttps://github.com/512433465/JacocoPlus\n关键点 JaCoCo分析覆盖率有时候结果不准确，通常跟这两个关键点有关：\n一是dump，dump的频次很重要，建议每次分析时都dump一次，保证覆盖率数据是最新的。分布式集群会有多份覆盖率数据，可以使用ExecFileLoader的load和save方法将多份数据合并为一份。应用重启会丢失覆盖率数据，可以做一些备份。\n二是class，在生成报告时，会将class和src进行比对，以统计覆盖率，只有当class和src完全匹配才能得到准确的覆盖率，假如我们自己拉最新的代码进行编译得到class，由于编译环境和版本的影响，编译出来的class说不定跟src就不匹配了，统计结果也就不会准确。最好是从应用上拉取部署的class来进行对比，以保证class和src的完全匹配。\nJaCoCo - Java Agent https://www.jacoco.org/jacoco/trunk/doc/agent.html\nJaCoCo - API Usage Examples https://www.jacoco.org/jacoco/trunk/doc/api.html\n增量代码覆盖率工具 https://tech.youzan.com/yzicov/\n有赞精准测试实践 https://tech.youzan.com/thanos/\nJava覆盖率Jacoco插桩的不同形式总结和踩坑记录 https://testerhome.com/topics/20632\njacoco 代码覆盖率使用中遇到的一些坑 https://testerhome.com/topics/16925\njacoco—增量代码覆盖率实现 https://blog.csdn.net/qq_34811445/article/details/127556617\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E7%99%BD%E7%9B%92%E6%B5%8B%E8%AF%95/000005-jacocojacoco%E5%A2%9E%E9%87%8F%E8%A6%86%E7%9B%96%E7%8E%87%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"什么是增量覆盖率\"\u003e什么是增量覆盖率\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fd75a2d267c34c9f9e5e1344e30f39de.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如图所示，在master分支提交了HelloController，然后从master拉了个新分支test；提交了第1次代码，增加了WorldController；提交了第2次代码，增加了DonController。增量的获取方式有两种：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e版本对比：在分支上，第2次提交和第1次提交版本对比的增量代码，就是DonController；第2次提交和刚从master拉取分支时版本对比的增量代码，除了DonController还有WorldController；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e分支对比：把test分支和master分支进行对比，增量代码就是DonController和WorldController；\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e增量，就是通过版本对比或分支对比，多出来的新增加的那部分代码。覆盖率是指程序运行以后，有多少代码被执行到了，除以总的代码数算出来的，即\u003ccode\u003e覆盖率=执行代码行数/总代码行数\u003c/code\u003e。增量覆盖率是针对增量代码来计算的，也就是\u003ccode\u003e增量覆盖率=执行代码行数/增量代码行数\u003c/code\u003e。\u003c/p\u003e\n\u003ch2 id=\"基本实现原理\"\u003e基本实现原理\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d4e75938a47841e2a138fe08e79f3e4e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e使用JaCoCo的On-the-fly模式，以tcpserver方式启动，将远程机器的覆盖率数据通过TCP通信dump到本地jacoco.exec文件，二次开发JaCoCo源码，实现增量覆盖率分析和输出HTML报告。\u003c/p\u003e\n\u003ch2 id=\"jacoco默认覆盖率\"\u003eJaCoCo默认覆盖率\u003c/h2\u003e\n\u003cp\u003eJaCoCo是一个开源的覆盖率工具，它的On-the-fly模式，无须侵入应用启动脚本，只需在JVM中通过-javaagent参数指定jar文件启动的代理程序，代理程序在ClassLoader装载一个class前判断是否需要注入class文件，将统计代码插入class，覆盖率分析就可以在JVM执行的过程中完成。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/83fa0888fb0149e88af6400b6c1b367a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e启动脚本示例：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ejava -javaagent:jacocoagent.jar -jar target/app.jar\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e默认情况下，\u003cstrong\u003eJaCoCo会在JVM停掉以后，生成覆盖率数据\u003c/strong\u003e，一个jacoco.exec文件。\u003c/p\u003e\n\u003cp\u003e官方手册也有相关说明：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b149f503647f4fc0ac34516cb7015989.png\"\u003e\u003c/p\u003e\n\u003cp\u003eoutput默认为file：At VM termination execution data is written to the file specified in the \u003ccode\u003edestfile\u003c/code\u003e attribute。这种方式有2个局限，一是必须停掉应用；二是只能把覆盖率数据生成到本地文件。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eoutput还有另外一个选项，tcpserver\u003c/strong\u003e：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ejava -javaagent:jacocoagent.jar=output=tcpserver,address=0.0.0.0,port=2014 -jar target/app.jar\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eaddress：服务端地址；\u003c/p\u003e\n\u003cp\u003eport：服务端端口；\u003c/p\u003e\n\u003cp\u003e通过这种方式启动后，会同时启动一个TCP服务端，使用\u003ccode\u003elsof -i :2014\u003c/code\u003e命令查看端口进程：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a0182f7309474db987e11467d47be108.png\"\u003e\u003c/p\u003e\n\u003cp\u003e然后就能通过TCP通信，来获取覆盖率数据，也就是所谓的”\u003cstrong\u003edump\u003c/strong\u003e“。\u003c/p\u003e\n\u003cp\u003e代码片段：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edump\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edumpPath\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eip\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eport\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003edumpPath\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFileUtil\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetAbsolutePath\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edumpPath\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eFileUtil\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexist\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edumpPath\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eFileUtil\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003emkdir\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edumpPath\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eFileOutputStream\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003elocalFile\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFileOutputStream\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003edumpPath\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFile\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eseparator\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;jacoco.exec\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eExecutionDataWriter\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003elocalWriter\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eExecutionDataWriter\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elocalFile\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSocketAddress\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esocketAddress\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eInetSocketAddress\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eip\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eport\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSocket\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esocket\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eSocket\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003esocket\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003econnect\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esocketAddress\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e10000\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eRemoteControlWriter\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ewriter\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRemoteControlWriter\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esocket\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetOutputStream\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eRemoteControlReader\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ereader\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRemoteControlReader\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003esocket\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetInputStream\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003ereader\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esetSessionInfoVisitor\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elocalWriter\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003ereader\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esetExecutionDataVisitor\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elocalWriter\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003elog\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003einfo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;开始dump:{} {}\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eip\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eport\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003ewriter\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evisitDumpCommand\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etrue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efalse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003ereader\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eread\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                \u003c/span\u003elog\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eerror\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Socket closed unexpectedly\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Socket closed unexpectedly.\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003elog\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003einfo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dump完成:{} {}\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eip\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eport\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ecatch\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003elog\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003einfo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dump失败:{}\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetMessage\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinally\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003esocket\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eclose\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003elocalFile\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eclose\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003edump经过TCP通信，将远程机器的覆盖率数据取到本地后，生成jacoco.exec文件，然后可以使用jacococli.jar生成HTML报告：\u003c/p\u003e","title":"JaCoCo增量覆盖率的基本实现原理"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n请求分发 Servlet的RequestDispatcher接口定义：\npackage javax.servlet; import java.io.IOException; public interface RequestDispatcher { String FORWARD_REQUEST_URI = \u0026#34;javax.servlet.forward.request_uri\u0026#34;; String FORWARD_CONTEXT_PATH = \u0026#34;javax.servlet.forward.context_path\u0026#34;; String FORWARD_PATH_INFO = \u0026#34;javax.servlet.forward.path_info\u0026#34;; String FORWARD_SERVLET_PATH = \u0026#34;javax.servlet.forward.servlet_path\u0026#34;; String FORWARD_QUERY_STRING = \u0026#34;javax.servlet.forward.query_string\u0026#34;; String INCLUDE_REQUEST_URI = \u0026#34;javax.servlet.include.request_uri\u0026#34;; String INCLUDE_CONTEXT_PATH = \u0026#34;javax.servlet.include.context_path\u0026#34;; String INCLUDE_PATH_INFO = \u0026#34;javax.servlet.include.path_info\u0026#34;; String INCLUDE_SERVLET_PATH = \u0026#34;javax.servlet.include.servlet_path\u0026#34;; String INCLUDE_QUERY_STRING = \u0026#34;javax.servlet.include.query_string\u0026#34;; String ERROR_EXCEPTION = \u0026#34;javax.servlet.error.exception\u0026#34;; String ERROR_EXCEPTION_TYPE = \u0026#34;javax.servlet.error.exception_type\u0026#34;; String ERROR_MESSAGE = \u0026#34;javax.servlet.error.message\u0026#34;; String ERROR_REQUEST_URI = \u0026#34;javax.servlet.error.request_uri\u0026#34;; String ERROR_SERVLET_NAME = \u0026#34;javax.servlet.error.servlet_name\u0026#34;; String ERROR_STATUS_CODE = \u0026#34;javax.servlet.error.status_code\u0026#34;; void forward(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; void include(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; } 它有2个方法，forward和include：\npublic void forward(ServletRequest request,ServletResponse response)throws ServletException,java.io.IOException: Forwards a request from a servlet to another resource (servlet, JSP file, or HTML file) on the server. public void include(ServletRequest request,ServletResponse response)throws ServletException,java.io.IOException: Includes the content of a resource (servlet, JSP page, or HTML file) in the response. forward方法将请求从一个Servlet转发到另一个资源（Servlet、JSP页面或HTML文件）。转发后，客户端的URL不会改变，且请求和响应对象在转发过程中保持不变。使用场景：\n在服务器内部资源之间传递请求 需要在处理请求时共享相同的请求和响应对象 include方法将另一个资源的内容包含到当前响应中。与forward不同，include不会终止当前Servlet的执行，而是将目标资源的输出包含在当前响应中。使用场景：\n在生成响应时需要包含其他资源的内容 需要在一个页面中动态地插入其他资源的内容 代码实践\n通过登录页面，输入用户名和密码，如果密码是servlet，forward到welcome servlet，否则include到登录页面，展示错误信息。\nlogin.html 获取用户输入\n\u0026lt;form action=\u0026#34;go\u0026#34; method=\u0026#34;get\u0026#34;\u0026gt; Name:\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;userName\u0026#34;/\u0026gt;\u0026lt;br/\u0026gt; Password:\u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;userPass\u0026#34;/\u0026gt;\u0026lt;br/\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;login\u0026#34;/\u0026gt; \u0026lt;/form\u0026gt; Simple.java Servlet类，如果密码是servlet，forward到welcome servlet\nimport javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class Simple extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = response.getWriter(); String p = request.getParameter(\u0026#34;userPass\u0026#34;); if (p.equals(\u0026#34;servlet\u0026#34;)) { RequestDispatcher rd = request.getRequestDispatcher(\u0026#34;welcome\u0026#34;); rd.forward(request, response); } else { out.print(\u0026#34;Sorry username or password error!\u0026#34;); RequestDispatcher rd = request.getRequestDispatcher(\u0026#34;login.html\u0026#34;); rd.include(request, response); } } } WelcomeServlet.java Servlet类，返回welcome信息\nimport javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class WelcomeServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = response.getWriter(); String n = request.getParameter(\u0026#34;userName\u0026#34;); out.print(\u0026#34;Welcome \u0026#34; + n); } } web.xml 部署描述文件，servlet信息\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;web-app version=\u0026#34;2.5\u0026#34; xmlns=\u0026#34;http://java.sun.com/xml/ns/javaee\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd\u0026#34;\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;description\u0026gt;This is the description of my J2EE component\u0026lt;/description\u0026gt; \u0026lt;display-name\u0026gt;This is the display name of my J2EE component\u0026lt;/display-name\u0026gt; \u0026lt;servlet-name\u0026gt;Simple\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;Simple\u0026lt;/servlet-class\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;description\u0026gt;This is the description of my J2EE component\u0026lt;/description\u0026gt; \u0026lt;display-name\u0026gt;This is the display name of my J2EE component\u0026lt;/display-name\u0026gt; \u0026lt;servlet-name\u0026gt;WelcomeServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;WelcomeServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;Simple\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/go\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;WelcomeServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/welcome\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;welcome-file-list\u0026gt; \u0026lt;welcome-file\u0026gt;login.html\u0026lt;/welcome-file\u0026gt; \u0026lt;/welcome-file-list\u0026gt; \u0026lt;/web-app\u0026gt; web.xml在低版本Servlet是必须的，在高版本Servlet已经不再需要。\n注意Simple.java的else逻辑：\nout.print(\u0026#34;Sorry username or password error!\u0026#34;); RequestDispatcher rd = request.getRequestDispatcher(\u0026#34;login.html\u0026#34;); rd.include(request, response); 如果把这里的include改为forward，会发生什么？输出内容不会显示，因为forward方法会将控制权转交给login.html，并且login.html的内容将替代当前响应内容。\n总结，用include方法时，当前Servlet的输出内容会保留，并且目标资源的内容会被包含在当前响应中。使用forward方法时，当前Servlet的执行将终止，控制权将交给目标资源，目标资源的内容将替代当前响应内容。\n重定向 我们在浏览器看到的302，就是重定向。通常在重定向后，会自动发起新的HTTP请求。\nHttpServletResponse接口的sendRedirect方法：\nvoid sendRedirect(String var1) throws IOException; 示例：\nimport javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class MySearcher extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter(\u0026#34;name\u0026#34;); response.sendRedirect(\u0026#34;https://www.google.co.in/#q=\u0026#34; + name); } } forward和sendRedirect的区别如下：\nforward是在服务端，sendRedirect是在客户端\nforward发送相同的request和response对象给另一个servlet，sendRedirect总是生成新的request和response对象\nforward只能在服务器内部，sendRedirect可以跨不同Web应用或服务器\n总结，forward适用于服务器内部资源之间的请求转发，效率较高，客户端URL不变。sendRedirect适用于通知客户端资源位置改变或跨Web应用/服务器的请求转发，客户端URL会改变，有额外的网络开销。\n参考资料：\nhttps://www.javatpoint.com/requestdispatcher-in-servlet\nhttps://www.javatpoint.com/sendRedirect()-method\nhttps://www.liaoxuefeng.com/wiki/1252599548343744/1328761739935778\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000005-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servlet%E8%AF%B7%E6%B1%82%E5%88%86%E5%8F%91%E4%B8%8E%E9%87%8D%E5%AE%9A%E5%90%91/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"请求分发\"\u003e请求分发\u003c/h1\u003e\n\u003cp\u003eServlet的RequestDispatcher接口定义：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epackage\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejava.io.IOException\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eRequestDispatcher\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFORWARD_REQUEST_URI\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.forward.request_uri\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFORWARD_CONTEXT_PATH\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.forward.context_path\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFORWARD_PATH_INFO\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.forward.path_info\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFORWARD_SERVLET_PATH\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.forward.servlet_path\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFORWARD_QUERY_STRING\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.forward.query_string\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eINCLUDE_REQUEST_URI\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.include.request_uri\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eINCLUDE_CONTEXT_PATH\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.include.context_path\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eINCLUDE_PATH_INFO\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.include.path_info\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eINCLUDE_SERVLET_PATH\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.include.servlet_path\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eINCLUDE_QUERY_STRING\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.include.query_string\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eERROR_EXCEPTION\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.error.exception\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eERROR_EXCEPTION_TYPE\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.error.exception_type\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eERROR_MESSAGE\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.error.message\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eERROR_REQUEST_URI\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.error.request_uri\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eERROR_SERVLET_NAME\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.error.servlet_name\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eERROR_STATUS_CODE\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;javax.servlet.error.status_code\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eforward\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar2\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletException\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003einclude\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003evar2\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletException\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e它有2个方法，forward和include：\u003c/p\u003e","title":"JavaWeb开发基础Servlet请求分发与重定向"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n对象Object 对象有2个特征：状态state和行为behavior。我们可以用真实世界的对象来做类比，这样有助于理解面向对象编程，比如狗的状态（名字、颜色、品种）和行为（叫、抓、摇尾巴），自行车的状态（当前档位、当前踏板节奏、当前速度）和行为（切换档位、切换踏板节奏、踩刹车）。\n软件对象也是由状态和行为组成：\n状态，在软件对象中，叫做字段fields，或变量variables。\n行为，在软件对象中，叫做方法methods，或函数functions。\n所谓的数据封装data encapsulation，就是字段只在内部，不对外暴露，仅由方法进行操作，对象与对象之间通过方法来相互访问。\nJava是纯粹的面向对象编程语言。\n类Class 类，可以视为对象的模版，基于类来创造出同一类对象。\n比如自行车的类：\nclass Bicycle { int cadence = 0; int speed = 0; int gear = 1; void changeCadence(int newValue) { cadence = newValue; } void changeGear(int newValue) { gear = newValue; } void speedUp(int increment) { speed = speed + increment; } void applyBrakes(int decrement) { speed = speed - decrement; } void printStates() { System.out.println(\u0026#34;cadence:\u0026#34; + cadence + \u0026#34; speed:\u0026#34; + speed + \u0026#34; gear:\u0026#34; + gear); } } 类也是有2个特征：字段和方法。比如Bicycle类有字段cadence、speed、gear和方法changeCadence()、changeGear()、speedUp()等。\n你可能会发现这个类里面没有main方法，因为Bicycle类只是定义了对象的模版，可以新建一个类来实现Bicycle类对象的创建和使用：\nclass BicycleDemo { public static void main(String[] args) { // Create two different // Bicycle objects Bicycle bike1 = new Bicycle(); Bicycle bike2 = new Bicycle(); // Invoke methods on // those objects bike1.changeCadence(50); bike1.speedUp(10); bike1.changeGear(2); bike1.printStates(); bike2.changeCadence(50); bike2.speedUp(10); bike2.changeGear(2); bike2.changeCadence(40); bike2.speedUp(10); bike2.changeGear(3); bike2.printStates(); } } 运行输出：\ncadence:50 speed:10 gear:2 cadence:40 speed:20 gear:3 继承Inheritance 自行车有很多种，自行车的共同点可以用父类来表示，自行车的不同点可以用子类继承父类，然后添加自己的差异点：\n图中的Bicycle是父类，MountainBike、RoadBike和TandemBike是子类。父类的字段和方法会同步给所有的子类，而子类可以继续添加自己的字段和方法。\n继承使用extends来定义：\nclass MountainBike extends Bicycle { // new fields and methods defining // a mountain bike would go here } Java的子类只能有1个父类，但是父类可以有无限多个子类。\n接口Interface 就像电视机开关一样，你跟电视机交互，只需要按开关就可以，电视机暴露出来的是方法。接口就是一组方法，只有方法声明，没有具体实现。比如：\ninterface Bicycle { // wheel revolutions per minute void changeCadence(int newValue); void changeGear(int newValue); void speedUp(int increment); void applyBrakes(int decrement); } 类使用implements来实现接口方法：\nclass ACMEBicycle implements Bicycle { int cadence = 0; int speed = 0; int gear = 1; // The compiler will now require that methods // changeCadence, changeGear, speedUp, and applyBrakes // all be implemented. Compilation will fail if those // methods are missing from this class. void changeCadence(int newValue) { cadence = newValue; } void changeGear(int newValue) { gear = newValue; } void speedUp(int increment) { speed = speed + increment; } void applyBrakes(int decrement) { speed = speed - decrement; } void printStates() { System.out.println(\u0026#34;cadence:\u0026#34; + cadence + \u0026#34; speed:\u0026#34; + speed + \u0026#34; gear:\u0026#34; + gear); } } Java为什么要发明接口呢？我们知道对象与对象只能通过方法来相互访问，而接口其实就是一种契约，编译器会在编译时强制执行这份契约。就像文章的大纲一样。\n实现接口的类，必须实现接口中的所有方法。\n包Package 包相当于文件夹，用来组织接口和类。\nJava API，也叫做Java类库，内置了很多包：\nhttps://docs.oracle.com/javase/8/docs/api/index.html\n包列表：\n包里面的接口、类：\n参考资料：\nObjects, Classes, Interfaces, Packages, and Inheritance https://dev.java/learn/oop/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000005-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B02%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"对象object\"\u003e对象Object\u003c/h3\u003e\n\u003cp\u003e对象有2个特征：\u003cstrong\u003e状态state和行为behavior\u003c/strong\u003e。我们可以用真实世界的对象来做类比，这样有助于理解面向对象编程，比如狗的状态（名字、颜色、品种）和行为（叫、抓、摇尾巴），自行车的状态（当前档位、当前踏板节奏、当前速度）和行为（切换档位、切换踏板节奏、踩刹车）。\u003c/p\u003e\n\u003cp\u003e软件对象也是由状态和行为组成：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8ea4654355834e17ab6dd437db1d8243.png\"\u003e\u003c/p\u003e\n\u003cp\u003e状态，在软件对象中，叫做字段fields，或变量variables。\u003c/p\u003e\n\u003cp\u003e行为，在软件对象中，叫做方法methods，或函数functions。\u003c/p\u003e\n\u003cp\u003e所谓的数据封装data encapsulation，就是字段只在内部，不对外暴露，仅由方法进行操作，对象与对象之间通过方法来相互访问。\u003c/p\u003e\n\u003cp\u003eJava是\u003cstrong\u003e纯粹的\u003c/strong\u003e面向对象编程语言。\u003c/p\u003e\n\u003ch3 id=\"类class\"\u003e类Class\u003c/h3\u003e\n\u003cp\u003e类，可以视为对象的\u003cstrong\u003e模版\u003c/strong\u003e，基于类来创造出同一类对象。\u003c/p\u003e\n\u003cp\u003e比如自行车的类：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eBicycle\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecadence\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e0\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e0\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003egear\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003echangeCadence\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e         \u003c/span\u003ecadence\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003echangeGear\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e         \u003c/span\u003egear\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003enewValue\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003espeedUp\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eincrement\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e         \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eincrement\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e   \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eapplyBrakes\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edecrement\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e         \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003edecrement\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eprintStates\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e         \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;cadence:\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e             \u003c/span\u003ecadence\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34; speed:\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e             \u003c/span\u003espeed\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34; gear:\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003egear\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e类也是有2个特征：字段和方法。比如Bicycle类有字段cadence、speed、gear和方法changeCadence()、changeGear()、speedUp()等。\u003c/p\u003e","title":"Java官方笔记2面向对象编程基本概念"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n测试计划（Test Plan），是JMeter目录树的初始根节点，它的编辑区如下：\nName 带有业务含义的名字。\nComments 备注。\nUser Defined Variables 用户自定义变量，实际上就是全局变量。\n表格共两列，第一列是Name变量名，第二列是Value变量值。表格下方的功能按钮，用来对表格进行增删改查和上下移动，还能直接从剪贴板添加。定义好的变量通过${}格式来使用，比如变量a值为1，使用时写作${a}。\n这个地方定义的实际上是字符串常量，比如刚才已经定义了一个变量名a变量值1，再定义变量名b变量值${a} + 1，b的实际值不是整型2而是字符串\u0026quot;${a} + 1\u0026quot;！\nRun Thread Groups consecutively 控制多个线程组是顺序执行或并行执行，比如：\n如果勾选了这个选项，顺序执行：前一个线程组执行完以后，再执行下一个线程组。\n如果不勾选这个选项，并行执行：执行没有先后顺序，结果是随机的。\nRun tearDown Thread Groups after shutdown of main threads 线程组包括初始化线程组和清理线程组：\n这个选项默认已勾选，会执行清理线程组。\nFuntional Test Mode 没用，不学。\nAdd directory or jar to classpath 要搞懂这个，得先学习Java中的classpath和jar包。Java源文件是.java文件，编译后是.class文件。Java的执行器JVM（Java虚拟机），只能执行.class文件。为了让JVM找到那个想要的.class文件，需要把.class文件路径添加到classpath中。为了方便管理，会把多个.class文件打成一个包，变成一个.jar文件，叫做jar包。\nJMeter是用Java写的，可以添加jar包扩展功能，添加入口有2个：\n第一个入口是测试计划此选项，Add directory or jar to classpath。 推荐！第二个入口是把jar包放入lib/ext目录。 小结 JMeter测试计划是根节点，适合定义全局变量和设定全局配置，编辑区的选项也体现了这一特点。\n参考资料：\nhttps://www.liaoxuefeng.com/wiki/1252599548343744/1260466914339296\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000005-jmeterjmeter%E6%B5%8B%E8%AF%95%E8%AE%A1%E5%88%92%E9%85%8D%E7%BD%AE%E9%A1%B9%E8%A7%A3%E6%9E%90/","summary":"测试计划（Test Plan），是JMeter目录树的初始根节点，它的编辑区如下：","title":"JMeter测试计划配置项解析"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n条件测试 JUnit5支持条件注解，根据布尔值判断是否执行测试。\n自定义条件 @EnabledIf和@DisabledIf注解用来设置自定义条件，示例：\n@Test @EnabledIf(\u0026#34;customCondition\u0026#34;) void enabled() { // ... } @Test @DisabledIf(\u0026#34;customCondition\u0026#34;) void disabled() { // ... } boolean customCondition() { return true; } 其中customCondition()方法用来返回布尔值，它可以接受一个ExtensionContext类型的参数。如果定义在测试类外部，那么需要是static方法。\n内置条件 JUnit5的org.junit.jupiter.api.condition包中内置了一些条件注解。\n操作系统条件\n@EnabledOnOs和DisabledOnOs，示例：\n@Test @EnabledOnOs(MAC) void onlyOnMacOs() { // ... } @TestOnMac void testOnMac() { // ... } @Test @EnabledOnOs({ LINUX, MAC }) void onLinuxOrMac() { // ... } @Test @DisabledOnOs(WINDOWS) void notOnWindows() { // ... } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Test @EnabledOnOs(MAC) @interface TestOnMac { } JRE条件\n@EnabledOnJre和@DisabledOnJre用于指定版本，@EnabledForJreRange和@DisabledForJreRange用于指定版本范围，示例：\n@Test @EnabledOnJre(JAVA_8) void onlyOnJava8() { // ... } @Test @EnabledOnJre({ JAVA_9, JAVA_10 }) void onJava9Or10() { // ... } @Test @EnabledForJreRange(min = JAVA_9, max = JAVA_11) void fromJava9to11() { // ... } @Test @EnabledForJreRange(min = JAVA_9) void fromJava9toCurrentJavaFeatureNumber() { // ... } @Test @EnabledForJreRange(max = JAVA_11) void fromJava8To11() { // ... } @Test @DisabledOnJre(JAVA_9) void notOnJava9() { // ... } @Test @DisabledForJreRange(min = JAVA_9, max = JAVA_11) void notFromJava9to11() { // ... } @Test @DisabledForJreRange(min = JAVA_9) void notFromJava9toCurrentJavaFeatureNumber() { // ... } @Test @DisabledForJreRange(max = JAVA_11) void notFromJava8to11() { // ... } JVM系统属性条件\n@EnabledIfSystemProperty和@DisabledIfSystemProperty，示例：\n@Test @EnabledIfSystemProperty(named = \u0026#34;os.arch\u0026#34;, matches = \u0026#34;.*64.*\u0026#34;) void onlyOn64BitArchitectures() { // ... } @Test @DisabledIfSystemProperty(named = \u0026#34;ci-server\u0026#34;, matches = \u0026#34;true\u0026#34;) void notOnCiServer() { // ... } 环境变量条件\n@EnabledIfEnvironmentVariable和@DisabledIfEnvironmentVariable，示例：\n@Test @EnabledIfEnvironmentVariable(named = \u0026#34;ENV\u0026#34;, matches = \u0026#34;staging-server\u0026#34;) void onlyOnStagingServer() { // ... } @Test @DisabledIfEnvironmentVariable(named = \u0026#34;ENV\u0026#34;, matches = \u0026#34;.*development.*\u0026#34;) void notOnDeveloperWorkstation() { // ... } 嵌套测试 嵌套测试可以帮助我们对测试结构进行分层。借助于Java嵌套类的语法，JUnit5可以通过@Nested注解，实现嵌套测试，示例：\nimport static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.EmptyStackException; import java.util.Stack; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @DisplayName(\u0026#34;A stack\u0026#34;) class TestingAStackDemo { Stack\u0026lt;Object\u0026gt; stack; @Test @DisplayName(\u0026#34;is instantiated with new Stack()\u0026#34;) void isInstantiatedWithNew() { new Stack\u0026lt;\u0026gt;(); } @Nested @DisplayName(\u0026#34;when new\u0026#34;) class WhenNew { @BeforeEach void createNewStack() { stack = new Stack\u0026lt;\u0026gt;(); } @Test @DisplayName(\u0026#34;is empty\u0026#34;) void isEmpty() { assertTrue(stack.isEmpty()); } @Test @DisplayName(\u0026#34;throws EmptyStackException when popped\u0026#34;) void throwsExceptionWhenPopped() { assertThrows(EmptyStackException.class, stack::pop); } @Test @DisplayName(\u0026#34;throws EmptyStackException when peeked\u0026#34;) void throwsExceptionWhenPeeked() { assertThrows(EmptyStackException.class, stack::peek); } @Nested @DisplayName(\u0026#34;after pushing an element\u0026#34;) class AfterPushing { String anElement = \u0026#34;an element\u0026#34;; @BeforeEach void pushAnElement() { stack.push(anElement); } @Test @DisplayName(\u0026#34;it is no longer empty\u0026#34;) void isNotEmpty() { assertFalse(stack.isEmpty()); } @Test @DisplayName(\u0026#34;returns the element when popped and is empty\u0026#34;) void returnElementWhenPopped() { assertEquals(anElement, stack.pop()); assertTrue(stack.isEmpty()); } @Test @DisplayName(\u0026#34;returns the element when peeked but remains not empty\u0026#34;) void returnElementWhenPeeked() { assertEquals(anElement, stack.peek()); assertFalse(stack.isEmpty()); } } } } 外部测试类通过@BeforeEach向内部测试类传递变量。\n执行后结果：\n重复测试 @RepeatedTest注解能控制测试方法的重复执行次数，示例：\nimport static org.junit.jupiter.api.Assertions.assertEquals; import java.util.logging.Logger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.RepetitionInfo; import org.junit.jupiter.api.TestInfo; class RepeatedTestsDemo { private Logger logger = // ... @BeforeEach void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) { int currentRepetition = repetitionInfo.getCurrentRepetition(); int totalRepetitions = repetitionInfo.getTotalRepetitions(); String methodName = testInfo.getTestMethod().get().getName(); logger.info(String.format(\u0026#34;About to execute repetition %d of %d for %s\u0026#34;, // currentRepetition, totalRepetitions, methodName)); } @RepeatedTest(10) void repeatedTest() { // ... } @RepeatedTest(5) void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) { assertEquals(5, repetitionInfo.getTotalRepetitions()); } @RepeatedTest(value = 1, name = \u0026#34;{displayName} {currentRepetition}/{totalRepetitions}\u0026#34;) @DisplayName(\u0026#34;Repeat!\u0026#34;) void customDisplayName(TestInfo testInfo) { assertEquals(\u0026#34;Repeat! 1/1\u0026#34;, testInfo.getDisplayName()); } @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME) @DisplayName(\u0026#34;Details...\u0026#34;) void customDisplayNameWithLongPattern(TestInfo testInfo) { assertEquals(\u0026#34;Details... :: repetition 1 of 1\u0026#34;, testInfo.getDisplayName()); } @RepeatedTest(value = 5, name = \u0026#34;Wiederholung {currentRepetition} von {totalRepetitions}\u0026#34;) void repeatedTestInGerman() { // ... } } 其中name可以用来自定义重复测试的显示名字，{currentRepetition}和{totalRepetitions}是当前次数和总共次数的变量。\n执行结果：\n├─ RepeatedTestsDemo ✔ │ ├─ repeatedTest() ✔ │ │ ├─ repetition 1 of 10 ✔ │ │ ├─ repetition 2 of 10 ✔ │ │ ├─ repetition 3 of 10 ✔ │ │ ├─ repetition 4 of 10 ✔ │ │ ├─ repetition 5 of 10 ✔ │ │ ├─ repetition 6 of 10 ✔ │ │ ├─ repetition 7 of 10 ✔ │ │ ├─ repetition 8 of 10 ✔ │ │ ├─ repetition 9 of 10 ✔ │ │ └─ repetition 10 of 10 ✔ │ ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔ │ │ ├─ repetition 1 of 5 ✔ │ │ ├─ repetition 2 of 5 ✔ │ │ ├─ repetition 3 of 5 ✔ │ │ ├─ repetition 4 of 5 ✔ │ │ └─ repetition 5 of 5 ✔ │ ├─ Repeat! ✔ │ │ └─ Repeat! 1/1 ✔ │ ├─ Details... ✔ │ │ └─ Details... :: repetition 1 of 1 ✔ │ └─ repeatedTestInGerman() ✔ │ ├─ Wiederholung 1 von 5 ✔ │ ├─ Wiederholung 2 von 5 ✔ │ ├─ Wiederholung 3 von 5 ✔ │ ├─ Wiederholung 4 von 5 ✔ │ └─ Wiederholung 5 von 5 ✔ 小结 本文分别对JUnit5的条件测试、嵌套测试、重复测试进行了介绍，它们可以使得测试更加灵活和富有层次。除了这些，JUnit5还支持另一个重要且常见的测试：参数化测试。\n参考资料：\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-conditional-execution\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-nested\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-repeated-tests\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000005-junitjunit5%E7%9A%84%E6%9D%A1%E4%BB%B6%E6%B5%8B%E8%AF%95%E5%B5%8C%E5%A5%97%E6%B5%8B%E8%AF%95%E9%87%8D%E5%A4%8D%E6%B5%8B%E8%AF%95/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"条件测试\"\u003e条件测试\u003c/h1\u003e\n\u003cp\u003eJUnit5支持条件注解，根据布尔值判断是否执行测试。\u003c/p\u003e\n\u003ch2 id=\"自定义条件\"\u003e自定义条件\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e@EnabledIf\u003c/code\u003e和\u003ccode\u003e@DisabledIf\u003c/code\u003e注解用来设置自定义条件，示例：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@EnabledIf\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;customCondition\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eenabled\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// ...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@DisabledIf\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;customCondition\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edisabled\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// ...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eboolean\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003ecustomCondition\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etrue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e其中customCondition()方法用来返回布尔值，它可以接受一个ExtensionContext类型的参数。如果定义在测试类外部，那么需要是static方法。\u003c/p\u003e\n\u003ch2 id=\"内置条件\"\u003e内置条件\u003c/h2\u003e\n\u003cp\u003eJUnit5的\u003ccode\u003eorg.junit.jupiter.api.condition\u003c/code\u003e包中内置了一些条件注解。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e操作系统条件\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e@EnabledOnOs\u003c/code\u003e和\u003ccode\u003eDisabledOnOs\u003c/code\u003e，示例：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@EnabledOnOs\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMAC\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eonlyOnMacOs\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// ...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@TestOnMac\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etestOnMac\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// ...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@EnabledOnOs\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e({\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eLINUX\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMAC\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eonLinuxOrMac\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// ...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@DisabledOnOs\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eWINDOWS\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003enotOnWindows\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// ...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Target\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eElementType\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eMETHOD\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Retention\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eRetentionPolicy\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eRUNTIME\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@EnabledOnOs\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMAC\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@interface\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTestOnMac\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eJRE条件\u003c/strong\u003e\u003c/p\u003e","title":"JUnit5的条件测试、嵌套测试、重复测试"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n用过unittest的朋友，肯定知道可以借助DDT实现参数化。用过JMeter的朋友，肯定知道JMeter自带了4种参数化方式（见参考资料）。pytest同样支持参数化，而且很简单很实用。\n语法 在《pytest封神之路第三步 精通fixture》和《pytest封神之路第四步 内置和自定义marker》两篇文章中，都提到了pytest参数化。那么本文就趁着热乎，赶紧聊一聊pytest的参数化是怎么玩的。\n@pytest.mark.parametrize\n@pytest.mark.parametrize(\u0026#34;test_input,expected\u0026#34;, [(\u0026#34;3+5\u0026#34;, 8), (\u0026#34;2+4\u0026#34;, 6), (\u0026#34;6*9\u0026#34;, 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected 可以自定义变量，test_input对应的值是\u0026quot;3+5\u0026quot; \u0026ldquo;2+4\u0026rdquo; \u0026ldquo;6*9\u0026rdquo;，expected对应的值是8 6 42，多个变量用tuple，多个tuple用list\n参数化的变量是引用而非复制，意味着如果值是list或dict，改变值会影响后续的test\n重叠产生笛卡尔积\nimport pytest @pytest.mark.parametrize(\u0026#34;x\u0026#34;, [0, 1]) @pytest.mark.parametrize(\u0026#34;y\u0026#34;, [2, 3]) def test_foo(x, y): pass @pytest.fixture()\n@pytest.fixture(scope=\u0026#34;module\u0026#34;, params=[\u0026#34;smtp.gmail.com\u0026#34;, \u0026#34;mail.python.org\u0026#34;]) def smtp_connection(request): smtp_connection = smtplib.SMTP(request.param, 587, timeout=5) 只能使用request.param来引用\n参数化生成的test带有ID，可以使用-k来筛选执行。默认是根据函数名[参数名]来的，可以使用ids来定义\n// list @pytest.fixture(params=[0, 1], ids=[\u0026#34;spam\u0026#34;, \u0026#34;ham\u0026#34;]) // function @pytest.fixture(params=[0, 1], ids=idfn) 使用--collect-only 命令行参数可以看到生成的IDs。\n参数添加marker\n我们知道了参数化后会生成多个tests，如果有些test需要marker，可以用pytest.param来添加\nmarker方式\n## content of test_expectation.py import pytest @pytest.mark.parametrize( \u0026#34;test_input,expected\u0026#34;, [(\u0026#34;3+5\u0026#34;, 8), (\u0026#34;2+4\u0026#34;, 6), pytest.param(\u0026#34;6*9\u0026#34;, 42, marks=pytest.mark.xfail)], ) def test_eval(test_input, expected): assert eval(test_input) == expected fixture方式\n## content of test_fixture_marks.py import pytest @pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)]) def data_set(request): return request.param def test_data(data_set): pass pytest_generate_tests\n用来自定义参数化方案。使用到了hook，hook的知识我会写在《pytest hook》中，欢迎关注公众号dongfanger获取最新文章。\n## content of conf.py def pytest_generate_tests(metafunc): if \u0026#34;test_input\u0026#34; in metafunc.fixturenames: metafunc.parametrize(\u0026#34;test_input\u0026#34;, [0, 1]) ## content of test.py def test(test_input): assert test_input == 0 定义在conftest.py文件中 metafunc有5个属性，fixturenames，module，config，function，cls metafunc.parametrize() 用来实现参数化 多个metafunc.parametrize() 的参数名不能重复，否则会报错 参数化误区 在讲示例之前，先简单分享我的菜鸡行为。假设我们现在需要对50个接口测试，验证某一角色的用户访问这些接口会返回403。我的做法是，把接口请求全部参数化了，test函数里面只有断言，伪代码大致如下\ndef api(): params = [] def func(): return request() params.append(func) ... @pytest.mark.parametrize(\u0026#39;req\u0026#39;, api()) def test(): res = req() assert res.status_code == 403 这样参数化以后，会产生50个tests，如果断言失败了，会单独标记为failed，不影响其他test结果。咋一看还行，但是有个问题，在回归的时候，可能只需要验证其中部分接口，就没有办法灵活的调整，必须全部跑一遍才行。这是一个相对错误的示范，至于正确的应该怎么写，相信每个人心中都有一个答案，能解决问题就是ok的。我想表达的是，参数化要适当，不要滥用，最好只对测试数据做参数化。\n实践 本文的重点来了，参数化的语法比较简单，实际应用是关键。这部分通过11个例子，来实践一下。示例覆盖的知识点有点多，建议留大段时间细看。\n1.使用hook添加命令行参数\u0026ndash;all，\u0026ldquo;param1\u0026quot;是参数名，带\u0026ndash;all参数时是range(5) == [0, 1, 2, 3, 4]，生成5个tests。不带参数时是range(2)。\n## content of test_compute.py def test_compute(param1): assert param1 \u0026lt; 4 ## content of conftest.py def pytest_addoption(parser): parser.addoption(\u0026#34;--all\u0026#34;, action=\u0026#34;store_true\u0026#34;, help=\u0026#34;run all combinations\u0026#34;) def pytest_generate_tests(metafunc): if \u0026#34;param1\u0026#34; in metafunc.fixturenames: if metafunc.config.getoption(\u0026#34;all\u0026#34;): end = 5 else: end = 2 metafunc.parametrize(\u0026#34;param1\u0026#34;, range(end)) 2.testdata是测试数据，包括2组。test_timedistance_v0不带ids。test_timedistance_v1带list格式的ids。test_timedistance_v2的ids为函数。test_timedistance_v3使用pytest.param同时定义测试数据和id。\n## content of test_time.py from datetime import datetime, timedelta import pytest testdata = [ (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)), (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)), ] @pytest.mark.parametrize(\u0026#34;a,b,expected\u0026#34;, testdata) def test_timedistance_v0(a, b, expected): diff = a - b assert diff == expected @pytest.mark.parametrize(\u0026#34;a,b,expected\u0026#34;, testdata, ids=[\u0026#34;forward\u0026#34;, \u0026#34;backward\u0026#34;]) def test_timedistance_v1(a, b, expected): diff = a - b assert diff == expected def idfn(val): if isinstance(val, (datetime,)): # note this wouldn\u0026#39;t show any hours/minutes/seconds return val.strftime(\u0026#34;%Y%m%d\u0026#34;) @pytest.mark.parametrize(\u0026#34;a,b,expected\u0026#34;, testdata, ids=idfn) def test_timedistance_v2(a, b, expected): diff = a - b assert diff == expected @pytest.mark.parametrize( \u0026#34;a,b,expected\u0026#34;, [ pytest.param( datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1), id=\u0026#34;forward\u0026#34; ), pytest.param( datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1), id=\u0026#34;backward\u0026#34; ), ], ) def test_timedistance_v3(a, b, expected): diff = a - b assert diff == expected 3.兼容unittest的testscenarios\n## content of test_scenarios.py def pytest_generate_tests(metafunc): idlist = [] argvalues = [] for scenario in metafunc.cls.scenarios: idlist.append(scenario[0]) items = scenario[1].items() argnames = [x[0] for x in items] argvalues.append([x[1] for x in items]) metafunc.parametrize(argnames, argvalues, ids=idlist, scope=\u0026#34;class\u0026#34;) scenario1 = (\u0026#34;basic\u0026#34;, {\u0026#34;attribute\u0026#34;: \u0026#34;value\u0026#34;}) scenario2 = (\u0026#34;advanced\u0026#34;, {\u0026#34;attribute\u0026#34;: \u0026#34;value2\u0026#34;}) class TestSampleWithScenarios: scenarios = [scenario1, scenario2] def test_demo1(self, attribute): assert isinstance(attribute, str) def test_demo2(self, attribute): assert isinstance(attribute, str) 4.初始化数据库连接\n## content of test_backends.py import pytest def test_db_initialized(db): # a dummy test if db.__class__.__name__ == \u0026#34;DB2\u0026#34;: pytest.fail(\u0026#34;deliberately failing for demo purposes\u0026#34;) ## content of conftest.py import pytest def pytest_generate_tests(metafunc): if \u0026#34;db\u0026#34; in metafunc.fixturenames: metafunc.parametrize(\u0026#34;db\u0026#34;, [\u0026#34;d1\u0026#34;, \u0026#34;d2\u0026#34;], indirect=True) class DB1: \u0026#34;one database object\u0026#34; class DB2: \u0026#34;alternative database object\u0026#34; @pytest.fixture def db(request): if request.param == \u0026#34;d1\u0026#34;: return DB1() elif request.param == \u0026#34;d2\u0026#34;: return DB2() else: raise ValueError(\u0026#34;invalid internal test config\u0026#34;) 5.如果不加indirect=True，会生成2个test，fixt的值分别是\u0026quot;a\u0026quot;和\u0026quot;b\u0026rdquo;。如果加了indirect=True，会先执行fixture，fixt的值分别是\u0026quot;aaa\u0026quot;和\u0026quot;bbb\u0026quot;。indirect=True结合fixture可以在生成test前，对参数变量额外处理。\nimport pytest @pytest.fixture def fixt(request): return request.param * 3 @pytest.mark.parametrize(\u0026#34;fixt\u0026#34;, [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;], indirect=True) def test_indirect(fixt): assert len(fixt) == 3 6.多个参数时，indirect赋值list可以指定某些变量应用fixture，没有指定的保持原值。\n## content of test_indirect_list.py import pytest @pytest.fixture(scope=\u0026#34;function\u0026#34;) def x(request): return request.param * 3 @pytest.fixture(scope=\u0026#34;function\u0026#34;) def y(request): return request.param * 2 @pytest.mark.parametrize(\u0026#34;x, y\u0026#34;, [(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;)], indirect=[\u0026#34;x\u0026#34;]) def test_indirect(x, y): assert x == \u0026#34;aaa\u0026#34; assert y == \u0026#34;b\u0026#34; 7.兼容unittest参数化\n## content of ./test_parametrize.py import pytest def pytest_generate_tests(metafunc): # called once per each test function funcarglist = metafunc.cls.params[metafunc.function.__name__] argnames = sorted(funcarglist[0]) metafunc.parametrize( argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist] ) class TestClass: # a map specifying multiple argument sets for a test method params = { \u0026#34;test_equals\u0026#34;: [dict(a=1, b=2), dict(a=3, b=3)], \u0026#34;test_zerodivision\u0026#34;: [dict(a=1, b=0)], } def test_equals(self, a, b): assert a == b def test_zerodivision(self, a, b): with pytest.raises(ZeroDivisionError): a / b 8.在不同python解释器之间测试对象序列化。python1把对象pickle-dump到文件。python2从文件中pickle-load对象。\n\u0026#34;\u0026#34;\u0026#34; module containing a parametrized tests testing cross-python serialization via the pickle module. \u0026#34;\u0026#34;\u0026#34; import shutil import subprocess import textwrap import pytest pythonlist = [\u0026#34;python3.5\u0026#34;, \u0026#34;python3.6\u0026#34;, \u0026#34;python3.7\u0026#34;] @pytest.fixture(params=pythonlist) def python1(request, tmpdir): picklefile = tmpdir.join(\u0026#34;data.pickle\u0026#34;) return Python(request.param, picklefile) @pytest.fixture(params=pythonlist) def python2(request, python1): return Python(request.param, python1.picklefile) class Python: def __init__(self, version, picklefile): self.pythonpath = shutil.which(version) if not self.pythonpath: pytest.skip(\u0026#34;{!r} not found\u0026#34;.format(version)) self.picklefile = picklefile def dumps(self, obj): dumpfile = self.picklefile.dirpath(\u0026#34;dump.py\u0026#34;) dumpfile.write( textwrap.dedent( r\u0026#34;\u0026#34;\u0026#34; import pickle f = open({!r}, \u0026#39;wb\u0026#39;) s = pickle.dump({!r}, f, protocol=2) f.close() \u0026#34;\u0026#34;\u0026#34;.format( str(self.picklefile), obj ) ) ) subprocess.check_call((self.pythonpath, str(dumpfile))) def load_and_is_true(self, expression): loadfile = self.picklefile.dirpath(\u0026#34;load.py\u0026#34;) loadfile.write( textwrap.dedent( r\u0026#34;\u0026#34;\u0026#34; import pickle f = open({!r}, \u0026#39;rb\u0026#39;) obj = pickle.load(f) f.close() res = eval({!r}) if not res: raise SystemExit(1) \u0026#34;\u0026#34;\u0026#34;.format( str(self.picklefile), expression ) ) ) print(loadfile) subprocess.check_call((self.pythonpath, str(loadfile))) @pytest.mark.parametrize(\u0026#34;obj\u0026#34;, [42, {}, {1: 3}]) def test_basic_objects(python1, python2, obj): python1.dumps(obj) python2.load_and_is_true(\u0026#34;obj == {}\u0026#34;.format(obj)) 9.假设有个API，basemod是原始版本，optmod是优化版本，验证二者结果一致。\n## content of conftest.py import pytest @pytest.fixture(scope=\u0026#34;session\u0026#34;) def basemod(request): return pytest.importorskip(\u0026#34;base\u0026#34;) @pytest.fixture(scope=\u0026#34;session\u0026#34;, params=[\u0026#34;opt1\u0026#34;, \u0026#34;opt2\u0026#34;]) def optmod(request): return pytest.importorskip(request.param) ## content of base.py def func1(): return 1 ## content of opt1.py def func1(): return 1.0001 ## content of test_module.py def test_func1(basemod, optmod): assert round(basemod.func1(), 3) == round(optmod.func1(), 3) 10.使用pytest.param添加marker和id。\n## content of test_pytest_param_example.py import pytest @pytest.mark.parametrize( \u0026#34;test_input,expected\u0026#34;, [ (\u0026#34;3+5\u0026#34;, 8), pytest.param(\u0026#34;1+7\u0026#34;, 8, marks=pytest.mark.basic), pytest.param(\u0026#34;2+4\u0026#34;, 6, marks=pytest.mark.basic, id=\u0026#34;basic_2+4\u0026#34;), pytest.param( \u0026#34;6*9\u0026#34;, 42, marks=[pytest.mark.basic, pytest.mark.xfail], id=\u0026#34;basic_6*9\u0026#34; ), ], ) def test_eval(test_input, expected): assert eval(test_input) == expected 11.使用pytest.raises让部分test抛出Error。\nfrom contextlib import contextmanager import pytest // 3.7+ from contextlib import nullcontext as does_not_raise @contextmanager def does_not_raise(): yield @pytest.mark.parametrize( \u0026#34;example_input,expectation\u0026#34;, [ (3, does_not_raise()), (2, does_not_raise()), (1, does_not_raise()), (0, pytest.raises(ZeroDivisionError)), ], ) def test_division(example_input, expectation): \u0026#34;\u0026#34;\u0026#34;Test how much I know division.\u0026#34;\u0026#34;\u0026#34; with expectation: assert (6 / example_input) is not None 简要回顾 本文先讲了参数化的语法，包括marker，fixture，hook方式，以及如何给参数添加marker，然后重点列举了几个实战示例。参数化用好了能节省编码，达到事半功倍的效果。\n参考资料\ndocs-pytest-org-en-stable\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/pytest/000005-pytestpytest%E5%8F%82%E6%95%B0%E5%8C%96%E8%BF%9B%E9%98%B6/","summary":"用过unittest的朋友，肯定知道可以借助DDT实现参数化。用过JMeter的朋友，肯定知道JMeter自带了4种参数化方式（见参考资料）。pytest同样支持参数化，而且很简单很实用。","title":"pytest参数化进阶"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nBuild Systems 选择Maven or Gradle，而不要Ant（not particularly well supported）\nIn practice, you do not need to provide a version for any of these dependencies (spring-boot-dependencies) in your build configuration, as Spring Boot manages that for you. When you upgrade Spring Boot itself, these dependencies are upgraded as well in a consistent way.\nEach release of Spring Boot is associated with a base version of the Spring Framework.\nStarters are a set of convenient dependency descriptors that you can include in your application.\nstarter命名区别：\nofficial spring-boot-starter-*\nthird-party thirdpartyproject-spring-boot-starter\nStructuring We generally recommend that you locate your main application class in a root package above other classes.\ncom +- example +- myapplication +- MyApplication.java | +- customer | +- Customer.java | +- CustomerController.java | +- CustomerService.java | +- CustomerRepository.java | +- order +- Order.java +- OrderController.java +- OrderService.java +- OrderRepository.java 实际项目见得多的是controller、service这样的package，然后里面放多个文件。\nConfiguration Although it is possible to use SpringApplication with XML sources, we generally recommend that your primary source be a single @Configuration class.\nThe @Import annotation can be used to import additional configuration classes. Alternatively, you can use @ComponentScan to automatically pick up all Spring components, including @Configuration classes.\n（因为@SpringBootApplication包含了@ComponentScan，所以能自动扫到@Configuration的配置类）\n@ImportResource annotation to load XML configuration files.\nAuto-configuration Spring Boot auto-configuration attempts to automatically configure your Spring application based on the jar dependencies that you have added.\nFor example, if HSQLDB is on your classpath, and you have not manually configured any database connection beans, then Spring Boot auto-configures an in-memory database. If you add your own DataSource bean, the default embedded database support backs away.\nexclude auto-configuration：\n①annotation\nimport org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) public class MyApplication { } If the class is not on the classpath, you can use the excludeName attribute of the annotation and specify the fully qualified name instead.\n②property\nspring.autoconfigure.exclude Spring Beans and Dependency Injection Spring Beans可以理解为功能模块\n@ComponentScan to find beans，all of your application components (@Component, @Service, @Repository, @Controller, and others) are automatically registered as Spring Beans.\nconstructor injection：\nimport org.springframework.stereotype.Service; @Service public class MyAccountService implements AccountService { private final RiskAssessor riskAssessor; public MyAccountService(RiskAssessor riskAssessor) { this.riskAssessor = riskAssessor; } // ... } If a bean has more than one constructor, you will need to mark the one you want Spring to use with @Autowired:\nimport java.io.PrintStream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MyAccountService implements AccountService { private final RiskAssessor riskAssessor; private final PrintStream out; @Autowired public MyAccountService(RiskAssessor riskAssessor) { this.riskAssessor = riskAssessor; this.out = System.out; } public MyAccountService(RiskAssessor riskAssessor, PrintStream out) { this.riskAssessor = riskAssessor; this.out = out; } // ... } （更常见的做法是直接在类变量定义时使用@Autowired或@Resource，省去constructor）\n@SpringBootApplication @EnableAutoConfiguration: enable Spring Boot’s auto-configuration mechanism\n@ComponentScan: enable @Component scan on the package where the application is located (see the best practices)\n@SpringBootConfiguration: enable registration of extra beans in the context or the import of additional configuration classes. An alternative to Spring’s standard @Configuration that aids configuration detection in your integration tests.\nimport org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // Same as @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } Running jar包：using an embedded HTTP server\nwar包：your server\n参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000005-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B02%E4%BD%BF%E7%94%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"build-systems\"\u003eBuild Systems\u003c/h1\u003e\n\u003cp\u003e选择Maven or Gradle，而不要Ant（not particularly well supported）\u003c/p\u003e\n\u003cp\u003eIn practice, you do not need to provide a \u003cstrong\u003eversion\u003c/strong\u003e for any of these dependencies (spring-boot-dependencies) in your build configuration, as Spring Boot manages that for you. When you upgrade Spring Boot itself, these dependencies are upgraded as well in a consistent way.\u003c/p\u003e\n\u003cp\u003eEach release of Spring Boot is associated with a base version of \u003cstrong\u003ethe Spring Framework\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStarters\u003c/strong\u003e are a set of convenient dependency descriptors that you can include in your application.\u003c/p\u003e","title":"SpringBoot官方笔记2使用"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\ntep是一款测试工具，在pytest测试框架基础上集成了第三方包，提供项目脚手架，帮助以写Python代码方式，快速实现自动化项目落地。fixture是pytest核心技术，本文聊聊如何使用fixture完成变量、接口、用例3个级别复用。\n技术原理 fixture是一种特殊函数，实现了依赖注入，pytest规定，只要在conftest.py模块中定义了fixture，作为参数传给测试函数test(fixture_name)就可以使用，无需import。tep在conftest.py上封装了一层fixtures，类似于Django初始化数据加载的fixtures，定义了env_vars环境变量，预置了登录接口等。整体思路如下图所示：\n实线：表示fixture流转，从tep.fixture预置fixture，到fixtures，到conftest.py，到tests。 虚线：表示动态变量流转，参考JMeter vars.put()和vars.get()，实现用例级别复用。 蓝色线：表示静态变量流转，参考JMeter自定义用户变量，实现变量级别复用。 tep.fixture tep.fixture第一块内容是预置fixture的实现代码，如url、faker_ch等，对使用者隐藏。第二块内容是类TepVar，预置了1个Python字典vars_和2个存取方法put()、get()，划分了一块key-value形式存储区域。\nfixtures fixtures是一个包，里面包括多个fixture_*.py格式的Python模块，定义的全部都是fixture，只对外提供fixture，不提供function。\n自动注册 conftest.py是pytest的特殊文件，文件名固定，tep在其中实现了自动注册功能，它会自动查找fixtures目录下，所有以fixture_开头，以.py结尾的Python模块进行导入，无需手动添加。注册后，tests才能够找到fixtures中定义的fixture，否则报错fixture not found。\ntests tests存放所有测试用例，tep建议每个Python模块中只定义1个test()函数，用例分离，独立运行。\n参考了HttpRunner每个yaml文件1条用例的做法。复杂场景可以定义多个测试test()函数或Test类，pytest都支持。\n使用示例 变量级别复用 函数局部变量\nimport jmespath from loguru import logger from tep.client import request def test_post(faker_ch, url, login): # description logger.info(\u0026#34;test post\u0026#34;) # data fake = faker_ch # request response = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users\u0026#34;), headers=login.jwt_headers, json={ \u0026#34;name\u0026#34;: fake.name() } ) # assert assert response.status_code \u0026lt; 400 # extract user_id = jmespath.search(\u0026#34;id\u0026#34;, response.json()) 函数内部变量，作用域是整条测试用例，可以在多个接口之间进行复用，比如参数化和关联：\n全局环境变量 #!/usr/bin/python # encoding=utf-8 \u0026#34;\u0026#34;\u0026#34; Can only be modified by the administrator. Only fixtures are provided. \u0026#34;\u0026#34;\u0026#34; from tep.dao import mysql_engine from tep.fixture import * @pytest.fixture(scope=\u0026#34;session\u0026#34;) def env_vars(config): class Clazz(TepVars): env = config[\u0026#34;env\u0026#34;] \u0026#34;\u0026#34;\u0026#34;Variables define start\u0026#34;\u0026#34;\u0026#34; # Environment and variables mapping = { \u0026#34;qa\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://qa.com\u0026#34;, \u0026#34;mysql_engine\u0026#34;: mysql_engine(\u0026#34;127.0.0.1\u0026#34;, # host \u0026#34;2306\u0026#34;, # port \u0026#34;root\u0026#34;, # username \u0026#34;123456\u0026#34;, # password \u0026#34;qa\u0026#34;), # db_name }, \u0026#34;release\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://release.com\u0026#34;, \u0026#34;mysql_engine\u0026#34;: mysql_engine(\u0026#34;127.0.0.1\u0026#34;, \u0026#34;2306\u0026#34;, \u0026#34;root\u0026#34;, \u0026#34;123456\u0026#34;, \u0026#34;release\u0026#34;), } # Add your environment and variables } # Define properties for auto display domain = mapping[env][\u0026#34;domain\u0026#34;] mysql_engine = mapping[env][\u0026#34;mysql_engine\u0026#34;] \u0026#34;\u0026#34;\u0026#34;Variables define end\u0026#34;\u0026#34;\u0026#34; return Clazz() @pytest.fixture def project_level(): pass 使用：\ndef test(env_vars, env_vars_your_name): print(env_vars.common_var) print(env_vars_your_name.my_var) 全局环境变量由fixtures提供：\n核心数据是env_vars.Clazz类里面的mapping、domain、mysql_engine等属性：\n这么做的目的是为了在PyCharm中敲代码，输入.后能自动带出来，提高编码效率。\nfixture_admin.py建议由管理员维护项目级别的公共变量，fixture_your_name.py由团队成员定义自己的环境变量，避免冲突。它们还有一个区别是fixture_admin.py的env_vars继承了tep.fixture.TepVar，支持env_vars.put()和env_vars.get()。\n变量都由fixture提供。\n接口级别复用 tep预置登录接口fixture_login.py已经实现了接口级别复用，其他接口可以参照实现：\nfrom tep.client import request from tep.fixture import * def _jwt_headers(token): return {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;authorization\u0026#34;: f\u0026#34;Bearer {token}\u0026#34;} @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login(url): # Code your login logger.info(\u0026#34;Administrator login\u0026#34;) response = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users/login\u0026#34;), headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 response_token = jmespath.search(\u0026#34;token\u0026#34;, response.json()) class Clazz: token = response_token jwt_headers = _jwt_headers(response_token) return Clazz 使用：\nfrom loguru import logger def test_login(login): logger.info(login.token) 用例级别复用 a_test.py，定义1条测试用例test_a()，使用faker_ch生成随机name，通过env_vars.put()方法写入TepVar.vars_中：\ndef test_a(faker_ch, env_vars): name = faker_ch.name() env_vars.put(\u0026#34;name\u0026#34;, name) reuse_a_test.py，定义另外1条测试用例test()，import test_a后手动调用执行，从TepVar.vars_中读取name：\nfrom tests.sample.case_reuse.a_test import test_a def test(faker_ch, env_vars): test_a(faker_ch, env_vars) print(env_vars.get(\u0026#34;name\u0026#34;)) 注意test_a()函数参数引用的所有fixture，需要在test()函数参数中重新申明一遍，比如示例中的(faker_ch, env_vars)。\ntep是借助conftest.py和fixture，用类变量来做的，参考了JMeter BeanShell的vars。\n也可以外接redis。\ntep0.6.0更新 tep用户手册是根据0.5.3版本写的，现在更新了0.6.0版本，一是支持本文提到的3个级别复用，微调了fixtures，二是把项目脚手架代码尽量封装到tep中，减少后续升级成本。涉及到4个文件：conftest.py、fixture_admin.py、fixture_login.py、fixture_your_name.py。\n更新tep\npip install tep==0.6.0 如果还没有用过tep，使用这条命令安装后，执行tep startproject project_name就可以创建0.6.0版本的项目脚手架，包含了预置代码结构和sample测试用例。 如果已经安装了tep，也需要执行这条命令升级0.6.0版本。 老项目升级（可选）\n老项目升级也很简单，先在项目根目录执行命令：\ntep -U 它会自动替换conftest.py，考虑到老项目fixtures已经有真实数据了，这里给出了version0.6.0文件包，命令执行后在项目根目录生成：\n把老项目的真实数据提取出来，手动替换一下就可以了：\n小结 本文介绍了如何使用pytest测试框架实现变量、接口、用例3个级别复用，给出了在tep中的使用示例，然后提供了安装升级tep0.6.0的方法。这套思路借鉴了JMeter和HttpRunner的部分设计思想，以及我使用过的接口测试平台经验，在pytest上实践了一下。tep只是测试工具，本质上还是写pytest，感兴趣可以试一下哦。\n参考资料：\n源码 https://github.com/dongfanger/tep\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000005-tep0.6.0%E6%9B%B4%E6%96%B0%E8%81%8A%E8%81%8Apytest%E5%8F%98%E9%87%8F%E6%8E%A5%E5%8F%A3%E7%94%A8%E4%BE%8B3%E4%B8%AA%E7%BA%A7%E5%88%AB%E5%A4%8D%E7%94%A8/","summary":"tep是一款测试工具，在pytest测试框架基础上集成了第三方包，提供项目脚手架，帮助以写Python代码方式，快速实现自动化项目落地。fixture是pytest核心技术，本文聊聊如何使用fixture完成变量、接口、用例3个级别复用。","title":"tep0.6.0更新聊聊pytest变量接口用例3个级别复用"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文开发内容 用例管理是对用例进行增删改查，按照前面文章的思路，把它做出来应该不难，如果你已经自己写好了，那么可以和本文提交的代码比较下看看。除了增删改查，用例管理还需要提供运行用例的入口，在操作列添加一个运行按钮，单条用例运行，并弹窗展示运行结果。用例列表需要能看到每条用例执行情况，添加表格列用于展示，其中“运行结果”列要有超链接，点击查看上次运行结果。为了避免修改别人用例出错，还需要有个复制用例功能。除了在线编辑，平台应支持下载项目环境到本地，无缝切换到PyCharm，让新用户快速上手。综上所述，本文开发内容如下：\n用例增删改查。 运行按钮，点击后弹出“运行用例结果”。 添加表格展示列，其中“运行结果”用颜色区分不同结果状态：passed，failed，error，点击超链接后弹出“查看用例结果”。 复制按钮，点击后生成一条复制的新用例。 新增/编辑页面，添加运行按钮。 运行/查看页面，添加编辑按钮。 下载环境到本地。 工作量主要在前端，后端先做一部分。\n编写后端代码 编辑teprunner/urls.py，添加路由：\n除了增删改查，还添加了2个接口，一个用于获取用例运行的结果，一个用于复制测试用例。\n编辑teprunner/models.py，添加模型：\n第一个Model是Case，没有定义name字段，实际上用例ID和用例描述已经够用了，name不知道填什么，有点多余。第二个Model是CaseResult，会记录运行结果，这个运行结果是从pytest执行日志最后一行的Summary截取出来的，比如1 passed，1 failed，耗时也是从Summary截取出来的。output存放代码运行后打到控制台的输出日志。最后三个字段记录运行环境、运行人和运行时间，运行时间取的数据库记录被添加/更新的时间。添加好以后迁移到数据库中：\npython manage.py makemigrations python manage.py migrate 编辑teprunner/serializers.py，添加序列化器，第一个序列化器是CaseSerializer：\n只关联了Case模型的字段。\n第二个序列化器是CaseListSerializer：\n它主要用来返回用例列表的数据，除了用例本身，还需要用例结果。serializers.SerializerMethodField用于自定义序列化字段，定义好字段后，在下面添加get_前缀+字段名的方法，这是Django的规则，然后在方法内部添加取值代码，比如get_result()，instance是指Meta.model定义的模型，这里就等同于Case，通过instance.id获取到Case.id并赋值给case_id变量，从CaseResult的对象中过滤case_id匹配的用例结果，按运行时间倒序，取最新1条结果的result字段的值。其他字段的处理是类似的，runTime这里做了下日期格式化：\n第三个序列化器是CaseResultSerializer：\n只关联了CaseResult模型的字段，runTime同样做了日期格式化。从这里也能更清晰的看到，instance就是指的自己！\n新建teprunner/views/case.py文件，添加视图，先把简单的两个视图做了，第一个视图是case_result：\n这里是个知识点，采用@api_view注解的函数视图，如果想用序列化器，那么采用这种方式。\n第二个视图是copy_case：\n根据case_id查找到现有Case对象，在case.desc后面添加--复制后缀，其他字段数据复用，保存，就完成复制用例了。\n第三个视图是CaseViewSet：\n继承了ModelViewSet，重写了list列表查询，因为如果不重写，默认返回的结果是serializer_class = CaseSerializer序列化器的内容。我们需要的是CaseListSerializer这个序列化器的内容。这里也是一个知识点！如果想在类视图中，使用serializer_class以外的序列化器加分页，采用这种方式。\n重写的list方法首先添加了project_id、case_id、desc、api四个过滤条件，与前端的当前项目、用例ID搜索框、用例描述搜索框、API路径搜索框一一对应。__icontains表示包含，相当于模糊匹配，i忽略大小写。接着用到了自定义分页的类，按照统一的分页格式，返回序列化器的数据。\n最后，重写了update方法，用现有的创建人进行更新。\n编写前端代码 编辑router/index.js，添加路由：\n编辑views/teprunner/index.vue，添加菜单项：\n新建views/teprunner/case/CaseManagement.vue文件，增删改查基础代码不再另加赘述，挑选新代码：\n在运行结果列，添加了超链接，点击后打开用例结果弹窗，这个弹窗查看和运行用例都会用到，用第一个参数type做区分：\ngetResultColor方法返回颜色样式：\n由于用例结果弹窗和用例列表表格都会有这个字段，于是把resultColor方法提取到了utils/commonMethods.js文件中：\nCaseManagement.vue是一个父组件，它有3个子组件，如下图所示：\n通过import后，添加标签代码引入：\n是否显示弹窗通过DialogFormVisible的true/false来控制，DialogTitle指定弹窗标题。:开头是父组件向子组件传值， @开头是接受子组件发过来的emit事件通知，从而调用父组件中方法。这样就把这几个组件绑定在一起了！\n从这里开始，这几个组件之间的关系会稍微有点绕，读不懂的话多结合源码看看！\n有个小细节，运行按钮打开的弹框在关闭后，需要刷新用例列表更新数据，表格超链接打开的弹框关闭后不需要刷新：\n代码的意思是在打开弹窗时给this.afterCloseRefreshList赋值为true/false，在关闭弹窗时判断要不要刷新列表。\n接下来对这三个弹窗逐一进行开发。新建views/teprunner/case/AddCase.vue文件，增改代码不再赘述，添加1个运行按钮：\n点击后调用：\n首先校验必填项，校验通过后，发起请求：\n增加了一个type参数，因为选择保存或者运行，是两种类型，只有点击运行按钮，才用emit注册运行事件。新增用例的时候，没有用例ID，就不能直接运行用例，为了解决这个问题，把保存后接口响应中的ID，作为运行用例ID。\n新建views/teprunner/case/CaseResult.vue文件：\n把代码编辑区设置为只读和自动换行。添加编辑按钮：\n点击后调用：\n关闭弹窗，注册编辑事件。\n这里把刚才结合到一起说一下，“运行/查看用例结果”弹框和“新增/编辑用例”弹框可以通过运行/编辑按钮相互切换，它们是通过注册事件，发送到父组件CaseManagement.vue，再分别通知下去来实现的：\n打开结果弹窗后，调用接口查询用例描述和创建人：\n根据类型执行runCase或getResult：\n最后，新建views/teprunner/case/WriteDown.vue文件，大同小异，新代码是：\n从百度搜来的，把后端返回的二进制文件字节流组装成压缩包，打开系统弹窗，下载文件。\n小插曲 这一版比第一版代码优化了不少，尤其是几个组件之间如何传值这里，简化了很多，比如把执行用例从CaseManagement.vue转移到CaseResult.vue中来调用。在折腾过程中，也基本掌握了组件传值的写法，总结如下：\n①父组件传值给子组件：子组件props接收，父组件标签绑定data传值，比如：\n## 父组件 \u0026lt;CaseResultDialog :runEnv.sync=\u0026#34;runEnv\u0026#34; /\u0026gt; data() { return { runEnv: \u0026#34;\u0026#34;, }; }, ## 子组件 props: { runEnv: { type: String, default: \u0026#34;\u0026#34;, }, }, ②子组件通知父组件：子组件this.$emit注册事件，父组件标签@调用，比如：\n## 父组件 \u0026lt;CaseResultDialog @closeCaseResultDialog=\u0026#34;closeCaseResultDialog\u0026#34; /\u0026gt; closeCaseResultDialog() { console.log(\u0026#34;close\u0026#34;); }, ## 子组件 close() { this.$emit(\u0026#34;closeCaseResultDialog\u0026#34;); }, ③子组件修改父组件传值：先把子组件props赋值给子组件data，再watch子组件data，比如：\n## 父组件 \u0026lt;CaseResultDialog :runEnv.sync=\u0026#34;runEnv\u0026#34; /\u0026gt; data() { return { runEnv: \u0026#34;\u0026#34;, }; }, ## 子组件 props: { runEnv: { type: String, default: \u0026#34;\u0026#34;, }, }, data() { return { runEnv: this.runEnv, }; }, watch: { runEnv(val) { this.caseForm.runEnv = val; }, }, 第一版有很多第三种方式的代码，因为要把CaseManagement的执行结果传到CaseResult中去，在关闭弹窗时又需要清空，这些数据是从CaseManagement传过来的，直接改Vue会报错：禁止子组件直接修改父组件数据，防止数据错乱。比较麻烦，重写这一版优化了很多。\n小结 本文的后端开发除了增删改查，还给出了Django REST framework函数视图如何引用序列化器，类视图重写方法如何引用非serializer_class绑定序列化器加分页。前端开发稍微有点复杂，尤其是父子组件如何传值，可能结合代码看看会更便于理解。从开篇写到这里，用户管理、项目管理、环境变量、fixtures、用例增删改查、运行用例、结果展示、下载环境，以tep为中间层的测试平台壳子已经做好了，万事已具备，只等pytest注入灵魂。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000005-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E5%BC%80%E5%8F%91%E7%94%A8%E4%BE%8B%E7%AE%A1%E7%90%86%E4%B8%8D%E5%8F%AA%E6%9C%89%E5%A2%9E%E5%88%A0%E6%94%B9%E6%9F%A5/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"本文开发内容\"\u003e本文开发内容\u003c/h1\u003e\n\u003cp\u003e用例管理是对用例进行增删改查，按照前面文章的思路，把它做出来应该不难，如果你已经自己写好了，那么可以和本文提交的代码比较下看看。除了增删改查，用例管理还需要提供运行用例的入口，在操作列添加一个运行按钮，单条用例运行，并弹窗展示运行结果。用例列表需要能看到每条用例执行情况，添加表格列用于展示，其中“运行结果”列要有超链接，点击查看上次运行结果。为了避免修改别人用例出错，还需要有个复制用例功能。除了在线编辑，平台应支持下载项目环境到本地，无缝切换到PyCharm，让新用户快速上手。综上所述，本文开发内容如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e用例增删改查。\u003c/li\u003e\n\u003cli\u003e运行按钮，点击后弹出“运行用例结果”。\u003c/li\u003e\n\u003cli\u003e添加表格展示列，其中“运行结果”用颜色区分不同结果状态：passed，failed，error，点击超链接后弹出“查看用例结果”。\u003c/li\u003e\n\u003cli\u003e复制按钮，点击后生成一条复制的新用例。\u003c/li\u003e\n\u003cli\u003e新增/编辑页面，添加运行按钮。\u003c/li\u003e\n\u003cli\u003e运行/查看页面，添加编辑按钮。\u003c/li\u003e\n\u003cli\u003e下载环境到本地。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e工作量主要在前端，后端先做一部分。\u003c/p\u003e\n\u003ch1 id=\"编写后端代码\"\u003e编写后端代码\u003c/h1\u003e\n\u003cp\u003e编辑teprunner/urls.py，添加路由：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/212bda04278c4eea864f64edbe8e6c2b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e除了增删改查，还添加了2个接口，一个用于获取用例运行的结果，一个用于复制测试用例。\u003c/p\u003e\n\u003cp\u003e编辑teprunner/models.py，添加模型：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ebbca98ae0734477b8b8ee8a2b2713bd.png\"\u003e\u003c/p\u003e\n\u003cp\u003e第一个Model是Case，没有定义name字段，实际上用例ID和用例描述已经够用了，name不知道填什么，有点多余。第二个Model是CaseResult，会记录运行结果，这个运行结果是从pytest执行日志最后一行的Summary截取出来的，比如1 passed，1 failed，耗时也是从Summary截取出来的。output存放代码运行后打到控制台的输出日志。最后三个字段记录运行环境、运行人和运行时间，运行时间取的数据库记录被添加/更新的时间。添加好以后迁移到数据库中：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython manage.py makemigrations\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython manage.py migrate\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e编辑teprunner/serializers.py，添加序列化器，第一个序列化器是CaseSerializer：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fd7c0235d6684a7aae21827c39a2af96.png\"\u003e\u003c/p\u003e\n\u003cp\u003e只关联了Case模型的字段。\u003c/p\u003e\n\u003cp\u003e第二个序列化器是CaseListSerializer：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/096c709c347f463d9d4c9d1f946d7947.png\"\u003e\u003c/p\u003e\n\u003cp\u003e它主要用来返回用例列表的数据，除了用例本身，还需要用例结果。serializers.SerializerMethodField用于自定义序列化字段，定义好字段后，在下面添加\u003ccode\u003eget_前缀+字段名\u003c/code\u003e的方法，这是Django的规则，然后在方法内部添加取值代码，比如get_result()，instance是指Meta.model定义的模型，这里就等同于Case，通过instance.id获取到Case.id并赋值给case_id变量，从CaseResult的对象中过滤case_id匹配的用例结果，按运行时间倒序，取最新1条结果的result字段的值。其他字段的处理是类似的，runTime这里做了下日期格式化：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/499311186fe74d6bbcee6823a98568e9.png\"\u003e\u003c/p\u003e\n\u003cp\u003e第三个序列化器是CaseResultSerializer：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4b27b96941f743458fa42081b8e935fb.png\"\u003e\u003c/p\u003e\n\u003cp\u003e只关联了CaseResult模型的字段，runTime同样做了日期格式化。从这里也能更清晰的看到，instance就是指的自己！\u003c/p\u003e\n\u003cp\u003e新建teprunner/views/case.py文件，添加视图，先把简单的两个视图做了，第一个视图是case_result：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/81e74079171b400ea9fa0cc15e2d4f84.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这里是个知识点，采用@api_view注解的函数视图，如果想用序列化器，那么采用这种方式。\u003c/p\u003e\n\u003cp\u003e第二个视图是copy_case：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d4b66089366b4b229daddb4fc2a34f1a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e根据case_id查找到现有Case对象，在case.desc后面添加\u003ccode\u003e--复制\u003c/code\u003e后缀，其他字段数据复用，保存，就完成复制用例了。\u003c/p\u003e\n\u003cp\u003e第三个视图是CaseViewSet：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/90921afa5bca4b8cb2702cad9dd684b2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e继承了ModelViewSet，重写了list列表查询，因为如果不重写，默认返回的结果是serializer_class = CaseSerializer序列化器的内容。我们需要的是CaseListSerializer这个序列化器的内容。这里也是一个知识点！如果想在类视图中，使用serializer_class以外的序列化器加分页，采用这种方式。\u003c/p\u003e\n\u003cp\u003e重写的list方法首先添加了project_id、case_id、desc、api四个过滤条件，与前端的当前项目、用例ID搜索框、用例描述搜索框、API路径搜索框一一对应。\u003ccode\u003e__icontains\u003c/code\u003e表示包含，相当于模糊匹配，\u003ccode\u003ei\u003c/code\u003e忽略大小写。接着用到了自定义分页的类，按照统一的分页格式，返回序列化器的数据。\u003c/p\u003e\n\u003cp\u003e最后，重写了update方法，用现有的创建人进行更新。\u003c/p\u003e\n\u003ch1 id=\"编写前端代码\"\u003e编写前端代码\u003c/h1\u003e\n\u003cp\u003e编辑router/index.js，添加路由：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/69b335361608450fb0556aba32e09d7c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e编辑views/teprunner/index.vue，添加菜单项：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/640655751b4b436dac0745857a6277fb.png\"\u003e\u003c/p\u003e\n\u003cp\u003e新建views/teprunner/case/CaseManagement.vue文件，增删改查基础代码不再另加赘述，挑选新代码：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/2781b1acd3b94379bbcf00bd2bd680c2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在运行结果列，添加了超链接，点击后打开用例结果弹窗，这个弹窗查看和运行用例都会用到，用第一个参数type做区分：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4dc3075bbd7a4f8c8fa13fd55a671c43.png\"\u003e\u003c/p\u003e\n\u003cp\u003egetResultColor方法返回颜色样式：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/410d96b7b51e4d6dbe08cd0630826c06.png\"\u003e\u003c/p\u003e\n\u003cp\u003e由于用例结果弹窗和用例列表表格都会有这个字段，于是把resultColor方法提取到了utils/commonMethods.js文件中：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0ed151d7f10d4d7eb7c372791fd864e0.png\"\u003e\u003c/p\u003e\n\u003cp\u003eCaseManagement.vue是一个父组件，它有3个子组件，如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/acca7566c3964a9c9054b84a6e155371.png\"\u003e\u003c/p\u003e\n\u003cp\u003e通过import后，添加标签代码引入：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c0bb29a4d8d8477f931c89d133ddfa77.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a6572cbc9616490aaa6fb03a7e576ac3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e是否显示弹窗通过DialogFormVisible的true/false来控制，DialogTitle指定弹窗标题。\u003ccode\u003e:\u003c/code\u003e开头是父组件向子组件传值， \u003ccode\u003e@\u003c/code\u003e开头是接受子组件发过来的emit事件通知，从而调用父组件中方法。这样就把这几个组件绑定在一起了！\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e从这里开始，这几个组件之间的关系会稍微有点绕，读不懂的话多结合源码看看！\u003c/p\u003e\n\u003c/blockquote\u003e","title":"teprunner测试平台开发用例管理不只有增删改查"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n什么是Playwright？ Playwright是一个开源框架，用于对Web应用进行端到端测试。\n什么是端到端测试？ 这里的“端”，不是指A模块或B模块，而是可以理解为从前端到后端。举个例子，对于登录功能来说，如果只测前端，那么测试范围就是前端页面，可能会对后端接口进行mock。如果只测后端，那么测试范围就是后端接口等。而端到端呢，就是前端输入用户名密码，点击登录，与后端交互，查询数据库，鉴权，后端返回登录成功，前端进行页面跳转。端到端测试，可以理解为全流程测试，也可以理解为模拟用户场景测试。\nPlaywright框架特点 官方网站首页：https://playwright.dev/python/\n总结下重点，一是跨浏览器、跨平台、跨语言；二是自动等待；三是多用户场景测试；四是只需要一次登录；五是可以录制脚本。\n快速入门 在官网的顶部菜单可以切换不同编程语言的教程：\n本文及后续文章均选择Python。\n安装 安装Python的Playwright，这里结合了pytest：\npip install pytest-playwright 成功后安装浏览器：\nplaywright install 这里也可以不安装浏览器，而是在脚本里面指定现有浏览器路径。\n示例 test_example.py\nimport re from playwright.sync_api import Page, expect def test_has_title(page: Page): page.goto(\u0026#34;https://playwright.dev/\u0026#34;) # Expect a title \u0026#34;to contain\u0026#34; a substring. expect(page).to_have_title(re.compile(\u0026#34;Playwright\u0026#34;)) def test_get_started_link(page: Page): page.goto(\u0026#34;https://playwright.dev/\u0026#34;) # Click the get started link. page.get_by_role(\u0026#34;link\u0026#34;, name=\u0026#34;Get started\u0026#34;).click() # Expects page to have a heading with the name of Installation. expect(page.get_by_role(\u0026#34;heading\u0026#34;, name=\u0026#34;Installation\u0026#34;)).to_be_visible() 执行pytest后默认会选择chrome在headless模式下运行，可以通过日志观察运行过程。\n更新命令：\npip install pytest-playwright playwright -U 编写测试 访问url：\npage.goto(\u0026#34;https://playwright.dev/\u0026#34;) 通过locator定位页面元素进行交互：\n## Create a locator. get_started = page.get_by_role(\u0026#34;link\u0026#34;, name=\u0026#34;Get started\u0026#34;) ## Click it. get_started.click() 也可以写成一行：\npage.get_by_role(\u0026#34;link\u0026#34;, name=\u0026#34;Get started\u0026#34;).click() 常用的有以下方法：\n更多请访问：https://playwright.dev/python/docs/api/class-locator\n断言：\nimport re from playwright.sync_api import expect expect(page).to_have_title(re.compile(\u0026#34;Playwright\u0026#34;)) 常见断言如下：\n更多请访问：https://playwright.dev/python/docs/test-assertions\nPlaywright的test是相互独立的：\nfrom playwright.sync_api import Page def test_example_test(page: Page): pass # \u0026#34;page\u0026#34; belongs to an isolated BrowserContext, created for this specific test. def test_another_test(page: Page): pass # \u0026#34;page\u0026#34; in this second test is completely isolated from the first test. pytest的fixture也可以直接使用，比如在所有测试开始前和结束前执行某些操作：\nimport pytest from playwright.sync_api import Page, expect @pytest.fixture(scope=\u0026#34;function\u0026#34;, autouse=True) def before_each_after_each(page: Page): print(\u0026#34;before the test runs\u0026#34;) # Go to the starting url before each test. page.goto(\u0026#34;https://playwright.dev/\u0026#34;) yield print(\u0026#34;after the test runs\u0026#34;) def test_main_navigation(page: Page): # Assertions use the expect API. expect(page).to_have_url(\u0026#34;https://playwright.dev/\u0026#34;) 生成测试 使用codegen：\nplaywright codegen demo.playwright.dev/todomvc 然后就可以录制脚本了：\n更多用法阅读：https://playwright.dev/python/docs/codegen\n生成locators：\n运行和调试 运行命令基本都是基于pytest，然后加了些参数：\npytest pytest --headed pytest --browser webkit pytest --browser webkit --browser firefox pytest test_login.py pytest tests/test_todo_page.py tests/test_landing_page.py pytest -k test_add_a_todo_item pytest --numprocesses 2 调试通过Playwright Inspector进行：\n可以根据IDE比如PyCharm或Visual Studio，查找相应调试方法。\nTrace viewer 监控Playwright，开启命令：\npytest --tracing on 打开监控：\nplaywright show-trace trace.zip 如果你打算做前端UI自动化，可以试试Playwright这款开源框架。\n参考资料\nhttps://playwright.dev/python/\nhttps://learn.microsoft.com/zh-cn/training/modules/build-with-playwright/2-introduction-to-playwright\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E8%87%AA%E5%8A%A8%E5%8C%96/000005-playwright%E7%AB%AF%E5%88%B0%E7%AB%AF%E8%87%AA%E5%8A%A8%E5%8C%96%E7%9A%84%E5%BC%80%E6%BA%90%E6%A1%86%E6%9E%B6/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"什么是playwright\"\u003e什么是Playwright？\u003c/h1\u003e\n\u003cp\u003ePlaywright是一个开源框架，用于对Web应用进行端到端测试。\u003c/p\u003e\n\u003ch1 id=\"什么是端到端测试\"\u003e什么是端到端测试？\u003c/h1\u003e\n\u003cp\u003e这里的“端”，不是指A模块或B模块，而是可以理解为从前端到后端。举个例子，对于登录功能来说，如果只测前端，那么测试范围就是前端页面，可能会对后端接口进行mock。如果只测后端，那么测试范围就是后端接口等。而端到端呢，就是前端输入用户名密码，点击登录，与后端交互，查询数据库，鉴权，后端返回登录成功，前端进行页面跳转。端到端测试，可以理解为全流程测试，也可以理解为模拟用户场景测试。\u003c/p\u003e\n\u003ch1 id=\"playwright框架特点\"\u003ePlaywright框架特点\u003c/h1\u003e\n\u003cp\u003e官方网站首页：\u003ca href=\"https://playwright.dev/python/\"\u003ehttps://playwright.dev/python/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e总结下重点，一是跨浏览器、跨平台、跨语言；二是自动等待；三是多用户场景测试；四是只需要一次登录；五是可以录制脚本。\u003c/p\u003e\n\u003ch1 id=\"快速入门\"\u003e快速入门\u003c/h1\u003e\n\u003cp\u003e在官网的顶部菜单可以切换不同编程语言的教程：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/24bbbd01c6594bc08a753059ab61356f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文及后续文章均选择Python。\u003c/p\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003cp\u003e安装Python的Playwright，这里结合了pytest：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epip install pytest-playwright\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e成功后安装浏览器：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eplaywright install\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这里也可以不安装浏览器，而是在脚本里面指定现有浏览器路径。\u003c/p\u003e\n\u003ch2 id=\"示例\"\u003e示例\u003c/h2\u003e\n\u003cp\u003etest_example.py\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eimport re\nfrom playwright.sync_api import Page, expect\n\ndef test_has_title(page: Page):\n    page.goto(\u0026#34;https://playwright.dev/\u0026#34;)\n\n    # Expect a title \u0026#34;to contain\u0026#34; a substring.\n    expect(page).to_have_title(re.compile(\u0026#34;Playwright\u0026#34;))\n\ndef test_get_started_link(page: Page):\n    page.goto(\u0026#34;https://playwright.dev/\u0026#34;)\n\n    # Click the get started link.\n    page.get_by_role(\u0026#34;link\u0026#34;, name=\u0026#34;Get started\u0026#34;).click()\n\n    # Expects page to have a heading with the name of Installation.\n    expect(page.get_by_role(\u0026#34;heading\u0026#34;, name=\u0026#34;Installation\u0026#34;)).to_be_visible()\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e执行pytest后默认会选择chrome在headless模式下运行，可以通过日志观察运行过程。\u003c/p\u003e\n\u003cp\u003e更新命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epip install pytest-playwright playwright -U\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"编写测试\"\u003e编写测试\u003c/h2\u003e\n\u003cp\u003e访问url：\u003c/p\u003e","title":"端到端自动化的开源框架"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n这3个问题本质上都是测试流程问题，解决的底层逻辑是建立自己的流程并灵活调整。\n我们先来分析下问题的关键点是什么？缺少时间。然后看看是为什么没有时间？第1个问题是因为公司要抢占市场，还没有时间来建设流程。第2个问题是需求任务太多，才没有时间写用例。第3个问题是多个测试任务并行，没时间走测试流程。\n部分观点是，在小公司哪管得了那么多，快速交付，抢占市场，能活下来才是最重要的。这个观点没有问题，这是我们无法改变的客观事实，要想推动建设公司流程，很难。但我们希望能找到一种方式，能够让“自己”的工作更顺畅点。\n聚焦到自己的工作，建立自己的流程，形成闭环。测试流程最核心的4个节点是用例设计、测试执行、缺陷管理、测试报告。按照这4个核心节点去顺序执行测试流程。一定要顺序执行，并灵活调整形式。\n比如说，研发提测了，时间还够就用XMind设计用例，时间不够就用文本写测试点，完全没时间，你自己也要先分析要测哪些内容，怎么测。把用例设计这一步做完以后，再去做测试执行。缺陷管理没有平台就聊天交流，你本地还是要做好记录，有哪些缺陷，哪些解决了，哪些没解决，写清楚。最后测完了要上线了，有时间就写份测试报告发个邮件，没时间也要评估上线风险，定测试结论，同步遗留问题。\n认知提升以后，针对第2个问题，就不会提出“哪还有时间写用例”这种经典问题了，因为用例设计这一步我是花时间做了的，只是形式可能不同，如果需要某种特定产物，就可以说“我现在只简单列了测试点，还没写成XMind用例”，这是可以理解的。\n按照测试流程有序进行工作，有助于构建软件测试知识体系。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000005-%E8%A7%A3%E5%86%B3%E6%B5%8B%E8%AF%95%E6%B5%81%E7%A8%8B%E9%97%AE%E9%A2%98%E7%9A%84%E5%BA%95%E5%B1%82%E9%80%BB%E8%BE%91/","summary":"这3个问题本质上都是测试流程问题，解决的底层逻辑是建立自己的流程并灵活调整。","title":"解决“测试流程”问题的底层逻辑"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPython 是强类型语言，在学习 Python 时，有必要了解 Python 有哪些基本数据类型，一共 6 个：Number（数字）、String（字符串）、List（列表）、Tuple（元组）、Set（集合）、Dictionary（字典）。数据类型的知识是非常多的，一篇文章讲不明白，本文仍然属于入门系列，内容主要是基础简介。\n更深层次的知识会在进阶篇再做剖析。进阶篇计划 2021 年 1 月份公众号改名后开始更新哦。\n赋值即用的变量 不像其他语言，变量必须先声明了才能使用，Python 的变量不需要声明，直接赋值就可以用了，而且必须赋值才能用哦，赋值后变量才会被创建。例如：\ncounter = 100 # 整型变量 miles = 1000.0 # 浮点型变量 name = \u0026#34;runoob\u0026#34; # 字符串 如果有多个变量，可以在一条语句中一起赋值，例如：\na = b = c = 1 或者：\na, b, c = 1, 2, \u0026#34;runoob\u0026#34; a 的值为 1，b 的值为 2，c 的值为 \u0026ldquo;runoob\u0026rdquo;。\nNumber（数字） 数字包括 int、float、bool、complex。\nint\n整型，例如：\na = 1 b = 2 float\n浮点数，也就是小数，例如：\na = 1.2 b = 3.4 bool\n布尔值，只有 True 和 False 2个值。\ncomplex\n复数，例如：\na = 3.14j b = 3e+26j String（字符串） 用单引号、多引号或三引号框起来的就是字符串，例如：\na = \u0026#39;hello\u0026#39; a = \u0026#34;hello\u0026#34; a = \u0026#34;\u0026#34;\u0026#34;hello\u0026#34;\u0026#34;\u0026#34; 可以使用 + 把多个字符串拼接在一起：\na = \u0026#34;Hello\u0026#34; + \u0026#34;World\u0026#34; 也可以使用 * 来重复：\na = \u0026#34;x\u0026#34; * 3 # 值为xxx 可以通过 变量[头下标:尾下标] 来截取部分字符串，正数表示从左边起，以 0 开始。负数表示从右边起，以 -1 开始。例如：\na = \u0026#34;qwert\u0026#34; b = a[0:2] # qw c = a[2:] # ert d = a[-2:-1] # r 倒数第二个字符 e = a[-1:] # t 最后一个字符 List（列表） 列表是由多个数据组成的，列表中的数据类型可以不同。它是用中括号 [] 括起来的，格式和索引如下：\n跟字符串一样，列表也可以使用 + 来合并列表，使用 * 来重复元素，使用 变量[头下标:尾下标] 来截取子列表。\n除此之外，列表使用 append() 函数来添加元素：\nb = [] b.append(4) b.append(5) # b 的值为 [4, 5] 使用 for ... in ... 来遍历：\na = [1, 2, 3] for i in a: print(i) Tuple（元组） 元组和列表类似，区别在于元组是使用小括号 () 括起来的，并且元组不能修改。例如：\ntup1 = () # 空元组 tup2 = (20,) # 一个元素，需要在元素后添加逗号 tup3 = (1, 2, 3) Set（集合） 集合和列表类似，区别在于集合的数据不能重复，所以常用来删除重复元素和做集合运算。例如：\nsites = {\u0026#39;Google\u0026#39;, \u0026#39;Taobao\u0026#39;, \u0026#39;Runoob\u0026#39;, \u0026#39;Facebook\u0026#39;, \u0026#39;Zhihu\u0026#39;, \u0026#39;Baidu\u0026#39;} # {\u0026#39;Zhihu\u0026#39;, \u0026#39;Baidu\u0026#39;, \u0026#39;Taobao\u0026#39;, \u0026#39;Runoob\u0026#39;, \u0026#39;Google\u0026#39;, \u0026#39;Facebook\u0026#39;} print(sites) # 输出集合，重复的元素被自动去掉 集合运算：\n# set可以进行集合运算 a = set(\u0026#39;abracadabra\u0026#39;) b = set(\u0026#39;alacazam\u0026#39;) # {\u0026#39;r\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;d\u0026#39;} print(a - b) # a 和 b 的差集 # {\u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;a\u0026#39;, \u0026#39;z\u0026#39;, \u0026#39;m\u0026#39;, \u0026#39;r\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;d\u0026#39;} print(a | b) # a 和 b 的并集 # {\u0026#39;c\u0026#39;, \u0026#39;a\u0026#39;} print(a \u0026amp; b) # a 和 b 的交集 # {\u0026#39;z\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;m\u0026#39;, \u0026#39;r\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;d\u0026#39;} print(a ^ b) # a 和 b 中不同时存在的元素 Dictionary（字典） 字典也是由多个数据组成的，是用花括号 {}框起来的，键(key) : 值(value) 形式的数据。例如：\na = {} # 创建空字典 b = dict() # 创建空字典 c = {\u0026#34;x\u0026#34;: 1, \u0026#34;y\u0026#34;: 2} # 创建字典同时赋值 c[\u0026#34;z\u0026#34;] = 3 # 使用中括号[]添加 print(c[\u0026#34;x\u0026#34;]) # 使用中括号[]根据key查询value 字典的遍历需要同时使用 for ... in ... 和 items() 函数：\na = {\u0026#34;x\u0026#34;: 1, \u0026#34;y\u0026#34;: 2} for k, v in a.items(): print(k, v) 数据类型转换 不同数据类型是可以相互转换的，以下是用于转换的函数：\n函数 描述 int(x) 将x转换为一个整数 float(x) 将x转换到一个浮点数 complex(real [,imag]) 创建一个复数 str(x) 将对象 x 转换为字符串 tuple(s) 将序列 s 转换为一个元组 list(s) 将序列 s 转换为一个列表 set(s) 转换为可变集合 dict(d) 创建一个字典。d 必须是一个 (key, value)元组序列。 简单的转换使用单个函数即可，但有时候我们会需要写点代码来实现特定的数据类型转换。比如把 2 个列表，转换为字典，一个列表作为 key，一个列表作为 value：\na = [\u0026#34;q\u0026#34;, \u0026#34;w\u0026#34;, \u0026#34;e\u0026#34;] b = [1, 2, 3] d = dict(zip(a, b)) # {\u0026#39;q\u0026#39;: 1, \u0026#39;w\u0026#39;: 2, \u0026#39;e\u0026#39;: 3} 小结 本文简要介绍了 Python 6 个基本数据类型的知识，点到为止。看到这篇文章的朋友可能会有疑问，入门系列文章的内容是如何界定的？我对入门系列的定位是前菜，先垫垫肚子，了解 Python 的基本知识。等到正餐硬菜上来的时候，不会无从下咽，不会慌不择食，不会有阅读门槛。我认为入门就应该简单点，不然要浅不浅，要深不深，不懂的人看着懵，懂的人看着这也没讲那也没讲。我写一下《流畅的 Python》前 4 章的标题，可以先感受一下，《第 1 章 Python 数据模型》、《第 2 章 序列构成的数组》、《第 3 章 字典和集合》、《第 4 章 文本和字节序列》。\n参考：\nhttps://www.runoob.com/python3/python3-data-type.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000005-python%E4%BD%A0%E7%9F%A5%E9%81%93python%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%98%AF%E5%93%AA6%E4%B8%AA%E4%B9%88/","summary":"Python 是强类型语言，在学习 Python 时，有必要了解 Python 有哪些基本数据类型，一共 6 个：Number（数字）、String（字符串）、List（列表）、Tuple（元组）、Set（集合）、Dictionary（字典）。数据类型的知识是非常多的，一篇文章讲不明白，本文仍然属于入门系列，内容主要是基础简介。","title":"你知道Python基本数据类型是哪6个么"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n字符集其实是个很简单的东西，但很多人当时看懂了，过了就忘了。\n本篇文章的目的就是，不一定要把字符集的知识讲的有多深，但是一定要在工作上够用。\n并且记得足够牢足够久。\n工作中遇到的“词汇”，主要是ASCII、GB2312、GBK、Unicode、UTF-8，还有URL Encode、URL Escape。\n编码和解码 编码，encode。\n解码，decode。\n编码，编成计算机能懂的码，二进制。\n解码，解成人类能读懂的码，人语言。\n想让计算机懂，你得编它。\n想让人类能懂，你得解释。\n字符代码和字符编码 字符代码就是序号，比如65。\n字符编码二进制序列，比如01000001。\n由于基本相同，所以我们平时就忽略了差异。\n差异体现在哪呢？UTF-8。\n字符集发展历史 美国，英文字母、标点符号字符、阿拉伯数字，ASCII。\n欧洲，加入带重音的字符、希腊字母等，Latin-1。\n中国！加入汉字！ GB2312 。\n加入生僻字、繁体字及日韩汉字等，GBK。\n各说各的，乱码。\nISO国际标准化组织一统江湖，Unicode。\n再编码，UTF-8。\n因为Unicode需要遵循统一的存储，就造成了资源浪费，比如汉字和英文，占用的空间是不一样了。\n为了支持这种差异，节约存储，高效传输，对Unicode进行了再编码，也就是UTF-8，也是用的最广泛的字符集之一。\nURL Encode和URL Escape Web服务器只允许URL中包含ASCII。\n那怎么办？只有先encode成二进制，让服务器自己decode咯。\n所以URL Encode后是“百分号+2位十六进制”的形式，比如%20。\n需要Encode的包括\n非ASCII字符 ASCII控制字符，因为不可打印 保留字符，如\u0026amp;，因为URL自身是用\u0026amp;分隔的，参数内容包括\u0026amp;的话就要Encode 不安全字符，如空格，可能造成歧义 用Python来实际看看\nfrom urllib.parse import urlencode x = urlencode({\u0026#34;a\u0026#34;: 1, \u0026#34;b\u0026#34;: \u0026#34;a\u0026#34;, \u0026#34;c\u0026#34;: \u0026#34;汉\u0026#34;, \u0026#34;d\u0026#34;: \u0026#34; \u0026#34;, \u0026#34;e\u0026#34;: \u0026#34;\u0026amp;\u0026#34;}) print(x) # a=1\u0026amp;b=a\u0026amp;c=%E6%B1%89\u0026amp;d=+\u0026amp;e=%26 URL Escape，也就是URL转义，碰到最多的就是把\u0026lt;、\u0026gt;、\u0026amp;转成\u0026amp;lt;、\u0026amp;gt;、\u0026amp;amp;。\nPython html.escape函数，一切尽在代码中\ndef escape(s, quote=True): \u0026#34;\u0026#34;\u0026#34; Replace special characters \u0026#34;\u0026amp;\u0026#34;, \u0026#34;\u0026lt;\u0026#34; and \u0026#34;\u0026gt;\u0026#34; to HTML-safe sequences. If the optional flag quote is true (the default), the quotation mark characters, both double quote (\u0026#34;) and single quote (\u0026#39;) characters are also translated. \u0026#34;\u0026#34;\u0026#34; s = s.replace(\u0026#34;\u0026amp;\u0026#34;, \u0026#34;\u0026amp;amp;\u0026#34;) # Must be done first! s = s.replace(\u0026#34;\u0026lt;\u0026#34;, \u0026#34;\u0026amp;lt;\u0026#34;) s = s.replace(\u0026#34;\u0026gt;\u0026#34;, \u0026#34;\u0026amp;gt;\u0026#34;) if quote: s = s.replace(\u0026#39;\u0026#34;\u0026#39;, \u0026#34;\u0026amp;quot;\u0026#34;) s = s.replace(\u0026#39;\\\u0026#39;\u0026#39;, \u0026#34;\u0026amp;#x27;\u0026#34;) return s 有时候还会碰到被转成了\\u003e、\\u003c、\\u0026，这是JSON序列化搞出来的Unicode。\n简要回顾 本文首先讲了怎么记忆编码和解码，然后通过字符代码和字符编码的差异，引出了如今常用的UTF-8，接着从发展历史来看，字符集是怎么发展到现在的。最后解释了Web URL中容易混淆的2个概念，URL Encode和URL Escape。\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%9F%A5%E8%AF%86/000005-%E5%AD%97%E7%AC%A6%E9%9B%86%E5%85%B6%E5%AE%9E%E5%BE%88%E7%AE%80%E5%8D%95/","summary":"字符集其实是个很简单的东西，但很多人当时看懂了，过了就忘了。","title":"字符集其实很简单"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDjango已经提供了开箱即用的认证系统，但是可能并不满足我们的个性化需求。自定义认证系统需要知道哪些地方可以扩展，哪些地方可以替换。本文就来介绍自定义Django认证系统的相关技术细节。\n自定义认证后端 AUTHENTICATION_BACKENDS Django默认认证后端为：\n[\u0026#39;django.contrib.auth.backends.ModelBackend\u0026#39;] 可以在settings.py中配置AUTHENTICATION_BACKENDS为自定义的认证后端，其本质是Python class，在调用django.contrib.auth.authenticate()时会进行遍历：\ndef authenticate(request=None, **credentials): \u0026#34;\u0026#34;\u0026#34; If the given credentials are valid, return a User object. \u0026#34;\u0026#34;\u0026#34; for backend, backend_path in _get_backends(return_tuples=True): backend_signature = inspect.signature(backend.authenticate) try: backend_signature.bind(request, **credentials) except TypeError: # This backend doesn\u0026#39;t accept these credentials as arguments. Try the next one. continue try: user = backend.authenticate(request, **credentials) except PermissionDenied: # This backend says to stop in our tracks - this user should not be allowed in at all. break if user is None: continue # Annotate the user object with the path of the backend. user.backend = backend_path return user # The credentials supplied are invalid to all backends, fire signal user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request) 列表中的认证后端是有先后顺序的，Django会依次进行认证，只要有后端认证成功，就会结束认证，如果有后端抛出PermissionDenied异常，也会停止认证。\n如果修改了认证后端，想要用户重新认证，那么需要调用Session.objects.all().delete()清除session数据，因为session中会缓存已认证过的认证后端。\n编写认证后端 先看看默认认证后端的源码片段：\nclass ModelBackend(BaseBackend): \u0026#34;\u0026#34;\u0026#34; Authenticates against settings.AUTH_USER_MODEL. \u0026#34;\u0026#34;\u0026#34; def authenticate(self, request, username=None, password=None, **kwargs): if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) if username is None or password is None: return try: user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: # Run the default password hasher once to reduce the timing # difference between an existing and a nonexistent user (#20760). UserModel().set_password(password) else: if user.check_password(password) and self.user_can_authenticate(user): return user ... def get_user(self, user_id): try: user = UserModel._default_manager.get(pk=user_id) except UserModel.DoesNotExist: return None return user if self.user_can_authenticate(user) else None 总结一下：\n继承BaseBackend。\n实现了authenticate()。（backend也有个authenticate方法，跟django.contrib.auth.authenticate()不一样哦）authenticate(request=None, **credentials)方法的第一个入参是request，可为空，第二个入参是credentials（用户凭证如用户名、密码），示例：\nfrom django.contrib.auth.backends import BaseBackend class MyBackend(BaseBackend): def authenticate(self, request, username=None, password=None): # Check the username/password and return a user. ... 用户凭证也可以是token：\nfrom django.contrib.auth.backends import BaseBackend class MyBackend(BaseBackend): def authenticate(self, request, token=None): # Check the token and return a user. ... 如果认证成功就返回User对象，如果认证失败就返回None。\n实现了get_user()。get_user(user_id)方法入参是user_id，可以是username/数据库ID等，必须是User的主键，返回值为User对象或者None。\n我们试着来编写一个认证后端，为了演示效果，我们不用客户端服务器模式，而是在settings.py文件中增加2个配置，然后用我们自定义的认证后端进行认证，代码如下：\nfrom django.conf import settings from django.contrib.auth.backends import BaseBackend from django.contrib.auth.hashers import check_password from django.contrib.auth.models import User class SettingsBackend(BaseBackend): \u0026#34;\u0026#34;\u0026#34; 认证settings中ADMIN_LOGIN和ADMIN_PASSWORD变量，比如： ADMIN_LOGIN = \u0026#39;admin\u0026#39; ADMIN_PASSWORD = \u0026#39;pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M=\u0026#39; \u0026#34;\u0026#34;\u0026#34; def authenticate(self, request, username=None, password=None): login_valid = (settings.ADMIN_LOGIN == username) pwd_valid = check_password(password, settings.ADMIN_PASSWORD) if login_valid and pwd_valid: try: user = User.objects.get(username=username) except User.DoesNotExist: # 创建一个新用户 user = User(username=username) user.is_staff = True user.is_superuser = True user.save() return user return None def get_user(self, user_id): try: return User.objects.get(pk=user_id) except User.DoesNotExist: return None 自定义认证后端授权 认证后端可以重写方法get_user_permissions(), get_group_permissions(), get_all_permissions(), has_perm(), has_module_perms(), with_perm()来实现授权。示例：\nfrom django.contrib.auth.backends import BaseBackend class MagicAdminBackend(BaseBackend): def has_perm(self, user_obj, perm, obj=None): # 如果是超管，就会获得所有权限，因为不管perm是什么，都返回True return user_obj.username == settings.ADMIN_LOGIN 可以根据业务编写具体的判断逻辑，给不同用户/组授予不同权限。\nuser_obj可以是django.contrib.auth.models.AnonymousUser，用来给匿名用户授予某些权限。\nUser有个is_active字段，ModelBackend和RemoteUserBackend不能给is_active=False的用户授权，如果想授权，可以使用AllowAllUsersModelBackend或AllowAllUsersRemoteUserBackend。\n自定义新权限 除了增删改查权限，有时我们需要更多的权限，例如，为myapp中的BlogPost创建一个can_publish权限：\n方法1 meta中配置\nclass BlogPost(models.Model): ... class Meta: permissions = ( (\u0026#34;can_publish\u0026#34;, \u0026#34;Can Publish Posts\u0026#34;), ) 方法2 使用create()函数\nfrom myapp.models import BlogPost from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType content_type = ContentType.objects.get_for_model(BlogPost) permission = Permission.objects.create( codename=\u0026#39;can_publish\u0026#39;, name=\u0026#39;Can Publish Posts\u0026#39;, content_type=content_type, ) 在使用python manage.py migrate命令后，就会创建这个新权限，接着就可以在view中编写代码判断用户是否有这个权限来决定能否发表文章。\n扩展User模型 代理模型 如果不需要修改表结构，只扩展行为，那么可以使用代理模型。示例：\nfrom django.contrib.auth.models import User class MyUser(User): class Meta: proxy = True def do_something(self): # ... pass OneToOneField 如果需要扩展字段，那么可以使用OneToOneField。示例：\nfrom django.contrib.auth.models import User class Employee(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) department = models.CharField(max_length=100) 这样会新增一张表：\nCREATE TABLE `user_employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `department` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `user_id` int(11) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `user_id` (`user_id`), CONSTRAINT `user_employee_user_id_9b2edd10_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 在代码中使用User也能访问到Employee的属性：\n\u0026gt;\u0026gt;\u0026gt; u = User.objects.get(username=\u0026#39;fsmith\u0026#39;) \u0026gt;\u0026gt;\u0026gt; freds_department = u.employee.department 虽然这种方式能实现扩展，但是OneToOneField会增加数据库查询的复杂度，加重数据库处理负担，并不建议采用。\n替换User模型 新版Django的推荐做法是，如果不想用默认User模型，那么就把它替换掉。Django除了User模型，还有2个抽象模型AbstractUser和AbstractBaseUser，从源码中可以看到它们的继承关系：\nclass User(AbstractUser): class AbstractUser(AbstractBaseUser, PermissionsMixin): class AbstractBaseUser(models.Model): 为什么不用User模型，还要做2个抽象模型呢？这是因为一般继承有2个用途，一是继承父类的属性和方法，并做出自己的改变或扩展，实现代码重用。但是这种方式会导致子类也包含了父类的实现代码，代码强耦合，所以实践中不会这么做。而是采用第二种方式，把共性的内容抽象出来，只定义属性和方法，不提供具体实现（如java中的接口类），并且只能被继承，不能被实例化。AbstractUser和AbstractBaseUser就是对User的不同程度的抽象，AbstractUser是User的完整实现，可用于扩展User，AbstractBaseUser是高度抽象，可用于完全自定义User。\n继承AbstractUser 除了代理模型和OneToOneField，扩展User的新方式是定义新的MyUser并继承AbstractUser，把User替换掉，再添加额外信息。具体操作步骤我们通过示例来了解：\n替换User最好是创建项目后，首次python manage.py migrate前，就进行替换，否则数据库的表已经生成，再中途替换，会有各种各样的依赖问题，只能手动解决。\n第一步，myapp.models中新建MyUser，继承AbstractUser：\nfrom django.contrib.auth.models import AbstractUser class MyUser(AbstractUser): pass 第二步，settings.py中配置AUTH_USER_MODEL，指定新的用户模型：\nAUTH_USER_MODEL = \u0026#39;myapp.MyUser\u0026#39; 第三步，settings.py中配置INSTALLED_APPS：\nINSTALLED_APPS = [ \u0026#39;django.contrib.admin\u0026#39;, \u0026#39;django.contrib.auth\u0026#39;, \u0026#39;django.contrib.contenttypes\u0026#39;, \u0026#39;django.contrib.sessions\u0026#39;, \u0026#39;django.contrib.messages\u0026#39;, \u0026#39;django.contrib.staticfiles\u0026#39;, \u0026#39;myapp.apps.MyappConfig\u0026#39; # 新增 ] 第四步（可选），如果需要使用Django自带管理后台，那么要在admin.py中注册：\nfrom django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import MyUser admin.site.register(MyUser, UserAdmin) 我们看下数据库中的效果，提交数据迁移：\npython manage.py makemigrations 执行数据迁移：\npython manage.py migrate 从表能看出来，默认User已经替换为MyUser了：\n替换之后，就可以进行扩展了。比如自定义表名：\nfrom django.contrib.auth.models import AbstractUser class MyUser(AbstractUser): class Meta: db_table = \u0026#34;user\u0026#34; pass 替换User后，就不能直接引用django.contrib.auth.models.User了，可以使用get_user_model()函数或者settings.AUTH_USER_MODEL。\n继承AbstractBaseUser 继承AbstractUser只能做扩展，如果我们想完全自定义用户模型，那么就需要继承AbstractBaseUser，再重写属性和方法。\nUSERNAME_FIELD\nUSERNAME_FIELD是用户模型的唯一标识符，不一定是username，也可以是email、phone等。\n唯一标识符是Django认证后端的要求，如果你实现了自定义认证后端，那么也可以用非唯一标识符作为USERNAME_FIELD。\n我们可以参考AbstractUser的实现：\nusername = models.CharField( _(\u0026#39;username\u0026#39;), max_length=150, unique=True, help_text=_(\u0026#39;Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.\u0026#39;), validators=[username_validator], error_messages={ \u0026#39;unique\u0026#39;: _(\u0026#34;A user with that username already exists.\u0026#34;), }, ) USERNAME_FIELD = \u0026#39;username\u0026#39; 修改为自定义：\nclass MyUser(AbstractBaseUser): identifier = models.CharField(max_length=40, unique=True) ... USERNAME_FIELD = \u0026#39;identifier\u0026#39; EMAIL_FIELD\n参考AbstractUser的实现：\nemail = models.EmailField(_(\u0026#39;email address\u0026#39;), blank=True) EMAIL_FIELD = \u0026#39;email\u0026#39; REQUIRED_FIELDS\nREQUIRED_FIELDS是指必填字段。参考AbstractUser的实现：\nREQUIRED_FIELDS = [\u0026#39;email\u0026#39;] 这表示email是必填的，在使用createsuperuser命令时，会提示必须输入。\n修改为自定义：\nclass MyUser(AbstractBaseUser): ... date_of_birth = models.DateField() height = models.FloatField() ... REQUIRED_FIELDS = [\u0026#39;date_of_birth\u0026#39;, \u0026#39;height\u0026#39;] 不需要再填USERNAME_FIELD和password，因为Django已经默认包含了，只需要填其他字段即可。\nis_active\n可以用来做软删（不删除数据而是把is_active置为False）。参考AbstractUser的实现：\nis_active = models.BooleanField( _(\u0026#39;active\u0026#39;), default=True, help_text=_( \u0026#39;Designates whether this user should be treated as active. \u0026#39; \u0026#39;Unselect this instead of deleting accounts.\u0026#39; ), ) get_full_name()\n参考AbstractUser的实现：\ndef get_full_name(self): \u0026#34;\u0026#34;\u0026#34; Return the first_name plus the last_name, with a space in between. \u0026#34;\u0026#34;\u0026#34; full_name = \u0026#39;%s %s\u0026#39; % (self.first_name, self.last_name) return full_name.strip() get_short_name()\n参考AbstractUser的实现：\ndef get_short_name(self): \u0026#34;\u0026#34;\u0026#34;Return the short name for the user.\u0026#34;\u0026#34;\u0026#34; return self.first_name 更多属性和方法请看源码。\n查看源码的方法：在from django.contrib.auth.models import AbstractBaseUser代码上，按住CTRL点击AbstractBaseUser即可。\n重写manager 如果自定义用户模型改变了username, email, is_staff, is_active, is_superuser, last_login, and date_joined字段，那么可能需要继承BaseUserManager，并重写以下2个方法：\ncreate_user(username_field, password=None, **other_fields)\ncreate_user(username_field, password=None, **other_fields)\n示例：\nfrom django.contrib.auth.models import BaseUserManager class CustomUserManager(BaseUserManager): def create_user(self, email, date_of_birth, password=None): # create user here ... def create_superuser(self, email, date_of_birth, password=None): # create superuser here ... 重写权限 从AbstractUser的定义可以看到是继承了PermissionsMixin类的：\nclass AbstractUser(AbstractBaseUser, PermissionsMixin): 所以重写权限就是重写PermissionsMixin的属性和方法，如get_user_permissions()、has_perm()等。\n一个完整示例 我们把email作为USERNAME_FIELD，并且让date_of_birth必填。\nmodels.py\nfrom django.db import models from django.contrib.auth.models import ( BaseUserManager, AbstractBaseUser ) class MyUserManager(BaseUserManager): def create_user(self, email, date_of_birth, password=None): \u0026#34;\u0026#34;\u0026#34; Creates and saves a User with the given email, date of birth and password. \u0026#34;\u0026#34;\u0026#34; if not email: raise ValueError(\u0026#39;Users must have an email address\u0026#39;) user = self.model( email=self.normalize_email(email), date_of_birth=date_of_birth, ) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, date_of_birth, password=None): \u0026#34;\u0026#34;\u0026#34; Creates and saves a superuser with the given email, date of birth and password. \u0026#34;\u0026#34;\u0026#34; user = self.create_user( email, password=password, date_of_birth=date_of_birth, ) user.is_admin = True user.save(using=self._db) return user class MyUser(AbstractBaseUser): email = models.EmailField( verbose_name=\u0026#39;email address\u0026#39;, max_length=255, unique=True, ) date_of_birth = models.DateField() is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) objects = MyUserManager() USERNAME_FIELD = \u0026#39;email\u0026#39; REQUIRED_FIELDS = [\u0026#39;date_of_birth\u0026#39;] def __str__(self): return self.email def has_perm(self, perm, obj=None): \u0026#34;Does the user have a specific permission?\u0026#34; # Simplest possible answer: Yes, always return True def has_module_perms(self, app_label): \u0026#34;Does the user have permissions to view the app `app_label`?\u0026#34; # Simplest possible answer: Yes, always return True @property def is_staff(self): \u0026#34;Is the user a member of staff?\u0026#34; # Simplest possible answer: All admins are staff return self.is_admin 不要忘了在settings.py中修改AUTH_USER_MODEL哦：\nAUTH_USER_MODEL = \u0026#39;customauth.MyUser\u0026#39; 小结 写了这2篇关于Django认证系统的文章，明白了以前似懂非懂的技术细节。如果平时有需求想自己做个小网站，完全可以用Django来快速实现后端，开箱即用还是有点香。Template和Form不属于前后端分离的技术，在学习时可以选择性跳过。公众号后台回复“加群”,“Python互助讨论群”欢迎你。\n参考资料：\nhttps://docs.djangoproject.com/en/3.1/topics/auth/customizing/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000005-django%E8%87%AA%E5%AE%9A%E4%B9%89django%E8%AE%A4%E8%AF%81%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88/","summary":"Django已经提供了开箱即用的认证系统，但是可能并不满足我们的个性化需求。自定义认证系统需要知道哪些地方可以扩展，哪些地方可以替换。本文就来介绍自定义Django认证系统的相关技术细节。","title":"自定义Django认证系统的技术方案"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在使用django-admin startproject创建项目后，Django就默认安装了一个采用session实现的认证系统。这是Django相比于其他框架的一大特点：自带认证系统，开箱即用。有人说它方便，有人说它鸡肋，但它作为Django的重要组成部分，学习它有助于我们理解Django框架的核心技术。\n安装 Django默认已安装，可以在settings.py中的INSTALLED_APPS看到：\ndjango.contrib.auth：认证系统内核，以及默认models等。 django.contrib.contenttypes：用于关联权限和models，从而赋予models的添加/删除等权限。 contrib翻译为普通发布版。\n在MIDDLEWARE可以看到：\nSessionMiddleware：session中间件。 AuthenticationMiddleware：认证中间件。 使用python manage.py migrate后，数据库会新增认证系统的这些表：\n认证与授权 认证的英文是authentication，授权的英文是authorization。单词不一样，咋看有点像。认证是指验证用户是谁。授权是指授予已认证用户权限。由于认证授权在某种程序上是耦合的，所以Django把它们统称为“认证”。\n认证系统概览 认证系统的组成部分如下：\n用户 权限 组 密码管理 登录相关表单（前后端分离不需要）和视图（接受Web请求并且返回Web响应） Django框架是MTV模式，类似于MVC模式。Django的View对应MVC的Controller。\n可配置的backend 以上是Django自带内容，如果需要更多功能，可以安装第三方包：\n密码增强校验 登录限流 OAuth 对象级权限（django-guardian） 以Article举例，Django是模型级权限，用户只能具有全部文章的权限。django-guardian提供了对象级权限，可以对单篇文章进行授权。\nmodels.User User模型是Django认证系统的核心，它的主要属性包括：\nid username email password is_active is_superuser last_login date_joined django.contrib.auth.models，在django.db.models之上封装了AbstractBaseUser、AbstractUser、User等模型。\n创建超级管理员 cmd中使用createsuperuser命令：\n$ python manage.py createsuperuser 根据提示输入username、email、password后，就会在数据库中创建1条超管用户。\n创建用户 方法1 代码创建\n在代码中使用create_user()函数来创建用户：\n\u0026gt;\u0026gt;\u0026gt; from django.contrib.auth.models import User # 创建用户并保存到数据库 \u0026gt;\u0026gt;\u0026gt; user = User.objects.create_user(\u0026#39;john\u0026#39;, \u0026#39;lennon@thebeatles.com\u0026#39;, \u0026#39;johnpassword\u0026#39;) # 修改其他字段值 \u0026gt;\u0026gt;\u0026gt; user.last_name = \u0026#39;Lennon\u0026#39; \u0026gt;\u0026gt;\u0026gt; user.save() 方法2 管理后台创建\n访问http://127.0.0.1:8000/admin/，用超管登录后，在界面上创建：\n修改密码 方法1 命令行修改\npython manage.py changepassword username 根据提示输入旧密码、新密码、确认密码即可。\n方法2 代码中修改\n\u0026gt;\u0026gt;\u0026gt; from django.contrib.auth.models import User \u0026gt;\u0026gt;\u0026gt; u = User.objects.get(username=\u0026#39;john\u0026#39;) \u0026gt;\u0026gt;\u0026gt; u.set_password(\u0026#39;new password\u0026#39;) \u0026gt;\u0026gt;\u0026gt; u.save() 方法3 管理后台修改\n用户认证 框架底层使用authenticate()函数对用户进行认证：\nauthenticate(request=None, **credentials) credentials是用户凭证，如用户名、密码。\n示例：\nfrom django.contrib.auth import authenticate user = authenticate(username=\u0026#39;john\u0026#39;, password=\u0026#39;secret\u0026#39;) if user is not None: # A backend authenticated the credentials else: # No backend authenticated the credentials 如果认证成功，authenticate()会返回User。如果用户凭证无效或者权限不足，认证后端抛出了PermissionDenied，authenticate()会返回None。\n认证后端 认证后端（authentication backends）是Django做用户验证的后端模块，默认为['django.contrib.auth.backends.ModelBackend']，只会简单比较请求的用户名密码和数据库中的用户名密码是否匹配。可以切换成其他认证后端，也可以重写authenticate()进行自定义。我点开了源码，发现除了Django的认证后端，DRF已经封装了Session、Token、JWT的认证：\n权限管理 权限一般分为add、change、delete、view，也就是增删改查。\n默认权限 Django会在python manage.py migrate的时候，为每个model创建4种权限：add、change、delete、view。\n比如有个app叫做foo，它有个model叫做Bar，可以使用has_perm()函数来检查权限：\nadd：user.has_perm(\u0026lsquo;foo.add_bar\u0026rsquo;) change：user.has_perm(\u0026lsquo;foo.change_bar\u0026rsquo;) delete：user.has_perm(\u0026lsquo;foo.delete_bar\u0026rsquo;) view：user.has_perm(\u0026lsquo;foo.view_bar\u0026rsquo;) 创建新权限 除了增删改查权限，有时我们需要更多的权限，例如，为myapp中的BlogPost创建一个can_publish权限：\n方法1 meta中配置\nclass BlogPost(models.Model): ... class Meta: permissions = ( (\u0026#34;can_publish\u0026#34;, \u0026#34;Can Publish Posts\u0026#34;), ) 方法2 使用create()函数\nfrom myapp.models import BlogPost from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType content_type = ContentType.objects.get_for_model(BlogPost) permission = Permission.objects.create( codename=\u0026#39;can_publish\u0026#39;, name=\u0026#39;Can Publish Posts\u0026#39;, content_type=content_type, ) 在使用python manage.py migrate命令后，就会创建这个新权限，接着就可以在view中编写代码判断用户是否有这个权限来决定能否发表文章。\n授权 可以在管理后台对用户授权：\n或者把用户分组后，按组来进行授权：\n从数据库这6张表就能看出来，有用户表、分组表、权限表，以及它们的关联关系表：\n其代码实现是把permission赋值给User.user_permissions或者Group.permissions属性。\n代理模型权限 代理模型是从某个模型继承来的，不影响表结构，用于扩展行为实现代码解耦。\n代理模型不会继承父类的权限，例如：\nclass Person(models.Model): class Meta: permissions = [(\u0026#39;can_eat_pizzas\u0026#39;, \u0026#39;Can eat pizzas\u0026#39;)] # 代理模型 class Student(Person): class Meta: proxy = True permissions = [(\u0026#39;can_deliver_pizzas\u0026#39;, \u0026#39;Can deliver pizzas\u0026#39;)] \u0026gt;\u0026gt;\u0026gt; # 注意代理模型取ContentType需要加for_concrete_model=False \u0026gt;\u0026gt;\u0026gt; content_type = ContentType.objects.get_for_model(Student, for_concrete_model=False) \u0026gt;\u0026gt;\u0026gt; student_permissions = Permission.objects.filter(content_type=content_type) \u0026gt;\u0026gt;\u0026gt; [p.codename for p in student_permissions] [\u0026#39;add_student\u0026#39;, \u0026#39;change_student\u0026#39;, \u0026#39;delete_student\u0026#39;, \u0026#39;view_student\u0026#39;, \u0026#39;can_deliver_pizzas\u0026#39;] # 没有父类的can_eat_pizzas权限 Session认证 Django认证系统是基于Session的。Django把Web请求封装成了request（HttpRequest类），然后通过中间件设置了session相关的属性：request.session、request.site、request.user。其中request.user就代表当前用户，如果未登陆它的值是AnonymousUser（匿名用户）的实例，如果已登陆它的值是User的实例。可以通过is_authenticated来判断是否已认证：\nif request.user.is_authenticated: # Do something for authenticated users. ... else: # Do something for anonymous users. ... 用户登录 我们先简单回顾一下基于session的登录过程：\nDjango提供了login()函数来登录，把用户凭证保存到session中。它的函数签名如下：\nlogin(request, user, backend=None)\n示例：\nimport json from django.contrib.auth import authenticate, login from django.http import HttpResponse # Create your views here. def my_view(request): request_body = json.loads(request.body) username = request_body[\u0026#34;username\u0026#34;] password = request_body[\u0026#34;password\u0026#34;] user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return HttpResponse(\u0026#34;logged in\u0026#34;) else: return HttpResponse(\u0026#34;invalid login\u0026#34;) 除了保存用户凭证，Django还会把认证后端也保存到session中，便于相同的认证后端下次可以直接获取到用户信息。至于保存哪个认证后端，Django按以下顺序选取：\n使用login()函数的backend参数值，如果赋值了的话。 使用user.backend的值，如果有的话。 使用settings中AUTHENTICATION_BACKENDS的值，默认 ['django.contrib.auth.backends.ModelBackend']。 否则抛出异常。 用户登出 Django提供了logout()函数来登出。它的函数签名如下：\nlogout(request)\n示例：\nfrom django.contrib.auth import logout def logout_view(request): logout(request) # Redirect to a success page. 登出后session会被销毁，所有数据都会被清除，以防止其他人使用相同的浏览器再次登录后获取到之前用户的session数据。\nlogin_required 对于未登陆的用户，需要进行限制，必须先登陆才能进行访问。\n传统方法\n使用request.user.is_authenticated判断，然后重定向到登录页面：\nfrom django.conf import settings from django.shortcuts import redirect def my_view(request): if not request.user.is_authenticated: return redirect(\u0026#39;%s?next=%s\u0026#39; % (settings.LOGIN_URL, request.path)) # ... 或者错误页面：\nfrom django.shortcuts import render def my_view(request): if not request.user.is_authenticated: return render(request, \u0026#39;myapp/login_error.html\u0026#39;) # ... login_required装饰器\nlogin_required(redirect_field_name='next', login_url=None)\n示例：\nfrom django.contrib.auth.decorators import login_required @login_required def my_view(request): ... 它的处理是这样的：\n如果用户没有登录，就重定向到settings.LOGIN_URL（默认值/accounts/login/），同时把当前的绝对路径添加到查询字符串中，如：/accounts/login/?next=/polls/3/。\n如果用户已经登录了，正常执行view代码。\nlogin_required的redirect_field_name参数是指登陆认证成功后重定向的页面，默认保存在叫做next的查询字符串参数中（如/accounts/login/?next=/polls/3/）。可以修改为自定义：\nfrom django.contrib.auth.decorators import login_required @login_required(redirect_field_name=\u0026#39;my_redirect_field\u0026#39;) def my_view(request): ... 不过修改后还需要同时修改login模板等。\nlogin_required的login_url参数是指登录页面的url，可以自定义，默认是/accounts/login/，需要在URLconf中关联登陆视图：\nfrom django.contrib.auth import views as auth_views path(\u0026#39;accounts/login/\u0026#39;, auth_views.LoginView.as_view()), function views和class-based views function views（函数视图），视图是个函数：\nfrom django.http import HttpResponse def my_view(request): if request.method == \u0026#39;GET\u0026#39;: # \u0026lt;view logic\u0026gt; return HttpResponse(\u0026#39;result\u0026#39;) class-based views（基于类的视图），视图是个类：\nfrom django.views import View class MyView(View): def get(self, request): # \u0026lt;view logic\u0026gt; return HttpResponse(\u0026#39;result\u0026#39;) 为什么需要cbv？因为类可以继承，提高代码复用。由于Django的URLconf只能接受函数，所以cbv有个as_view()方法用来返回一个函数：\n# urls.py from django.urls import path from myapp.views import MyView urlpatterns = [ path(\u0026#39;about/\u0026#39;, MyView.as_view()), ] LoginRequiredMixin Mixin是为了代码复用，从多个父类继承而来的类。如果使用的是class-based views，那么可以使用LoginRequiredMixin，来实现login_required的效果，例如：\nfrom django.contrib.auth.mixins import LoginRequiredMixin class MyView(LoginRequiredMixin, View): login_url = \u0026#39;/login/\u0026#39; redirect_field_name = \u0026#39;redirect_to\u0026#39; permission_required 除了需要登录，有些视图还需要权限。Django提供了permission_required装饰器，它的函数签名如下：\npermission_required(perm, login_url=None, raise_exception=False)\n示例：\nfrom django.contrib.auth.decorators import permission_required @permission_required(\u0026#39;polls.add_choice\u0026#39;) def my_view(request): ... permission_required的perm参数，指的是权限，可以是单个权限，也可以是权限列表。\npermission_required的login_url参数和login_required的login_url作用一样。\npermission_required的raise_exception参数，可以用来抛出异常，赋值为True后会跳转到403（HTTP Forbidden）页面而非登录页面。\n如果既想抛出异常 ，又想跳转到登录页面，那么可以同时添加这2个装饰器：\nfrom django.contrib.auth.decorators import login_required, permission_required @login_required @permission_required(\u0026#39;polls.add_choice\u0026#39;, raise_exception=True) def my_view(request): ... PermissionRequiredMixin 如果使用的是class-based views，那么可以使用PermissionRequiredMixin，来实现permission_required的效果，例如：\nfrom django.contrib.auth.mixins import PermissionRequiredMixin class MyView(PermissionRequiredMixin, View): permission_required = \u0026#39;polls.add_choice\u0026#39; # Or multiple of permissions: permission_required = (\u0026#39;polls.view_choice\u0026#39;, \u0026#39;polls.change_choice\u0026#39;) 修改密码导致session失效 登录成功后，Django会把加密后的密码hash值存入session中，每次请求时，会校验session中的密码和数据库中的密码是否匹配。如果修改了密码，数据库中的密码改变了，而session中的密码没有更新，那么密码就会匹配不上，导致session失效。django.contrib.auth的PasswordChangeView和user_change_password视图会在修改密码时更新session中的密码hash，来避免session失效。如果对修改密码的视图进行了自定义，那么可以使用update_session_auth_hash(request, user)来更新session中的密码，防止修改密码导致session失效。\n认证视图 Django提供了登录、登出、密码管理等视图。最简单的使用方式是在URLconf中配置：\nurlpatterns = [ path(\u0026#39;accounts/\u0026#39;, include(\u0026#39;django.contrib.auth.urls\u0026#39;)), ] 它会包含这些URL patterns：\naccounts/login/ [name=\u0026#39;login\u0026#39;] accounts/logout/ [name=\u0026#39;logout\u0026#39;] accounts/password_change/ [name=\u0026#39;password_change\u0026#39;] accounts/password_change/done/ [name=\u0026#39;password_change_done\u0026#39;] accounts/password_reset/ [name=\u0026#39;password_reset\u0026#39;] accounts/password_reset/done/ [name=\u0026#39;password_reset_done\u0026#39;] accounts/reset/\u0026lt;uidb64\u0026gt;/\u0026lt;token\u0026gt;/ [name=\u0026#39;password_reset_confirm\u0026#39;] accounts/reset/done/ [name=\u0026#39;password_reset_complete\u0026#39;] name是别名，可以使用reverse()函数来获取，如reverse('login')。\n但有时我们需要自定义url，在URLconf中添加自定义url后，再加上相应视图即可，例如：\nfrom django.contrib.auth import views as auth_views urlpatterns = [ path(\u0026#39;change-password/\u0026#39;, auth_views.PasswordChangeView.as_view()), ] 所有的这些视图都是class-based views，便于继承后重写进行自定义。\nDjango提供的相关视图有LoginView、LogoutView、PasswordChangeView、PasswordChangeDoneView、PasswordResetView、PasswordResetDoneView、PasswordResetConfirmView、PasswordResetCompleteView。\n快速上手体验 如果想快速上手体验，可以按如下步骤进行操作：\npip install django，安装Django。\ndjango-admin startproject project_name，创建Django项目。\npython manage.py migrate，数据迁移，使用自带SQLite数据库即可。\npython manage.py createsuperuser，创建超级管理员。\npython manage.py runserver，启动项目。\n访问http://127.0.0.1:8000/admin/，用超管登录管理后台。\n就可以使用Django自带认证系统了。\n小结 本文介绍了Django自带的基于session的认证系统，阐述了用户、组、认证与授权的相关概念，以及session认证的技术细节，最后讲解了如何快速上手体验的操作步骤。虽然如今基于session认证用的很少了，但它却是理解Token、JWT认证的基础，仍然值得我们学习。\n参考资料：\nhttps://docs.djangoproject.com/en/3.1/topics/auth/\nhttps://docs.djangoproject.com/en/3.1/topics/auth/default/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000004-djangodjango%E8%AE%A4%E8%AF%81%E7%B3%BB%E7%BB%9F%E5%B9%B6%E4%B8%8D%E9%B8%A1%E8%82%8B%E5%8F%8D%E8%80%8C%E5%BE%88%E9%87%8D%E8%A6%81/","summary":"在使用\u003ccode\u003edjango-admin startproject\u003c/code\u003e创建项目后，Django就默认安装了一个采用session实现的认证系统。这是Django相比于其他框架的一大特点：自带认证系统，开箱即用。有人说它方便，有人说它鸡肋，但它作为Django的重要组成部分，学习它有助于我们理解Django框架的核心技术。","title":"Django认证系统并不鸡肋反而很重要"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\ntyping是Python标准库，用来做类型提示。FastAPI使用typing做了：\n编辑器支持；\n类型检查；\n定义类型，request path parameters, query parameters, headers, bodies, dependencies等等；\n类型转换；\n数据验证，并且在验证失败时自动生成错误；\nOpenAPI文档，自动生成接口参数；\n编辑器支持typing 先看个简单的例子：\ndef get_full_name(first_name, last_name): full_name = first_name.title() + \u0026#34; \u0026#34; + last_name.title() return full_name print(get_full_name(\u0026#34;john\u0026#34;, \u0026#34;doe\u0026#34;)) 输出：\nJohn Doe 也就是把首字母转换成大写后，用空格拼接起来。\n假如在写程序时，你并不知道这个转换大写的函数名是title，那么你会输入.之后查看：\n结果根据没有提示。\n添加typing类型提示：\ndef get_full_name(first_name: str, last_name: str): full_name = first_name.title() + \u0026#34; \u0026#34; + last_name.title() return full_name print(get_full_name(\u0026#34;john\u0026#34;, \u0026#34;doe\u0026#34;)) 再试一把：\n有了。\n再比如：\ndef get_name_with_age(name: str, age: int): name_with_age = name + \u0026#34; is this old: \u0026#34; + age return name_with_age 因为明确了类型，编辑器会做类型检查。\n声明类型 简单类型：\nint float bool bytes def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes): return item_a, item_b, item_c, item_d, item_d, item_e List\nPython不同版本对typing的支持是不一样的，3.6以上版本：\nfrom typing import List def process_items(items: List[str]): for item in items: print(item) 而3.9以上版本：\ndef process_items(items: list[str]): for item in items: print(item) 不需要import typing，小写的list就行，跟常规用法一致。\n这样编辑器就能提供支持：\nTuple和Set\n3.9以上版本：\ndef process_items(items_t: tuple[int, int, str], items_s: set[bytes]): return items_t, items_s Dict\n3.9以上版本：\ndef process_items(prices: dict[str, float]): for item_name, item_price in prices.items(): print(item_name) print(item_price) Union\n多种类型，比如int或str。\n3.6以上版本：\nfrom typing import Union def process_item(item: Union[int, str]): print(item) 3.10以上版本：\ndef process_item(item: int | str): print(item) 不需要import，使用|就可以了。\nNone\n可能为None。\n3.6以上版本：\nfrom typing import Optional def say_hi(name: Optional[str] = None): if name is not None: print(f\u0026#34;Hey {name}!\u0026#34;) else: print(\u0026#34;Hello World\u0026#34;) from typing import Union def say_hi(name: Union[str, None] = None): if name is not None: print(f\u0026#34;Hey {name}!\u0026#34;) else: print(\u0026#34;Hello World\u0026#34;) 3.10以上版本：\ndef say_hi(name: str | None = None): if name is not None: print(f\u0026#34;Hey {name}!\u0026#34;) else: print(\u0026#34;Hello World\u0026#34;) Classes\n把某个类作为类型提示：\nclass Person: def __init__(self, name: str): self.name = name def get_person_name(one_person: Person): return one_person.name 也能得到编辑器支持：\nPydantic模型 Pydantic模型有点类似于Java的POJO，就是定义一个类，里面有一堆属性，这些属性都有类型。在实例化的时候，会做类型检查或类型转换。\nfrom datetime import datetime from pydantic import BaseModel class User(BaseModel): id: int name = \u0026#34;John Doe\u0026#34; signup_ts: datetime | None = None friends: list[int] = [] external_data = { \u0026#34;id\u0026#34;: \u0026#34;123\u0026#34;, \u0026#34;signup_ts\u0026#34;: \u0026#34;2017-06-01 12:22\u0026#34;, \u0026#34;friends\u0026#34;: [1, \u0026#34;2\u0026#34;, b\u0026#34;3\u0026#34;], } user = User(**external_data) print(user) ## \u0026gt; User id=123 name=\u0026#39;John Doe\u0026#39; signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] print(user.id) ## \u0026gt; 123 注意，类型提示使用的是:，初始化赋值使用的是=。\nPydantic对于可选类型有个特殊语法...，表示可以为None，但是必填：\nfrom pydantic import BaseModel, Field, ValidationError class Model(BaseModel): a: int | None b: int | None = ... c: int | None = Field(...) print(Model(b=1, c=2)) #\u0026gt; a=None b=1 c=2 try: Model(a=1, b=2) except ValidationError as e: print(e) \u0026#34;\u0026#34;\u0026#34; 1 validation error for Model c field required (type=value_error.missing) \u0026#34;\u0026#34;\u0026#34; a、b、c都能接受None，比如Model(a=None, b=None, c=None)；\na可选，比如Model(b=1, c=2)是ok的；\nb和c都是必填，比如Model(a=1, b=2)会报错；\nPython版本 从示例代码来看，Python3.10版本提供了更简洁的语法，能让代码看起来更优雅。而且Python3.10版本也引入了switch语句，如果没有什么历史原因，推荐大家使用Python3.10版本。\n参考资料：\nPython Types Intro - FastAPI https://fastapi.tiangolo.com/python-types/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/fastapi/000004-fastapifastapi%E4%BD%BF%E7%94%A8typing%E7%B1%BB%E5%9E%8B%E6%8F%90%E7%A4%BA/","summary":"typing是Python标准库，用来做类型提示。FastAPI使用typing做了：","title":"FastAPI使用typing类型提示"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n一个Flask应用（Flask Application）是Flask类（Flask Class）的实例。在前面的文章中，都是通过定义全局Flask实例的方式来编写的Flask应用代码，比如：\nfrom flask import Flask app = Flask(__name__) @app.route(\u0026#34;/\u0026#34;) def hello_world(): return \u0026#34;Hello, World!\u0026#34; Python的模块（Module）天然就是单例的，模块级别的对象自然而然也是单例的。这种方式在写简单的脚本时很方便，但如果是写项目，随着项目逐渐扩大，会出现越来越棘手的问题。\n除了定义全局实例，还有一种方式叫做工厂函数（Factory Function），在Flask这，也能叫应用工厂（Application Factory），Flask应用工厂函数的返回是Flask类的实例对象。\n典型的Flask应用工厂函数如下所示：\nimport os from flask import Flask def create_app(test_config=None): # create and configure the app app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( SECRET_KEY=\u0026#39;dev\u0026#39;, DATABASE=os.path.join(app.instance_path, \u0026#39;flaskr.sqlite\u0026#39;), ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile(\u0026#39;config.py\u0026#39;, silent=True) else: # load the test config if passed in app.config.from_mapping(test_config) # ensure the instance folder exists try: os.makedirs(app.instance_path) except OSError: pass # a simple page that says hello @app.route(\u0026#39;/hello\u0026#39;) def hello(): return \u0026#39;Hello, World!\u0026#39; return app 这段代码可以放到flaskr目录下面的__init__.py文件中：\n/home/user/Projects/flask-tutorial ├── flaskr/ │ ├── __init__.py 通过这段代码，我们可以看看Flask应用启动时，都干了些啥：\napp = Flask(__name__, instance_relative_config=True)创建了Flask实例。__name__是指当前模块的名字，这样Flask就能找到应用启动的入口。instance_relative_config=True告诉Flask配置文件是用的实例文件夹的相对路径。\n所谓实例文件夹，是指和flaskr同级的一个名字为instance的文件夹，适合存放私有配置的秘钥或者本地数据库等不需要上传到Git的文件，可以通过Flask.instance_path获取完整路径。\napp.config.from_mapping()设置了默认配置。SECRET_KEY是Flask用来给数据加密的私钥。DATABASE指定了数据库文件路径。\napp.config.from_pyfile()用来覆盖默认配置，from_pyfile方法从文件获取配置，from_mapping通过键值对设定配置。\nos.makedirs(app.instance_path)确保创建了实例文件夹。\n@app.route()定义了路由和处理函数。\n总的来说，Flask可以通过应用工厂（Application Factory）函数，在Flask应用启动时干这些事：实例化Flask对象→设定配置→创建实例文件夹→定义路由→定义处理函数。\nFlask启动后能看到以下日志：\n* Serving Flask app \u0026#34;flaskr\u0026#34; * Environment: development * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 855-212-761 除了python命令启动Flask应用，也可以直接用flask命令，示例如下：\n\u0026gt; set FLASK_APP=flaskr \u0026gt; set FLASK_ENV=development \u0026gt; flask run 参考资料：\nhttps://flask.palletsprojects.com/en/2.0.x/tutorial/factory/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/flask/000004-flaskflask%E5%90%AF%E5%8A%A8%E5%BA%94%E7%94%A8%E6%97%B6%E9%83%BD%E5%B9%B2%E4%BA%86%E4%BA%9B%E5%95%A5/","summary":"一个Flask应用（Flask Application）是Flask类（Flask Class）的实例。在前面的文章中，都是通过定义\u003cstrong\u003e全局Flask实例\u003c/strong\u003e的方式来编写的Flask应用代码，比如：","title":"Flask启动应用时都干了些啥"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nHttpRunner3的变量可以在测试类的用例配置中通过variables添加，也可以在测试步骤中使用extract()、with_jmespath()提取出来放到变量x，再用$x传递给下一个接口使用，比如登录到下单流程的部分测试脚本如下：\nfrom httprunner import HttpRunner, Config, Step, RunRequest class TestLoginPay(HttpRunner): config = ( Config(\u0026#34;登录到下单流程\u0026#34;) .variables( **{ \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34; } ) .base_url(\u0026#34;http://127.0.0.1:5000\u0026#34;) ) teststeps = [ Step( RunRequest(\u0026#34;登录\u0026#34;) .post(\u0026#34;/login\u0026#34;) .with_headers(**{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}) .with_json({\u0026#34;username\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.token\u0026#34;, \u0026#34;token\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), Step( RunRequest(\u0026#34;搜索商品\u0026#34;) .get(\u0026#34;searchSku?skuName=电子书\u0026#34;) .with_headers(**{\u0026#34;token\u0026#34;: \u0026#34;$token\u0026#34;}) .extract() .with_jmespath(\u0026#34;body.skuId\u0026#34;, \u0026#34;skuId\u0026#34;) .with_jmespath(\u0026#34;body.price\u0026#34;, \u0026#34;skuPrice\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), ] 首先来看用例配置的这段代码：\nconfig = ( Config(\u0026#34;登录到下单流程\u0026#34;) .variables( **{ \u0026#34;skuNum\u0026#34;: \u0026#34;3\u0026#34; } ) .base_url(\u0026#34;http://127.0.0.1:5000\u0026#34;) ) 是怎么实现的。Config的定义如下：\nclass Config(object): def __init__(self, name: Text): self.__name = name self.__variables = {} self.__base_url = \u0026#34;\u0026#34; self.__verify = False self.__export = [] self.__weight = 1 caller_frame = inspect.stack()[1] self.__path = caller_frame.filename @property def name(self) -\u0026gt; Text: return self.__name @property def path(self) -\u0026gt; Text: return self.__path @property def weight(self) -\u0026gt; int: return self.__weight def variables(self, **variables) -\u0026gt; \u0026#34;Config\u0026#34;: self.__variables.update(variables) return self def base_url(self, base_url: Text) -\u0026gt; \u0026#34;Config\u0026#34;: self.__base_url = base_url return self def verify(self, verify: bool) -\u0026gt; \u0026#34;Config\u0026#34;: self.__verify = verify return self def export(self, *export_var_name: Text) -\u0026gt; \u0026#34;Config\u0026#34;: self.__export.extend(export_var_name) return self def locust_weight(self, weight: int) -\u0026gt; \u0026#34;Config\u0026#34;: self.__weight = weight return self def perform(self) -\u0026gt; TConfig: return TConfig( name=self.__name, base_url=self.__base_url, verify=self.__verify, variables=self.__variables, export=list(set(self.__export)), path=self.__path, weight=self.__weight, ) 其中variables的定义是：\ndef variables(self, **variables) -\u0026gt; \u0026#34;Config\u0026#34;: self.__variables.update(variables) return self self.__variables = {}是个字典。为什么要加个前缀**呢？这个**换个模样估计就看懂了：\ndef foo(**kwargs): print(kwargs) \u0026gt;\u0026gt;\u0026gt; foo(x=1, y=2) {\u0026#39;x\u0026#39;: 1, \u0026#39;y\u0026#39;: 2} \u0026gt;\u0026gt;\u0026gt; foo(**{\u0026#39;x\u0026#39;: 1, \u0026#39;y\u0026#39;: 2}) {\u0026#39;x\u0026#39;: 1, \u0026#39;y\u0026#39;: 2} self.__variables.update(variables)里面的upate方法是字典添加到字典里，比如：\ntinydict = {\u0026#39;Name\u0026#39;: \u0026#39;Zara\u0026#39;, \u0026#39;Age\u0026#39;: 7} tinydict2 = {\u0026#39;Sex\u0026#39;: \u0026#39;female\u0026#39; } tinydict.update(tinydict2) # {\u0026#39;Age\u0026#39;: 7, \u0026#39;Name\u0026#39;: \u0026#39;Zara\u0026#39;, \u0026#39;Sex\u0026#39;: \u0026#39;female\u0026#39;} 整个方法的意思就是从variables中取出variables字典，然后添加到self.__variables字典里。\n这个Config会在runner.py里面的HttpRunner类初始化时加载到self.__config：\ndef __init_tests__(self) -\u0026gt; NoReturn: self.__config = self.config.perform() self.__teststeps = [] for step in self.teststeps: self.__teststeps.append(step.perform()) 然后self.__config会在各个地方调用。比如在__run_step_request中把base_url拼装起来。：\n## prepare arguments method = parsed_request_dict.pop(\u0026#34;method\u0026#34;) url_path = parsed_request_dict.pop(\u0026#34;url\u0026#34;) url = build_url(self.__config.base_url, url_path) parsed_request_dict[\u0026#34;verify\u0026#34;] = self.__config.verify parsed_request_dict[\u0026#34;json\u0026#34;] = parsed_request_dict.pop(\u0026#34;req_json\u0026#34;, {}) 而接下来的代码是真把我看晕了。\n第一个问题：config里面的变量是怎么用到测试步骤里面的？\n答案就是：\nstep.variables = merge_variables(step.variables, self.__config.variables) 通过merge_variables函数合并到了step.variables，step是下面这个类的实例：\nclass TStep(BaseModel): name: Name request: Union[TRequest, None] = None testcase: Union[Text, Callable, None] = None variables: VariablesMapping = {} setup_hooks: Hooks = [] teardown_hooks: Hooks = [] # used to extract request\u0026#39;s response field extract: VariablesMapping = {} # used to export session variables from referenced testcase export: Export = [] validators: Validators = Field([], alias=\u0026#34;validate\u0026#34;) validate_script: List[Text] = [] step.variables在run_testcase里面赋值：\n第一部分是把前面步骤提取的变量合并进来。 第二部分是把用例配置里面的变量合并进来，这就是第一个问题的答案。 第二个问题：变量是怎么提取出来的？\n先看看RequestWithOptionalArgs类的extract方法：\ndef extract(self) -\u0026gt; StepRequestExtraction: return StepRequestExtraction(self.__step_context) class StepRequestExtraction(object): def __init__(self, step_context: TStep): self.__step_context = step_context def with_jmespath(self, jmes_path: Text, var_name: Text) -\u0026gt; \u0026#34;StepRequestExtraction\u0026#34;: self.__step_context.extract[var_name] = jmes_path return self # def with_regex(self): # # TODO: extract response html with regex # pass # # def with_jsonpath(self): # # TODO: extract response json with jsonpath # pass def validate(self) -\u0026gt; StepRequestValidation: return StepRequestValidation(self.__step_context) def perform(self) -\u0026gt; TStep: return self.__step_context 这就是在测试脚本中用到的extract()和with_jmespath()。\n可以看到作者这里写了TODO支持正则表达式和JsonPath表达式。\n然后把变量名和JmesPath表达式存入了self.__step_context.extract中，这会用在：\n从而传入另外这个ResponseObject类的extract方法：\n然后self._search_jmespath根据表达式把值找到：\ndef _search_jmespath(self, expr: Text) -\u0026gt; Any: resp_obj_meta = { \u0026#34;status_code\u0026#34;: self.status_code, \u0026#34;headers\u0026#34;: self.headers, \u0026#34;cookies\u0026#34;: self.cookies, \u0026#34;body\u0026#34;: self.body, } if not expr.startswith(tuple(resp_obj_meta.keys())): return expr try: check_value = jmespath.search(expr, resp_obj_meta) except JMESPathError as ex: logger.error( f\u0026#34;failed to search with jmespath\\n\u0026#34; f\u0026#34;expression: {expr}\\n\u0026#34; f\u0026#34;data: {resp_obj_meta}\\n\u0026#34; f\u0026#34;exception: {ex}\u0026#34; ) raise return check_value 存入extract_mapping中：\n再存入step_data.export_vars：\n然后在__run_step中返回：\n最后通过extracted_variables存入到self.__session_variables中：\nself.__session_variables是runner.py模块中HttpRunne类的属性，可以理解为一个session级别的变量池。\n第三个问题：为什么用$就能直接使用变量？\n在run_testcase方法中有一段代码，解析变量：\n## parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions ) parse_variables_mapping的定义如下：\ndef parse_variables_mapping( variables_mapping: VariablesMapping, functions_mapping: FunctionsMapping = None ) -\u0026gt; VariablesMapping: parsed_variables: VariablesMapping = {} while len(parsed_variables) != len(variables_mapping): for var_name in variables_mapping: if var_name in parsed_variables: continue var_value = variables_mapping[var_name] variables = extract_variables(var_value) # check if reference variable itself if var_name in variables: # e.g. # variables_mapping = {\u0026#34;token\u0026#34;: \u0026#34;abc$token\u0026#34;} # variables_mapping = {\u0026#34;key\u0026#34;: [\u0026#34;$key\u0026#34;, 2]} raise exceptions.VariableNotFound(var_name) # check if reference variable not in variables_mapping not_defined_variables = [ v_name for v_name in variables if v_name not in variables_mapping ] if not_defined_variables: # e.g. {\u0026#34;varA\u0026#34;: \u0026#34;123$varB\u0026#34;, \u0026#34;varB\u0026#34;: \u0026#34;456$varC\u0026#34;} # e.g. {\u0026#34;varC\u0026#34;: \u0026#34;${sum_two($a, $b)}\u0026#34;} raise exceptions.VariableNotFound(not_defined_variables) try: parsed_value = parse_data( var_value, parsed_variables, functions_mapping ) except exceptions.VariableNotFound: continue parsed_variables[var_name] = parsed_value return parsed_variables 非常的复杂。其中有个函数extract_variables：\ndef extract_variables(content: Any) -\u0026gt; Set: \u0026#34;\u0026#34;\u0026#34; extract all variables in content recursively. \u0026#34;\u0026#34;\u0026#34; if isinstance(content, (list, set, tuple)): variables = set() for item in content: variables = variables | extract_variables(item) return variables elif isinstance(content, dict): variables = set() for key, value in content.items(): variables = variables | extract_variables(value) return variables elif isinstance(content, str): return set(regex_findall_variables(content)) return set() 里面有个regex_findall_variables函数：\ndef regex_findall_variables(raw_string: Text) -\u0026gt; List[Text]: try: match_start_position = raw_string.index(\u0026#34;$\u0026#34;, 0) except ValueError: return [] vars_list = [] while match_start_position \u0026lt; len(raw_string): # Notice: notation priority # $$ \u0026gt; $var # search $$ dollar_match = dolloar_regex_compile.match(raw_string, match_start_position) if dollar_match: match_start_position = dollar_match.end() continue # search variable like ${var} or $var var_match = variable_regex_compile.match(raw_string, match_start_position) if var_match: var_name = var_match.group(1) or var_match.group(2) vars_list.append(var_name) match_start_position = var_match.end() continue curr_position = match_start_position try: # find next $ location match_start_position = raw_string.index(\u0026#34;$\u0026#34;, curr_position + 1) except ValueError: # break while loop break return vars_list 在这个函数中看到了$符号的身影，就是在这里解析的了。而整个解析过程那是相当的复杂，没有用现成的包，而是作者自己实现的。并且还有个长达548行的parser_test.py测试代码，要说清楚，估计得另外再写一篇专项文章了。\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/httprunner/000004-httprunnerhttprunner3%E7%9A%84%E5%8F%98%E9%87%8F%E6%98%AF%E5%A6%82%E4%BD%95%E4%BC%A0%E9%80%92%E7%9A%84/","summary":"HttpRunner3的变量可以在测试类的用例配置中通过\u003ccode\u003evariables\u003c/code\u003e添加，也可以在测试步骤中使用\u003ccode\u003eextract()\u003c/code\u003e、\u003ccode\u003ewith_jmespath()\u003c/code\u003e提取出来放到变量\u003ccode\u003ex\u003c/code\u003e，再用\u003ccode\u003e$x\u003c/code\u003e传递给下一个接口使用，比如登录到下单流程的部分测试脚本如下：","title":"HttpRunner3的变量是如何传递的"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nServlet生命周期 Servlet的生命周期由Servlet容器（如Tomcat、Jetty等）管理，主要包括以下5个阶段：\n加载Servlet类\n创建Servlet实例\n调用init方法\n调用service方法\n调用destroy方法\n加载（Loading）：\n当Servlet容器启动或第一次接收到对某个Servlet的请求时，它会加载Servlet类 这个过程只发生一次，除非Servlet被卸载或容器重新启动 实例化（Instantiation）：\n在加载Servlet类之后，容器会创建一个Servlet实例 这个实例化过程也是一次性的，除非Servlet被卸载或容器重新启动 初始化（Initialization）：\n容器调用Servlet实例的init()方法进行初始化\ninit()方法只被调用一次，用于执行一次性的初始化任务，如读取配置参数、初始化资源等\n你可以重写init()方法以执行自定义的初始化逻辑\npublic void init(ServletConfig config) 请求处理（Request Handling）：\n每当有请求到达时，容器会调用Servlet的service()方法来处理请求\nservice()方法根据请求的类型（如GET、POST等）调用相应的处理方法（如doGet()、doPost()等）\npublic void service(ServletRequest request, ServletResponse response) 销毁（Destruction）：\n当容器决定卸载Servlet（如容器关闭或Servlet被重新加载）时，它会调用Servlet的destroy()方法\ndestroy()方法用于执行清理任务，如关闭资源、保存状态等\ndestroy()方法只被调用一次\npublic void destroy() Servlet重新加载\nServlet被重新加载通常发生在以下几种情况下：\nServlet类文件被修改：\n如果在开发过程中修改了Servlet类文件，Servlet容器可能会检测到这些更改并重新加载Servlet 这通常发生在开发环境中，生产环境中一般不会自动重新加载 容器配置发生变化：\n如果修改了Servlet容器的配置文件（如web.xml），容器可能会重新加载相关的Servlet 容器重启：\n当Servlet容器重启时，所有的Servlet都会被重新加载 手动重新加载：\n某些Servlet容器提供了手动重新加载Servlet的功能，通常通过管理控制台或命令行工具 当Servlet被重新加载时，Servlet容器会执行以下步骤：\n销毁现有的Servlet实例：\n容器调用现有Servlet实例的destroy()方法，执行清理任务 释放资源，如关闭数据库连接、释放文件句柄等 卸载Servlet类：\n容器卸载现有的Servlet类，释放类加载器持有的资源 重新加载Servlet类：\n容器重新加载Servlet类文件，创建新的类加载器实例 实例化新的Servlet对象：\n容器创建新的Servlet实例 初始化新的Servlet实例：\n容器调用新的Servlet实例的init()方法进行初始化 Servlet代码示例 有3种方式使用Servlet：\n①实现Servlet接口\nimport java.io.*; import javax.servlet.*; public class First implements Servlet { ServletConfig config = null; public void init(ServletConfig config) { this.config = config; System.out.println(\u0026#34;servlet is initialized\u0026#34;); } public void service(ServletRequest req, ServletResponse res) throws IOException, ServletException { res.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = res.getWriter(); out.print(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;b\u0026gt;hello simple servlet\u0026lt;/b\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); } public void destroy() { System.out.println(\u0026#34;servlet is destroyed\u0026#34;); } public ServletConfig getServletConfig() { return config; } public String getServletInfo() { return \u0026#34;copyright 2007-1010\u0026#34;; } } ②继承GenericServlet类\nimport java.io.*; import javax.servlet.*; public class First extends GenericServlet { public void service(ServletRequest req, ServletResponse res) throws IOException, ServletException { res.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = res.getWriter(); out.print(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;b\u0026gt;hello generic servlet\u0026lt;/b\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); } } ③继承HttpServlet类\nimport javax.servlet.http.*; import javax.servlet.*; import java.io.*; public class DemoServ extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter pw = res.getWriter(); String name = req.getParameter(\u0026#34;name\u0026#34;); pw.println(\u0026#34;Welcome \u0026#34; + name); } } 继承HttpServlet类是最常用的方式，接下来通过六个步骤，看看具体实践。\n第一步，创建目录结构。\n第二步，创建Servlet。\nimport javax.servlet.http.*; import javax.servlet.*; import java.io.*; public class DemoServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter pw = res.getWriter(); pw.println(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); pw.println(\u0026#34;Welcome to servlet\u0026#34;); pw.println(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); pw.close(); } } 第三步，编译Servlet。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javax.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 编译后将class文件拷贝到WEB-INF/classes。\n第四步，创建web.xml。\n\u0026lt;web-app\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;sonoojaiswal\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;DemoServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;sonoojaiswal\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/welcome\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;/web-app\u0026gt; web.xml是一个部署描述符文件，用于配置Java Web应用程序。\nJava Servlet 3.0引入了@WebServlet注解，用于简化Servlet的配置：\nimport javax.servlet.http.*; import javax.servlet.*; import java.io.*; import javax.servlet.annotation.WebServlet; @WebServlet(\u0026#34;/welcome\u0026#34;) public class DemoServlet extends HttpServlet { private static final long serialVersionUID = 1L; public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter pw = res.getWriter(); pw.println(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); pw.println(\u0026#34;Welcome to servlet\u0026#34;); pw.println(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); pw.close(); } } web.xml在低版本Servlet是必须的，在高版本Servlet已经不再需要。\n第五步，部署Servlet。\n将项目文件复制到Apache Tomcat的webapps目录下，启动Tomcat服务。\n第六步，访问Servlet。\n浏览器请求http://localhost:9999/demo/welcome，就能看到Servlet响应内容。\nServlet工作原理 请求映射：\n浏览器发送一个HTTP请求到服务器\n服务器根据web.xml文件中的配置，将请求映射到相应的Servlet\n创建请求和响应对象：\n服务器为每个请求创建HttpServletRequest和HttpServletResponse对象。这些对象包含了请求的所有信息和用于生成响应的方法 调用service方法：\n服务器在一个新的线程中调用Servlet的service方法来处理请求 调用public service方法：\npublic service 方法内部会调用protected service方法。public service方法是HttpServlet类的一部分，它负责处理所有类型的HTTP请求\npublic void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { if (req instanceof HttpServletRequest \u0026amp;\u0026amp; res instanceof HttpServletResponse) { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; this.service(request, response); } else { throw new ServletException(\u0026#34;non-HTTP request or response\u0026#34;); } } 调用protected service方法：\nprotected service方法根据请求的类型（如GET、POST等）调用相应的处理方法（如doGet、doPost等）\nprotected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long lastModified; if (method.equals(\u0026#34;GET\u0026#34;)) { lastModified = this.getLastModified(req); if (lastModified == -1L) { this.doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader(\u0026#34;If-Modified-Since\u0026#34;); if (ifModifiedSince \u0026lt; lastModified) { this.maybeSetLastModified(resp, lastModified); this.doGet(req, resp); } else { resp.setStatus(304); } } } else if (method.equals(\u0026#34;HEAD\u0026#34;)) { lastModified = this.getLastModified(req); this.maybeSetLastModified(resp, lastModified); this.doHead(req, resp); } else if (method.equals(\u0026#34;POST\u0026#34;)) { this.doPost(req, resp); } else if (method.equals(\u0026#34;PUT\u0026#34;)) { this.doPut(req, resp); } else if (method.equals(\u0026#34;DELETE\u0026#34;)) { this.doDelete(req, resp); } else if (method.equals(\u0026#34;OPTIONS\u0026#34;)) { this.doOptions(req, resp); } else if (method.equals(\u0026#34;TRACE\u0026#34;)) { this.doTrace(req, resp); } else { String errMsg = lStrings.getString(\u0026#34;http.method_not_implemented\u0026#34;); Object[] errArgs = new Object[]{method}; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(501, errMsg); } } 调用doGet方法：\n如果请求类型是GET，protected service方法会调用doGet方法。doGet方法处理请求并生成响应 生成响应并发送给客户端：\ndoGet方法生成响应（如HTML、JSON等），并通过HttpServletResponse对象将响应发送回客户端 删除请求和响应对象：\n在响应发送完毕后，Web容器会删除HttpServletRequest和HttpServletResponse对象，以释放资源 处理请求的线程要么被返回到线程池，要么被删除，这取决于服务器的实现 总结，Servlet的生命周期包括初始化（init）、请求处理（service，调用doGet、doPost等）、和销毁（destroy）。工作原理是：客户端请求，服务器分发，创建请求和响应对象，调用service方法，生成并发送响应，删除请求和响应对象。\n参考资料：\nhttps://www.javatpoint.com/life-cycle-of-a-servlet\nhttps://www.javatpoint.com/steps-to-create-a-servlet-using-tomcat-server\nhttps://www.javatpoint.com/how-servlet-works\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000004-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servlet%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E4%B8%8E%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"servlet生命周期\"\u003eServlet生命周期\u003c/h1\u003e\n\u003cp\u003eServlet的生命周期由Servlet容器（如Tomcat、Jetty等）管理，主要包括以下5个阶段：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e加载Servlet类\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e创建Servlet实例\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e调用init方法\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e调用service方法\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e调用destroy方法\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1165ac5a6f1c4eb9b037d57e7c48ea6a.png\"\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e加载（Loading）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e当Servlet容器启动或第一次接收到对某个Servlet的请求时，它会加载Servlet类\u003c/li\u003e\n\u003cli\u003e这个过程只发生一次，除非Servlet被卸载或容器重新启动\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e实例化（Instantiation）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e在加载Servlet类之后，容器会创建一个Servlet实例\u003c/li\u003e\n\u003cli\u003e这个实例化过程也是一次性的，除非Servlet被卸载或容器重新启动\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e初始化（Initialization）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e容器调用Servlet实例的\u003ccode\u003einit()\u003c/code\u003e方法进行初始化\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003einit()\u003c/code\u003e方法只被调用一次，用于执行一次性的初始化任务，如读取配置参数、初始化资源等\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e你可以重写\u003ccode\u003einit()\u003c/code\u003e方法以执行自定义的初始化逻辑\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003einit\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletConfig\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e请求处理（Request Handling）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e每当有请求到达时，容器会调用Servlet的\u003ccode\u003eservice()\u003c/code\u003e方法来处理请求\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eservice()\u003c/code\u003e方法根据请求的类型（如GET、POST等）调用相应的处理方法（如\u003ccode\u003edoGet()\u003c/code\u003e、\u003ccode\u003edoPost()\u003c/code\u003e等）\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eservice\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e销毁（Destruction）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e当容器决定卸载Servlet（如容器关闭或Servlet被重新加载）时，它会调用Servlet的\u003ccode\u003edestroy()\u003c/code\u003e方法\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003edestroy()\u003c/code\u003e方法用于执行清理任务，如关闭资源、保存状态等\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003edestroy()\u003c/code\u003e方法只被调用一次\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edestroy\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003eServlet重新加载\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eServlet被重新加载通常发生在以下几种情况下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eServlet类文件被修改：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如果在开发过程中修改了Servlet类文件，Servlet容器可能会检测到这些更改并重新加载Servlet\u003c/li\u003e\n\u003cli\u003e这通常发生在开发环境中，生产环境中一般不会自动重新加载\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e容器配置发生变化：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如果修改了Servlet容器的配置文件（如\u003ccode\u003eweb.xml\u003c/code\u003e），容器可能会重新加载相关的Servlet\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e容器重启：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e当Servlet容器重启时，所有的Servlet都会被重新加载\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e手动重新加载：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e某些Servlet容器提供了手动重新加载Servlet的功能，通常通过管理控制台或命令行工具\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e当Servlet被重新加载时，Servlet容器会执行以下步骤：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e销毁现有的Servlet实例：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e容器调用现有Servlet实例的\u003ccode\u003edestroy()\u003c/code\u003e方法，执行清理任务\u003c/li\u003e\n\u003cli\u003e释放资源，如关闭数据库连接、释放文件句柄等\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e卸载Servlet类：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e容器卸载现有的Servlet类，释放类加载器持有的资源\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e重新加载Servlet类：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e容器重新加载Servlet类文件，创建新的类加载器实例\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e实例化新的Servlet对象：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e容器创建新的Servlet实例\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e初始化新的Servlet实例：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e容器调用新的Servlet实例的\u003ccode\u003einit()\u003c/code\u003e方法进行初始化\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"servlet代码示例\"\u003eServlet代码示例\u003c/h1\u003e\n\u003cp\u003e有3种方式使用Servlet：\u003c/p\u003e\n\u003cp\u003e①实现Servlet接口\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejava.io.*\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ejavax.servlet.*\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eFirst\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eimplements\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServlet\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eServletConfig\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econfig\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enull\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003einit\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletConfig\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003econfig\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;servlet is initialized\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eservice\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eServletRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ereq\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eres\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eres\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esetContentType\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;text/html\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003ePrintWriter\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eout\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eres\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetWriter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eout\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eout\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026lt;b\u0026gt;hello simple servlet\u0026lt;/b\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eout\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003edestroy\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;servlet is destroyed\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eServletConfig\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetServletConfig\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econfig\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetServletInfo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;copyright 2007-1010\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e②继承GenericServlet类\u003c/p\u003e","title":"JavaWeb开发基础Servlet生命周期与工作原理"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n你可能已经迫不及待想安装Java，写个Java程序跑起来了。但是在这之前，有些概念需要提前了解，因为Java跟C、C++和Python都有点不一样。\n编译和执行 我们在文本文件中编写英文代码，这些英文计算机是看不懂的，因此需要做一下转换，转换为计算机能识别和运行的格式，这个转换，是由编译器来完成的。有些语言没有编译器，但是Java是有的。\n编译器转换后的文件，通常叫做二进制文件，或者可执行文件。文件内容从英文变成了字节码。字节码只有计算机能看懂，人是看不懂的，我们也不用关心，只需要保证我们编写的英文代码，能正确通过编译即可。执行的事，就交给计算机来做。\nJava代码文件以.java结尾，Java可执行文件以.class结尾。\n创建Java类 Java文件里面的所有代码，都必须放在Java Class里面：\npublic class MyFirstClass { } 文件名必须叫做MyFirstClass.java，文件名跟类名必须保持一致。为什么Java要设计得这么严格呢？方便，让你看到文件名，就能知道类名是啥，不用去猜。\n安装JDK 如何编译类文件呢？下载Java。\n下载Java，就是下载JDK，Java Development Kit，里面包含了Java编译器，将MyFirstClass.java 编译为MyFirstClass.class。\n所谓的JRE，Java Runtime Environment，它是JDK的一部分，只能用来运行Java程序，不能用来编译。\n下载地址： https://jdk.java.net/\n安装后需要根据操作系统（Windows、Linux、macOS）设置环境变量JAVA_HOME和PATH。\n验证安装成功：\njava -version 编译Class 使用javac命令编译：\njavac MyFirstClass.java 如果代码有问题，会出现报错。如果没问题，就会生成MyFirstClass.class文件。\n运行HelloWorld 添加代码：\npublic class MyFirstClass { public static void main(String... args) { System.out.println(\u0026#34;Hello, World!\u0026#34;); } } 重新编译，确保生成了class文件，使用java命令运行：\njava MyFirstClass 运行成功，控制台会打印一句：Hello, World!\n常见问题 问题1：javac HelloWorldApp，报错：\nClass names, \u0026#39;HelloWorldApp\u0026#39;, are only accepted if annotation processing is explicitly requested javac HelloWorldApp.java 编译命令javac后面应该跟文件名。\n问题2：java HelloWorldApp.class，报错：\nCould not find or load main class HelloWorldApp.class java HelloWorldApp 运行命令java后面应该跟类名。\n可以命令Help看到区别：\njavac Usage: javac \u0026lt;options\u0026gt; \u0026lt;source files\u0026gt; java Usage: java [-options] class [args...] (to execute a class) or java [-options] -jar jarfile [args...] (to execute a jar file) 在复杂应用开发时，并不会直接使用javac和java命令，而是使用IDE，Integrated Development Environment，集成开发环境，比如Eclipse、NetBeans和IntelliJ IDEA。\n参考资料：\nGetting Started with Java https://dev.java/learn/getting-started\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000004-%E5%AE%98%E6%96%B9java%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B01%E7%BC%96%E5%86%99%E8%BF%90%E8%A1%8Cjava%E7%A8%8B%E5%BA%8F/","summary":"你可能已经迫不及待想安装Java，写个Java程序跑起来了。但是在这之前，有些概念需要提前了解，因为Java跟C、C++和Python都有点不一样。","title":"Java官方笔记1编写运行Java程序"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n从一个问题说起 对于以下测试脚本：\n为了能调用进入房间接口，需要从考场接口获取考场token。为了调用考场接口，需要从登陆接口获取登陆token。元件说明如下：\n学生登录，提取登录${token}传入下个接口参数。\n添加HTTP Header Manager：\n考场token，提取考场${exam_token}传入下个接口参数。\n添加HTTP Header Manager：\n进入房间\n貌似挺合理，HTTP Header Manager会给下方的接口添加请求头，运行结果真的如我们所料么？\n运行顺序 在回答这个问题之前，有必要搞懂JMeter元件的运行顺序，它是由2 个维度来决定的：从上往下和元件类型。\n从上往下，即目录树从上往下。元件类型，分为3类：\n线程组、逻辑控制器。 取样器。 配置元件、前置处理器、定时器、后置处理器、断言、监听器。 它们的运行顺序如下：\n配置元件（如果存在）\n前置处理器（如果存在）\n定时器（如果存在）\n取样器（如果存在）\n后置处理器（如果存在且取样器的结果不为空）\n断言（如果存在且取样器的结果不为空）\n监听器（如果存在且取样器的结果不为空）\n换句话说，假设我们新建了1个线程，想用这个线程去发请求。\n第一步，初始化配置，比如参数化、设置Header、Cookie等，用到配置元件。\n第二步，可能需要给线程加点参数，比如用户参数，用到前置处理器。\n第三步，在发送请求前可能会等待一段时间，用到定时器。\n第四步，发送请求，用到取样器。\n第五步，可能需要提取响应数据，比如正则表达式提取器、JMESPath提取器，用到后置处理器。\n第六步，验证结果符合预期，用到断言。\n第七步，查看请求响应数据和测试结果，用到监听器。\n实践指南 对于以下所列元件：\nJMeter会按以下步骤运行：\n线程组（如果有多个线程组可以在测试计划设置是顺序执行还是同时执行） 简单控制器（父节点） HTTP Cookie管理器（配置元件） 用户参数（前置处理器） Synchronizing Timer（定时器） HTTP 请求1（取样器） 正则表达式提取器（后置处理器） 响应断言（断言） HTTP Cookie管理器（配置元件） 用户参数（前置处理器） Synchronizing Timer（定时器） HTTP 请求2（取样器） 正则表达式提取器（后置处理器） 响应断言（断言） HTTP 请求3（取样器） 察看结果树（严格来讲是与第 6 步并行，也就是取样器之后） 作用域 其中有个观察作用域实际效果的关键元件：HTTP请求2，它的前后并没有元件，但是也被作用上了。在JMeter中，同一层级的元件具有相同的作用域！\n简单控制器是一个执行单元，本身没有内容，它的作用是把元件进行分组：\n因为简单控制器通过分组给元件划分了层级，所以简单控制器下面的这些同层级元件，作用域相同，既会作用于 HTTP请求1，也会作用于HTTP请求2。注意了！配置元件、前置处理器、定时器、后置处理器、断言、监听器，这六个组件，会作用到范围内的所有取样器。\n除了同级作用域，还有上下级，JMeter的上级作用域包含下级作用域，但是下级是不能作用到上级。比如HTTP请求3，简单控制器下级的元件，是不会作用到HTTP请求3的。\n回答开头的问题 HTTP Header Manager是配置元件，会作用到范围内的所有取样器。这里有2个HTTP Header Manager，都位于同一层级，它们会一起执行。在JMeter同一执行单元中，如果相同类型的元件有多个，那么它们会被当做一个一起执行！\n测试一下，把最后一个HTTP Header Manager的authorization重命名为authorization2，查看考场接口的Headers：\n两个HTTP Header Manager都作用上了。\n为了避免混乱，在实际使用时建议：\n根据先后顺序，从上往下合理的放置元件的顺序。 对于配置元件、前置处理器、定时器、后置处理器、断言这六类元件，它们都是为取样器服务的，如果只想作用于单个取样器，那么最好放在这个取样器的下级。 按照建议调整后的测试脚本如下：\nUser Defined Variables和CSV Data Set Config，是配置元件，且跟取样器同级，会同时作用到这 3 个取样器上面。\n小结 本文开头引入了我在实际工作中碰到的问题，为了解决，先搞懂了JMeter元件运行顺序，然后参考了实践指南，发现了同一层级作用域相同这个原理，总结出了使用建议，配置元件、前置处理器、定时器、后置处理器、断言这六类元件，最好放在取样器的下级，调整后脚本如期运行。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000004-jmeterjmeter%E5%85%83%E4%BB%B6%E4%BD%9C%E7%94%A8%E5%9F%9F%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"从一个问题说起\"\u003e从一个问题说起\u003c/h1\u003e\n\u003cp\u003e对于以下测试脚本：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/08bb056c95a34670a750060f1e04625a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e为了能调用进入房间接口，需要从考场接口获取考场token。为了调用考场接口，需要从登陆接口获取登陆token。元件说明如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e学生登录，提取登录\u003ccode\u003e${token}\u003c/code\u003e传入下个接口参数。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e添加HTTP Header Manager：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9f6015fb46ff4c3eba94cf40c38d5841.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e考场token，提取考场\u003ccode\u003e${exam_token}\u003c/code\u003e传入下个接口参数。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e添加HTTP Header Manager：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4b3a68c97249434aae097fd43cada2f6.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e进入房间\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e貌似挺合理，HTTP Header Manager会给下方的接口添加请求头，运行结果真的如我们所料么？\u003c/p\u003e\n\u003ch1 id=\"运行顺序\"\u003e运行顺序\u003c/h1\u003e\n\u003cp\u003e在回答这个问题之前，有必要搞懂JMeter元件的运行顺序，它是由2 个维度来决定的：从上往下和元件类型。\u003c/p\u003e\n\u003cp\u003e从上往下，即目录树从上往下。元件类型，分为3类：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e线程组、逻辑控制器。\u003c/li\u003e\n\u003cli\u003e取样器。\u003c/li\u003e\n\u003cli\u003e配置元件、前置处理器、定时器、后置处理器、断言、监听器。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e它们的运行顺序如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e配置元件（如果存在）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e前置处理器（如果存在）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e定时器（如果存在）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e取样器（如果存在）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e后置处理器（如果存在且取样器的结果不为空）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e断言（如果存在且取样器的结果不为空）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e监听器（如果存在且取样器的结果不为空）\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e换句话说，假设我们新建了1个线程，想用这个线程去发请求。\u003c/p\u003e\n\u003cp\u003e第一步，初始化配置，比如参数化、设置Header、Cookie等，用到配置元件。\u003c/p\u003e\n\u003cp\u003e第二步，可能需要给线程加点参数，比如用户参数，用到前置处理器。\u003c/p\u003e\n\u003cp\u003e第三步，在发送请求前可能会等待一段时间，用到定时器。\u003c/p\u003e\n\u003cp\u003e第四步，发送请求，用到取样器。\u003c/p\u003e\n\u003cp\u003e第五步，可能需要提取响应数据，比如正则表达式提取器、JMESPath提取器，用到后置处理器。\u003c/p\u003e\n\u003cp\u003e第六步，验证结果符合预期，用到断言。\u003c/p\u003e\n\u003cp\u003e第七步，查看请求响应数据和测试结果，用到监听器。\u003c/p\u003e\n\u003ch1 id=\"实践指南\"\u003e实践指南\u003c/h1\u003e\n\u003cp\u003e对于以下所列元件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/107fe872d0c84a08a81dcb909d80a028.png\"\u003e\u003c/p\u003e\n\u003cp\u003eJMeter会按以下步骤运行：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e线程组（如果有多个线程组可以在测试计划设置是顺序执行还是同时执行）\u003c/li\u003e\n\u003cli\u003e简单控制器（父节点）\u003c/li\u003e\n\u003cli\u003eHTTP Cookie管理器（配置元件）\u003c/li\u003e\n\u003cli\u003e用户参数（前置处理器）\u003c/li\u003e\n\u003cli\u003eSynchronizing Timer（定时器）\u003c/li\u003e\n\u003cli\u003eHTTP 请求1（取样器）\u003c/li\u003e\n\u003cli\u003e正则表达式提取器（后置处理器）\u003c/li\u003e\n\u003cli\u003e响应断言（断言）\u003c/li\u003e\n\u003cli\u003eHTTP Cookie管理器（配置元件）\u003c/li\u003e\n\u003cli\u003e用户参数（前置处理器）\u003c/li\u003e\n\u003cli\u003eSynchronizing Timer（定时器）\u003c/li\u003e\n\u003cli\u003eHTTP 请求2（取样器）\u003c/li\u003e\n\u003cli\u003e正则表达式提取器（后置处理器）\u003c/li\u003e\n\u003cli\u003e响应断言（断言）\u003c/li\u003e\n\u003cli\u003eHTTP 请求3（取样器）\u003c/li\u003e\n\u003cli\u003e察看结果树（严格来讲是与第 6 步并行，也就是取样器之后）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"作用域\"\u003e作用域\u003c/h1\u003e\n\u003cp\u003e其中有个观察作用域实际效果的关键元件：HTTP请求2，它的前后并没有元件，但是也被作用上了。在JMeter中，同一层级的元件具有相同的作用域！\u003c/p\u003e","title":"JMeter元件作用域实践指南"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nTag JUnit5可以使用@Tag注解给测试类和测试方法打tag，这些tag能用来在执行时进行过滤，它跟group有点类似。\ntag应该遵循以下规则：\n不能为null或者为空。 不能包含空格。 不能包含ISO控制字符。 不能包含保留字符：, ( ) \u0026amp; | ! 示例代码：\nimport org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @Tag(\u0026#34;fast\u0026#34;) @Tag(\u0026#34;model\u0026#34;) class TaggingDemo { @Test @Tag(\u0026#34;taxes\u0026#34;) void testingTaxCalculation() { } } @Tag还可以通过元注解和组合注解，实现自定义注解，参考：\nhttps://dongfanger.gitee.io/blog/JUnit/002-JUnit5%E6%B3%A8%E8%A7%A3%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%BC%95.html#id2\nFilter 打好了tag后，在执行时可以进行过滤，比如Maven配置：\n\u0026lt;!-- ... --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-surefire-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.22.2\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;groups\u0026gt;acceptance | !feature-a\u0026lt;/groups\u0026gt; \u0026lt;excludedGroups\u0026gt;integration, regression\u0026lt;/excludedGroups\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;!-- ... --\u0026gt; groups用来指定包含哪些tag，excludedGroups用来指定排除哪些tag。\nOrder JUnit5默认使用了某种算法来确定test方法的执行顺序。我们可以通过@TestMethodOrder进行自定义，既可以使用内置类，也可以使用实现了MethodOrderer接口的类。\n内置类如下：\nDisplayName 按DisplayName的字母数字顺序 OrderAnnotation 通过@Order注解指定顺序 Random 随机顺序 Alphanumeric 按test方法名和参数列表的字母数字顺序 示例：\nimport org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @TestMethodOrder(OrderAnnotation.class) class OrderedTestsDemo { @Test @Order(1) void nullValues() { // perform assertions against null values } @Test @Order(2) void emptyValues() { // perform assertions against empty values } @Test @Order(3) void validValues() { // perform assertions against valid values } } 也可以配置全局的JUnit5的默认执行顺序，比如在src/test/resources/junit-platform.properties中：\njunit.jupiter.testmethod.order.default = \\ org.junit.jupiter.api.MethodOrderer$OrderAnnotation Lifecycle JUnit5默认会在执行测试方法前给每个测试类创建一个实例对象，让测试方法相互独立，这叫做per-method测试实例生命周期。\n就算测试方法被disable了也会创建实例。\n如果想让每个测试类只创建一个实例对象，测试方法共用这一个实例，那么可以使用注解@TestInstance(Lifecycle.PER_CLASS)，这叫做pre-class测试实例生命周期。\npre-class有一些好处，比如：\n在实例中存储变量，然后通过@BeforeEach或@AfterEach修改。 @BeforeAll、@AfterAll可以作用于非静态方法和接口default方法。 @BeforeAll、@AfterAll可以作用于@Nested嵌套测试类。 有两种方式可以设置全局的生命周期模式，第一种是JVM启动参数：\n-Djunit.jupiter.testinstance.lifecycle.default=per_class 第二种方式是配置文件，比如src/test/resources/junit-platform.properties：\njunit.jupiter.testinstance.lifecycle.default = per_class 如果要进行全局配置，建议使用配置文件，这样在出现问题时方便进行追溯。\n小结 本文首先介绍了给测试类和测试方法打tag进行分组，然后可以在运行时根据tag进行过滤，接着介绍了如何制定测试方法的执行顺序，最后介绍了两种生命周期：per-method和pre-class。\n参考资料：\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-tagging-and-filtering\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-test-execution-order\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000004-junitjunit5%E7%9A%84tagfilterorderlifecycle/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"tag\"\u003eTag\u003c/h1\u003e\n\u003cp\u003eJUnit5可以使用\u003ccode\u003e@Tag\u003c/code\u003e注解给测试类和测试方法打tag，这些tag能用来在执行时进行过滤，它跟group有点类似。\u003c/p\u003e\n\u003cp\u003etag应该遵循以下规则：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e不能为null或者为空。\u003c/li\u003e\n\u003cli\u003e不能包含空格。\u003c/li\u003e\n\u003cli\u003e不能包含ISO控制字符。\u003c/li\u003e\n\u003cli\u003e不能包含保留字符：\u003ccode\u003e,\u003c/code\u003e \u003ccode\u003e(\u003c/code\u003e \u003ccode\u003e)\u003c/code\u003e \u003ccode\u003e\u0026amp;\u003c/code\u003e \u003ccode\u003e|\u003c/code\u003e \u003ccode\u003e!\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e示例代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Tag\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Test\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Tag\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;fast\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Tag\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;model\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eTaggingDemo\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Tag\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;taxes\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003etestingTaxCalculation\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003e@Tag\u003c/code\u003e还可以通过元注解和组合注解，实现自定义注解，参考：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://dongfanger.gitee.io/blog/JUnit/002-JUnit5%E6%B3%A8%E8%A7%A3%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%BC%95.html#id2\"\u003ehttps://dongfanger.gitee.io/blog/JUnit/002-JUnit5%E6%B3%A8%E8%A7%A3%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%BC%95.html#id2\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"filter\"\u003eFilter\u003c/h1\u003e\n\u003cp\u003e打好了tag后，在执行时可以进行过滤，比如Maven配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e\u0026lt;!-- ... --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;build\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;plugins\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003emaven-surefire-plugin\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2.22.2\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groups\u0026gt;\u003c/span\u003eacceptance | !feature-a\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groups\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;excludedGroups\u0026gt;\u003c/span\u003eintegration, regression\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/excludedGroups\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/plugins\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/build\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e\u0026lt;!-- ... --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003egroups用来指定包含哪些tag，excludedGroups用来指定排除哪些tag。\u003c/p\u003e\n\u003ch1 id=\"order\"\u003eOrder\u003c/h1\u003e\n\u003cp\u003eJUnit5默认使用了某种算法来确定test方法的执行顺序。我们可以通过\u003ccode\u003e@TestMethodOrder\u003c/code\u003e进行自定义，既可以使用内置类，也可以使用实现了MethodOrderer接口的类。\u003c/p\u003e\n\u003cp\u003e内置类如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDisplayName 按DisplayName的字母数字顺序\u003c/li\u003e\n\u003cli\u003eOrderAnnotation 通过\u003ccode\u003e@Order\u003c/code\u003e注解指定顺序\u003c/li\u003e\n\u003cli\u003eRandom 随机顺序\u003c/li\u003e\n\u003cli\u003eAlphanumeric 按test方法名和参数列表的字母数字顺序\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e示例：\u003c/p\u003e","title":"JUnit5的Tag、Filter、Order、Lifecycle"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nOkHttp是什么 OkHttp是一个HTTP客户端，它不只是简单的发送请求和接收响应，还包含了失败重试、响应缓存等特性功能，并且能支持同步和异步请求。\nOkHttp的语法相当简洁，比如Get：\nOkHttpClient client = new OkHttpClient(); String run(String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } 比如Post：\npublic static final MediaType JSON = MediaType.get(\u0026#34;application/json; charset=utf-8\u0026#34;); OkHttpClient client = new OkHttpClient(); String post(String url, String json) throws IOException { RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder() .url(url) .post(body) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } 常见用法 Get（同步） private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;https://publicobject.com/helloworld.txt\u0026#34;) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); Headers responseHeaders = response.headers(); for (int i = 0; i \u0026lt; responseHeaders.size(); i++) { System.out.println(responseHeaders.name(i) + \u0026#34;: \u0026#34; + responseHeaders.value(i)); } System.out.println(response.body().string()); } } Get（异步） private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;http://publicobject.com/helloworld.txt\u0026#34;) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); Headers responseHeaders = response.headers(); for (int i = 0, size = responseHeaders.size(); i \u0026lt; size; i++) { System.out.println(responseHeaders.name(i) + \u0026#34;: \u0026#34; + responseHeaders.value(i)); } System.out.println(responseBody.string()); } } }); } 访问Header private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;https://api.github.com/repos/square/okhttp/issues\u0026#34;) .header(\u0026#34;User-Agent\u0026#34;, \u0026#34;OkHttp Headers.java\u0026#34;) .addHeader(\u0026#34;Accept\u0026#34;, \u0026#34;application/json; q=0.5\u0026#34;) .addHeader(\u0026#34;Accept\u0026#34;, \u0026#34;application/vnd.github.v3+json\u0026#34;) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); System.out.println(\u0026#34;Server: \u0026#34; + response.header(\u0026#34;Server\u0026#34;)); System.out.println(\u0026#34;Date: \u0026#34; + response.header(\u0026#34;Date\u0026#34;)); System.out.println(\u0026#34;Vary: \u0026#34; + response.headers(\u0026#34;Vary\u0026#34;)); } } Post JSON public static final MediaType JSON = MediaType.get(\u0026#34;application/json; charset=utf-8\u0026#34;); OkHttpClient client = new OkHttpClient(); String post(String url, String json) throws IOException { RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder() .url(url) .post(body) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } Post字符串 public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse(\u0026#34;text/x-markdown; charset=utf-8\u0026#34;); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { String postBody = \u0026#34;\u0026#34; + \u0026#34;Releases\\n\u0026#34; + \u0026#34;--------\\n\u0026#34; + \u0026#34;\\n\u0026#34; + \u0026#34; * _1.0_ May 6, 2013\\n\u0026#34; + \u0026#34; * _1.1_ June 15, 2013\\n\u0026#34; + \u0026#34; * _1.2_ August 11, 2013\\n\u0026#34;; Request request = new Request.Builder() .url(\u0026#34;https://api.github.com/markdown/raw\u0026#34;) .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody)) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); System.out.println(response.body().string()); } } Post Streaming public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse(\u0026#34;text/x-markdown; charset=utf-8\u0026#34;); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return MEDIA_TYPE_MARKDOWN; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8(\u0026#34;Numbers\\n\u0026#34;); sink.writeUtf8(\u0026#34;-------\\n\u0026#34;); for (int i = 2; i \u0026lt;= 997; i++) { sink.writeUtf8(String.format(\u0026#34; * %s = %s\\n\u0026#34;, i, factor(i))); } } private String factor(int n) { for (int i = 2; i \u0026lt; n; i++) { int x = n / i; if (x * i == n) return factor(x) + \u0026#34; × \u0026#34; + i; } return Integer.toString(n); } }; Request request = new Request.Builder() .url(\u0026#34;https://api.github.com/markdown/raw\u0026#34;) .post(requestBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); System.out.println(response.body().string()); } } Post文件 public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse(\u0026#34;text/x-markdown; charset=utf-8\u0026#34;); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { File file = new File(\u0026#34;README.md\u0026#34;); Request request = new Request.Builder() .url(\u0026#34;https://api.github.com/markdown/raw\u0026#34;) .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file)) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); System.out.println(response.body().string()); } } Post parameters private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { RequestBody formBody = new FormBody.Builder() .add(\u0026#34;search\u0026#34;, \u0026#34;Jurassic Park\u0026#34;) .build(); Request request = new Request.Builder() .url(\u0026#34;https://en.wikipedia.org/w/index.php\u0026#34;) .post(formBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); System.out.println(response.body().string()); } } Post multipart /** * The imgur client ID for OkHttp recipes. If you\u0026#39;re using imgur for anything other than running * these examples, please request your own client ID! https://api.imgur.com/oauth2 */ private static final String IMGUR_CLIENT_ID = \u0026#34;...\u0026#34;; private static final MediaType MEDIA_TYPE_PNG = MediaType.parse(\u0026#34;image/png\u0026#34;); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart(\u0026#34;title\u0026#34;, \u0026#34;Square Logo\u0026#34;) .addFormDataPart(\u0026#34;image\u0026#34;, \u0026#34;logo-square.png\u0026#34;, RequestBody.create(MEDIA_TYPE_PNG, new File(\u0026#34;website/static/logo-square.png\u0026#34;))) .build(); Request request = new Request.Builder() .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Client-ID \u0026#34; + IMGUR_CLIENT_ID) .url(\u0026#34;https://api.imgur.com/3/image\u0026#34;) .post(requestBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); System.out.println(response.body().string()); } } 解析JSON响应为Java对象 private final OkHttpClient client = new OkHttpClient(); private final Moshi moshi = new Moshi.Builder().build(); private final JsonAdapter\u0026lt;Gist\u0026gt; gistJsonAdapter = moshi.adapter(Gist.class); public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;https://api.github.com/gists/c2a7c39532239ff261be\u0026#34;) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); Gist gist = gistJsonAdapter.fromJson(response.body().source()); for (Map.Entry\u0026lt;String, GistFile\u0026gt; entry : gist.files.entrySet()) { System.out.println(entry.getKey()); System.out.println(entry.getValue().content); } } } static class Gist { Map\u0026lt;String, GistFile\u0026gt; files; } static class GistFile { String content; } 响应缓存 private final OkHttpClient client; public CacheResponse(File cacheDirectory) throws Exception { int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(cacheDirectory, cacheSize); client = new OkHttpClient.Builder() .cache(cache) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;http://publicobject.com/helloworld.txt\u0026#34;) .build(); String response1Body; try (Response response1 = client.newCall(request).execute()) { if (!response1.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response1); response1Body = response1.body().string(); System.out.println(\u0026#34;Response 1 response: \u0026#34; + response1); System.out.println(\u0026#34;Response 1 cache response: \u0026#34; + response1.cacheResponse()); System.out.println(\u0026#34;Response 1 network response: \u0026#34; + response1.networkResponse()); } String response2Body; try (Response response2 = client.newCall(request).execute()) { if (!response2.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response2); response2Body = response2.body().string(); System.out.println(\u0026#34;Response 2 response: \u0026#34; + response2); System.out.println(\u0026#34;Response 2 cache response: \u0026#34; + response2.cacheResponse()); System.out.println(\u0026#34;Response 2 network response: \u0026#34; + response2.networkResponse()); } System.out.println(\u0026#34;Response 2 equals Response 1? \u0026#34; + response1Body.equals(response2Body)); } 取消调用 private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;http://httpbin.org/delay/2\u0026#34;) // This URL is served with a 2 second delay. .build(); final long startNanos = System.nanoTime(); final Call call = client.newCall(request); // Schedule a job to cancel the call in 1 second. executor.schedule(new Runnable() { @Override public void run() { System.out.printf(\u0026#34;%.2f Canceling call.%n\u0026#34;, (System.nanoTime() - startNanos) / 1e9f); call.cancel(); System.out.printf(\u0026#34;%.2f Canceled call.%n\u0026#34;, (System.nanoTime() - startNanos) / 1e9f); } }, 1, TimeUnit.SECONDS); System.out.printf(\u0026#34;%.2f Executing call.%n\u0026#34;, (System.nanoTime() - startNanos) / 1e9f); try (Response response = call.execute()) { System.out.printf(\u0026#34;%.2f Call was expected to fail, but completed: %s%n\u0026#34;, (System.nanoTime() - startNanos) / 1e9f, response); } catch (IOException e) { System.out.printf(\u0026#34;%.2f Call failed as expected: %s%n\u0026#34;, (System.nanoTime() - startNanos) / 1e9f, e); } } 超时 private final OkHttpClient client; public ConfigureTimeouts() throws Exception { client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;http://httpbin.org/delay/2\u0026#34;) // This URL is served with a 2 second delay. .build(); try (Response response = client.newCall(request).execute()) { System.out.println(\u0026#34;Response completed: \u0026#34; + response); } } Per-call Configuration private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;http://httpbin.org/delay/1\u0026#34;) // This URL is served with a 1 second delay. .build(); // Copy to customize OkHttp for this request. OkHttpClient client1 = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build(); try (Response response = client1.newCall(request).execute()) { System.out.println(\u0026#34;Response 1 succeeded: \u0026#34; + response); } catch (IOException e) { System.out.println(\u0026#34;Response 1 failed: \u0026#34; + e); } // Copy to customize OkHttp for this request. OkHttpClient client2 = client.newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build(); try (Response response = client2.newCall(request).execute()) { System.out.println(\u0026#34;Response 2 succeeded: \u0026#34; + response); } catch (IOException e) { System.out.println(\u0026#34;Response 2 failed: \u0026#34; + e); } } 处理认证鉴权 private final OkHttpClient client; public Authenticate() { client = new OkHttpClient.Builder() .authenticator(new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { if (response.request().header(\u0026#34;Authorization\u0026#34;) != null) { return null; // Give up, we\u0026#39;ve already attempted to authenticate. } System.out.println(\u0026#34;Authenticating for response: \u0026#34; + response); System.out.println(\u0026#34;Challenges: \u0026#34; + response.challenges()); String credential = Credentials.basic(\u0026#34;jesse\u0026#34;, \u0026#34;password1\u0026#34;); return response.request().newBuilder() .header(\u0026#34;Authorization\u0026#34;, credential) .build(); } }) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url(\u0026#34;http://publicobject.com/secrets/hellosecret.txt\u0026#34;) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); System.out.println(response.body().string()); } } 跳过重试：\nif (credential.equals(response.request().header(\u0026#34;Authorization\u0026#34;))) { return null; // If we already failed with these credentials, don\u0026#39;t retry. } 重试次数：\nif (responseCount(response) \u0026gt;= 3) { return null; // If we\u0026#39;ve failed 3 times, give up. } 参考资料：\nOkHttp官网 https://square.github.io/okhttp/\nRecipes https://square.github.io/okhttp/recipes/\n源码 https://github.com/square/okhttp\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%9F%A5%E8%AF%86/000004-okhttp%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"okhttp是什么\"\u003eOkHttp是什么\u003c/h1\u003e\n\u003cp\u003eOkHttp是一个HTTP客户端，它不只是简单的发送请求和接收响应，还包含了失败重试、响应缓存等特性功能，并且能支持同步和异步请求。\u003c/p\u003e\n\u003cp\u003eOkHttp的语法相当简洁，比如Get：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e比如Post：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eJSON\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eget\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;application/json; charset=utf-8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ejson\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003eRequestBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ebody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequestBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecreate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ejson\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eJSON\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"常见用法\"\u003e常见用法\u003c/h1\u003e\n\u003ch2 id=\"get同步\"\u003eGet（同步）\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://publicobject.com/helloworld.txt\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eHeaders\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponseHeaders\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheaders\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e0\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponseHeaders\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esize\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#0550ae\"\u003e++\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponseHeaders\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponseHeaders\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"get异步\"\u003eGet（异步）\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://publicobject.com/helloworld.txt\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eenqueue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCallback\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eonFailure\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eCall\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecall\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintStackTrace\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eonResponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eCall\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecall\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponseBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponseBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003eHeaders\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponseHeaders\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheaders\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e0\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esize\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponseHeaders\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esize\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esize\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#0550ae\"\u003e++\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponseHeaders\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ename\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponseHeaders\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003evalue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponseBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e});\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"访问header\"\u003e访问Header\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://api.github.com/repos/square/okhttp/issues\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;User-Agent\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;OkHttp Headers.java\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eaddHeader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Accept\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;application/json; q=0.5\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eaddHeader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Accept\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;application/vnd.github.v3+json\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Server: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Server\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Date: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Date\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Vary: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheaders\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Vary\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"post-json\"\u003ePost JSON\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eJSON\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eget\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;application/json; charset=utf-8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ejson\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003eRequestBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ebody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequestBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecreate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ejson\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eJSON\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"post字符串\"\u003ePost字符串\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMEDIA_TYPE_MARKDOWN\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eparse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;text/x-markdown; charset=utf-8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epostBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Releases\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;--------\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34; * _1.0_ May 6, 2013\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34; * _1.1_ June 15, 2013\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34; * _1.2_ August 11, 2013\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://api.github.com/markdown/raw\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eRequestBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecreate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMEDIA_TYPE_MARKDOWN\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003epostBody\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"post-streaming\"\u003ePost Streaming\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMEDIA_TYPE_MARKDOWN\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eparse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;text/x-markdown; charset=utf-8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequestBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequestBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequestBody\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003econtentType\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMEDIA_TYPE_MARKDOWN\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003ewriteTo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eBufferedSink\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esink\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003esink\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ewriteUtf8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Numbers\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003esink\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ewriteUtf8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;-------\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e2\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e997\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#0550ae\"\u003e++\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003esink\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ewriteUtf8\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eformat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34; * %s = %s\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003efactor\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e)));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003efactor\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e2\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#0550ae\"\u003e++\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ex\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ex\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003efactor\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ex\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34; × \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ei\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eInteger\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003etoString\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003en\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e};\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://api.github.com/markdown/raw\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequestBody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"post文件\"\u003ePost文件\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMEDIA_TYPE_MARKDOWN\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eparse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;text/x-markdown; charset=utf-8\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eFile\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003efile\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFile\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;README.md\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://api.github.com/markdown/raw\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eRequestBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecreate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMEDIA_TYPE_MARKDOWN\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003efile\u003cspan style=\"color:#1f2328\"\u003e))\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"post-parameters\"\u003ePost parameters\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequestBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eformBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFormBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eadd\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;search\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Jurassic Park\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://en.wikipedia.org/w/index.php\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eformBody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"post-multipart\"\u003ePost multipart\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e/**\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e   * The imgur client ID for OkHttp recipes. If you\u0026#39;re using imgur for anything other than running\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e   * these examples, please request your own client ID! https://api.imgur.com/oauth2\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e   */\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIMGUR_CLIENT_ID\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;...\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMEDIA_TYPE_PNG\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMediaType\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eparse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;image/png\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequestBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequestBody\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMultipartBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esetType\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMultipartBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eFORM\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eaddFormDataPart\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;title\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Square Logo\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eaddFormDataPart\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;image\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;logo-square.png\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eRequestBody\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecreate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMEDIA_TYPE_PNG\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eFile\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;website/static/logo-square.png\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)))\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Authorization\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Client-ID \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIMGUR_CLIENT_ID\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://api.imgur.com/3/image\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003epost\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequestBody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"解析json响应为java对象\"\u003e解析JSON响应为Java对象\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMoshi\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emoshi\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eMoshi\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eJsonAdapter\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eGist\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003egistJsonAdapter\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emoshi\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eadapter\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eGist\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eclass\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://api.github.com/gists/c2a7c39532239ff261be\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eGist\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003egist\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003egistJsonAdapter\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003efromJson\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esource\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eMap\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eEntry\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eString\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eGistFile\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eentry\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003egist\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003efiles\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eentrySet\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eentry\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetKey\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eentry\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetValue\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003econtent\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eGist\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eMap\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eString\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eGistFile\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003efiles\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eGistFile\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econtent\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"响应缓存\"\u003e响应缓存\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eCacheResponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eFile\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecacheDirectory\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecacheSize\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e10\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1024\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1024\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 10 MiB\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eCache\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecache\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCache\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecacheDirectory\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecacheSize\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecache\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ecache\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://publicobject.com/helloworld.txt\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1Body\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse1\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eresponse1Body\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 1 response:          \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 1 cache response:    \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecacheResponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 1 network response:  \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enetworkResponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse2Body\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse2\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse2\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse2\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eresponse2Body\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse2\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 2 response:          \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse2\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 2 cache response:    \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse2\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecacheResponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 2 network response:  \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse2\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enetworkResponse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 2 equals Response 1? \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse1Body\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eequals\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse2Body\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"取消调用\"\u003e取消调用\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eScheduledExecutorService\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eexecutor\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eExecutors\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewScheduledThreadPool\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://httpbin.org/delay/2\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// This URL is served with a 2 second delay.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003elong\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartNanos\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enanoTime\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCall\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecall\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// Schedule a job to cancel the call in 1 second.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eexecutor\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eschedule\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRunnable\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintf\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%.2f Canceling call.%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enanoTime\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartNanos\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1e9f\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003ecall\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecancel\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintf\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%.2f Canceled call.%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enanoTime\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartNanos\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1e9f\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTimeUnit\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eSECONDS\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintf\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%.2f Executing call.%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enanoTime\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartNanos\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1e9f\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecall\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintf\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%.2f Call was expected to fail, but completed: %s%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enanoTime\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartNanos\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1e9f\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ecatch\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintf\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;%.2f Call failed as expected: %s%n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enanoTime\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003estartNanos\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1e9f\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"超时\"\u003e超时\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eConfigureTimeouts\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003econnectTimeout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e10\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTimeUnit\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eSECONDS\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ewriteTimeout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e10\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTimeUnit\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eSECONDS\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ereadTimeout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e30\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTimeUnit\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eSECONDS\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://httpbin.org/delay/2\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// This URL is served with a 2 second delay.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response completed: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"per-call-configuration\"\u003ePer-call Configuration\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://httpbin.org/delay/1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// This URL is served with a 1 second delay.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// Copy to customize OkHttp for this request.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient1\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ereadTimeout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e500\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTimeUnit\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eMILLISECONDS\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient1\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 1 succeeded: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ecatch\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 1 failed: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// Copy to customize OkHttp for this request.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient2\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ereadTimeout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e3000\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eTimeUnit\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eMILLISECONDS\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient2\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 2 succeeded: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ecatch\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Response 2 failed: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ee\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"处理认证鉴权\"\u003e处理认证鉴权\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eAuthenticate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eclient\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eOkHttpClient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eauthenticator\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eAuthenticator\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eauthenticate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eRoute\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eroute\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003erequest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Authorization\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enull\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e              \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enull\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// Give up, we\u0026#39;ve already attempted to authenticate.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Authenticating for response: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Challenges: \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003echallenges\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecredential\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCredentials\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebasic\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;jesse\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;password1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e            \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003erequest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eheader\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Authorization\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecredential\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e                \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e          \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e})\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003erun\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003eRequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003erequest\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eRequest\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eBuilder\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eurl\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://publicobject.com/secrets/hellosecret.txt\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebuild\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003etry\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eResponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eclient\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewCall\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003erequest\u003cspan style=\"color:#1f2328\"\u003e).\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eexecute\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e!\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eisSuccessful\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e())\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrow\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eIOException\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Unexpected code \u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eresponse\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ebody\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003estring\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e跳过重试：\u003c/p\u003e","title":"OkHttp学习笔记"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n可以通过命令行查看所有marker，包括内置和自定义的\npytest --markers 内置marker 内置marker本文先讲usefixtures 、filterwarnings 、skip 、skipif 、xfail这5个。参数化的marker我会写在《pytest参数化》中，hook的marker我会写在《pytest hook》中，插件的marker（pytest-ordering、allure等）我会写在《pytest插件》中。当前只需知道有以上这些分类的marker即可，更多内容请关注后续文章。\nusefixtures 如果我们只想把fixture注入到test中，test不直接访问fixture的时候，就需要用到usefixtures。\n示例，test需要一个临时目录，但是并不需要知道这个目录具体路径在哪\n## content of conftest.py import os import shutil import tempfile import pytest @pytest.fixture def cleandir(): old_cwd = os.getcwd() newpath = tempfile.mkdtemp() os.chdir(newpath) yield os.chdir(old_cwd) shutil.rmtree(newpath) ## content of test_setenv.py import os import pytest @pytest.mark.usefixtures(\u0026#34;cleandir\u0026#34;) class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] with open(\u0026#34;myfile\u0026#34;, \u0026#34;w\u0026#34;) as f: f.write(\u0026#34;hello\u0026#34;) def test_cwd_again_starts_empty(self): assert os.listdir(os.getcwd()) == [] TestDirectoryInit的测试方法需要一个临时目录作为当前工作目录，在类上添加@pytest.mark.usefixtures(\u0026quot;cleandir\u0026quot;)，类的方法不加fixture也能有\u0026quot;cleandir\u0026quot;的效果。\nusefixtures可以添加多个fixture\n@pytest.mark.usefixtures(\u0026#34;cleandir\u0026#34;, \u0026#34;anotherfixture\u0026#34;) usefixtures可以用在pytestmark，作用域是定义所在module的所有tests\npytestmark = pytest.mark.usefixtures(\u0026#34;cleandir\u0026#34;) usefixtures也可以用在pytest.ini，作用域是整个项目的所有tests\n## content of pytest.ini [pytest] usefixtures = cleandir 不过需要注意的是fixture函数本身是不能用usefixtures的，如果想要嵌套fixture，只能通过在fixture修饰的函数中，添加参数这种方式。\nfilterwarnings 过滤警告信息。\n示例，api_v1()抛出了“api v1”的警告，test_one()函数使用filterwarnings过滤掉了\nimport warnings def api_v1(): warnings.warn(UserWarning(\u0026#34;api v1, should use functions from v2\u0026#34;)) return 1 @pytest.mark.filterwarnings(\u0026#34;ignore:api v1\u0026#34;) def test_one(): assert api_v1() == 1 同样可以添加到pytestmark和pytest.ini中。\nskip 跳过，不测试。\n示例，skip需要添加reason哦\n@pytest.mark.skip(reason=\u0026#34;no way of currently testing this\u0026#34;) def test_the_unknown(): ... 不过，更实用的方式是调用pytest.skip(reason)函数，而不是用mark，这样就可以用if判断跳不跳\ndef test_function(): if not valid_config(): pytest.skip(\u0026#34;unsupported configuration\u0026#34;) allow_module_level 可以跳过整个module\nimport sys import pytest if not sys.platform.startswith(\u0026#34;win\u0026#34;): pytest.skip(\u0026#34;skipping windows-only tests\u0026#34;, allow_module_level=True) skipif if判断跳不跳，还可以用skipif。\n示例，如果Python版本小于3.6就跳过测试\nimport sys @pytest.mark.skipif(sys.version_info \u0026lt; (3, 6), reason=\u0026#34;requires python3.6 or higher\u0026#34;) def test_function(): ... 如果想在summary中看到reason，需要添加-rs参数。\n可以把skipif赋值给变量，然后直接引用变量，或者把变量import到其他module中使用\n## content of test_mymodule.py import mymodule minversion = pytest.mark.skipif( mymodule.__versioninfo__ \u0026lt; (1, 1), reason=\u0026#34;at least mymodule-1.1 required\u0026#34; ) @minversion def test_function(): ... ## test_myothermodule.py from test_mymodule import minversion @minversion def test_anotherfunction(): ... skipif添加到class上，会跳过类中所有方法。\n可以使用pytestmark跳过module内所有test\n## test_module.py pytestmark = pytest.mark.skipif(...) 如果function有多个skipif作用，只要有一个为True，就会跳过。\nxfail 明知失败，依然前行！不好意思跑偏了。xfail就是expected fail，预期失败\n@pytest.mark.xfail def test_function(): ... 执行后summary不会统计为\u0026quot;failed\u0026quot;，会单独列出来。如果结果失败了，“expected to fail” (XFAIL)；如果结果成功了，“unexpectedly passing” (XPASS)。但是整个执行结果是”Tests passed“。\nif判断\ndef test_function(): if not valid_config(): pytest.xfail(\u0026#34;failing configuration (but should work)\u0026#34;) 值得注意的是，marker会继续执行所有test代码，pytest.xfail()函数会抛出异常，中断执行后续代码\n添加condition，判断条件\n@pytest.mark.xfail(sys.platform == \u0026#34;win32\u0026#34;, reason=\u0026#34;bug in a 3rd party library\u0026#34;) def test_function(): ... 添加reason，理由\n@pytest.mark.xfail(reason=\u0026#34;known parser issue\u0026#34;) def test_function(): ... 添加raises，抛出异常/错误\n@pytest.mark.xfail(raises=RuntimeError) def test_function(): ... 添加run，禁止运行\n@pytest.mark.xfail(run=False) def test_function(): ... 添加strict，严格模式，即使xpass也会强制失败，summary中有输出信息”[XPASS(strict)] “，测试结果为”Tests failed“。\n@pytest.mark.xfail(strict=True) def test_function(): ... 断言成功也强制失败，确实够强势的！\n可以在ini文件中定义全局strict\n[pytest] xfail_strict=true 在命令行添加**\u0026ndash;runxfail**，忽略xfail marker，相当于没有添加这个标记的效果，该成功就成功，该失败就失败，再强势也不虚，哈哈，恶灵退散。\npytest --runxfail pytest --runxfail 再强势也不虚，恶灵退散，哈哈。\n自定义marker 通过注解自定义marker\n## content of test_server.py import pytest @pytest.mark.webtest def test_send_http(): pass # perform some webtest test for your app def test_something_quick(): pass def test_another(): pass class TestClass: def test_method(self): pass 在命令行通过-m指定运行mark打标的test\n$ pytest -v -m webtest 也可以反选\n$ pytest -v -m \u0026#34;not webtest\u0026#34; 但是，这样定义的marker是未注册的！在执行后会警告，PytestUnknownMarkWarning。如果添加了命令行参数--strict-markers ，未注册的marker会报错。\n可以在pytest.ini文件中注册，冒号后面的所有代码都是marker说明，包括换行\n[pytest] markers = slow: marks tests as slow (deselect with \u0026#39;-m \u0026#34;not slow\u0026#34;\u0026#39;) serial 更高级的，可以在pytest_configure hook函数中注册，这主要用在第三方插件\ndef pytest_configure(config): config.addinivalue_line( \u0026#34;markers\u0026#34;, \u0026#34;env(name): mark test to run only on named environment\u0026#34; ) 简要回顾 本文介绍了5个pytest内置的marker，接着介绍了如何自定义marker和注册marker。通过marker，可以让我们更灵活的执行用例。\n参考资料\ndocs-pytest-org-en-stable\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/pytest/000004-pytestpytest%E5%86%85%E7%BD%AE%E5%92%8C%E8%87%AA%E5%AE%9A%E4%B9%89marker/","summary":"可以通过命令行查看所有marker，包括内置和自定义的","title":"pytest内置和自定义marker"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n数学里面的加减乘除，就是运算符，但是 Python 的运算符更多样，更复杂，分为算术运算符、比较运算符、赋值运算符、位运算符、逻辑运算符、成员运算符、身份运算符。为了更直观的看到运算符的使用，本文采用代码和注释结合的方式进行讲解。\n算术运算符 a = 21 b = 10 c = 0 # 加 c = a + b # 31 # 减 c = a - b # 11 # 乘 c = a * b # 210 # 除 c = a / b # 2.1 # 求余数 c = a % b # 1 # 幂（次方） a = 2 b = 3 c = a**b # 8 # 取整数，向下取接近商的整数 c = 9//2 # 4 c = -9//2 # -5 比较运算符 代码中的 if ... else ... 是指“如果，满足条件，做什么事，否则，做其他事”。\na = 21 b = 10 c = 0 # 等于 if ( a == b ): print(\u0026#34;a 等于 b\u0026#34;) else: print(\u0026#34;a 不等于 b\u0026#34;) # 不等于 if ( a != b ): print(\u0026#34;a 不等于 b\u0026#34;) else: print(\u0026#34;a 等于 b\u0026#34;) # 小于 if ( a \u0026lt; b ): print(\u0026#34;a 小于 b\u0026#34;) else: print(\u0026#34;a 大于等于 b\u0026#34;) # 大于 if ( a \u0026gt; b ): print(\u0026#34;a 大于 b\u0026#34;) else: print(\u0026#34;a 小于等于 b\u0026#34;) a = 5 b = 20 # 小于等于 if ( a \u0026lt;= b ): print(\u0026#34;a 小于等于 b\u0026#34;) else: print(\u0026#34;a 大于 b\u0026#34;) # 大于等于 if ( b \u0026gt;= a ): print(\u0026#34;b 大于等于 a\u0026#34;) else: print(\u0026#34;b 小于 a\u0026#34;) 赋值运算符 = 是赋值运算符，算术运算符都可以右边加上 = 变成“算术赋值”运算符。\na = 21 b = 10 c = 0 # 简单的赋值 c = a + b # 31 # 等价于 c = c + a c += a # 52 # 等价于 c = c - a c -= a # 31 # 等价于 c = c * a c *= a # 651 # 等价于 c = c / a c /= a # 31.0 c = 2 # 等价于 c = c % a c %= a # 2 # 等价于 c = c ** a c **= a # 2097152 # 等价于 c = c // a c //= a # 99864 Python3.8 新增了海象运算符 :=，可以在表达式内部为变量赋值：\nif (n := len(a)) \u0026gt; 10: print(f\u0026#34;List is too long ({n} elements, expected \u0026lt;= 10)\u0026#34;) 位运算符 我们常见的数字是十进制的，十进制可以转换为 01 形式的二进制，例如 60 的转换为二进制后变成了 0011 1100。位运算符就是把数字看作二进制来进行计算。\na = 60 # 60 = 0011 1100 b = 13 # 13 = 0000 1101 c = 0 # 与，两个值都为1，结果为1，否则为0 c = a \u0026amp; b # 12 = 0000 1100 # 或，两个值有一个为1，结果就为1 c = a | b # 61 = 0011 1101 # 异或，两个值不同，结果为1 c = a ^ b # 49 = 0011 0001 # 取反，0变成1,1变成0 c = ~a # -61 = 1100 0011 # 左移，高位丢弃，低位补0 c = a \u0026lt;\u0026lt; 2 # 240 = 1111 0000 # 右移，低位丢弃，高位补0 c = a \u0026gt;\u0026gt; 2 # 15 = 0000 1111 逻辑运算符 经常用在判断条件里面，判断条件为 True / False。\na = 10 b = 20 # 布尔“与”，2个值都为True，结果为True if ( a and b ): print(\u0026#34;变量 a 和 b 都为 true\u0026#34;) else: print(\u0026#34;变量 a 和 b 有一个不为 true\u0026#34;) # 布尔“或”，只要有一个为True，结果就为True if ( a or b ): print(\u0026#34;变量 a 和 b 都为 true，或其中一个变量为 true\u0026#34;) else: print(\u0026#34;变量 a 和 b 都不为 true\u0026#34;) # 修改变量 a 的值 a = 0 if ( a and b ): print(\u0026#34;变量 a 和 b 都为 true\u0026#34;) else: print(\u0026#34;变量 a 和 b 有一个不为 true\u0026#34;) if ( a or b ): print(\u0026#34;变量 a 和 b 都为 true，或其中一个变量为 true\u0026#34;) else: print(\u0026#34;变量 a 和 b 都不为 true\u0026#34;) if not( a and b ): print(\u0026#34;变量 a 和 b 都为 false，或其中一个变量为 false\u0026#34;) else: print(\u0026#34;变量 a 和 b 都为 true\u0026#34;) 注意，逻辑运算符是从左到右依次运算的，只要不符合条件就不会继续运算，比如 and，只要出现了 False，就会停止运算，结果直接判定为 False。比如 or，只要出现了 True，就会停止运算，结果直接判定为 True。\n成员运算符 成员是指字符串、列表、元组等系列中的元素，成员运算符用于判断元素是否在系列里面。\na = 10 b = 20 list = [1, 2, 3, 4, 5 ] # in 在里面 if ( a in list ): print(\u0026#34;变量 a 在给定的列表中 list 中\u0026#34;) else: print(\u0026#34;变量 a 不在给定的列表中 list 中\u0026#34;) # not in 不在里面 if ( b not in list ): print(\u0026#34;变量 b 不在给定的列表中 list 中\u0026#34;) else: print(\u0026#34;变量 b 在给定的列表中 list 中\u0026#34;) 身份运算符 Python 里面的对象其实都有一个内存地址，可以使用 id() 函数来获取，身份就是指的内存地址。\na = 20 b = a if ( a is b ): print(\u0026#34;a 和 b 有相同的标识\u0026#34;) # 结果 else: print(\u0026#34;a 和 b 没有相同的标识\u0026#34;) if ( id(a) == id(b) ): print(\u0026#34;a 和 b 有相同的标识\u0026#34;) # 结果 else: print(\u0026#34;a 和 b 没有相同的标识\u0026#34;) # 修改变量 b 的值 b = 30 if ( a is b ): print(\u0026#34;a 和 b 有相同的标识\u0026#34;) else: print(\u0026#34;a 和 b 没有相同的标识\u0026#34;) # 结果 if ( a is not b ): print(\u0026#34;a 和 b 没有相同的标识\u0026#34;) # 结果 else: print(\u0026#34;a 和 b 有相同的标识\u0026#34;) 我们在前面知道 == 是可以判断变量是否相等的，注意，== 判断的是值是否相等，is 判断的是引用（内存地址）是否相等。\n运算符优先级 不用刻意记优先级，实际编码时请尽情滥用小括号 ()，防止优先级算错。\n运算符 描述 ** 指数 (最高优先级) ~ + - 按位翻转, 一元加号和减号 (最后两个的方法名为 +@ 和 -@) * / % // 乘，除，求余数和取整除 + - 加法减法 \u0026raquo; \u0026laquo; 右移，左移运算符 \u0026amp; 位 \u0026lsquo;AND\u0026rsquo; ^ | 位运算符 \u0026lt;= \u0026lt; \u0026gt; \u0026gt;= 比较运算符 == != 等于运算符 = %= /= //= -= += *= **= 赋值运算符 is is not 身份运算符 in not in 成员运算符 not and or 逻辑运算符 小结 本文以实例的形式直观的介绍了 Python 运算符的使用。运算符是 Python 中相对简单的知识点。\n参考资料：\nhttps://www.runoob.com/python3/python3-basic-operators.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000004-pythonpython%E8%BF%90%E7%AE%97%E7%AC%A6%E5%8F%AF%E4%B8%8D%E5%8F%AA%E6%9C%89%E5%8A%A0%E5%87%8F%E4%B9%98%E9%99%A4/","summary":"数学里面的加减乘除，就是运算符，但是 Python 的运算符更多样，更复杂，分为算术运算符、比较运算符、赋值运算符、位运算符、逻辑运算符、成员运算符、身份运算符。为了更直观的看到运算符的使用，本文采用代码和注释结合的方式进行讲解。","title":"Python运算符可不只有加减乘除"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n这两天要在阿里云日志中操作UI，以输入关键字搜索日志。\n在send_keys时报错element not interactable。\niframe 第一个问题是iframe，通过切换iframe解决：\ndriver.switch_to.frame(0) # 索引从0开始 span 第二个问题是span。\nF12查看html长这样：\n①find_element(pre).click 有效，说明iframe切换成功了\n②find_element(span2).send_keys报错\nelement not interactable span不支持send_keys。\n第一个方法，通过调js来解决，设置span的innerText\nsearch_text = \u0026#39;error\u0026#39; log_search_span = driver.find_element_by_xpath( \u0026#39;//*[@id=\u0026#34;queryEl\u0026#34;]/div[2]/div/div[6]/div[1]/div/div/div/div[5]/div/pre\u0026#39;) js = \u0026#39;arguments[0].innerText=arguments[1];\u0026#39; driver.execute_script(js, log_search_span, search_text) 第二个方法，添加属性contenteditable\nsearch_text = \u0026#39;error\u0026#39; log_search_span = driver.find_element_by_xpath( \u0026#39;//*[@id=\u0026#34;queryEl\u0026#34;]/div[2]/div/div[6]/div[1]/div/div/div/div[5]/div/pre\u0026#39;) js = \u0026#39;arguments[0].setAttribute(\u0026#34;contenteditable\u0026#34;, \u0026#34;true\u0026#34;);\u0026#39; driver.execute_script(js, log_search_span) log_search_span.send_keys(search_text) # 可编辑后send_keys成功 不简单 虽然值设置成功了，但是不能用，在查询时，还是用空值在查询。\n经过分析，得出以下线索：\n设置的元素值是已经生成后的dom 程序真正的dom隐藏在js/后端中 span是由js或后端动态生成的 要想模拟出来，需要分析js实现代码 控制台的js代码是加密混淆过的 selenium的send_keys已经是模拟键盘输入了，不知道为啥没有触发相关代码，存值到dom 网上很多的方案是\u0026lt;input\u0026gt;标签，比较好解决，阿里的这个span还真有点不简单。\nchrome console调试js 最后分享一下chrome console调试js的方法。\nF12后，在元素上右键，选择Copy selector，复制selector\n切换到console，输入即可定位到该元素：\ndocument.querySelector(\u0026#39;#queryEl \u0026gt; div.react-codemirror2 \u0026gt; div \u0026gt; div.CodeMirror-scroll \u0026gt; div.CodeMirror-sizer \u0026gt; div \u0026gt; div \u0026gt; div \u0026gt; div.CodeMirror-code \u0026gt; div \u0026gt; pre \u0026gt; span \u0026gt; span\u0026#39;).innerText; ","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E8%87%AA%E5%8A%A8%E5%8C%96/000004-ui%E8%87%AA%E5%8A%A8%E5%8C%96send_keys%E6%8A%A5%E9%94%99element_not_interactable/","summary":"这两天要在阿里云日志中操作UI，以输入关键字搜索日志。","title":"send_keys报错element_not_interactable"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nSpring Boot helps you to create stand-alone, production-grade Spring-based applications that you can run. Most Spring Boot applications need very little Spring configuration.\nBuild Tool：Maven\nServlet Container：Tomcat\n快速创建：start.spring.io\n手动创建：\nMaven pom.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;myproject\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.1\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;!-- Additional lines to be added here... --\u0026gt; \u0026lt;/project\u0026gt; Spring Boot provides a number of “Starters” that let you add jars to your classpath. “Starters” provide dependencies that you are likely to need when developing a specific type of application.\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 编写代码：\nimport org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class MyApplication { @RequestMapping(\u0026#34;/\u0026#34;) String home() { return \u0026#34;Hello World!\u0026#34;; } public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } @Controller handling incoming web requests\n@RequestMapping provides “routing” information\n@SpringBootApplication combines @SpringBootConfiguration, @EnableAutoConfiguration and @ComponentScan\nWe need to pass MyApplication.class as an argument to the run method to tell SpringApplication which is the primary Spring component.\n运行：\n$ mvn spring-boot:run . ____ _ __ _ _ /\\\\ / ___\u0026#39;_ __ _ _(_)_ __ __ _ \\ \\ \\ \\ ( ( )\\___ | \u0026#39;_ | \u0026#39;_| | \u0026#39;_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) \u0026#39; |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.1.1) ....... . . . ....... . . . (log output here) ....... . . . ........ Started MyApplication in 0.906 seconds (process running for 6.514) 打jar包：\nSpringBoot打jar包，用Maven打包就行了，SpringBoot提供了maven-plugin\n\u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; $ mvn package [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building myproject 0.0.1-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] .... .. [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject --- [INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar [INFO] [INFO] --- spring-boot-maven-plugin:3.1.1:repackage (default) @ myproject --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ 运行jar：\n$ java -jar target/myproject-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\\\\ / ___\u0026#39;_ __ _ _(_)_ __ __ _ \\ \\ \\ \\ ( ( )\\___ | \u0026#39;_ | \u0026#39;_| | \u0026#39;_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) \u0026#39; |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.1.1) ....... . . . ....... . . . (log output here) ....... . . . ........ Started MyApplication in 0.999 seconds (process running for 1.253) 参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#getting-started\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000004-springspringboot%E5%AE%98%E6%96%B9%E7%AC%94%E8%AE%B01%E5%85%A5%E9%97%A8/","summary":"Spring Boot helps you to create stand-alone, production-grade \u003cstrong\u003eSpring-based\u003c/strong\u003e applications that you can run. Most Spring Boot applications need \u003cstrong\u003every little Spring configuration\u003c/strong\u003e.","title":"SpringBoot官方笔记1入门"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n静态接入和动态接入 SpingBoot可以通过2种方式接入JaCoCo：Maven和Agent。Maven方式是静态接入，在编译时计算代码覆盖率。Agent方式是动态接入，服务启起来以后，能实时根据代码命中情况计算代码覆盖率。\n教程代码已上传开源项目：https://github.com/dongfanger/mini\nMaven pom.xml添加依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jacoco\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jacoco-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.8.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 添加配置：\n\u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${project.parent.version}\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.jacoco\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jacoco-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.8.7\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;default-prepare-agent\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;prepare-agent\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;default-report\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;report\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 点击maven的install：\ntarget/site/jacoco目录下就生成了覆盖率报告：\nAgent 下载jacoco文件后解压，提取2个文件：\n使用命令启动SpringBoot，启动参数添加-javaagent：\njava -javaagent:jacoco/jacocoagent.jar -jar target/mini-0.0.1-SNAPSHOT.jar 此时会生成1个jacoco.exec文件，这个文件就是覆盖率数据文件，采用以下命令根据数据文件生成HTML报告：\njava -jar jacoco/jacococli.jar report jacoco.exec --classfiles ./target/classes/ --sourcefiles ./src/main/java/ --html ./report 接下来就来验证动态特性。把report目录和jacoco.exec都删了，因为jacoco每次部署只能分析一次。把服务停掉后，重新启动。这一次不直接分析，而是先用postman调一下接口：\n然后再分析一把：\n可以看到覆盖率变成100%，说明postman调接口这个操作，命中了代码，被jacoco统计到了。javaagent相当于是个代理，在服务运行期间，所有调用都会经过javagent，这样就能实时统计代码覆盖率。换句话说，只要服务运行起来以后，只要你能命中代码，无论是跑单元测试还是调接口，或者是手工测试，都能生成代码覆盖率数据。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E7%99%BD%E7%9B%92%E6%B5%8B%E8%AF%95/000004-jacocospringboot%E6%8E%A5%E5%85%A5jacoco%E5%AE%9E%E8%B7%B5%E6%95%99%E7%A8%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"静态接入和动态接入\"\u003e静态接入和动态接入\u003c/h2\u003e\n\u003cp\u003eSpingBoot可以通过2种方式接入JaCoCo：Maven和Agent。\u003cstrong\u003eMaven方式是静态接入\u003c/strong\u003e，在编译时计算代码覆盖率。\u003cstrong\u003eAgent方式是动态接入\u003c/strong\u003e，服务启起来以后，能实时根据代码命中情况计算代码覆盖率。\u003c/p\u003e\n\u003cp\u003e教程代码已上传开源项目：https://github.com/dongfanger/mini\u003c/p\u003e\n\u003ch2 id=\"maven\"\u003eMaven\u003c/h2\u003e\n\u003cp\u003epom.xml添加依赖：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.jacoco\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003ejacoco-maven-plugin\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e0.8.7\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e添加配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;build\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;plugins\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-maven-plugin\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e${project.parent.version}\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.jacoco\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003ejacoco-maven-plugin\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e0.8.7\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;executions\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;execution\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;id\u0026gt;\u003c/span\u003edefault-prepare-agent\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/id\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;goals\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;goal\u0026gt;\u003c/span\u003eprepare-agent\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/goal\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/goals\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/execution\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;execution\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;id\u0026gt;\u003c/span\u003edefault-report\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/id\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;goals\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;goal\u0026gt;\u003c/span\u003ereport\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/goal\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/goals\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/execution\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/executions\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/plugin\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/plugins\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/build\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e点击maven的install：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/cf4e615a5587484c82882989c814b72d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003etarget/site/jacoco\u003c/code\u003e目录下就生成了覆盖率报告：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/796fd3dead8d4d18aed6fe21d54b3584.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/2ab44387da96478f88ba4ce10204be14.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"agent\"\u003eAgent\u003c/h2\u003e\n\u003cp\u003e下载jacoco文件后解压，提取2个文件：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ad7eed0bddcf4841ae0472bf2d6843e3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e使用命令启动SpringBoot，启动参数添加\u003ccode\u003e-javaagent\u003c/code\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ejava -javaagent:jacoco/jacocoagent.jar -jar target/mini-0.0.1-SNAPSHOT.jar\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e此时会生成1个\u003ccode\u003ejacoco.exec\u003c/code\u003e文件，这个文件就是覆盖率数据文件，采用以下命令根据数据文件生成HTML报告：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ejava -jar jacoco/jacococli.jar report jacoco.exec --classfiles ./target/classes/ --sourcefiles ./src/main/java/ --html ./report\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6eb9a090cf014a16895a3b7472aef243.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/617ef96e107e478dbf3e8f7b93a667a8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接下来就来验证动态特性。把report目录和\u003ccode\u003ejacoco.exec\u003c/code\u003e都删了，因为\u003cstrong\u003ejacoco每次部署只能分析一次\u003c/strong\u003e。把服务停掉后，重新启动。这一次不直接分析，而是先用postman调一下接口：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/02799720006d4855bd8b9a4dd8be63d3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e然后再分析一把：\u003c/p\u003e","title":"SpringBoot接入JaCoCo实践教程"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文开发内容 现在正式进入测试相关功能开发。teprunner测试平台底层是pytest，中间层是tep，还没了解的朋友可以先看看tep的文章，整个平台的设计思路和后面用例的执行都会基于这个工具。tep的测试用例是放在.py文件里面的，全局变量或者说环境变量是引用的env_vars，公共函数和复用接口是引用的fixtures，在做成平台后，需要把这两个部分独立为两个功能模块。多个项目的接口自动化数据需要隔离开来，要有个项目管理功能。本文将开发四个用例前置模块：\n后台管理\u0026ndash;项目管理 接口自动化\u0026ndash;语法说明 接口自动化\u0026ndash;环境变量 接口自动化\u0026ndash;fixtures 语法说明给出了环境变量、fixtures、用例示例（单个接口、多个接口\u0026ndash;增删改查）、请求方法（get、post、put、delete）的使用方法。\n编写后端代码 先创建名为teprunner的app：\ndjango-admin startapp teprunner 在settings的INSTALLED_APPS中添加配置让app生效：\n并在urls.py添加路由：\n编辑user/fixtures/user.json，添加一个接口自动化的菜单：\n截图只截了管理员，测试和开发角色，也需要添加上。access均为true，这三个角色都可以访问。添加后把数据同步到数据库：\npython manage.py loaddata user 编辑teprunner/models.py，新增模型：\n包括了项目、环境变量和fixture三张表。Project.env_config用于给项目配置多个环境。不同项目不同环境的环境变量不一样，在EnvVar中添加了project_id和env_name，并按(\u0026ldquo;project_id\u0026rdquo;, \u0026ldquo;env_name\u0026rdquo;, \u0026ldquo;name\u0026rdquo;)作为唯一键，不允许重复。不同项目的fixtures不同，不关心环境，在Fixture中只添加了project_id。\nFixture.code用于存放代码，字段类型为models.TextField，容纳更多字符。\nmodels编写好后，迁移到数据库：\npython manage.py makemigrations python manage.py migrate 新建teprunner/serializers.py文件，新增序列化器：\n数据库字段的命名是用的下划线，接口返回前端和前端传参是用的驼峰，所以这里通过source这种方式给env_config、project_id、env_name、creator_nickname进行了序列化器的重命名。为了前端更方便处理，使用serializers.CharField把int转化为了str，由于请求时id非必须，增加参数required=False。\nteprunner会有很多视图，写在一个文件里面有点臃肿，这里创建views文件夹，新增views/project.py项目视图：\nDjango REST framework的ModelViewSet视图类能节省很多代码，只需要三行就把项目增删改查接口做了。project_env是函数视图，请求方法为GET，它的作用是返回项目环境列表，当前项目和当前环境，默认为第一个项目和第一个环境。\n前端需要切换不同项目和不同环境，下拉框数据来源于这个接口。\n新建views/envvar.py新增环境变量视图：\n环境变量跟项目和环境是强关联的，需要根据项目环境进行过滤，所以重写了list方法，从前端请求拿到curProjectId和curEnvName，通过EnvVar.objects.filter进行过滤。from django.db.models import Q支持多种过滤条件，这在后面的代码中还能看到，比如模糊匹配。\n新建views/fixture.py新增fixture视图：\nfixture跟项目是强关联的，需要根据项目进行过滤，所以重写了list方法，从前端请求拿到curProjectId，通过Fixture.objects.filter进行过滤。Fixture有个creatorNickname字段，新增fixture时使用的是当前登录用户的昵称，修改fixture时需要用已保存的创建者，重写update方法来实现这个处理。\n最后，新建teprunner/urls.py文件，把视图添加到路由中：\n编写前端代码 编辑package.json，添加依赖：\nfixtures是代码形式的，这是测试平台第一次使用代码编辑组件。添加依赖后记得执行npm install进行安装。\n编辑router/index.js：\n添加接口自动化的路由，并把home重定向改为语法说明路由。children表示子路由。path是路由地址。meta.title用于显示菜单名。\n编辑views/console/index.vue：\n添加项目管理菜单。新建ProjectManagement.vue和AddProject.vue，添加项目管理增删改查代码，跟用户管理类似，不再另加赘述。值得注意的是环境配置placeholder=\u0026quot;请输入环境英文名（多个环境英文逗号分隔）\u0026quot;，因为tep.env_vars.mapping会用到环境名作为key，所以只能用英文名！\n编辑components/WrapComponent.vue，添加一个插槽：\n用于切换项目和环境。新建components/ProjectEnv.vue文件：\n定义了两个下拉框：项目和环境。v-if判断是否需要显示。@change在切换下拉选项时调用对应方法。@click.native指在点击打开时获取数据。然后用v-for遍历列表展示下拉选项。环境和项目数据是从localStorage中读取的：\n可以打开F12切换到Application，点击左侧Storage/Local Storage检查是否有数据：\n数据写入的地方稍后会讲到，先接着讲ProjectEnv.vue文件：\n切换项目会更新环境列表和当前环境数据，这样就把这两个下拉框关联了起来。切换环境会更新当前环境数据。图中框起来的this.$emit是个重要知识点。ProjectEnv.vue是个子组件，它是要嵌套到其他组件去的，比如EnvVar.vue，它们是父子组件的关系。有这么个需求，切换项目时，执行一次查询操作，把查询结果更新一下。切换项目的代码是ProjectEnv.vue子组件提供的，执行查询操作的代码是EnvVar.vue父组件提供的，子组件就是通过this.$emit把这个消息通知给父组件的。\n新建views/teprunner/index.vue文件，添加接口自动化菜单：\n刚才说的项目环境数据的写入就是在这里进行的：\n每次加载时，如果localStorage没有projectEnvList的话，就请求/teprunner/projects/env去拿。\n新建Grammar.vue，语法说明是个硬编码的静态文件，纯展示，无交互。接着新建EnvVar.vue和AddEnvVar.vue，编写环境变量的增删改查，新建Fixture.vue和AddFixture.vue，编写fixtures的增删改查，比较类似，不再另加赘述。通过fixtures模块的代码，看看ProjectEnv.vue是如何用的：\n:showEnv与ProjectEnv.vue子组件的属性对应，用来控制是否显示环境，Fxiture不需要显示。@changeProject与ProjectEnv.vue的this.$emit对应，接收子组件消息，执行查询列表操作：\nAddFixture.vue用到了代码编辑器：\n@init指定了初始化方法：\n离代码完美显示还差最后一步：代码高亮。在assets/js添加highlight.js，并在main.js中导入：\n小结 经过本文的开发，如何添加菜单，前后端如何完成基本的增删改查开发已经进行了很充分的展示。遇到不会写的代码，也都能从已公开源码中找到参考。行动起来吧！边学边练，动手试试把用例管理的增删改查写出来，等我把下一篇写出来后，比较比较，可能会有更深刻的印象。CRUD会了，定制化开发还会远么。\n参考资料：\n前端源码 https://github.com/dongfanger/teprunner-frontend\n后端源码 https://github.com/dongfanger/teprunner-backend\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000004-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E7%94%A8%E4%BE%8B%E5%89%8D%E7%BD%AE%E6%A8%A1%E5%9D%97%E5%BC%80%E5%8F%91/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"本文开发内容\"\u003e本文开发内容\u003c/h1\u003e\n\u003cp\u003e现在正式进入测试相关功能开发。teprunner测试平台底层是pytest，中间层是tep，还没了解的朋友可以先看看tep的文章，整个平台的设计思路和后面用例的执行都会基于这个工具。tep的测试用例是放在.py文件里面的，全局变量或者说环境变量是引用的env_vars，公共函数和复用接口是引用的fixtures，在做成平台后，需要把这两个部分独立为两个功能模块。多个项目的接口自动化数据需要隔离开来，要有个项目管理功能。本文将开发四个用例前置模块：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e后台管理\u0026ndash;项目管理\u003c/li\u003e\n\u003cli\u003e接口自动化\u0026ndash;语法说明\u003c/li\u003e\n\u003cli\u003e接口自动化\u0026ndash;环境变量\u003c/li\u003e\n\u003cli\u003e接口自动化\u0026ndash;fixtures\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e语法说明给出了环境变量、fixtures、用例示例（单个接口、多个接口\u0026ndash;增删改查）、请求方法（get、post、put、delete）的使用方法。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"编写后端代码\"\u003e编写后端代码\u003c/h1\u003e\n\u003cp\u003e先创建名为teprunner的app：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edjango-admin startapp teprunner\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e在settings的INSTALLED_APPS中添加配置让app生效：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/67d966cf73d6483297dd107366706b05.png\"\u003e\u003c/p\u003e\n\u003cp\u003e并在urls.py添加路由：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f3bd4250cbe84c6a89367a48fa40ff3f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e编辑user/fixtures/user.json，添加一个接口自动化的菜单：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9322e66b340042d9b8304e1c9fd9bc67.png\"\u003e\u003c/p\u003e\n\u003cp\u003e截图只截了管理员，测试和开发角色，也需要添加上。access均为true，这三个角色都可以访问。添加后把数据同步到数据库：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython manage.py loaddata user\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e编辑teprunner/models.py，新增模型：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d6cffae9dd7a4a0b877eb1d08f9e07e1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e包括了项目、环境变量和fixture三张表。Project.env_config用于给项目配置多个环境。不同项目不同环境的环境变量不一样，在EnvVar中添加了project_id和env_name，并按(\u0026ldquo;project_id\u0026rdquo;, \u0026ldquo;env_name\u0026rdquo;, \u0026ldquo;name\u0026rdquo;)作为唯一键，不允许重复。不同项目的fixtures不同，不关心环境，在Fixture中只添加了project_id。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eFixture.code用于存放代码，字段类型为models.TextField，容纳更多字符。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003emodels编写好后，迁移到数据库：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython manage.py makemigrations\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython manage.py migrate\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e新建teprunner/serializers.py文件，新增序列化器：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/cc0b2795d4c842c6a8ab80ad49c2eb37.png\"\u003e\u003c/p\u003e\n\u003cp\u003e数据库字段的命名是用的下划线，接口返回前端和前端传参是用的驼峰，所以这里通过source这种方式给env_config、project_id、env_name、creator_nickname进行了序列化器的重命名。为了前端更方便处理，使用serializers.CharField把int转化为了str，由于请求时id非必须，增加参数required=False。\u003c/p\u003e\n\u003cp\u003eteprunner会有很多视图，写在一个文件里面有点臃肿，这里创建views文件夹，新增views/project.py项目视图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/96cf05fb5b6f4256b5e9d6934db0c2d4.png\"\u003e\u003c/p\u003e\n\u003cp\u003eDjango REST framework的ModelViewSet视图类能节省很多代码，只需要三行就把项目增删改查接口做了。project_env是函数视图，请求方法为GET，它的作用是返回项目环境列表，当前项目和当前环境，默认为第一个项目和第一个环境。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e前端需要切换不同项目和不同环境，下拉框数据来源于这个接口。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e新建views/envvar.py新增环境变量视图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/97f48f6c1b464ffba72d16be472fcd1d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e环境变量跟项目和环境是强关联的，需要根据项目环境进行过滤，所以重写了list方法，从前端请求拿到curProjectId和curEnvName，通过EnvVar.objects.filter进行过滤。\u003ccode\u003efrom django.db.models import Q\u003c/code\u003e支持多种过滤条件，这在后面的代码中还能看到，比如模糊匹配。\u003c/p\u003e\n\u003cp\u003e新建views/fixture.py新增fixture视图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/7ea4f53b5ed1471c9c2bc6efb0d007fd.png\"\u003e\u003c/p\u003e\n\u003cp\u003efixture跟项目是强关联的，需要根据项目进行过滤，所以重写了list方法，从前端请求拿到curProjectId，通过Fixture.objects.filter进行过滤。Fixture有个creatorNickname字段，新增fixture时使用的是当前登录用户的昵称，修改fixture时需要用已保存的创建者，重写update方法来实现这个处理。\u003c/p\u003e\n\u003cp\u003e最后，新建teprunner/urls.py文件，把视图添加到路由中：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5a8ef67ff3fa4b1e80cbaeaa359a7248.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"编写前端代码\"\u003e编写前端代码\u003c/h1\u003e\n\u003cp\u003e编辑package.json，添加依赖：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/faf9d8dbca8b43bbb0e7e5bffbd6185a.png\"\u003e\u003c/p\u003e\n\u003cp\u003efixtures是代码形式的，这是测试平台第一次使用代码编辑组件。添加依赖后记得执行\u003ccode\u003enpm install\u003c/code\u003e进行安装。\u003c/p\u003e\n\u003cp\u003e编辑router/index.js：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/50f53177e95740ad880a2d48d9345569.png\"\u003e\u003c/p\u003e\n\u003cp\u003e添加接口自动化的路由，并把home重定向改为语法说明路由。children表示子路由。path是路由地址。meta.title用于显示菜单名。\u003c/p\u003e\n\u003cp\u003e编辑views/console/index.vue：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b94dc3624d8b4109974bc8139f55b935.png\"\u003e\u003c/p\u003e\n\u003cp\u003e添加项目管理菜单。新建ProjectManagement.vue和AddProject.vue，添加项目管理增删改查代码，跟用户管理类似，不再另加赘述。值得注意的是环境配置\u003ccode\u003eplaceholder=\u0026quot;请输入环境英文名（多个环境英文逗号分隔）\u0026quot;\u003c/code\u003e，因为tep.env_vars.mapping会用到环境名作为key，所以只能用英文名！\u003c/p\u003e\n\u003cp\u003e编辑components/WrapComponent.vue，添加一个插槽：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/07f2e7bbb30a4093abaef4f6b5c1acd7.png\"\u003e\u003c/p\u003e","title":"teprunner测试平台用例前置模块开发"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n# 方式：把判断放进 SQL，一条语句的原子性 rows = db.execute(\u0026#34;\u0026#34;\u0026#34; UPDATE pay_record SET status = \u0026#39;PAID\u0026#39; WHERE order_no = ? AND status = \u0026#39;INIT\u0026#39; \u0026#34;\u0026#34;\u0026#34;, order_no) if rows == 0: return \u0026#34;已处理\u0026#34; # 别人改过了，我不是第一个 grant_coupon(order_no) # 只有改成功的那个才发券 return \u0026#34;success\u0026#34; grant_coupon失败会发生什么？\n券永久丢失，该发没发，资损。\n第一次请求：\nUPDATE status: INIT → PAID ✅ 已提交，改不回去了 grant_coupon(order_no) ❌ 抛异常 / 超时 / 下游挂了 第二次请求：\nrows = db.execute(\u0026#34;UPDATE ... WHERE status = \u0026#39;INIT\u0026#39;\u0026#34;) # status已是PAID # rows == 0 return \u0026#34;已处理\u0026#34; # ← 被自己的幂等逻辑拦住，不会执行grant_coupon 包进一个事务能解决吗 把update和grant_coupon这两个操作，包进一个事务里，要么同时执行，要么同时回滚，是不是就解决了？\n无法解决，因为grant_coupon一般是RPC操作，数据库事务管不了。\nRPC真的失败，DB回滚 —— 没问题 RPC超时(实际发券了)，DB回滚 —— 重试，券重复发 RPC成功，但commit失败 —— 回滚，重试后券重复发 包进一个事务虽然能解决“券不发”的问题，但是会出现“券超发”的新问题。\n解决方案 问题根因：status字段有两个职责，一是“支付幂等锁”，二是“发券进度标记”，发券失败时，这两个职责就冲突了。\n核心思路：在本地事务里，把“要发券”持久化成一条记录，RPC交给可以重试的异步任务。\n表设计\n-- 已有:支付记录,status 只管\u0026#34;支付是否已处理\u0026#34; pay_record(order_no, status, ...) -- 新增:发券任务表,单独管\u0026#34;发券进度\u0026#34; CREATE TABLE coupon_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0=待发 1=已发 2=终态失败 retry_count INT NOT NULL DEFAULT 0, next_retry_at DATETIME, UNIQUE KEY uk_order_no (order_no) -- 防止重复插任务 ); 第一步，同一个事务处理状态管理和“要发券”\ndef on_pay_callback(order_no): with db.transaction(): # 本地事务:两条都是本地库操作 rows = db.execute(\u0026#34;\u0026#34;\u0026#34; UPDATE pay_record SET status = \u0026#39;PAID\u0026#39; WHERE order_no = ? AND status = \u0026#39;INIT\u0026#39; \u0026#34;\u0026#34;\u0026#34;, order_no) if rows == 0: return\u0026#34;已处理\u0026#34; # 幂等:别人已处理过 # 关键:同一事务里记下\u0026#34;要发券\u0026#34; db.execute(\u0026#34;\u0026#34;\u0026#34; INSERT INTO coupon_task (order_no, status, next_retry_at) VALUES (?, 0, NOW()) \u0026#34;\u0026#34;\u0026#34;, order_no) # 事务提交:状态改动 和 “要发券” 要么都成、要么都不成 try_grant(order_no) # 快路径:立即试发一次 return\u0026#34;success\u0026#34; update和insert是同一个库的数据库操作，InnoDB能保证它们的原子性。\ncoupon_task记录了“要发券”，就永远不会丢。\n第二步，发券幂等，标记完成\ndef try_grant(order_no): try: grant_coupon(order_no) # 必须幂等!靠 order_no 去重 db.execute(\u0026#34;\u0026#34;\u0026#34; UPDATE coupon_task SET status = 1 WHERE order_no = ? AND status = 0 \u0026#34;\u0026#34;\u0026#34;, order_no) except Exception: # 失败要推进退避,否则 next_retry_at 不变,兜底任务扫到就一直撞 db.execute(\u0026#34;\u0026#34;\u0026#34; UPDATE coupon_task SET retry_count = retry_count + 1, next_retry_at = DATE_ADD(NOW(), INTERVAL POW(2, retry_count) MINUTE), status = IF(retry_count + 1 \u0026gt;= 5, 2, 0) -- 超阈值置终态失败 WHERE order_no = ? AND status = 0 \u0026#34;\u0026#34;\u0026#34;, order_no) 第三步，定时扫描，兜底重试\ndef retry_pending(): tasks = db.query(\u0026#34;\u0026#34;\u0026#34; SELECT order_no FROM coupon_task WHERE status = 0 AND next_retry_at \u0026lt;= NOW() AND retry_count \u0026lt; 5 LIMIT 100 \u0026#34;\u0026#34;\u0026#34;) for t in tasks: try_grant(t.order_no) # 复用同一发券逻辑 # retry_count 超阈值 → status=2 终态失败,告警人工介入 再看幂等的重要性 发券的幂等是用order_no来做的，发过券的订单不再重复发。\n如果没有幂等，看这个场景：\ngrant_coupon(order_no) ✅ 券发出去了 UPDATE coupon_task SET status = 1 ❌ 这一步 DB 抖动,失败了 由于加了兜底重试，更新失败的记录，会再次被扫到执行，就又会调一次grant_coupon，重复发券。\n有重试的地方，必须要考虑幂等。\n总结 数据库事务只能保证本地数据库的原子性，管不了RPC。\n所以不能硬把RPC塞到事务里。可以通过本地消息表或RocketMQ事务消息来解决。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%90%8E%E7%AB%AF%E6%B5%8B%E8%AF%95/000004-%E6%8A%8Arpc%E5%8C%85%E8%BF%9B%E4%BA%8B%E5%8A%A1%E7%9A%84%E6%9A%97%E5%9D%91/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e# 方式：把判断放进 SQL，一条语句的原子性\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003erows \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e db\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eexecute\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    UPDATE pay_record SET status = \u0026#39;PAID\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    WHERE order_no = ? AND status = \u0026#39;INIT\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e order_no\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e rows \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;已处理\u0026#34;\u003c/span\u003e          \u003cspan style=\"color:#57606a\"\u003e# 别人改过了，我不是第一个\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egrant_coupon\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eorder_no\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e       \u003cspan style=\"color:#57606a\"\u003e# 只有改成功的那个才发券\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;success\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003egrant_coupon失败会发生什么？\u003c/p\u003e\n\u003cp\u003e券永久丢失，该发没发，资损。\u003c/p\u003e\n\u003cp\u003e第一次请求：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eUPDATE status: INIT → PAID   ✅ 已提交，改不回去了\ngrant_coupon(order_no)        ❌ 抛异常 / 超时 / 下游挂了\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e第二次请求：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003erows = db.execute(\u0026#34;UPDATE ... WHERE status = \u0026#39;INIT\u0026#39;\u0026#34;)  # status已是PAID\n# rows == 0\nreturn \u0026#34;已处理\u0026#34;   # ← 被自己的幂等逻辑拦住，不会执行grant_coupon\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"包进一个事务能解决吗\"\u003e包进一个事务能解决吗\u003c/h1\u003e\n\u003cp\u003e把update和grant_coupon这两个操作，包进一个事务里，要么同时执行，要么同时回滚，是不是就解决了？\u003c/p\u003e","title":"把RPC包进事务的暗坑"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n工作碎片化时间，理解，思考，总结，能够加速成长。\n在学校和在公司的学习方式，是不同的。在学校有大把时间，整段时间，可以充分进行学习。在公司，工作8小时内需要干活，工作8小时外精力有限，要么在上班时偷偷学习，要么在下班后痛苦学习，时间有限且碎片化。\n三八理论，一个普通成年人的一天分为“三个八”：八小时工作、八小时睡觉、八小时自由安排时间。\n八小时工作，看似大多数人是一样的，并无多大变化，实际区别还是很大的。有些人工作十年，只有五年水平，有些人工作一年，能有三年水平。有个很重要的原因是，学习方式。\n对于测试工作来说，点点点是工作常态，但并不会占据全部工作时间。除了点点点以外，我们还能有一些可自由支配时间，这些时间非常关键。你可以偷偷学习，比如博客、编程语言、开源项目等。这种方式能带来一些收益，学习总比摸鱼强。但这不是最好的学习方式，因为这些事情在公司和在家都是可以做的，并没有充分利用公司资源。建议把不能利用公司资源的学习事项，通通放到家里去做。\n理解，思考，总结，才是真正带来成长的事情。它们的成果，能构建你的知识体系，比如功能测试需要具备哪些技能、知识、能力、素质，良好的知识体系能够让你在面试时回答得更加清晰。也能梳理你的价值产出，进而转化为简历上的项目经历。还能指引你的工作方向，更加出色地完成工作，增加升职加薪的概率。\n在工作间隙，停下来，想一想，写一写。这个过程相当痛苦，只要坚持下去，就能实现质的飞跃。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000004-%E5%B7%A5%E4%BD%9C%E4%BB%A5%E5%90%8E%E6%8D%A2%E7%A7%8D%E6%96%B9%E5%BC%8F%E5%AD%A6%E4%B9%A0/","summary":"工作碎片化时间，理解，思考，总结，能够加速成长。","title":"工作以后，换种方式学习"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\ntep的设计理念是让人人都可以用Python写自动化，本文就来介绍如何用tep完成增删改查接口自动化。\n环境变量 编辑fixtures/fixture_admin.py：\n\u0026#34;qa\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://qa.com\u0026#34;, }, 修改qa环境的domain。\n登录 因为非登录接口需要从登录接口拿token，放在请求参数中，所以先在fixtures/fixture_admin.py中实现登录，修改url和json：\nresponse = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users/login\u0026#34;), headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) 根据实际响应数据结构，修改response_token赋值：\nresponse_token = jmespath.search(\u0026#34;token\u0026#34;, response.json()) class Clazz: token = response_token jwt_headers = _jwt_headers(response_token) return Clazz 参考_jwt_headers()实现自定义headers：\ndef _jwt_headers(token): return {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;authorization\u0026#34;: f\u0026#34;Bearer {token}\u0026#34;} _表示内部函数，外部无法访问，遵循conftest.py只对外提供fixture的原则。\n完整代码\ndef _jwt_headers(token): return {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;authorization\u0026#34;: f\u0026#34;Bearer {token}\u0026#34;} @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login(): # Code your login logger.info(\u0026#34;Administrator login\u0026#34;) response = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users/login\u0026#34;), headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 response_token = jmespath.search(\u0026#34;token\u0026#34;, response.json()) class Clazz: token = response_token jwt_headers = _jwt_headers(response_token) return Clazz 说明：\n可以复制login为多个fixture，如login_admin、login_some_user，灵活运用。 scope=\u0026quot;session\u0026quot;，表示只登录一次，所有测试用例使用同一个token。可以改为function，让每条用例使用不同token。 写用例 新建测试 新建tests\\crud_test.py：\nfrom loguru import logger from tep.client import request def test(faker_ch, login, url): pytest的fixture作为参数传入test()函数来使用，faker_ch、login、 url是fixture_admin.py中定义好的fixture。\nfrom loguru import logger 用于在测试用例中打印日志 from tep.client import request tep封装了请求日志功能，也可以用原生from requests import request 新增 请求参数为nickname和phone，使用faker_ch造1条测试数据：\nfake = faker_ch nickname = fake.name() phone = fake.phone_number() 请求方法为post，headers取登录返回值login.jwt_headers：\nresponse = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users\u0026#34;), headers=login.jwt_headers, json={ \u0026#34;nickname\u0026#34;: nickname, \u0026#34;phone\u0026#34;: phone } ) 添加断言，简单判断下响应状态码\u0026lt;400：\nassert response.status_code \u0026lt; 400 也可以查数据库来断言。\n提取修改接口需要的数据：\nuser_id = jmespath.search(\u0026#34;id\u0026#34;, response.json()) created_at = jmespath.search(\u0026#34;createdAt\u0026#34;, response.json()) updated_at = jmespath.search(\u0026#34;updatedAt\u0026#34;, response.json()) 推荐用jmespath来提取json。\n查询 请求参数传入刚才定义的局部变量nickname，使用get请求并断言，headers取登录返回值login.jwt_headers：\nresponse = request( \u0026#34;get\u0026#34;, url=url(\u0026#34;/api/users\u0026#34;), headers=login.jwt_headers, params={ \u0026#34;page\u0026#34;: 1, \u0026#34;perPage\u0026#34;: 10, \u0026#34;keyword\u0026#34;: nickname } ) assert response.status_code \u0026lt; 400 get请求需要把json关键字改为params。\n修改 使用faker再造1条新数据：\nnickname_new = fake.name() phone_new = fake.phone_number() 请求方法为put，headers取登录返回值login.jwt_headers：\nresponse = request( \u0026#34;put\u0026#34;, url=url(f\u0026#34;/api/users/{user_id}\u0026#34;), headers=login.jwt_headers, json={ \u0026#34;id\u0026#34;: user_id, \u0026#34;createdAt\u0026#34;: created_at, \u0026#34;updatedAt\u0026#34;: updated_at, \u0026#34;phone\u0026#34;: phone_new, \u0026#34;nickname\u0026#34;: nickname_new } ) assert response.status_code \u0026lt; 400 请求参数中用到了新增接口提取的数据user_id、created_at、updated_at。\n删除 请求方法为delete，url中传入user_id，headers取登录返回值login.jwt_headers：\nresponse = request( \u0026#34;delete\u0026#34;, url=url(f\u0026#34;/api/users/{user_id}\u0026#34;), headers=login.jwt_headers ) assert response.status_code \u0026lt; 400 删除接口没有json和params。\n完整用例 \u0026#34;\u0026#34;\u0026#34; @Author : Don @Date : 12/25/2020 1:02 PM @Desc : 增删改查 \u0026#34;\u0026#34;\u0026#34; import jmespath from loguru import logger from tep.client import request def test(faker_ch, login, url): fake = faker_ch logger.info(\u0026#34;新增\u0026#34;) nickname = fake.name() phone = fake.phone_number() response = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users\u0026#34;), headers=login.jwt_headers, json={ \u0026#34;nickname\u0026#34;: nickname, \u0026#34;phone\u0026#34;: phone } ) assert response.status_code \u0026lt; 400 user_id = jmespath.search(\u0026#34;id\u0026#34;, response.json()) created_at = jmespath.search(\u0026#34;createdAt\u0026#34;, response.json()) updated_at = jmespath.search(\u0026#34;updatedAt\u0026#34;, response.json()) logger.info(\u0026#34;查询\u0026#34;) response = request( \u0026#34;get\u0026#34;, url=url(\u0026#34;/api/users\u0026#34;), headers=login.jwt_headers, params={ \u0026#34;page\u0026#34;: 1, \u0026#34;perPage\u0026#34;: 10, \u0026#34;keyword\u0026#34;: nickname } ) assert response.status_code \u0026lt; 400 logger.info(\u0026#34;修改\u0026#34;) nickname_new = fake.name() phone_new = fake.phone_number() response = request( \u0026#34;put\u0026#34;, url=url(f\u0026#34;/api/users/{user_id}\u0026#34;), headers=login.jwt_headers, json={ \u0026#34;id\u0026#34;: user_id, \u0026#34;createdAt\u0026#34;: created_at, \u0026#34;updatedAt\u0026#34;: updated_at, \u0026#34;phone\u0026#34;: phone_new, \u0026#34;nickname\u0026#34;: nickname_new } ) assert response.status_code \u0026lt; 400 logger.info(f\u0026#34;用户姓名手机 {nickname} {phone} 修改后 {nickname_new} {phone_new}\u0026#34;) logger.info(\u0026#34;删除\u0026#34;) response = request( \u0026#34;delete\u0026#34;, url=url(f\u0026#34;/api/users/{user_id}\u0026#34;), headers=login.jwt_headers ) assert response.status_code \u0026lt; 400 小结 本文介绍了tep的基本使用，先配置环境变量，再修改登录代码，然后新增测试，最后编写增删改查的接口请求。每条用例放在一个函数中，通过函数参数引用fixture来使用全局环境变量，函数内部可以定义测试需要的局部变量。每个接口是一个代码块，由接口描述、测试数据、请求、断言和数据提取5部分组成。接口之间通过变量实现参数化和关联。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000004-%E5%A6%82%E4%BD%95%E7%94%A8tep%E5%AE%8C%E6%88%90%E5%A2%9E%E5%88%A0%E6%94%B9%E6%9F%A5%E6%8E%A5%E5%8F%A3%E8%87%AA%E5%8A%A8%E5%8C%96/","summary":"tep的设计理念是让人人都可以用Python写自动化，本文就来介绍如何用tep完成增删改查接口自动化。","title":"如何用tep完成增删改查接口自动化"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n80道高频算法题来源于牛客网，这些答案都经过了我验证，可以复制粘贴后提交通过：\n掌握这80道题，99%的测试岗位算法考试都能通过。建议收藏后反复练习。本文为Python版本答案，对于Java版本答案，请在电子书《算法挑战》目录中查看。\n1、NC1 大数加法：中等 ### 计算两个数之和 ### @param s string字符串 表示第一个整数 ### @param t string字符串 表示第二个整数 ### @return string字符串 # class Solution: def solve(self , s: str, t: str) -\u0026gt; str: # write code here res = \u0026#34;\u0026#34; i, j, carry = len(s) - 1, len(t) - 1, 0 while i \u0026gt;= 0 or j \u0026gt;= 0: n1 = int(s[i]) if i \u0026gt;= 0 else 0 n2 = int(t[j]) if j \u0026gt;= 0 else 0 tmp = n1 + n2 + carry carry = tmp // 10 res = str(tmp % 10) + res i, j = i - 1, j - 1 return \u0026#34;1\u0026#34; + res if carry else res 2、NC3 链表中环的入口结点：中等 ### -*- coding:utf-8 -*- ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None class Solution: def EntryNodeOfLoop(self, pHead): # write code here slow = self.hasCycle(pHead) if slow == None: return None fast = pHead while fast != slow: fast = fast.next slow = slow.next return slow def hasCycle(self, head): if head == None: return None fast = head slow = head while fast != None and fast.next != None: fast = fast.next.next slow = slow.next if fast == slow: return slow return None 3、NC4 判断链表中是否有环：简单 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### ### @param head ListNode类 ### @return bool布尔型 # class Solution: def hasCycle(self , head: ListNode) -\u0026gt; bool: if not head: return False slow = head fast = head while fast != None and fast.next != None: fast = fast.next.next slow = slow.next if fast == slow: return True return False 4、NC6 二叉树中的最大路径和：困难 这道题的Python答案在牛客网无法通过，在力扣网能通过：\nhttps://leetcode.cn/problems/jC7MId/\n### Definition for a binary tree node. ### class TreeNode: ### def __init__(self, val=0, left=None, right=None): ### self.val = val ### self.left = left ### self.right = right class Solution: def __init__(self): self.maxSum = float(\u0026#34;-inf\u0026#34;) def maxPathSum(self, root: TreeNode) -\u0026gt; int: def maxGain(node): if not node: return 0 leftGain = max(maxGain(node.left), 0) rightGain = max(maxGain(node.right), 0) priceNewpath = node.val + leftGain + rightGain self.maxSum = max(self.maxSum, priceNewpath) return node.val + max(leftGain, rightGain) maxGain(root) return self.maxSum 5、NC11 将升序数组转化为平衡二叉搜索树：简单 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param nums int整型一维数组 ### @return TreeNode类 # class Solution: def sortedArrayToBST(self, nums: List[int]) -\u0026gt; TreeNode: # write code here if not nums: return None n = len(nums) k = n // 2 t = TreeNode(nums[k]) if n == 1: return t t.left = self.sortedArrayToBST(nums[:k]) t.right = self.sortedArrayToBST(nums[k+1:]) return t 6、NC12 重建二叉树：中等 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param preOrder int整型一维数组 ### @param vinOrder int整型一维数组 ### @return TreeNode类 # class Solution: def reConstructBinaryTree(self , preOrder: List[int], vinOrder: List[int]) -\u0026gt; TreeNode: # write code here if not preOrder: return None root = TreeNode(preOrder[0]) tmp = vinOrder.index(preOrder[0]) root.left = self.reConstructBinaryTree(preOrder[1:tmp+1], vinOrder[:tmp]) root.right = self.reConstructBinaryTree(preOrder[tmp+1:], vinOrder[tmp+1:]) return root 7、NC14 按之字形顺序打印二叉树：中等 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param pRoot TreeNode类 ### @return int整型二维数组 # import queue class Solution: def Print(self , pRoot: TreeNode) -\u0026gt; List[List[int]]: # write code here head = pRoot res = [] if not head: return res temp = queue.Queue() temp.put(head) flag = True while not temp.empty(): row = [] flag = not flag n = temp.qsize() for i in range(n): p = temp.get() row.append(p.val) if p.left: temp.put(p.left) if p.right: temp.put(p.right) if flag: row = row[::-1] res.append(row) return res 8、NC15 求二叉树的层序遍历：中等 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param root TreeNode类 ### @return int整型二维数组 # import queue class Solution: def levelOrder(self , root: TreeNode) -\u0026gt; List[List[int]]: # write code here res = [] if not root: return res q = queue.Queue() q.put(root) cur = None while not q.empty(): row = [] n = q.qsize() for i in range(n): cur = q.get() row.append(cur.val) if cur.left: q.put(cur.left) if cur.right: q.put(cur.right) res.append(row) return res 9、NC16 对称的二叉树：简单 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param pRoot TreeNode类 ### @return bool布尔型 # class Solution: def recursion(self, root1: TreeNode, root2: TreeNode): if not root1 and not root2: return True if not root1 or not root2 or root1.val != root2.val: return False return self.recursion(root1.left, root2.right) and self.recursion(root1.right, root2.left) def isSymmetrical(self , pRoot: TreeNode) -\u0026gt; bool: # write code here return self.recursion(pRoot, pRoot) 10、NC17 最长回文子串：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param A string字符串 ### @return int整型 # class Solution: def func(self, s: str, begin: int, end: int) -\u0026gt; int: while begin \u0026gt;= 0 and end \u0026lt; len(s) and s[begin] == s[end]: begin -= 1 end += 1 return end - begin - 1 def getLongestPalindrome(self , A: str) -\u0026gt; int: # write code here maxlen = 1 for i in range(len(A) - 1): maxlen = max(maxlen, max(self.func(A, i, i), self.func(A, i, i + 1))) return maxlen 11、NC18 顺时针旋转矩阵：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param mat int整型二维数组 ### @param n int整型 ### @return int整型二维数组 # class Solution: def rotateMatrix(self , mat: List[List[int]], n: int) -\u0026gt; List[List[int]]: # write code here for i in range(n): for j in range(i): mat[i][j], mat[j][i] = mat[j][i], mat[i][j] for i in range(n): mat[i].reverse() return mat 12、NC19 连续子数组的最大和：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param array int整型一维数组 ### @return int整型 # class Solution: def FindGreatestSumOfSubArray(self , array: List[int]) -\u0026gt; int: # write code here dp = [0 for i in range(len(array))] dp[0] = array[0] maxsum = dp[0] for i in range(1, len(array)): dp[i] = max(dp[i - 1] + array[i], array[i]) maxsum = max(maxsum, dp[i]) return maxsum 13、NC22 合并两个有序的数组：简单 # ### ### @param A int整型一维数组 ### @param B int整型一维数组 ### @return void # class Solution: def merge(self , A, m, B, n): # write code here i = m - 1 j = n - 1 p = m + n - 1 while i \u0026gt;= 0 and j \u0026gt;= 0: if A[i] \u0026gt; B[j]: A[p] = A[i] p -= 1 i -= 1 else: A[p] = B[j] p -= 1 j -= 1 while j \u0026gt;= 0: A[p] = B[j] p -= 1 j -= 1 14、NC24 删除有序链表中重复的元素-II：中等 跟简单的区别：要求重复元素全部删除\n### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param head ListNode类 ### @return ListNode类 # class Solution: def deleteDuplicates(self , head: ListNode) -\u0026gt; ListNode: # write code here if not head: return None res = ListNode(0) res.next = head cur = res while cur.next and cur.next.next: if cur.next.val == cur.next.next.val: temp = cur.next.val while cur.next != None and cur.next.val == temp: cur.next = cur.next.next else: cur = cur.next return res.next 15、NC25 删除有序链表中重复的元素-I：简单 跟中等的区别：要求重复元素保留一个\n### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head ListNode类 ### @return ListNode类 # class Solution: def deleteDuplicates(self, head: ListNode) -\u0026gt; ListNode: # write code here if not head: return None cur = head while cur and cur.next: if cur.val == cur.next.val: cur.next = cur.next.next else: cur = cur.next return head 16、NC26 括号生成：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param n int整型 ### @return string字符串一维数组 # class Solution: def recursion(self, left: int, right: int, temp: str, res: List[str], n: int): if left == n and right == n: res.append(temp) return if left \u0026lt; n: self.recursion(left + 1, right, temp + \u0026#34;(\u0026#34;, res, n) if right \u0026lt; n and left \u0026gt; right: self.recursion(left, right + 1, temp + \u0026#34;)\u0026#34;, res, n) def generateParenthesis(self , n: int) -\u0026gt; List[str]: # write code here res = list() temp = str() self.recursion(0, 0, temp, res, n) return res 17、NC27 集合的所有子集(一)：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param S int整型一维数组 ### @return int整型二维数组 # class Solution: def subsets(self , S: List[int]) -\u0026gt; List[List[int]]: # write code here if not S: return [[]] res = [] def dfs(dummy, tmp): res.append(tmp[:]) for i in range(dummy, len(S)): tmp.append(S[i]) dfs(i + 1, tmp) tmp.pop() dfs(0, []) return res 18、NC28 最小覆盖子串：困难 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param S string字符串 ### @param T string字符串 ### @return string字符串 # class Solution: def minWindow(self, S: str, T: str) -\u0026gt; str: # write code here cnt = len(S) + 1 hash = dict() for i in range(len(T)): if T[i] in hash: hash[T[i]] -= 1 else: hash[T[i]] = -1 slow = 0 fast = 0 left = -1 right = -1 while fast \u0026lt; len(S): c = S[fast] if c in hash: hash[c] += 1 while Solution.check(self, hash): if cnt \u0026gt; fast - slow + 1: cnt = fast - slow + 1 left = slow right = fast c = S[slow] if c in hash: hash[c] -= 1 slow += 1 fast += 1 if left == -1: return \u0026#34;\u0026#34; return S[left : right + 1] def check(self, hash): for key, value in hash.items(): if value \u0026lt; 0: return False return True 19、NC30 缺失的第一个正整数：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param nums int整型一维数组 ### @return int整型 # class Solution: def minNumberDisappeared(self , nums: List[int]) -\u0026gt; int: # write code here n = len(nums) mp = dict() for i in range(n): if nums[i] in mp: mp[nums[i]] += 1 else: mp[nums[i]] = 1 res = 1 while res in mp: res += 1 return res 20、NC31 第一个只出现一次的字符：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param str string字符串 ### @return int整型 # class Solution: def FirstNotRepeatingChar(self , str: str) -\u0026gt; int: # write code here mp = dict() for i in str: if i in mp: mp[i] += 1 else: mp[i] = 1 for i in range(len(str)): if mp[str[i]] == 1: return i return -1 21、NC33 合并两个排序的链表：简单 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param pHead1 ListNode类 ### @param pHead2 ListNode类 ### @return ListNode类 # class Solution: def Merge(self , pHead1: ListNode, pHead2: ListNode) -\u0026gt; ListNode: # write code here if pHead1 == None: return pHead2 if pHead2 == None: return pHead1 head = ListNode(0) cur = head while pHead1 and pHead2: if pHead1.val \u0026lt;= pHead2.val: cur.next = pHead1 pHead1 = pHead1.next else: cur.next = pHead2 pHead2 = pHead2.next cur = cur.next if pHead1: cur.next = pHead1 else: cur.next = pHead2 return head.next 22、NC35 编辑距离(二)：困难 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### min edit cost ### @param str1 string字符串 the string ### @param str2 string字符串 the string ### @param ic int整型 insert cost ### @param dc int整型 delete cost ### @param rc int整型 replace cost ### @return int整型 # class Solution: def minEditCost(self, str1: str, str2: str, ic: int, dc: int, rc: int) -\u0026gt; int: # write code here dp = [[0 for _ in range(len(str2) + 1)] for _ in range(len(str1) + 1)] for i in range(1, len(str2) + 1): dp[0][i] = dp[0][i - 1] + ic for i in range(1, len(str1) + 1): dp[i][0] = dp[i - 1][0] + dc for i in range(1, len(str1) + 1): for j in range(1, len(str2) + 1): if str1[i - 1] == str2[j - 1]: dp[i][j] = dp[i - 1][j - 1] else: dp[i][j] = min(dp[i - 1][j - 1] + rc, dp[i][j - 1] + ic, dp[i - 1][j] + dc) return dp[-1][-1] 23、NC36 在两个长度相等的排序数组中找到上中位数：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### find median in two sorted array ### @param arr1 int整型一维数组 the array1 ### @param arr2 int整型一维数组 the array2 ### @return int整型 # class Solution: def findMedianinTwoSortedAray(self , arr1: List[int], arr2: List[int]) -\u0026gt; int: # write code here p1, p2 = 0, 0 ans = 0 for i in range(len(arr1)): if arr1[p1] \u0026lt;= arr2[p2]: ans = arr1[p1] p1 += 1 else: ans = arr2[p2] p2 += 1 return ans 24、NC37 合并区间：中等 from functools import cmp_to_key ### class Interval: ### def __init__(self, a=0, b=0): ### self.start = a ### self.end = b # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param intervals Interval类一维数组 ### @return Interval类一维数组 # class Solution: def merge(self, intervals: List[Interval]) -\u0026gt; List[Interval]: # write code here res = list() if len(intervals) == 0: return res intervals.sort(key=cmp_to_key(lambda a, b: a.start - b.start)) res.append(intervals[0]) for i in range(len(intervals)): if intervals[i].start \u0026lt;= res[-1].end: res[-1].end = max(res[-1].end, intervals[i].end) else: res.append(intervals[i]) return res 25、NC40 链表相加(二)：中等 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head1 ListNode类 ### @param head2 ListNode类 ### @return ListNode类 # class Solution: # 反转链表 def reverseList(self, pHead: ListNode): if pHead == None: return None cur = pHead pre = None while cur: # 断开链表，要记录后续一个 temp = cur.next # 当前的next指向前一个 cur.next = pre # 前一个更新为当前 pre = cur # 当前更新为刚刚记录的后一个 cur = temp return pre def addInList(self, head1: ListNode, head2: ListNode) -\u0026gt; ListNode: # 任意一个链表为空，返回另一个 if head1 == None: return head2 if head2 == None: return head1 # 反转两个链表 head1 = self.reverseList(head1) head2 = self.reverseList(head2) # 添加表头 res = ListNode(-1) head = res # 进位符号 carry = 0 # 只要某个链表还有或者进位还有 while head1 != None or head2 != None or carry != 0: # 链表不为空则取其值 val1 = 0 if head1 == None else head1.val val2 = 0 if head2 == None else head2.val # 相加 temp = val1 + val2 + carry # 获取进位 carry = (int)(temp / 10) temp %= 10 # 添加元素 head.next = ListNode(temp) head = head.next # 移动下一个 if head1: head1 = head1.next if head2: head2 = head2.next # 结果反转回来 return self.reverseList(res.next) 26、NC41 最长无重复子数组：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param arr int整型一维数组 the array ### @return int整型 # class Solution: def maxLength(self, arr: List[int]) -\u0026gt; int: # 哈希表记录窗口内非重复的数字 mp = dict() res = 0 left = 0 # 设置窗口左右边界 for right in range(len(arr)): if arr[right] in mp: # 窗口右移进入哈希表统计出现次数 mp[arr[right]] += 1 else: mp[arr[right]] = 1 # 出现次数大于1，则窗口内有重复 while mp[arr[right]] \u0026gt; 1: # 窗口左移，同时减去该数字的出现次数 mp[arr[left]] -= 1 left += 1 # 维护子数组长度最大值 res = max(res, right - left + 1) return res 27、NC42 有重复项数字的全排列：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param num int整型一维数组 ### @return int整型二维数组 # # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param arr int整型一维数组 the array ### @return int整型 # class Solution: def recursion( self, res: List[List[int]], num: List[int], temp: List[int], vis: List[int] ): # 临时数组满了加入输出 if len(temp) == len(num): res.append(temp.copy()) return # 遍历所有元素选取一个加入 for i in range(len(num)): # 如果该元素已经被加入了则不需要再加入了 if vis[i] == 1: continue if i \u0026gt; 0 and num[i - 1] == num[i] and not vis[i - 1]: # 当前的元素num[i]与同一层的前一个元素num[i-1]相同且num[i-1]已经用过了 continue # 标记为使用过 vis[i] = 1 # 加入数组 temp.append(num[i]) self.recursion(res, num, temp, vis) # 回溯 vis[i] = 0 temp.pop() def permuteUnique(self, num: List[int]) -\u0026gt; List[List[int]]: # 先按字典序排序 num.sort() # 标记每个位置的元素是否被使用过 vis = [0] * len(num) res = list(list()) temp = list() # 递归获取 self.recursion(res, num, temp, vis) return res 28、NC44 通配符匹配：困难 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### ### @param s string字符串 ### @param p string字符串 ### @return bool布尔型 # class Solution: def isMatch(self , s: str, p: str) -\u0026gt; bool: # write code here row = len(s) col = len(p) dp = [[False for _ in range(col + 1)] for _ in range(row + 1)] dp[0][0] = True for j in range(1, col + 1): if dp[0][j - 1]: if p[j - 1] == \u0026#34;*\u0026#34;: dp[0][j] = True else: break for i in range(0, row): for j in range(0, col): if p[j] == s[i] or p[j] == \u0026#34;?\u0026#34;: dp[i + 1][j + 1] = dp[i][j] elif p[j] == \u0026#34;*\u0026#34;: dp[i + 1][j + 1] = dp[i][j] or dp[i + 1][j] or dp[i][j + 1] return dp[row][col] 29、NC45 实现二叉树先序，中序和后序遍历：中等 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param root TreeNode类 the root of binary tree ### @return int整型二维数组 # class Solution: def threeOrders(self, root: TreeNode) -\u0026gt; List[List[int]]: # write code here self.res = [[], [], []] self.dfs(root) return self.res def dfs(self, root): if not root: return self.res[0].append(root.val) self.dfs(root.left) self.res[1].append(root.val) self.dfs(root.right) self.res[2].append(root.val) return 30、NC46 加起来和为目标值的组合(二)：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param num int整型一维数组 ### @param target int整型 ### @return int整型二维数组 # class Solution: def combinationSum2(self, num: List[int], target: int) -\u0026gt; List[List[int]]: # write code here \u0026#34;\u0026#34;\u0026#34; 回溯法 1.去重（好好理解一下） 2.剪枝（不剪枝会超时） \u0026#34;\u0026#34;\u0026#34; result = [] if not num: return result new_num = sorted(num) self.backtracking(new_num, target, 0, 0, [], result) return result def backtracking(self, num, target, cur, begin, arr, result): \u0026#34;\u0026#34;\u0026#34; num: 入参数组列表 target：目标值 cur：当前值 begin：开始指针 arr：临时存储数组 result：满足条件的组合 \u0026#34;\u0026#34;\u0026#34; if cur \u0026gt;= target: if cur == target: result.append(list(arr)) return result for i in range(begin, len(num)): if i \u0026gt; begin and num[i] == num[i - 1]: # 去重 continue # 减枝 arr.append(num[i]) self.backtracking(num, target, cur + num[i], i + 1, arr, result) arr.pop(-1) return result 31、NC49 最长的括号子串：困难 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param s string字符串 ### @return int整型 # class Solution: def longestValidParentheses(self, s: str) -\u0026gt; int: res = 0 # 记录上一次连续括号结束的位置 start = -1 st = [] for i in range(len(s)): # 左括号入栈 if s[i] == \u0026#34;(\u0026#34;: st.append(i) # 右括号 else: # 如果右括号时栈为空，不合法，设置为结束位置 if len(st) == 0: start = i else: # 弹出左括号 st.pop() # 栈中还有左括号，说明右括号不够，减去栈顶位置就是长度 if len(st) != 0: res = max(res, i - st[-1]) # 栈中没有括号，说明左右括号行号，减去上一次结束的位置就是长度 else: res = max(res, i - start) return res 32、NC50 链表中的节点每k个一组翻转：中等 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head ListNode类 ### @param k int整型 ### @return ListNode类 # class Solution: def reverseKGroup(self, head: ListNode, k: int) -\u0026gt; ListNode: # 找到每次翻转的尾部 tail = head # 遍历k次到尾部 for i in range(0, k): # 如果不足k到了链表尾，直接返回，不翻转 if tail == None: return head tail = tail.next # 翻转时需要的前序和当前节点 pre = None cur = head # 在到达当前段尾节点前 while cur != tail: # 翻转 temp = cur.next cur.next = pre pre = cur cur = temp # 当前尾指向下一段要翻转的链表 head.next = self.reverseKGroup(tail, k) return pre 33、NC51 合并k个已排序的链表：困难 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param lists ListNode类一维数组 ### @return ListNode类 # import sys ### 设置递归深度 sys.setrecursionlimit(100000) class Solution: # 两个有序链表合并函数 def Merge2(self, pHead1: ListNode, pHead2: ListNode) -\u0026gt; ListNode: # 一个已经为空了，直接返回另一个 if pHead1 == None: return pHead2 if pHead2 == None: return pHead1 # 加一个表头 head = ListNode(0) cur = head # 两个链表都要不为空 while pHead1 and pHead2: # 取较小值的节点 if pHead1.val \u0026lt;= pHead2.val: cur.next = pHead1 # 只移动取值的指针 pHead1 = pHead1.next else: cur.next = pHead2 # 只移动取值的指针 pHead2 = pHead2.next # 指针后移 cur = cur.next # 哪个链表还有剩，直接连在后面 if pHead1: cur.next = pHead1 else: cur.next = pHead2 # 返回值去掉表头 return head.next # 划分合并区间函数 def divideMerge(self, lists: List[ListNode], left: int, right: int) -\u0026gt; ListNode: if left \u0026gt; right: return None # 中间一个的情况 elif left == right: return lists[left] # 从中间分成两段，再将合并好的两段合并 mid = (int)((left + right) / 2) return self.Merge2( self.divideMerge(lists, left, mid), self.divideMerge(lists, mid + 1, right) ) def mergeKLists(self, lists: List[ListNode]) -\u0026gt; ListNode: # k个链表归并排序 return self.divideMerge(lists, 0, len(lists) - 1) 34、NC52 有效括号序列：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param s string字符串 ### @return bool布尔型 # class Solution: def isValid(self, s: str) -\u0026gt; bool: # 辅助栈 st = [] # 遍历字符串 for i, char in enumerate(s): # 遇到左小括号 if char == \u0026#34;(\u0026#34;: # 期待遇到右小括号 st.append(\u0026#34;)\u0026#34;) # 遇到左中括号 elif char == \u0026#34;[\u0026#34;: # 期待遇到右中括号 st.append(\u0026#34;]\u0026#34;) # 遇到左打括号 elif char == \u0026#34;{\u0026#34;: # 期待遇到右打括号 st.append(\u0026#34;}\u0026#34;) # 必须有左括号的情况下才能遇到右括号 elif len(st) == 0: return False # 右括号匹配则弹出 elif st[-1] == char: st.pop() # 栈中是否还有元素 return len(st) == 0 35、NC53 删除链表的倒数第n个节点：中等 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head ListNode类 ### @param n int整型 ### @return ListNode类 # class Solution: def removeNthFromEnd(self, head: ListNode, n: int) -\u0026gt; ListNode: # 添加表头 res = ListNode(-1) res.next = head # 当前节点 cur = head # 前序节点 pre = res fast = head # 快指针先行n步 while n: fast = fast.next n = n - 1 # 快慢指针同步，快指针到达末尾，慢指针就到了倒数第n个位置 while fast: fast = fast.next pre = cur cur = cur.next # 删除该位置的节点 pre.next = cur.next # 返回去掉头 return res.next 36、NC54 三数之和：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param num int整型一维数组 ### @return int整型二维数组 # class Solution: def threeSum(self, num: List[int]) -\u0026gt; List[List[int]]: res = list(list()) n = len(num) # 不够三元组 if n \u0026lt; 3: return res # 排序 num.sort() for i in range(n - 2): if i != 0 and num[i] == num[i - 1]: continue # 后续的收尾双指针 left = i + 1 right = n - 1 # 设置当前数的负值为目标 target = -num[i] while left \u0026lt; right: # 双指针指向的二值相加为目标，则可以与num[i]组成0 if num[left] + num[right] == target: res.append([num[i], num[left], num[right]]) while left + 1 \u0026lt; right and num[left] == num[left + 1]: # 去重 left += 1 while right - 1 \u0026gt; left and num[right] == num[right - 1]: # 去重 right -= 1 # 双指针向中间收缩 left += 1 right -= 1 # 双指针指向的二值相加大于目标，右指针向左 elif num[left] + num[right] \u0026gt; target: right -= 1 # 双指针指向的二值相加小于目标，左指针向右 else: left += 1 return res 37、NC55 最长公共前缀：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param strs string字符串一维数组 ### @return string字符串 # class Solution: def longestCommonPrefix(self, strs: List[str]) -\u0026gt; str: n = len(strs) # 空字符串数组 if n == 0: return \u0026#34;\u0026#34; # 遍历第一个字符串的长度 for i in range(len(strs[0])): temp = strs[0][i] # 遍历后续的字符串 for j in range(1, n): # 比较每个字符串该位置是否和第一个相同 if i == len(strs[j]) or strs[j][i] != temp: # 不相同则结束 return strs[0][0:i] # 后续字符串有整个字一个字符串的前缀 return strs[0] 38、NC57 反转数字：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param x int整型 ### @return int整型 # class Solution: def reverse(self, x): # write code here x = str(x) if x[0] == \u0026#34;-\u0026#34;: a = int(\u0026#34;-\u0026#34; + x[1:][::-1]) else: a = int(x[::-1]) return a if -2**31 \u0026lt; a \u0026lt; 2**31-1 else 0 39、NC60 判断一棵二叉树是否为搜索二叉树和完全二叉树：中等 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param root TreeNode类 the root ### @return bool布尔型一维数组 # class Solution: def judgeIt(self, root: TreeNode) -\u0026gt; List[bool]: from dataclasses import dataclass @dataclass class Info: mx: int # 整棵树的最大值 mi: int # 整棵树的最小值 height: int # 树的高度 is_bst: bool # 是否搜索二叉树 is_full: bool # 是否满二叉树 is_cbt: bool # 是否完全二叉树 def dfs(x): if not x: return Info(float(\u0026#34;-inf\u0026#34;), float(\u0026#34;inf\u0026#34;), 0, True, True, True) l, r = dfs(x.left), dfs(x.right) # 使用左右子树的信息得到当前节点的信息 mx = max(x.val, r.mx) mi = min(x.val, l.mi) height = max(l.height, r.height) + 1 is_bst = l.is_bst and r.is_bst and l.mx \u0026lt; x.val \u0026lt; r.mi is_full = l.is_full and r.is_full and l.height == r.height is_cbt = ( is_full or l.is_full and r.is_full and l.height - 1 == r.height or l.is_full and r.is_cbt and l.height == r.height or l.is_cbt and r.is_full and l.height - 1 == r.height ) return Info(mx, mi, height, is_bst, is_full, is_cbt) info = dfs(root) return info.is_bst, info.is_cbt 40、NC61 两数之和：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param numbers int整型一维数组 ### @param target int整型 ### @return int整型一维数组 # class Solution: def twoSum(self, numbers: List[int], target: int) -\u0026gt; List[int]: res = [] # 创建哈希表,两元组分别表示值、下标 hash = dict() # 在哈希表中查找target-numbers[i] for i in range(len(numbers)): temp = target - numbers[i] # 若是没找到，将此信息计入哈希表 if temp not in hash: hash[numbers[i]] = i else: # 哈希表中记录的是之前的数字，所以该索引比当前小 res.append(hash[temp] + 1) res.append(i + 1) break return res 41、NC62 判断是不是平衡二叉树：简单 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param pRoot TreeNode类 ### @return bool布尔型 # class Solution: # 计算该子树深度函数 def deep(self, root: TreeNode): if not root: return 0 # 递归算左右子树的深度 left = self.deep(root.left) right = self.deep(root.right) # 子树最大深度加上自己 return left + 1 if left \u0026gt; right else right + 1 def IsBalanced_Solution(self, pRoot: TreeNode) -\u0026gt; bool: # 空树为平衡二叉树 if not pRoot: return True left = self.deep(pRoot.left) right = self.deep(pRoot.right) # 左子树深度减去右子树相差绝对值大于1 if left - right \u0026gt; 1 or left - right \u0026lt; -1: return False # 同时，左右子树还必须是平衡的 return self.IsBalanced_Solution(pRoot.left) and self.IsBalanced_Solution( pRoot.right ) 42、NC63 扑克牌顺子：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param numbers int整型一维数组 ### @return bool布尔型 # class Solution: def IsContinuous(self, numbers: List[int]) -\u0026gt; bool: hash = dict() # 设置顺子上下界 max = 0 min = 13 # 遍历牌 for i in range(len(numbers)): if numbers[i] \u0026gt; 0: # 顺子不能重复 if numbers[i] in hash: return False else: # 将新牌加入哈希表 hash[numbers[i]] = i # 更新上下界 if numbers[i] \u0026gt;= max: max = numbers[i] if numbers[i] \u0026lt;= min: min = numbers[i] # 如果两张牌大于等于5，剩下三张牌无论如何也补不齐 if (max - min) \u0026gt;= 5: return False else: return True 43、NC65 斐波那契数列：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param n int整型 ### @return int整型 # class Solution: def Fibonacci(self, n): # write code here # 斐波拉契数的边界条件： F(0)=0 和 F(1)=1 if n \u0026lt; 2: return n else: a, b = 0, 1 for i in range(n - 1): a, b = b, a + b # 状态转移方程，每次滚动更新数组 return b 44、NC66 两个链表的第一个公共结点：简单 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # # ### @param pHead1 ListNode类 ### @param pHead2 ListNode类 ### @return ListNode类 # class Solution: def FindFirstCommonNode(self, pHead1, pHead2): # write code here # 首先判断两个链表是否为空 if pHead1 is None or pHead2 is None: return None # 定义链表1 的集合 set_A = set() node1, node2 = pHead1, pHead2 # 定义两个节点 # 遍历链表 1 ，把每个节点加入集合中 while node1: set_A.add(node1) node1 = node1.next # 遍历链表2 看当前节点是否在 集合中；如果存在，当前节点就是要找的第一个公共节点；否则继续比较下一个节点。 # 这里还要注意，如果遍历完链表 B，发现所有节点都不在集合中，则说明两个链表不相交，返回None。 while node2: if node2 in set_A: return node2 node2 = node2.next return None 45、NC68 跳台阶：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param number int整型 ### @return int整型 # class Solution: def jumpFloor(self, number: int) -\u0026gt; int: # 从0开始，第0项是1，第一项是1 if number \u0026lt;= 1: return 1 res = 0 a = 1 b = 1 # 初始化的时候把a=1，b=1 for i in range(2, number + 1): # 第三项开始是前两项的和,然后保留最新的两项，更新数据相加 res = a + b a = b b = res return res 46、NC70 单链表的排序：中等 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head ListNode类 the head node ### @return ListNode类 # class Solution: # 合并两段有序链表 def merge(self, pHead1: ListNode, pHead2: ListNode): # 一个已经为空了，直接返回另一个 if pHead1 == None: return pHead2 if pHead2 == None: return pHead1 # 加一个表头 head = ListNode(0) cur = head # 两个链表都要不为空 while pHead1 and pHead2: # 取较小值的节点 if pHead1.val \u0026lt;= pHead2.val: cur.next = pHead1 # 只移动取值的指针 pHead1 = pHead1.next else: cur.next = pHead2 # 只移动取值的指针 pHead2 = pHead2.next # 指针后移 cur = cur.next # 哪个链表还有剩，直接连在后面 if pHead1: cur.next = pHead1 else: cur.next = pHead2 # 返回值去掉表头 return head.next def sortInList(self, head): # 链表为空或者只有一个元素，直接就是有序的 if head == None or head.next == None: return head left = head mid = head.next right = head.next.next # 右边的指针到达末尾时，中间的指针指向该段链表的中间 while right and right.next: left = left.next mid = mid.next right = right.next.next # 左边指针指向左段的左右一个节点，从这里断开 left.next = None # 分成两段排序，合并排好序的两段 return self.merge(self.sortInList(head), self.sortInList(mid)) 47、NC72 二叉树的镜像：简单 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param pRoot TreeNode类 ### @return TreeNode类 # class Solution: def Mirror(self, pRoot: TreeNode) -\u0026gt; TreeNode: # 空树返回 if not pRoot: return None # 先递归子树 left = self.Mirror(pRoot.left) right = self.Mirror(pRoot.right) # 交换 pRoot.left = right pRoot.right = left return pRoot 48、NC73 数组中出现次数超过一半的数字：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param numbers int整型一维数组 ### @return int整型 # class Solution: def MoreThanHalfNum_Solution(self, numbers: List[int]) -\u0026gt; int: # 无序哈希表统计每个数字出现的次数 mp = dict() # 遍历数组 for i in range(len(numbers)): if numbers[i] in mp: # 哈希表中相应数字个数加1 mp[numbers[i]] += 1 else: mp[numbers[i]] = 1 # 一旦有个数大于长度一半的情况即可返回 if mp[numbers[i]] \u0026gt; (int)(len(numbers) / 2): return numbers[i] return 0 49、NC74 数字在升序数组中出现的次数：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param nums int整型一维数组 ### @param k int整型 ### @return int整型 # class Solution: # 二分查找 def bisearch(self, data: List[int], k: float) -\u0026gt; int: left = 0 right = len(data) - 1 # 二分左右界 while left \u0026lt;= right: mid = (left + right) // 2 if data[mid] \u0026lt; k: left = mid + 1 elif data[mid] \u0026gt; k: right = mid - 1 return left def GetNumberOfK(self, data: List[int], k: int) -\u0026gt; int: # 分别查找k+0.5和k-0.5应该出现的位置，中间的部分就全是k return self.bisearch(data, k + 0.5) - self.bisearch(data, k - 0.5) 50、NC76 用两个栈实现队列：简单 ### -*- coding:utf-8 -*- class Solution: def __init__(self): self.stack1 = [] self.stack2 = [] def push(self, node): self.stack1.append(node) def pop(self): # 将第一个栈中内容弹出放入第二个栈中 while self.stack1: self.stack2.append(self.stack1.pop()) # 第二个栈栈顶就是最先进来的元素，即队首 res = self.stack2.pop() # 再将第二个栈的元素放回第一个栈 while self.stack2: self.stack1.append(self.stack2.pop()) return res 51、NC78 反转链表：简单 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head ListNode类 ### @return ListNode类 # class Solution: # 返回ListNode def ReverseList(self, pHead): # write code here pre = None head = pHead while head: temp = head.next head.next = pre pre = head head = temp return pre 52、NC82 滑动窗口的最大值：困难 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param num int整型一维数组 ### @param size int整型 ### @return int整型一维数组 # class Solution: def maxInWindows(self, num: List[int], size: int) -\u0026gt; List[int]: res = [] # 窗口大于数组长度的时候，返回空 if size \u0026lt;= len(num) and size != 0: from collections import deque # 双向队列 dq = deque() # 先遍历一个窗口 for i in range(size): # 去掉比自己先进队列的小于自己的值 while len(dq) != 0 and num[dq[-1]] \u0026lt; num[i]: dq.pop() dq.append(i) # 遍历后续数组元素 for i in range(size, len(num)): res.append(num[dq[0]]) while len(dq) != 0 and dq[0] \u0026lt; (i - size + 1): # 弹出窗口移走后的值 dq.popleft() # 加入新的值前，去掉比自己先进队列的小于自己的值 while len(dq) != 0 and num[dq[-1]] \u0026lt; num[i]: dq.pop() dq.append(i) res.append(num[dq[0]]) return res 53、NC86 矩阵元素查找：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param mat int整型二维数组 ### @param n int整型 ### @param m int整型 ### @param x int整型 ### @return int整型一维数组 # class Solution: def findElement(self, mat: List[List[int]], n: int, m: int, x: int) -\u0026gt; List[int]: # write code here i = n - 1 j = 0 while i \u0026gt;= 0 and j \u0026lt; m: if mat[i][j] == x: return [i, j] elif x \u0026lt; mat[i][j]: i -= 1 elif x \u0026gt; mat[i][j]: j += 1 54、NC89 字符串变形：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param s string字符串 ### @param n int整型 ### @return string字符串 # class Solution: def trans(self, s: str, n: int) -\u0026gt; str: if n == 0: return s res = \u0026#34;\u0026#34; for i in range(n): # 大小写转换 if s[i] \u0026lt;= \u0026#34;Z\u0026#34; and s[i] \u0026gt;= \u0026#34;A\u0026#34;: res += chr(ord(s[i]) - ord(\u0026#34;A\u0026#34;) + ord(\u0026#34;a\u0026#34;)) elif s[i] \u0026gt;= \u0026#34;a\u0026#34; and s[i] \u0026lt;= \u0026#34;z\u0026#34;: res += chr(ord(s[i]) - ord(\u0026#34;a\u0026#34;) + ord(\u0026#34;A\u0026#34;)) else: # 空格直接复制 res += s[i] # 单词反序 res = list(res.split(\u0026#34; \u0026#34;))[::-1] print(res) return \u0026#34; \u0026#34;.join(res) 55、NC91 最长上升子序列(三)：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### retrun the longest increasing subsequence ### @param arr int整型一维数组 the array ### @return int整型一维数组 # import bisect class Solution: def LIS(self, arr): # write code here arrLen = len(arr) if arrLen \u0026lt; 2: return arr ansArr = [arr[0]] # 记录某个数字结尾时最长的最长递增子序列，初始化第一个数字 maxLen = [1] # 下标i时，最长的递增子序列长度，初始化1 for a in arr[1:]: if a \u0026gt; ansArr[-1]: # 当前数字大于ansArr最后一个数字，子数组保持递增 ansArr.append(a) maxLen.append(len(ansArr)) # 当前数字小于等于ansArr最后一个数字，二分查找ansArr中第一个比当前数字大的下标pos # 替换ansArr中下标pos的数字为当前数字，更新maxLen，记录当前最长递增子序列长度为：pos + 1(下标+1) else: pos = bisect.bisect_left(ansArr, a) ansArr[pos] = a maxLen.append(pos + 1) # 找到的ansArr不一定是最终结果，[2,1,5,3,6,4,8,9,7] - \u0026gt; [1, 3, 4, 7, 9] (不是最终结果) # [1, 1, 2, 2, 3, 3, 4, 5, 4] 从后往前遍历maxLen,依次找到等于len(arrLen)对应的 arr[i] ansLen = len(ansArr) for i in range(arrLen - 1, -1, -1): if maxLen[i] == ansLen: ansArr[ansLen - 1] = arr[i] ansLen -= 1 return ansArr 56、NC92 最长公共子序列(二)：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### longest common subsequence ### @param s1 string字符串 the string ### @param s2 string字符串 the string ### @return string字符串 # import sys ### 设置递归深度 sys.setrecursionlimit(100000) class Solution: def __init__(self): self.x = \u0026#34;\u0026#34; self.y = \u0026#34;\u0026#34; # 获取最长公共子序列 def ans(self, i: int, j: int, b: List[List[int]]): res = \u0026#34;\u0026#34; # 递归终止条件 if i == 0 or j == 0: return res # 根据方向，往前递归，然后添加本级字符 if b[i][j] == 1: res = res + self.ans(i - 1, j - 1, b) res = res + self.x[i - 1] elif b[i][j] == 2: res = res + self.ans(i - 1, j, b) elif b[i][j] == 3: res = res + self.ans(i, j - 1, b) return res def LCS(self, s1: str, s2: str) -\u0026gt; str: # 特殊情况 if s1 is None or s2 is None: return \u0026#34;-1\u0026#34; len1 = len(s1) len2 = len(s2) self.x = s1 self.y = s2 # dp[i][j]表示第一个字符串到第i位，第二个字符串到第j位为止的最长公共子序列长度 dp = [[0] * (len2 + 1) for i in range(len1 + 1)] # 动态规划数组相加的方向 b = [[0] * (len2 + 1) for i in range(len1 + 1)] # 遍历两个字符串每个位置求的最长长度 for i in range(1, len1 + 1): for j in range(1, len2 + 1): # 遇到两个字符相等 if s1[i - 1] == s2[j - 1]: # 考虑由二者都向前一位 dp[i][j] = dp[i - 1][j - 1] + 1 # 来自于左上方 b[i][j] = 1 # 遇到的两个字符不同 # 左边的选择更大，即第一个字符串后退一位 elif dp[i - 1][j] \u0026gt; dp[i][j - 1]: dp[i][j] = dp[i - 1][j] # 来自于左方 b[i][j] = 2 # 右边的选择更大，即第二个字符串后退一位 else: dp[i][j] = dp[i][j - 1] # 来自于上方 b[i][j] = 3 # 获取答案字符串 res = self.ans(len1, len2, b) # 检查答案是否位空 if res is None or res == \u0026#34;\u0026#34;: return \u0026#34;-1\u0026#34; else: return res 57、NC93 设计LRU缓存结构：困难 from collections import OrderedDict class Solution: def __init__(self, capacity: int): # write code here self.size = capacity self.lru_cache = OrderedDict() def get(self, key: int) -\u0026gt; int: # write code here if key in self.lru_cache: self.lru_cache.move_to_end(key) return self.lru_cache.get(key, -1) def set(self, key: int, value: int) -\u0026gt; None: # write code here if key in self.lru_cache: del self.lru_cache[key] self.lru_cache[key] = value if len(self.lru_cache) \u0026gt; self.size: self.lru_cache.popitem(last=False) 58、NC94 设计LFU缓存结构：困难 这道题放弃吧，绝对不会考。\n59、NC95 数组中的最长连续子序列：困难 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### max increasing subsequence ### @param arr int整型一维数组 the array ### @return int整型 # class Solution: def MLS(self, arr: List[int]) -\u0026gt; int: # write code here nums = set(arr) res = 0 for i in nums: if i - 1 not in nums: j = i + 1 while j in nums: j += 1 res = max(res, j - i) return res 60、NC96 判断一个链表是否为回文结构：简单 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head ListNode类 the head ### @return bool布尔型 # class Solution: def isPail(self, head: ListNode) -\u0026gt; bool: nums = [] # 将链表元素取出一次放入数组 while head: nums.append(head.val) head = head.next temp = nums.copy() # 准备一个数组承接翻转之后的数组 temp.reverse() for i in range(len(nums)): # 正向遍历与反向遍历相同 if nums[i] != temp[i]: return False return True 61、NC98 判断t1树中是否有与t2树完全相同的子树：简单 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param root1 TreeNode类 ### @param root2 TreeNode类 ### @return bool布尔型 # class Solution: def isContains(self, root1, root2): # write code here if not root1 and not root2: return True if not root1 or not root2: return False if root1.val == root2.val: return self.isContains(root1.left, root2.left) and self.isContains( root1.right, root2.right ) return self.isContains(root1.left, root2) or self.isContains(root1.right, root2) 62、NC100 把字符串转换成整数(atoi)：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param s string字符串 ### @return int整型 # class Solution: def StrToInt(self, s: str) -\u0026gt; int: res = 0 index = 0 # 去掉前导空格 s = s.strip() # 去掉空格就什么都没有了 n = len(s) if s == \u0026#34;\u0026#34;: return 0 sign = 1 # 处理第一个符号是正负号的情况 if s[index] == \u0026#34;+\u0026#34;: index += 1 elif s[index] == \u0026#34;-\u0026#34;: index += 1 sign = -1 # 去掉符号就什么都没有了 if index == n: return 0 while index \u0026lt; n: c = s[index] # 后续非法字符，截断 if c \u0026lt; \u0026#34;0\u0026#34; or c \u0026gt; \u0026#34;9\u0026#34;: break # 转数字 res = res * 10 + sign * ((int)(c) - (int)(\u0026#34;0\u0026#34;)) index += 1 # 输出处理越界 return min(max(res, -(2 ** 31)), 2 ** 31 - 1) 63、NC103 反转字符串：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 反转字符串 ### @param str string字符串 ### @return string字符串 # class Solution: def solve(self, str: str) -\u0026gt; str: # 左右双指针 left = 0 right = len(str) - 1 # 两指针往中间靠 while left \u0026lt; right: l_s = list(str) temp = l_s[left] l_s[left] = l_s[right] # 交换两边字符 l_s[right] = temp str = \u0026#34;\u0026#34;.join(l_s) left += 1 right -= 1 return str 64、NC105 二分查找-II：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 如果目标值存在返回下标，否则返回 -1 ### @param nums int整型一维数组 ### @param target int整型 ### @return int整型 # class Solution: def search(self, nums: List[int], target: int) -\u0026gt; int: # write code here front, end = 0, len(nums) - 1 while front \u0026lt;= end: temp = (front + end) // 2 if nums[temp] == target: temp1 = temp while temp1 \u0026gt;= 0 and nums[temp1] == target: temp1 -= 1 return temp1 + 1 elif nums[temp] \u0026gt; target: end = temp - 1 else: front = temp + 1 else: return -1 65、NC106 三个数的最大乘积：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 最大乘积 ### @param A int整型一维数组 ### @return long长整型 # class Solution: def solve(self, A: List[int]) -\u0026gt; int: # write code here A.sort() a1 = A[0] * A[1] * A[-1] a2 = A[-1] * A[-2] * A[-3] if a1 \u0026gt;= a2: return a1 else: return a2 66、NC107 寻找峰值：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param nums int整型一维数组 ### @return int整型 # class Solution: def findPeakElement(self, nums: List[int]) -\u0026gt; int: left = 0 right = len(nums) - 1 # 二分法 while left \u0026lt; right: mid = int((left + right) / 2) # 右边是往下，不一定有坡峰 if nums[mid] \u0026gt; nums[mid + 1]: right = mid # 右边是往上，一定能找到波峰 else: left = mid + 1 # 其中一个波峰 return right 67、NC111 最大数：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 最大数 ### @param nums int整型一维数组 ### @return string字符串 # class Solution: def solve(self, nums): # write code here # 将整型的数字转化为字符串 s = nums for i in range(len(nums)): s[i] = str(s[i]) for i in range(len(nums)): for j in range(len(nums) - i - 1): a = nums[j] b = nums[j + 1] if int(\u0026#34;\u0026#34;.join([b, a])) \u0026gt; int(\u0026#34;\u0026#34;.join([a, b])): s[j], s[j + 1] = s[j + 1], s[j] if s[0] == \u0026#34;0\u0026#34;: return \u0026#34;0\u0026#34; return \u0026#34;\u0026#34;.join(s) 68、NC113 验证IP地址：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 验证IP地址 ### @param IP string字符串 一个IP地址字符串 ### @return string字符串 # class Solution: def isIPv4(self, IP: str): s = IP.split(\u0026#34;.\u0026#34;) # IPv4必定为4组 if len(s) != 4: return False for i in range(len(s)): # 不可缺省，有一个分割为零，说明两个点相连 if len(s[i]) == 0: return False # 比较数字位数及不为零时不能有前缀零 if len(s[i]) \u0026lt; 0 or len(s[i]) \u0026gt; 3 or (s[i][0] == \u0026#34;0\u0026#34; and len(s[i]) != 1): return False # 遍历每个分割字符串，必须为数字 for j in range(len(s[i])): if s[i][j] \u0026lt; \u0026#34;0\u0026#34; or s[i][j] \u0026gt; \u0026#34;9\u0026#34;: return False # 转化为数字比较，0-255之间 num = int(s[i]) if num \u0026lt; 0 or num \u0026gt; 255: return False return True def isIPv6(self, IP: str): s = IP.split(\u0026#34;:\u0026#34;) # IPv6必定为8组 if len(s) != 8: return False for i in range(len(s)): # 每个分割不能缺省，不能超过4位 if len(s[i]) == 0 or len(s[i]) \u0026gt; 4: return False for j in range(len(s[i])): # 不能出现a-fA-F以外的大小写字符 if not ( s[i][j].isdigit() or s[i][j] \u0026gt;= \u0026#34;a\u0026#34; and s[i][j] \u0026lt;= \u0026#34;f\u0026#34; or s[i][j] \u0026gt;= \u0026#34;A\u0026#34; and s[i][j] \u0026lt;= \u0026#34;F\u0026#34; ): return False return True def solve(self, IP: str) -\u0026gt; str: if len(IP) == 0: return \u0026#34;Neither\u0026#34; if Solution.isIPv4(self, IP): return \u0026#34;IPv4\u0026#34; elif Solution.isIPv6(self, IP): return \u0026#34;IPv6\u0026#34; return \u0026#34;Neither\u0026#34; 69、NC116 把数字翻译成字符串：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 解码 ### @param nums string字符串 数字串 ### @return int整型 # class Solution: def solve(self, nums: str) -\u0026gt; int: # 排除0 if nums == \u0026#34;0\u0026#34;: return 0 # 排除只有一种可能的10 和 20 if nums == \u0026#34;10\u0026#34; or nums == \u0026#34;20\u0026#34;: return 1 # 当0的前面不是1或2时，无法译码，0种 for i in range(1, len(nums)): if nums[i] == \u0026#34;0\u0026#34;: if nums[i - 1] != \u0026#34;1\u0026#34; and nums[i - 1] != \u0026#34;2\u0026#34;: return 0 # 辅助数组初始化为1 dp = [1 for i in range(len(nums) + 1)] for i in range(2, len(nums) + 1): # 在11-19，21-26之间的情况 if (nums[i - 2] == \u0026#34;1\u0026#34; and nums[i - 1] != \u0026#34;0\u0026#34;) or ( nums[i - 2] == \u0026#34;2\u0026#34; and nums[i - 1] \u0026gt; \u0026#34;0\u0026#34; and nums[i - 1] \u0026lt; \u0026#34;7\u0026#34; ): dp[i] = dp[i - 1] + dp[i - 2] else: dp[i] = dp[i - 1] return dp[len(nums)] 70、NC117 合并二叉树：简单 ### class TreeNode: ### def __init__(self, x): ### self.val = x ### self.left = None ### self.right = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param t1 TreeNode类 ### @param t2 TreeNode类 ### @return TreeNode类 # class Solution: def mergeTrees(self, t1: TreeNode, t2: TreeNode) -\u0026gt; TreeNode: # 若只有一个节点返回另一个，两个都为NULL自然返回NULL if not t1: return t2 if not t2: return t1 # 根左右的方式递归 head = TreeNode(t1.val + t2.val) head.left = self.mergeTrees(t1.left, t2.left) head.right = self.mergeTrees(t1.right, t2.right) return head 71、NC119 最小的K个数：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param input int整型一维数组 ### @param k int整型 ### @return int整型一维数组 # class Solution: def GetLeastNumbers_Solution(self, input: List[int], k: int) -\u0026gt; List[int]: res = [] if len(input) \u0026gt;= k and k != 0: import heapq # 小根堆，每次输入要乘-1 pq = [] for i in range(k): # 构建一个k个大小的堆 heapq.heappush(pq, (-1 * input[i])) for i in range(k, len(input)): # 较小元素入堆 if (-1 * pq[0]) \u0026gt; input[i]: heapq.heapreplace(pq, (-1 * input[i])) # 堆中元素取出入数组 for i in range(k): res.append(-1 * pq[0]) heapq.heappop(pq) return res 72、NC121 字符串的排列：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param str string字符串 ### @return string字符串一维数组 # class Solution: def recursion(self, res: List[str], string: str, temp: str, vis: List[int]): # 临时字符串满了加入输出 if len(temp) == len(string): res.append(temp) return # 遍历所有元素选取一个加入 for i in range(len(string)): # 如果该元素已经被加入了则不需要再加入了 if vis[i] == 1: continue if i \u0026gt; 0 and string[i - 1] == string[i] and not vis[i - 1]: # 当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了 continue # 标记为使用过 vis[i] = 1 # 加入临时字符串 temp += string[i] self.recursion(res, string, temp, vis) # 回溯 vis[i] = 0 temp = temp[:-1] def Permutation(self, str: str) -\u0026gt; List[str]: # 先按字典序排序，使重复字符串相邻 str = \u0026#34;\u0026#34;.join((lambda x: (x.sort(), x)[1])(list(str))) # 标记每个位置的字符是否被使用过 vis = [0] * len(str) res = [] temp = \u0026#34;\u0026#34; # 递归获取 self.recursion(res, str, temp, vis) return res 73、NC127 最长公共子串：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### longest common substring ### @param str1 string字符串 the string ### @param str2 string字符串 the string ### @return string字符串 # class Solution: def LCS(self, str1: str, str2: str) -\u0026gt; str: # 让str1为较长的字符串 if len(str1) \u0026lt; len(str2): str1, str2 = str2, str1 res = \u0026#34;\u0026#34; max_len = 0 # 遍历str1的长度 for i in range(len(str1)): # 查找是否存在 if str1[i - max_len : i + 1] in str2: res = str1[i - max_len : i + 1] max_len += 1 return res 74、NC128 接雨水问题：困难 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### max water ### @param arr int整型一维数组 the array ### @return long长整型 # class Solution: def maxWater(self, arr: List[int]) -\u0026gt; int: # 排除空数组 if len(arr) == 0: return 0 res = 0 # 左右双指针 left = 0 right = len(arr) - 1 # 中间区域的边界高度 maxL = 0 maxR = 0 # 直到左右指针相遇 while left \u0026lt; right: # 每次维护往中间的最大边界 maxL = max(maxL, arr[left]) maxR = max(maxR, arr[right]) # 较短的边界确定该格子的水量 if maxR \u0026gt; maxL: res += maxL - arr[left] left += 1 else: res += maxR - arr[right] right -= 1 return res 75、NC132 环形链表的约瑟夫问题：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param n int整型 ### @param m int整型 ### @return int整型 # class Solution: def ysf(self, n, m): ls = list(range(1, n + 1)) pos = 0 for _ in range(n - 1): pos = (pos + m - 1) % len(ls) del ls[pos] return ls[0] 76、NC133 链表的奇偶重排：中等 ### class ListNode: ### def __init__(self, x): ### self.val = x ### self.next = None # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param head ListNode类 ### @return ListNode类 # class Solution: def oddEvenList(self, head: ListNode) -\u0026gt; ListNode: # 如果链表为空，不用重排 if head == None: return head # even开头指向第二个节点，可能为空 even = head.next # odd开头指向第一个节点 odd = head # 指向even开头 evenhead = even while even and even.next: # odd连接even的后一个，即奇数位 odd.next = even.next # odd进入后一个奇数位 odd = odd.next # even连接后一个奇数的后一位，即偶数位 even.next = odd.next # even进入后一个偶数位 even = even.next # even整体接在odd后面 odd.next = evenhead return head 77、NC134 买卖股票的最好时机(二)：中等 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 计算最大收益 ### @param prices int整型一维数组 股票每一天的价格 ### @return int整型 # class Solution: def maxProfit(self, prices: List[int]) -\u0026gt; int: n = len(prices) # dp[i][0]表示某一天不持股到该天为止的最大收益，dp[i][1]表示某天持股，到该天为止的最大收益 dp = [[0] * 2 for i in range(n)] # 第一天不持股，总收益为0 dp[0][0] = 0 # 第一天持股，总收益为减去该天的股价 dp[0][1] = -prices[0] # 遍历后续每天，状态转移 for i in range(1, n): dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]) # 最后一天不持股，到该天为止的最大收益 return dp[n - 1][0] 78、NC140 排序：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # ### 将给定数组排序 ### @param arr int整型一维数组 待排序的数组 ### @return int整型一维数组 # class Solution: def MySort(self, arr: List[int]) -\u0026gt; List[int]: # write code here for i in range(len(arr)): for j in range(i + 1, len(arr)): if arr[j] \u0026lt; arr[i]: arr[i], arr[j] = arr[j], arr[i] return arr 79、NC141 判断是否为回文字符串：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param str string字符串 待判断的字符串 ### @return bool布尔型 # class Solution: def judge(self, str: str) -\u0026gt; bool: # 首指针 left = 0 # 尾指针 right = len(str) - 1 # 首尾往中间靠 while left \u0026lt; right: # 比较前后是否相同 if str[left] != str[right]: return False left += 1 right -= 1 return True 80、NC156 数组中只出现一次的数（其它数出现k次）：简单 # ### 代码中的类名、方法名、参数名已经指定，请勿修改，直接返回方法规定的值即可 # # ### @param arr int整型一维数组 ### @param k int整型 ### @return int整型 # class Solution: def foundOnceNumber(self, arr, k): arr.sort() arrs = arr[1 : len(arr) + 1 : k] return sum(arr) - sum(arrs * k) 欢迎加入交流群，参与话题讨论：\n参考资料：\nhttps://www.nowcoder.com/exam/oj?job=2\u0026amp;page=1\u0026amp;pageSize=50\u0026amp;search=\u0026amp;tab=%E7%AE%97%E6%B3%95%E7%AF%87\u0026amp;topicId=196\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E7%AE%97%E6%B3%95/000003-%E7%AE%97%E6%B3%9580%E9%81%93%E9%AB%98%E9%A2%91%E7%AE%97%E6%B3%95%E9%A2%98python%E7%89%88/","summary":"80道高频算法题来源于牛客网，这些答案都经过了我验证，可以复制粘贴后提交通过：","title":"80道高频算法题Python版"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDjango官方并没有提供标准的项目结构，于是网上众说纷纭，百花齐放，一千个读者有一千个哈姆雷特。那我们该怎么设计项目结构呢？在回答这个问题之前，先了解一下Django原生的目录和文件都是干嘛的。\nproject 在使用django-admin startproject命令后就会创建这样的目录文件，如下：\nmysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py 根目录mysite/ 根目录的mysite/，仅仅是个目录而已，Django不会拿它做什么，它的名字可以随意修改。\n带短横线的根目录\n分享一个踩坑经验，有时候我们想把项目名命名为带短横线的，如 hello-world，通过命令直接创建会报错：\n...\\\u0026gt; django-admin startproject hello-world CommandError: \u0026#39;hello-world\u0026#39; is not a valid project name. Please make sure the name is a valid identifier. startproject不支持直接创建带短横线的项目名。\n方法1 加参数\n其实！它是支持的，只是我们漏了一个参数。startproject的完整格式为django-admin startproject name [directory]，可以在后面追加一个目录参数：\n...\\\u0026gt; django-admin startproject helloworld hello-world 就可以了。根目录是hello-world，里面的project是helloworld。\n方法2 改名字\n先用helloworld创建，然后再修改，可以①在资源管理器中直接修改文件夹，②也可以在 PyCharm 中修改：\n选择Rename directory：\n注意去掉勾选Search，不修改其他位置：\n只修改这个根目录的名字即可。\nmanage.py Django的命令行工具，使用频繁，格式为：\npython manage.py \u0026lt;command\u0026gt; [options] 内部mysite.py/ Django项目实际使用到的Python包，存放项目配置、URL路由等。\nmysite/__init__.py 表明这个目录是Python包。\nmysite/settings.py Django项目配置。\nmysite/urls.py URL路由。\nmysite/asgi.py 兼容ASGI Web服务器的入口。\nmysite/wsgi.py 兼容WSGI Web服务器的入口。\nASGI和WSGI，都是一种Python的Web服务网关接口协议，是在CGI通用网关接口，Common Gateway Interface）的标准上构建的。\napp app是Django项目的应用，一个project可以包含多个app。在使用django-admin startapp命令后就会创建这样的目录文件，如下：\npolls/ __init__.py admin.py apps.py migrations/ __init__.py models.py tests.py views.py polls/ app的Python包，可以放在任意路径。修改存放路径，相关配置中的路径也需要修改。\nadmin.py 存放Django自带后台管理的models。\napps.py app配置信息，例如：\nfrom django.apps import AppConfig class PollsConfig(AppConfig): name = \u0026#39;polls\u0026#39; 存放路径不同，name的值也不同。\nmigrations/ Django数据迁移生成的迁移记录文件。\nmodels.py 存放app的models。\ntests.py 单元测试。\nviews.py 视图函数（类）。\nurls.py app的路由配置，需要手动添加，可通过include()函数导入到mysite/urls.py中，例如：\nfrom django.contrib import admin from django.urls import include, path urlpatterns = [ path(\u0026#39;admin/\u0026#39;, admin.site.urls), path(\u0026#39;polls/\u0026#39;, include(\u0026#39;polls.urls\u0026#39;)), ] polls/urls.py：\nfrom django.urls import path from . import views urlpatterns = [ path(\u0026#39;\u0026#39;, views.index), ] 访问xxx/polls/就会路由到views.index。\n工程化项目参考 虽说是百家争鸣，但也有公认的工程化实践。我找了GitHub上star最多的叫做Django Edge的project skeleton，它的设计如下：\nTop-directory ├── docs ├── logs ├── README.md ├── requirements │ ├── base.txt │ ├── development.txt │ └── production.txt ├── requirements.txt └── src ├── Your project name │ ├── __init__.py │ ├── logger.py │ ├── settings │ │ ├── base.py │ │ ├── development.py │ │ ├── local.sample.env │ │ └── production.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── accounts │ ├── admin.py │ ├── forms.py │ ├── migrations │ ├── models.py │ ├── templates │ ├── tests.py │ ├── urls.py │ └── views.py ├── manage.py ├── media ├── profiles │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ ├── models.py │ ├── signals.py │ ├── templates │ ├── tests.py │ ├── urls.py │ └── views.py ├── static │ ├── bootstrap │ │ ├── css │ │ ├── fonts │ │ └── js │ └── site │ ├── css │ ├── ico │ ├── img │ └── js └── templates 非常的像那么回事。我又搜了其他实践的文章，找到了这张图：\n国内的文章也有提到类似这2种项目结构的设计，特点是①app放置在单独目录统一管理，②requirements和settings按环境做拆分，等等等。我说说我的观点，一开始没有必要就按照这种标准的工程化实践来写项目，我更倾向于先凭自己的能力写一版，再不断重构和优化的方式。业务不同，能力不同，经验不同，写出来的代码自然也不同。不过在碰到问题时，可以参考借鉴，很可能别人已经提供了解决方案。\n我看了一些Django开源项目代码，很少见到真像这么设计项目结构的，所以不必过分在意。\n小结 本文介绍了Django startproject和startapp命令产生的目录文件的结构和作用。然后给出了工程化项目参考。项目结构没有标准，只有适用于当前的最佳实践，可能这就是官方没有给出标准项目结构的原因吧。\n参考资料：\nhttps://docs.djangoproject.com/en/3.1/intro/tutorial01/\nhttps://django-edge.readthedocs.io/en/latest/Architecture/\nhttps://studygyaan.com/django/best-practice-to-structure-django-project-directories-and-files\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000003-djangodjango%E5%AE%98%E6%96%B9%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89%E6%A0%87%E5%87%86%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84/","summary":"Django官方并没有提供标准的项目结构，于是网上众说纷纭，百花齐放，一千个读者有一千个哈姆雷特。那我们该怎么设计项目结构呢？在回答这个问题之前，先了解一下Django原生的目录和文件都是干嘛的。","title":"Django官方为什么没有标准项目结构"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDockerfile是一个文本文件，包含一些Docker指令。执行docker build，Docker就会执行Dockerfile里面的指令，来自动创建镜像。\n用法 Dockerfile里面的指令可以访问context这些文件。\ncontext是递归的，PATH包含所有子目录，URL包含所有子模块。\n例子，把当前目录当做context，\n$ docker build . Sending build context to Docker daemon 6.51 MB ... build是由Docker daemon(守护进程)来运行，而不是CLI。\nbuild会把整个context发给daemon。所以最好把context设置为空目录，把Dockerfile放进去。只添加需要的文件，为了提高build性能，还可以添加.dockerignore来排除一些文件和目录。\nWarning！不要用系统根目录/作为PATH，不然会把根目录下所有东西都传给Docker daemon。\n一般会把Dockerfile放在context根目录下，也可以使用-f来指定其他路径，\n$ docker build -f /path/to/a/Dockerfile . 指定镜像存放仓库可以使用-t，\n$ docker build -t shykes/myapp . 支持多个，\n$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest . Docker daemon在执行Dockfile的指令前，会做检查，如果有语法错误会报错，\n$ docker build -t test/myapp . Sending build context to Docker daemon 2.048 kB Error response from daemon: Unknown instruction: RUNCMD Docker daemon执行指令，是一个一个执行，一个一个提交的。执行结束会生成镜像ID。自动清理context。\nRUN cd /tmp是无效的，因为daemon是独立执行每条指令的，不会作用到后面的指令。\n为了加速build过程，Docker会重复使用中间镜像(缓存)，在console日志中可以看到Using cache，\n$ docker build -t svendowideit/ambassador . Sending build context to Docker daemon 15.36 kB Step 1/4 : FROM alpine:3.2 ---\u0026gt; 31f630c65071 Step 2/4 : MAINTAINER SvenDowideit@home.org.au ---\u0026gt; Using cache ---\u0026gt; 2a1c91448f5f Step 3/4 : RUN apk update \u0026amp;\u0026amp; apk add socat \u0026amp;\u0026amp; rm -r /var/cache/ ---\u0026gt; Using cache ---\u0026gt; 21ed6e7fbb73 Step 4/4 : CMD env | grep _TCP= | (sed \u0026#39;s/.*_PORT_\\([0-9]*\\)_TCP=tcp:\\/\\/\\(.*\\):\\(.*\\)/socat -t 100000000 TCP4-LISTEN:\\1,fork,reuseaddr TCP4:\\2:\\3 \\\u0026amp;/\u0026#39; \u0026amp;\u0026amp; echo wait) | sh ---\u0026gt; Using cache ---\u0026gt; 7ea8aef582cc Successfully built 7ea8aef582cc cache来源于之前本地build过的镜像，或者使用docker load加载的镜像。\n如果想直接指定一个镜像作为cache，可以使用--cache-from。\n格式 ## Comment INSTRUCTION arguments #开头是注释或者parser directive（提示解析器做特殊处理）。\n指令是忽略大小写的，不过为了和参数区分，一般全大写。\nDockerfile从上往下顺序执行指令，第一条指令必须是FROM，定义build的parent image（父镜像）。没有parent的镜像叫base image。\n参数里面的#就不是注释了，是参数的一部分，\n## Comment RUN echo \u0026#39;we are running some # of cool things\u0026#39; 注释在Dockerfile指令执行前，会被移除。以下是等价的，\nRUN echo hello \\ ## comment world RUN echo hello \\ world 注意，注释不支持换行符\\。\n注释和指令前面的空格会被忽略，以下是等价的，\n# this is a comment-line RUN echo hello RUN echo world ## this is a comment-line RUN echo hello RUN echo world 但是参数里面的空格，是会被保留的，\nRUN echo \u0026#34;\\ hello\\ world\u0026#34; Parser directives ## directive=value Parser directives是一种特殊的注释，用来提示解析器做特殊处理。\n但是Parser directives并不会添加layers到build中，也不会被识别为build step。\n如果注释、空行、或者指令被运行后，Docker就不会再识别Parser directives了，所以必须把Parser directives放在Dockerfile的最前面的最前面。\nParser directives是忽略大小写的，不过一般约定为全小写。同时约定随后跟一个空行。\nParser directives不支持换行符。\n以下是一些无效示例，\n无效\u0026ndash;换行符\n## direc \\ tive=value 无效\u0026ndash;出现了2次\n## directive=value1 ## directive=value2 FROM ImageName 无效\u0026ndash;在指令之后就是普通的注释\nFROM ImageName ## directive=value 无效\u0026ndash;在普通注释之后也变成了普通注释\n## About my dockerfile ## directive=value FROM ImageName 无效\u0026ndash;未知命令会被视为普通注释，普通注释之后也是普通注释\n## unknowndirective=value ## knowndirective=value Parser directives同一行的空格会被忽略，以下是等价的，\n#directive=value ## directive =value #\tdirective= value ## directive = value #\tdIrEcTiVe=value 目前支持2个Parser directives，\nsyntax，依赖BuildKit escape escape 反斜杠（默认）\n## escape=\\ 或者反引号\n## escape=` 用来指定转义符。这个在Windows系统很有用，因为\\在Windows是路径分隔符。\n比如，\nFROM microsoft/nanoserver COPY testfile.txt c:\\\\ RUN dir c:\\ 会执行失败，\nPS C:\\John\u0026gt; docker build -t cmd . Sending build context to Docker daemon 3.072 kB Step 1/2 : FROM microsoft/nanoserver ---\u0026gt; 22738ff49c6d Step 2/2 : COPY testfile.txt c:\\RUN dir c: GetFileAttributesEx c:RUN: The system cannot find the file specified. PS C:\\John\u0026gt; 使用escape可以替换\\为`\n## escape=` FROM microsoft/nanoserver COPY testfile.txt c:\\ RUN dir c:\\ 执行成功，\nPS C:\\John\u0026gt; docker build -t succeeds --no-cache=true . Sending build context to Docker daemon 3.072 kB Step 1/3 : FROM microsoft/nanoserver ---\u0026gt; 22738ff49c6d Step 2/3 : COPY testfile.txt c:\\ ---\u0026gt; 96655de338de Removing intermediate container 4db9acbb1682 Step 3/3 : RUN dir c:\\ ---\u0026gt; Running in a2c157f842f5 Volume in drive C has no label. Volume Serial Number is 7E6D-E0F7 Directory of c:\\ 10/05/2016 05:04 PM 1,894 License.txt 10/05/2016 02:22 PM \u0026lt;DIR\u0026gt; Program Files 10/05/2016 02:14 PM \u0026lt;DIR\u0026gt; Program Files (x86) 10/28/2016 11:18 AM 62 testfile.txt 10/28/2016 11:20 AM \u0026lt;DIR\u0026gt; Users 10/28/2016 11:20 AM \u0026lt;DIR\u0026gt; Windows 2 File(s) 1,956 bytes 4 Dir(s) 21,259,096,064 bytes free ---\u0026gt; 01c7f3bef04f Removing intermediate container a2c157f842f5 Successfully built 01c7f3bef04f PS C:\\John\u0026gt; 环境替换 环境变量（使用ENV指令来定义环境变量）能够用在指令中作为变量，被Dockerfile解释。还可以处理转义符，以便在语句中照字面值地包含variable-like语法。\n使用$variable_name或${variable_name}来引用环境变量。\n可以使用双括弧和下划线来命名，如${foo}_bar。同时支持bash修饰符，\n${variable:-word} set variable后就是set的值，没有set variable值就是word ${variable:+word} set variable后值就是word，没有set variable就是空字符串 word既可以是string，也可以是另外一个环境变量。\n可以在变量前加转义符，比如\\$foo ，\\${foo}会被分别转义为$foo 和${foo}。\n示例，\nFROM busybox ENV foo /bar WORKDIR ${foo} # WORKDIR /bar ADD . $foo # ADD . /bar COPY \\$foo /quux # COPY $foo /quux Dockerfile的一下指令都支持环境变量\nADD COPY ENV EXPOSE FROM LABEL STOPSIGNAL USER VOLUME WORKDIR ONBUILD (结合以上指令使用) 需要注意的是，变量替换是针对整条指令的，\nENV abc=hello ENV abc=bye def=$abc ENV ghi=$abc def的值是hello，而不是bye，因为上一条指令赋值的hello。\nghi的值才会是bye。\n.dockerignore file .dockerignore文件位于context根目录，会把匹配到的文件和目录排除在context之外。\n这样就可以在使用ADD和COPY命令时，避免把一些大文件或者敏感信息文件和目录，发送到Docker daemon。\ncontext是由PATH和URL定义的，所以.dockerignore文件会匹配这2个路径。\n/foo/bar == foo/bar\n示例，\n## comment */temp* */*/temp* temp? Rule Behavior # comment 注释忽略 */temp* 排除root的子目录下，temp开头的文件和目录。 如/somedir/temporary.txt 和 /somedir/temp */*/temp* 排除root的二层目录下，temp开头的文件和目录。\n如 /somedir/subdir/temporary.txt temp? 排除root下， temp+1个字符的文件和目录。\n如 /tempa 和/tempb 匹配遵循Go语言的filepath.Match规则。\nDocker还支持**，匹配任意数量的目录（包括0）。如**/*.go排除.go结尾的，包括context root下所有目录。\n如果排除了一堆文件后，想只包含其中几个文件，可以使用异常规则!。\n示例，排除.md结尾的文件，包含README.md，\n*.md !README.md README-secret.md不会被排除，因为!README*.md能匹配到README-secret.md，又把README-secret.md包含进来了。\n.dockerignore文件甚至可以排除Dockerfile 和.dockerignore，然而并没有什么卵用，这些文件还是会被发送到Docker daemon，只是ADD和COPY命令不会把它们复制到镜像了。\nFROM FROM指令初始化一个新的buid stage，为后面的指令设置Parent Image。\nFROM [--platform=\u0026lt;platform\u0026gt;] \u0026lt;image\u0026gt; [AS \u0026lt;name\u0026gt;] 或\nFROM [--platform=\u0026lt;platform\u0026gt;] \u0026lt;image\u0026gt;[:\u0026lt;tag\u0026gt;] [AS \u0026lt;name\u0026gt;] 或\nFROM [--platform=\u0026lt;platform\u0026gt;] \u0026lt;image\u0026gt;[@\u0026lt;digest\u0026gt;] [AS \u0026lt;name\u0026gt;] --platform，用来定义image的平台，如linux/amd64, linux/arm64, 或者windows/amd64，这样就能支持多平台镜像。\ntag digest是可选的，都不填时，默认用最新的tag。如果找不到tag，builder就会报错。\nAS name可以给image取个别名，在后续FROM和COPY --from=\u0026lt;name|index\u0026gt;指令中可以使用这个别名。\n可以在一个Dockerfile文件中使用多个FROM。每个FROM都会把上个指令创建的状态清除。所以在每个新的FROM指令之前，记录commit输出的最后一个image ID。\nARG是唯一能在FROM之前的指令。\n比如--platform，默认情况下，会使用build请求的默认平台。也可以使用全局build参数，通过automatic platform ARGs（依赖BuildKit）来强制把stage指定为本地build平台（--platform=$BUILDPLATFORM），然后用它来在stage中cross-compile目标平台。\nFROM和ARG怎么结合使用呢？\nFROM指令支持出现在第一个FROM之前的ARG声明的变量。\nARG CODE_VERSION=latest FROM base:${CODE_VERSION} CMD /code/run-app FROM extras:${CODE_VERSION} CMD /code/run-extras FROM之前声明的ARG是在build stage之外的，所以它不能用在FROM后的任何指令中。如果要用，可以使用在build stage中的不带value的ARG指令，\nARG VERSION=latest FROM busybox:$VERSION ARG VERSION RUN echo $VERSION \u0026gt; image_version RUN RUN \u0026lt;command\u0026gt; (shell 格式，Linux /bin/sh -c Windowscmd /S /C) RUN [\u0026quot;executable\u0026quot;, \u0026quot;param1\u0026quot;, \u0026quot;param2\u0026quot;] (exec 格式) RUN指令会在当前镜像之上的新layer中执行命令，commit结果，commit后的镜像会在Dockerfile的下一个step中使用。\nRUN指令的commits符合Docker理念，commit is cheap，containers可以从image历史中任何记录创建，就像source control。\n可以使用不同的SHELL，\nshell格式\nRUN /bin/bash -c \u0026#39;source $HOME/.bashrc; echo $HOME\u0026#39; exec格式\nRUN [\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;echo hello\u0026#34;] shell格式会调用command shell，而exec格式不会，所以exec中$HOME是没用的，要用的话直接执行shell RUN [ \u0026quot;sh\u0026quot;, \u0026quot;-c\u0026quot;, \u0026quot;echo $HOME\u0026quot; ]。\n注意，exec格式被解析为JSON数组，所以只能用双引号。还需注意反斜杠，\n错误\nRUN [\u0026#34;c:\\windows\\system32\\tasklist.exe\u0026#34;] 正确\nRUN [\u0026#34;c:\\\\windows\\\\system32\\\\tasklist.exe\u0026#34;] 默认是会启动RUN的缓存的，比如RUN apt-get dist-upgrade -y会在下次build的时候复用。可以使用docker build --no-cache来禁用缓存。\n使用ADD和COPY指令也可以禁用RUN缓存。\nCMD CMD和RUN是不同的。RUN指令是在build过程中执行command和commit结果。CMD在build时不会执行任何command，而是为image定义command，在container（镜像创建的容器）启动的时候执行。\nCMD [\u0026quot;executable\u0026quot;,\u0026quot;param1\u0026quot;,\u0026quot;param2\u0026quot;] (exec 格式，首选) CMD [\u0026quot;param1\u0026quot;,\u0026quot;param2\u0026quot;] (ENTRYPOINT默认参数) CMD command param1 param2 (shell 格式) 一个Dockerfile只能有一个CMD指令，如果有多个，只有最后一个生效。\nshell格式会调用command shell，而exec格式不会，所以exec中$HOME是没用的，要用的话直接执行shell RUN [ \u0026quot;sh\u0026quot;, \u0026quot;-c\u0026quot;, \u0026quot;echo $HOME\u0026quot; ]。\n注意，exec格式被解析为JSON数组，所以只能用双引号。还需注意反斜杠。\n如果想要container每次运行相同的可执行文件，需要结合 ENTRYPOINT使用。\n如果docker run定义了参数，那么会覆盖CMD定义。\nLABEL LABEL \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; ... LABEL用来给image添加metadata，是key-value键值对的形式。\n示例，\nLABEL \u0026#34;com.example.vendor\u0026#34;=\u0026#34;ACME Incorporated\u0026#34; LABEL com.example.label-with-value=\u0026#34;foo\u0026#34; LABEL version=\u0026#34;1.0\u0026#34; LABEL description=\u0026#34;This text illustrates \\ that label-values can span multiple lines.\u0026#34; 一个image可以有多个label，一个label可以有多个键值对，以下是等价的，\nLABEL multi.label1=\u0026#34;value1\u0026#34; multi.label2=\u0026#34;value2\u0026#34; other=\u0026#34;value3\u0026#34; LABEL multi.label1=\u0026#34;value1\u0026#34; \\ multi.label2=\u0026#34;value2\u0026#34; \\ other=\u0026#34;value3\u0026#34; label会随着image继承，从base image或parent image继承到当前image。\n重复的label，会用最新的覆盖旧的。\n可以使用命令查看image的labels，\ndocker image inspect --format=\u0026#39;\u0026#39; myimage { \u0026#34;com.example.vendor\u0026#34;: \u0026#34;ACME Incorporated\u0026#34;, \u0026#34;com.example.label-with-value\u0026#34;: \u0026#34;foo\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;1.0\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;This text illustrates that label-values can span multiple lines.\u0026#34;, \u0026#34;multi.label1\u0026#34;: \u0026#34;value1\u0026#34;, \u0026#34;multi.label2\u0026#34;: \u0026#34;value2\u0026#34;, \u0026#34;other\u0026#34;: \u0026#34;value3\u0026#34; } MAINTAINER MAINTAINER 已经弃用了，直接使用LABLE，\nLABEL maintainer=\u0026#34;SvenDowideit@home.org.au\u0026#34; EXPOSE EXPOSE \u0026lt;port\u0026gt; [\u0026lt;port\u0026gt;/\u0026lt;protocol\u0026gt;...] EXPOSE定义了container监听的网络端口，支持TCP和UDP，默认TCP。\nEXPOSE并不真正的发布端口，而只是一种预定义。\n真正发布是在docker run的时候，使用-p或-P来发布。\n-p发布一个或多个端口，-P发布全部，并映射到高位端口。\n示例，默认TCP，可以定义UDP，\nEXPOSE 80/udp 也可以同时定义TCP和UDP，\nEXPOSE 80/tcp EXPOSE 80/udp 如果这里docker run使用了-P，将会暴露一次TCP端口和一次UDP端口，由于会映射到高位端口，它们的端口会不一样。\n使用-p指定端口，\ndocker run -p 80:80/tcp -p 80:80/udp ... 也可以使用docker network来创建网络在container之间通信而不需要暴露任何端口。因为container可以使用任何端口通信。\nENV ENV \u0026lt;key\u0026gt; \u0026lt;value\u0026gt; ENV \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; ... ENV用来设置环境变量。有2种形式，以下是等价的，\nENV myName=\u0026#34;John Doe\u0026#34; myDog=Rex\\ The\\ Dog \\ myCat=fluffy ENV myName John Doe ENV myDog Rex The Dog ENV myCat fluffy 可以使用docker inspect来查看环境变量。也可以使用docker run --env \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt;来修改环境变量。\nENV的作用域除了build，还包括container running。有时候会有副作用，比如ENV DEBIAN_FRONTEND noninteractive，所有操作都是非交互式的，无需向用户请求输入，直接运行命令。可能会使apt-get用户误认为是一个Debian-based image。正确的做法是为command添加单独的环境变量，如RUN apt-get install -y python3。\nADD ADD [--chown=\u0026lt;user\u0026gt;:\u0026lt;group\u0026gt;] \u0026lt;src\u0026gt;... \u0026lt;dest\u0026gt; ADD [--chown=\u0026lt;user\u0026gt;:\u0026lt;group\u0026gt;] [\u0026#34;\u0026lt;src\u0026gt;\u0026#34;,... \u0026#34;\u0026lt;dest\u0026gt;\u0026#34;] ADD有2种形式，第2种是为了支持路径包含空格，所以加了双引号。\n--chown只适用于Linux container，对Windows无效。\nADD的作用是从\u0026lt;src\u0026gt;复制新文件，目录或者远程文件URLs，然后添加到\u0026lt;desc\u0026gt;所在的image文件系统。\nsrc如果是文件和目录，那么就是相对路径，相对于build的context。同时支持通配符，遵循Golang的filepath.Match规则。\n示例，添加所有以\u0026quot;hom\u0026quot;开头的文件，\nADD hom* /mydir/ 用?匹配单个字符，\nADD hom?.txt /mydir/ \u0026lt;dest\u0026gt;是绝对路径，或者WORKDIR的相对路径。\n示例，绝对路径，\nADD test.txt /absoluteDir/ 相对路径，\u0026lt;WORKDIR\u0026gt;/relativeDir/，\nADD test.txt relativeDir/ 如果路径种包含特殊字符（如[和]），那么需要进行转义，\n示例，添加一个文件arr[0].txt，\nADD arr[[]0].txt /mydir/ 针对Linux，可以使用--chown定义username、groupname或者UID/GID，默认新文件和目录会被设置为UID为0，GID为0。\n如果只设置username不设置groupname，或只设置UID不设置GID，GID会使用和UID相同的数值。\nusername和groupname会被container\u0026rsquo;s root filesystem /etc/passwd and /etc/group 转换为UID/GID。如果container没有这2个文件，在设置了username/groupname后，就会报错。可以通过设置UID/GID来避免。\n示例，\nADD --chown=55:mygroup files* /somedir/ ADD --chown=bin files* /somedir/ ADD --chown=1 files* /somedir/ ADD --chown=10:11 files* /somedir/ 如果build使用STDIN (docker build - \u0026lt; somefile)，就没有build context，就只能用ADDURL。也可以在使用STDIN时添加压缩包 (docker build - \u0026lt; archive.tar.gz)，压缩包根目录的Dockerfile和其他压缩包会当做build context。\n如果src是一个远程文件URL，就会需要600权限(Linux)。如果远程文件有HTTP Last-Modified header，header的timestamp会用来设置到dest文件的mtime。但是mtime不会反映文件是否修改和缓存是否应该更新。\n如果URL文件需要授权，ADD是不支持的，需要使用RUN wget, RUN curl，或者container里面的其他工具。\nADD遵循以下规则：\n\u0026lt;src\u0026gt; 必须在build的context 中；不能 ADD ../something /something添加context父目录的东西。因为 docker build 的第一步是把context，目录及其子目录发送到docker daemon。 如果\u0026lt;src\u0026gt; 是URL，\u0026lt;dest\u0026gt; 没有以斜杠结尾，那么文件从直接从URL下载后，然后直接复制到 \u0026lt;dest\u0026gt;。 如果 \u0026lt;src\u0026gt; 是URL，\u0026lt;dest\u0026gt; 是以斜杠结尾的，那么会从URL解析出文件名，下载到\u0026lt;dest\u0026gt;/\u0026lt;filename\u0026gt;。比如， ADD http://example.com/foobar dest/ 会创建文件 dest/foobar。URL必须是明确的路径，以保证能找到合适的文件名(http://example.com 是无效的)。 如果 \u0026lt;src\u0026gt; 是目录，那么整个目录都会被复制，包括文件系统的metadata。（目录本身不复制，只是内容） 如果 \u0026lt;src\u0026gt;是本地压缩包（如gzip, bzip2 or xz），那么会被解压成目录。远程URL是不会解压的。解压相当于执行了 tar -x，如果dest路径下有文件冲突，会被重命名为“2”。（压缩包不是根据文件名判断的，而是根据内容，比如一个空文件命名为.tar.gz，是不会被解压复制的） 如果 \u0026lt;src\u0026gt; 是任何其他文件，就会随同它的metadata一起复制。此时 \u0026lt;dest\u0026gt; 以斜杠 /结尾的话，就会被认为是一个目录，\u0026lt;src\u0026gt;的内容会被写到\u0026lt;dest\u0026gt;/base(\u0026lt;src\u0026gt;)。 如果\u0026lt;src\u0026gt;定义的是多个资源，不论是直接还是通配符匹配到的， \u0026lt;dest\u0026gt; 必须是一个目录，且以斜杠/结尾。 如果 \u0026lt;dest\u0026gt; 不以斜杠结尾，那么就会被认为是一个普通文件，那么\u0026lt;src\u0026gt; 会被写到\u0026lt;dest\u0026gt;。 如果 \u0026lt;dest\u0026gt; 不存在，那么path中的所有未创建的目录都会自动创建。 如果src内容改变了，在第一次遇到ADD指令后，会禁用后续所有指令的缓存，包括RUN指令的缓存。\nCOPY COPY和ADD的区别在于ADD可以添加远程URLS，COPY不能。\nCOPY [--chown=\u0026lt;user\u0026gt;:\u0026lt;group\u0026gt;] \u0026lt;src\u0026gt;... \u0026lt;dest\u0026gt; COPY [--chown=\u0026lt;user\u0026gt;:\u0026lt;group\u0026gt;] [\u0026#34;\u0026lt;src\u0026gt;\u0026#34;,... \u0026#34;\u0026lt;dest\u0026gt;\u0026#34;] COPY有2种形式，第2种是为了支持路径包含空格，所以加了双引号。\n--chown只适用于Linux container，对Windows无效。\nCOPY的作用是从\u0026lt;src\u0026gt;复制新文件，目录，然后添加到\u0026lt;desc\u0026gt;所在的image文件系统。\nsrc如果是文件和目录，那么就是相对路径，相对于build的context。同时支持通配符，遵循Golang的filepath.Match规则。\n示例，添加所有以\u0026quot;hom\u0026quot;开头的文件，\nCOPY hom* /mydir/ 用?匹配单个字符，\nCOPY hom?.txt /mydir/ \u0026lt;dest\u0026gt;是绝对路径，或者WORKDIR的相对路径。\n示例，绝对路径，\nCOPY test.txt /absoluteDir/ 相对路径，\u0026lt;WORKDIR\u0026gt;/relativeDir/，\nCOPY test.txt relativeDir/ 如果路径种包含特殊字符（如[和]），那么需要进行转义，\n示例，添加一个文件arr[0].txt，\nCOPY arr[[]0].txt /mydir/ 针对Linux，可以使用--chown定义username、groupname或者UID/GID，默认新文件和目录会被设置为UID为0，GID为0。\n如果只设置username不设置groupname，或只设置UID不设置GID，GID会使用和UID相同的数值。\nusername和groupname会被container\u0026rsquo;s root filesystem /etc/passwd and /etc/group 转换为UID/GID。如果container没有这2个文件，在设置了username/groupname后，就会报错。可以通过设置UID/GID来避免。\n示例，\nCOPY --chown=55:mygroup files* /somedir/ COPY --chown=bin files* /somedir/ COPY --chown=1 files* /somedir/ COPY --chown=10:11 files* /somedir/ 如果build使用STDIN (docker build - \u0026lt; somefile)，就没有build context，就不能用COPY。\nCOPY支持--from=\u0026lt;name|index\u0026gt;，用来指定src为之前buid的image（通过FROM .. AS \u0026lt;name\u0026gt;创建的）来替换build context。既可以是name也可以是index数字（所有使用FROM指令建立的build stages）。如果通过name找不到build stage，就会去找同名的image。\nCOPY遵循以下规则：\n\u0026lt;src\u0026gt; 必须在build的context 中；不能 COPY ../something /something添加context父目录的东西。因为 docker build 的第一步是把context，目录及其子目录发送到docker daemon。 如果 \u0026lt;src\u0026gt; 是目录，那么整个目录都会被复制，包括文件系统的metadata。（目录本身不复制，只是内容） 如果 \u0026lt;src\u0026gt; 是任何其他文件，就会随同它的metadata一起复制。此时 \u0026lt;dest\u0026gt; 以斜杠 /结尾的话，就会被认为是一个目录，\u0026lt;src\u0026gt;的内容会被写到\u0026lt;dest\u0026gt;/base(\u0026lt;src\u0026gt;)。 如果\u0026lt;src\u0026gt;定义的是多个资源，不论是直接还是通配符匹配到的， \u0026lt;dest\u0026gt; 必须是一个目录，且以斜杠/结尾。 如果 \u0026lt;dest\u0026gt; 不以斜杠结尾，那么就会被认为是一个普通文件，那么\u0026lt;src\u0026gt; 会被写到\u0026lt;dest\u0026gt;。 如果 \u0026lt;dest\u0026gt; 不存在，那么path中的所有未创建的目录都会自动创建。 如果src内容改变了，在第一次遇到COPY指令后，会禁用后续所有指令的缓存，包括RUN指令的缓存。\nENTRYPOINT exec 格式\nENTRYPOINT [\u0026#34;executable\u0026#34;, \u0026#34;param1\u0026#34;, \u0026#34;param2\u0026#34;] shell 格式\nENTRYPOINT command param1 param2 ENTRYPOINT用来配置container作为可执行文件来运行。\n示例，使用默认内容启动nginx，监听80端口，\n$ docker run -i -t --rm -p 80:80 nginx docker run \u0026lt;image\u0026gt;的命令行参数，会被添加到exec格式中的所有元素之后，并覆盖CMD指令定义的元素。这样就可以把参数传递给entry point，也就是docker run \u0026lt;image\u0026gt; -d会把-d传递给entry point。可以使用docker run --entrypoint来覆盖ENTRYPOINT指令（但是只能把binary设置为exec，不能用sh -c）。\nshell格式会禁用掉CMD或者run命令行参数，但是有个缺点就是，ENTRYPOINT就不是作为/bin/sh -c的子命令来启动的了，也就是不能传递signals。也就意味着可执行文件，不是container的PID 1，也不会接收Unix signals（一种软件中断）。这样可执行文件就不会接收来自docker stop \u0026lt;container\u0026gt;的SIGTERM。\n只有Dockerfile的最后一个ENTRYPOINT才会生效。\nENTRYPOINT Exec示例 FROM ubuntu ENTRYPOINT [\u0026#34;top\u0026#34;, \u0026#34;-b\u0026#34;] CMD [\u0026#34;-c\u0026#34;] 当运行container，top是唯一进程，\n$ docker run -it --rm --name test top -H top - 08:25:00 up 7:27, 0 users, load average: 0.00, 0.01, 0.05 Threads: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem: 2056668 total, 1616832 used, 439836 free, 99352 buffers KiB Swap: 1441840 total, 0 used, 1441840 free. 1324440 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 root 20 0 19744 2336 2080 R 0.0 0.1 0:00.04 top 为了验证更多结果，使用docker exec，\n$ docker exec -it test ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 2.6 0.1 19752 2352 ? Ss+ 08:24 0:00 top -b -H root 7 0.0 0.1 15572 2164 ? R+ 08:25 0:00 ps aux top -b -H，其中top -b是ENTRYPOINT设置的，-H是docker命令行参数，添加到了ENTRYPOINT后面，覆盖了CMD的-c。\n然后可以优雅地使用docker stop test请求top shut down。\n示例，使用ENTRYPOINT在前台运行Apache（也就是PID 1），\nFROM debian:stable RUN apt-get update \u0026amp;\u0026amp; apt-get install -y --force-yes apache2 EXPOSE 80 443 VOLUME [\u0026#34;/var/www\u0026#34;, \u0026#34;/var/log/apache2\u0026#34;, \u0026#34;/etc/apache2\u0026#34;] ENTRYPOINT [\u0026#34;/usr/sbin/apache2ctl\u0026#34;, \u0026#34;-D\u0026#34;, \u0026#34;FOREGROUND\u0026#34;] 如果想编写单个可执行文件的启动脚本，可以使用exec和gosu命令，来确保可执行文件能够接收到Unix signals。\n#!/usr/bin/env bash set -e if [ \u0026#34;$1\u0026#34; = \u0026#39;postgres\u0026#39; ]; then chown -R postgres \u0026#34;$PGDATA\u0026#34; if [ -z \u0026#34;$(ls -A \u0026#34;$PGDATA\u0026#34;)\u0026#34; ]; then gosu postgres initdb fi exec gosu postgres \u0026#34;$@\u0026#34; fi exec \u0026#34;$@\u0026#34; 最后，如果在shutdown的时候需要做一些额外的清理（或者和其他containers交互），或者是多个协调而不是单个可执行文件，就可能需要确保ENTRYPOINT脚本能够接收Unix signals，传递，然后做更多工作，\n#!/bin/sh ## Note: I\u0026#39;ve written this using sh so it works in the busybox container too ## USE the trap if you need to also do manual cleanup after the service is stopped, ## or need to start multiple services in the one container trap \u0026#34;echo TRAPed signal\u0026#34; HUP INT QUIT TERM ## start service in background here /usr/sbin/apachectl start echo \u0026#34;[hit enter key to exit] or run \u0026#39;docker stop \u0026lt;container\u0026gt;\u0026#39;\u0026#34; read ## stop service and clean up here echo \u0026#34;stopping apache\u0026#34; /usr/sbin/apachectl stop echo \u0026#34;exited $0\u0026#34; 如果使用docker run -it --rm -p 80:80 --name test apache来运行这个image，那么就可以使用docker exec或docker top来验证container处理，然后使用脚本停止Apache，\n$ docker exec -it test ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.1 0.0 4448 692 ? Ss+ 00:42 0:00 /bin/sh /run.sh 123 cmd cmd2 root 19 0.0 0.2 71304 4440 ? Ss 00:42 0:00 /usr/sbin/apache2 -k start www-data 20 0.2 0.2 360468 6004 ? Sl 00:42 0:00 /usr/sbin/apache2 -k start www-data 21 0.2 0.2 360468 6000 ? Sl 00:42 0:00 /usr/sbin/apache2 -k start root 81 0.0 0.1 15572 2140 ? R+ 00:44 0:00 ps aux $ docker top test PID USER COMMAND 10035 root {run.sh} /bin/sh /run.sh 123 cmd cmd2 10054 root /usr/sbin/apache2 -k start 10055 33 /usr/sbin/apache2 -k start 10056 33 /usr/sbin/apache2 -k start $ /usr/bin/time docker stop test test real\t0m 0.27s user\t0m 0.03s sys\t0m 0.03s shell格式会调用command shell，而exec格式不会，所以exec中$HOME是没用的，要用的话直接执行shell RUN [ \u0026quot;sh\u0026quot;, \u0026quot;-c\u0026quot;, \u0026quot;echo $HOME\u0026quot; ]。\n注意，exec格式被解析为JSON数组，所以只能用双引号。还需注意反斜杠。\nENTRYPOINT Shell示例 ENTRYPOINT定义一个简单的string，然后它就会在/bin/sh -c中执行。shell格式使用shell processing来替代shell environment variables，然后会忽略任何CMD或docker run命令行参数。为了确保docker stop能直接signal任何运行的ENTRYPOINT可执行文件，记住使用exec开始，\nFROM ubuntu ENTRYPOINT exec top -b 运行这个image时，你会看到单个PID 1进程，\n$ docker run -it --rm --name test top Mem: 1704520K used, 352148K free, 0K shrd, 0K buff, 140368121167873K cached CPU: 5% usr 0% sys 0% nic 94% idle 0% io 0% irq 0% sirq Load average: 0.08 0.03 0.05 2/98 6 PID PPID USER STAT VSZ %VSZ %CPU COMMAND 1 0 root R 3164 0% 0% top -b 执行docker stop，也会干净的退出，\n$ /usr/bin/time docker stop test test real\t0m 0.20s user\t0m 0.02s sys\t0m 0.04s 如果忘了在ENTRYPOINT前添加exec，\nFROM ubuntu ENTRYPOINT top -b CMD --ignored-param1 运行（为下一步设置一个name），\n$ docker run -it --name test top --ignored-param2 Mem: 1704184K used, 352484K free, 0K shrd, 0K buff, 140621524238337K cached CPU: 9% usr 2% sys 0% nic 88% idle 0% io 0% irq 0% sirq Load average: 0.01 0.02 0.05 2/101 7 PID PPID USER STAT VSZ %VSZ %CPU COMMAND 1 0 root S 3168 0% 0% /bin/sh -c top -b cmd cmd2 7 1 root R 3164 0% 0% top -b 你就会看到ENTRYPOINT定义的top不是PID 1。\n如果执行docker stop test，container就不会干净地退出。stop命令会在超时后被强制发送一个SIGKILL，\n$ docker exec -it test ps aux PID USER COMMAND 1 root /bin/sh -c top -b cmd cmd2 7 root top -b 8 root ps aux $ /usr/bin/time docker stop test test real\t0m 10.19s user\t0m 0.04s sys\t0m 0.03s real 10.19s超时。\nCMD和ENTRYPOINT如何结合使用 CMD和ENTRYPOINT指令都定义了运行container时，哪些命令会执行。他们的结合有一些规则，\nDockerfile应该定义至少一个CMD 或ENTRYPOINT 。 如果使用container作为可执行文件，应该定义ENTRYPOINT 。 如果需要给ENTRYPOINT 定义默认参数，或者在container中执行ad-hoc（临时）命令，应该使用CMD。 以可选参数运行container时会覆盖CMD 。 下面这个表格展示了CMD和ENTRYPOINT指令的不同组合\nNo ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”] No CMD error, not allowed /bin/sh -c exec_entry p1_entry exec_entry p1_entry CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry exec_cmd p1_cmd CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry p1_cmd p2_cmd CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd 注意，如果CMD是从base image定义的，那么设置ENTRYPOINT会重置CMD为空值。此时如果要使用CMD，必须在当前image重新定义。\nVOLUME VOLUME [\u0026#34;/data\u0026#34;] VOLUME指令用来创建挂载点，把container挂载到native host（宿主机）或其他container。\nvalue可以是JSON array，如VOLUME [\u0026quot;/var/log/\u0026quot;]，也可以是string，如VOLUME /var/log或VOLUME /var/log /var/db。\ndocker run命令会用base image中定义的location中存在的任何数据，来初始化新创建的volumn。\n示例，\nFROM ubuntu RUN mkdir /myvol RUN echo \u0026#34;hello world\u0026#34; \u0026gt; /myvol/greeting VOLUME /myvol docker run会在/myvol创建一个挂载点，然后把greeting复制到新创建的volumn。\n遵循规则，\n**基于Windows的containers **: volumn的目标路径必须是以下之一: 不存在的或者空的目录 除 C:以外的驱动 在Dockerfile里面修改volumn: 在volumn已经被声明之后的任何build steps尝试修改volumn数据，都会被忽略。 JSON formatting: 要用双引号，不要用单引号. 在container run-time才会声明主机目录（挂载点）: 挂载点是依赖主机的。因为主机目录不能保证对所有主机都是有用的，为了保证image的可移植性，不能在Dockerfile中挂载主机目录，而是必须在创建或运行container的时候。VOLUME指令也不支持host-dir这样的参数。 USER USER \u0026lt;user\u0026gt;[:\u0026lt;group\u0026gt;] 或\nUSER \u0026lt;UID\u0026gt;[:\u0026lt;GID\u0026gt;] USER指令用于RUN, CMD 和ENTRYPOINT指令执行时指定user name / group。USER指令可以设置user name（或UID），可选用user group（或GID）。\n如果定义了user group，那么这个user就只有这个group的membership，任何其他配置的group memberships都会被忽略。\n如果user没有primary group，那么image（或者下一条指令）就会以root group运行。\n在Windows，如果不是内建账号，必须先创建。可以在Dockerfile中调用net user命令，\nFROM microsoft/windowsservercore ## Create Windows user in the container RUN net user /add patrick ## Set it for subsequent commands USER patrick WORKDIR WORKDIR /path/to/workdir WORKDIR为RUN, CMD, ENTRYPOINT, COPY and ADD指令设置工作目录。\n如果WORKDIR不存在，即使后面的Dockerfile不会用到，它仍然会被创建。\nWORKDIR指令可以在Dockerfile中定义多次。如果是相对路径，那么就是相对于上一条WORKDIR指令的路径。\n示例，\nWORKDIR /a WORKDIR b WORKDIR c RUN pwd pwd的结果是/a/b/c。\nWORKDIR可以引用ENV定义的环境变量，示例，\nENV DIRPATH /path WORKDIR $DIRPATH/$DIRNAME RUN pwd pwd的结果是/path/$DIRNAME。\nARG ARG \u0026lt;name\u0026gt;[=\u0026lt;default value\u0026gt;] ARG指令定义变量，用户可以在使用docker build命令带参数--build-arg \u0026lt;varname\u0026gt;=\u0026lt;value\u0026gt;，在build-time传递这个变量给builder。如果用户指定了一个build参数而没有在Dockerfile中定义，build会报warning，\n[Warning] One or more build-args [foo] were not consumed. 一个Dockerfile可以包含一个或多个ARG指令。\n示例，\nFROM busybox ARG user1 ARG buildno ## ... 警告！不建议使用build-time变量来传递私密数据，如github keys，用户认证信息等。因为image的任何用户都可以使用docker history查看build-time变量。\n默认值 ARG指令可以设置默认值（可选），\nFROM busybox ARG user1=someuser ARG buildno=1 ## ... 如果ARG指令有默认值，在build-time没有值传递，那么builder会用这个默认值。\n范围 ARG指令是在它被定义那一行生效的，而不是命令行被使用的时候，或者其他地方。\n示例，\nFROM busybox USER ${user:-some_user} ARG user USER $user ## ... 用户build这个文件，调用，\n$ docker build --build-arg user=what_user . 第2行的USER结果为some_user因为user变量是在第3行定义的。\n第4行的USER结果为what_user，因为user变量已经被定义了，在命令行传递了what_user值。\n在ARG指令定义之前，任何变量使用结果都是空string。\n在ARG定义的build stage结束时，ARG指令就超出范围了。为了在多个stages使用同一个arg，每个stage都必须包括ARG指令，\nFROM busybox ARG SETTINGS RUN ./run/setup $SETTINGS FROM busybox ARG SETTINGS RUN ./run/other $SETTINGS 使用ARG变量 可以使用ARG或ENV指令来为RUN指令定义变量。ENV定义的环境变量始终都会覆盖ARG定义的同名变量。\n示例，\nFROM ubuntu ARG CONT_IMG_VER ENV CONT_IMG_VER v1.0.0 RUN echo $CONT_IMG_VER 假设使用这条命令build image，\n$ docker build --build-arg CONT_IMG_VER=v2.0.1 . RUN会使用v1.0.0而不是ARG传递的v2.0.1。这个行为有点类似于shell脚本，一个局部变量会覆盖通过参数传递的变量，或者从环境定义继承的变量。\n还是上面的例子，定义不同的ENV会把ARG和ENV结合的更好用，\nFROM ubuntu ARG CONT_IMG_VER ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0} RUN echo $CONT_IMG_VER 不像ARG，ENV的值会在build image中持久化。如果不用--build-arg build，\n$ docker build . 用这个Dockerfile，CONT_IMG_VER仍然会持久化在这个image，它的值是v1.0.0，因为在第3行用ENV定义了默认值。\n在这个示例中，通过ENV指令，可以把命令行参数传递进来，然后持久化到最终的image，实现了变量扩展。变量扩展只支持Dockerfile指令的一部分指令。\nADD COPY ENV EXPOSE FROM LABEL STOPSIGNAL USER VOLUME WORKDIR ONBUILD (结合以上指令使用) 预定义ARGs Docker有一些预定义的ARG变量，你可以不使用ARG指令，直接用这些变量。\nHTTP_PROXY http_proxy HTTPS_PROXY https_proxy FTP_PROXY ftp_proxy NO_PROXY no_proxy 直接在命令行使用，\n--build-arg \u0026lt;varname\u0026gt;=\u0026lt;value\u0026gt; 默认这些预定义的变量是不会输出到docker history中的。这样可以降低在HTTP_PROXY变量中意外泄露敏感认证信息的风险。\n示例，使用--build-arg HTTP_PROXY=http://user:pass@proxy.lon.example.com来build Dockerfile，\nFROM ubuntu RUN echo \u0026#34;Hello World\u0026#34; HTTP_PROXY变量不会输出到docker history，也不会被缓存。如果代理服务器变成了http://user:pass@proxy.sfo.example.com，后续的build不会导致cache miss。\n可以使用ARG来覆盖这个默认行为，\nFROM ubuntu ARG HTTP_PROXY RUN echo \u0026#34;Hello World\u0026#34; 当build这个Dockerfile的时候，HTTP_PROXY会存到docker history中，如果它的值改变了，会把build缓存禁用掉。\n对缓存的影响 ARG变量并不会像ENV持久化到image，但是会以类似的方式，影响到build缓存。如果Dockerfile定义了一个ARG变量，这个变量和前一个build不一样，那么在第一次用这个变量的时候会发生\u0026quot;cache miss\u0026quot;（不是定义的时候）。尤其是，所有ARG后面的RUN指令一般都会使用ARG变量，这样就会导致cache miss。但是所有预定义ARGs是没有影响cache的，除非是在Dockerfile中有一个同名的ARG指令。\n示例，2个Dockerfile\nFROM ubuntu ARG CONT_IMG_VER RUN echo $CONT_IMG_VER FROM ubuntu ARG CONT_IMG_VER RUN echo hello 如果在命令行指定--build-arg CONT_IMG_VER=\u0026lt;value\u0026gt;，以上2个示例在第2行都不会cache miss，第3行会cache miss。ARG CONT_IMG_VER会导致RUN那一行被认为是执行了CONT_IMG_VER=\u0026lt;value\u0026gt; echo hello，所以如果\u0026lt;value\u0026gt;改变了，就cache miss了。\n另外一个示例，\nFROM ubuntu ARG CONT_IMG_VER ENV CONT_IMG_VER $CONT_IMG_VER RUN echo $CONT_IMG_VER 第3行会发生cache miss。因为ENV引用的ARG变量通过命令行改变了。另外，在这个示例中，ENV会导致image包含这个value（ENV会持久化到image中）。\n如果ENV和ARG指令重复，\nFROM ubuntu ARG CONT_IMG_VER ENV CONT_IMG_VER hello RUN echo $CONT_IMG_VER 第3行就不会发生cache miss，因为CONT_IMG_VER的值是常量（hello）。因此第4行RUN指令用到的环境变量和值在build之间不会改变。\nONBUILD ONBUILD \u0026lt;INSTRUCTION\u0026gt; ONBUILD指令会在image中添加一个trigger，这个trigger会在image作为base的时候触发。trigger会在下游的 build context中执行，就像在下游的Dockerfile 中，在 FROM指令之后，它就已经被立即嵌入了。\n任何build指令都可以注册为trigger。\n如果你build一个image，这个image会作为base来build其他images，这就很有用。比如，一个应用build环境或者一个deamon自定义配置。\n示例，如果一个image是可复用的Python应用builder（用来build新的应用image），那么它需要把应用源码添加到一个特定目录，然后调用build脚本。此时ADD和RUN指令是无法访问应用源码的，每个应用build的源码也可能不一样。你可以简单地，给应用开发者提供Dockerfile样本文件来复制粘贴到他们的应用中，但这是低效、易出错和困难去做更新的，因为这个和“应用定义”代码混淆了。\n可以使用ONBUILD指令来提前注册指令，在下个build stage再运行。\n过程如下，\n当碰到ONBUILD 指令，builder就会添加trigger到正在build的image的metadata。这条指令不会影响当前build。 在build的最后，所有的triggers都会被存储到image的manifest，在key OnBuild下面。可以用 docker inspect 命令查看。 然后image可能会被用来作为新build的base，使用 FROM 指令。 FROM 指令在处理时，下游builder会查找 ONBUILD triggers，然后按它们注册的顺序执行。如果有trigger失败了，FROM指令就会中断，build失败。如果triggers都成功了，那么FROM会完成，build成功。 Triggers会在执行后，从最后一个image中清除。也就是说，它们是不会随着“父子”build继承的。 比如你可能会添加这样的内容，\nONBUILD ADD . /app/src ONBUILD RUN /usr/local/bin/python-build --dir /app/src 注意，1.链式ONBUILD ONBUILD是不允许的。2.ONBUILD可能不会trigger FROM 或 MAINTAINER指令。\nSTOPSIGNAL STOPSIGNAL signal STOPSIGNAL指令设置system call signal，发送到container退出。signal可以是有效的unsigned number（匹配kernel’s syscall table里的position，比如9），也可以是SIGNAME（比如SIGKILL）。\nHEALTHCHECK 2种格式，\nHEALTHCHECK [OPTIONS] CMD command (通过运行container里面的命令来检查container) HEALTHCHECK NONE (禁用健康检查，从base image继承) HEALTHCHECK 指令用来告诉Docker怎样测试container是否还在工作。比如虽然server一直在运行，但是实际上已经死循环了，无法处理新连接了。\n当container定义了健康检查，就会把健康状态添加到status中。status初始化是starting。无论健康检查什么时候通过，它都会变为healthy（无论之前是什么状态）。在一定数量的连续失败后，它会变为unhealthy。\n第一种格式的OPTION可以是，\n--interval=DURATION (default: 30s) --timeout=DURATION (default: 30s) --start-period=DURATION (default: 0s) --retries=N (default: 3) 在container开始后的interval seconds ，会运行健康检查。每个健康检查完成后，等待interval seconds再次运行。\n如果健康检查运行的时候超过了timeout seconds，就认为失败。\n失败的次数如果达到了retries的值，就认为unhealthy。\nstart period指定了container需要启动的时间。在这期间探针失败（Probe failure）不会记作重试次数。但是，如果在这期间健康检查通过了，那么container就认为已经启动了，这之后的失败（all consecutive failures）就会记作重试次数。\n一个Dockerfile只能有一个HEALTHCHECK 指令。如果有多个，那么只有最后一个HEALTHCHECK 生效。\n第1种格式的command既可以是shell命令（如，HEALTHCHECK CMD /bin/check-running），也可以是exec数组。\ncommand的退出状态反应了container的健康状态，\n0: success - the container is healthy and ready for use 1: unhealthy - the container is not working correctly 2: reserved - do not use this exit code 示例，每5分钟检查1次，以确保web服务器能在3秒内为网站首页提供服务，\nHEALTHCHECK --interval=5m --timeout=3s \\ CMD curl -f http://localhost/ || exit 1 为了帮助debug失败探针（failing probes），任何写到stdout或stderr输出文本（UTF-8编码）都会被存储到健康状态，并且可以使用docker inspect查询。而且输出应该简短（目前只有最开始的4096 bytes会被存储）。\n当container的健康状态改变了，会用新的状态生成一个health_status事件。\nSHELL SHELL [\u0026#34;executable\u0026#34;, \u0026#34;parameters\u0026#34;] SHELL指令允许重写shell格式命令的默认shell。Linux的默认shell是[\u0026quot;/bin/sh\u0026quot;, \u0026quot;-c\u0026quot;]，Windows的默认shell是[\u0026quot;cmd\u0026quot;, \u0026quot;/S\u0026quot;, \u0026quot;/C\u0026quot;]。SHELL指令必须在Dockfile中写成JSON格式。\nSHELL指令在Windows特别有用，因为Windows有2个常用的不同的原生shell，cmd和powershell，也有可选用的shell，包括sh。\nSHELL指令可以出现多次。每个SHELL指令会覆盖所有之前的SHELL指令，影响随后的指令。\n示例，\nFROM microsoft/windowsservercore ## Executed as cmd /S /C echo default RUN echo default ## Executed as cmd /S /C powershell -command Write-Host default RUN powershell -command Write-Host default ## Executed as powershell -command Write-Host hello SHELL [\u0026#34;powershell\u0026#34;, \u0026#34;-command\u0026#34;] RUN Write-Host hello ## Executed as cmd /S /C echo hello SHELL [\u0026#34;cmd\u0026#34;, \u0026#34;/S\u0026#34;, \u0026#34;/C\u0026#34;] RUN echo hello 当shell格式的RUN，\tCMD，ENTRYPOINT出现在Dcokerfile中时，SHELL指令能影响这些指令。\n示例，Windows上常见的模式，可以通过使用SHELL指令进行简化，\nRUN powershell -command Execute-MyCmdlet -param1 \u0026#34;c:\\foo.txt\u0026#34; docker调用的命令，\ncmd /S /C powershell -command Execute-MyCmdlet -param1 \u0026#34;c:\\foo.txt\u0026#34; 这个有点低效，有2个原因。首先，有一个不必要的cmd.exe命令行处理器（aka shell）被调用了。其次，shell格式的RUN指令需要额外的前缀命令powershell -command。\n为了更高效，有2种机制。其一是使用JSON格式，\nRUN [\u0026#34;powershell\u0026#34;, \u0026#34;-command\u0026#34;, \u0026#34;Execute-MyCmdlet\u0026#34;, \u0026#34;-param1 \\\u0026#34;c:\\\\foo.txt\\\u0026#34;\u0026#34;] JSON格式是清晰的，不会使用不必要的cmd.exe。但是需要双引号和转义符，显得有点冗余。\n。其二是用SHELL指令和shell格式，这样可以给Windows用户更自然的语法，特别是和escape parser directive结合使用的时候，\n## escape=` FROM microsoft/nanoserver SHELL [\u0026#34;powershell\u0026#34;,\u0026#34;-command\u0026#34;] RUN New-Item -ItemType Directory C:\\Example ADD Execute-MyCmdlet.ps1 c:\\example\\ RUN c:\\example\\Execute-MyCmdlet -sample \u0026#39;hello world\u0026#39; 结果是，\nPS E:\\docker\\build\\shell\u0026gt; docker build -t shell . Sending build context to Docker daemon 4.096 kB Step 1/5 : FROM microsoft/nanoserver ---\u0026gt; 22738ff49c6d Step 2/5 : SHELL powershell -command ---\u0026gt; Running in 6fcdb6855ae2 ---\u0026gt; 6331462d4300 Removing intermediate container 6fcdb6855ae2 Step 3/5 : RUN New-Item -ItemType Directory C:\\Example ---\u0026gt; Running in d0eef8386e97 Directory: C:\\ Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 10/28/2016 11:26 AM Example ---\u0026gt; 3f2fbf1395d9 Removing intermediate container d0eef8386e97 Step 4/5 : ADD Execute-MyCmdlet.ps1 c:\\example\\ ---\u0026gt; a955b2621c31 Removing intermediate container b825593d39fc Step 5/5 : RUN c:\\example\\Execute-MyCmdlet \u0026#39;hello world\u0026#39; ---\u0026gt; Running in be6d8e63fe75 hello world ---\u0026gt; 8e559e9bf424 Removing intermediate container be6d8e63fe75 Successfully built 8e559e9bf424 PS E:\\docker\\build\\shell\u0026gt; SHELL指令也能被用来修改shell操作方式。比如在Windows用SHELL cmd /S /C /V:ON|OFF，可以修改延迟环境变量扩展语义。\nSHELL指令也可以用在Linux上，可选的shell有zsh, csh, tcsh等。\nDockerfile示例 ## Nginx # ## VERSION 0.0.1 FROM ubuntu LABEL Description=\u0026#34;This image is used to start the foobar executable\u0026#34; Vendor=\u0026#34;ACME Products\u0026#34; Version=\u0026#34;1.0\u0026#34; RUN apt-get update \u0026amp;\u0026amp; apt-get install -y inotify-tools nginx apache2 openssh-server ## Firefox over VNC # ## VERSION 0.3 FROM ubuntu ## Install vnc, xvfb in order to create a \u0026#39;fake\u0026#39; display and firefox RUN apt-get update \u0026amp;\u0026amp; apt-get install -y x11vnc xvfb firefox RUN mkdir ~/.vnc ## Setup a password RUN x11vnc -storepasswd 1234 ~/.vnc/passwd ## Autostart firefox (might not be the best way, but it does the trick) RUN bash -c \u0026#39;echo \u0026#34;firefox\u0026#34; \u0026gt;\u0026gt; /.bashrc\u0026#39; EXPOSE 5900 CMD [\u0026#34;x11vnc\u0026#34;, \u0026#34;-forever\u0026#34;, \u0026#34;-usepw\u0026#34;, \u0026#34;-create\u0026#34;] ## Multiple images example # ## VERSION 0.1 FROM ubuntu RUN echo foo \u0026gt; bar ## Will output something like ===\u0026gt; 907ad6c2736f FROM ubuntu RUN echo moo \u0026gt; oink ## Will output something like ===\u0026gt; 695d7793cbe4 ## You\u0026#39;ll now have two images, 907ad6c2736f with /bar, and 695d7793cbe4 with ## /oink. 以下内容可查看参考资料进一步阅读。\nBuildKit（第三方工具） Parser directives的命令syntax（依赖BuildKit） RUN已知bug（Issue 783） External implementation features（依赖BuildKit） Automatic platform ARGs in the global scope（依赖BuildKit） 参考资料\nhttps://docs.docker.com/engine/reference/builder/\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/golang/000003-golangdockerfile%E6%96%87%E4%BB%B6%E4%B8%87%E5%AD%97%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90/","summary":"\u003ccode\u003eDockerfile\u003c/code\u003e是一个文本文件，包含一些Docker指令。执行\u003ccode\u003edocker build\u003c/code\u003e，Docker就会执行\u003ccode\u003eDockerfile\u003c/code\u003e里面的指令，来自动创建镜像。","title":"Dockerfile文件万字全面解析"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nFlask有个特点：千人千面。它没有硬性规定，你必须采用哪种项目结构来组织代码，这就导致每个人都按照自己的习惯和喜好来写，写出来的项目结构往往是千差万别。在Flask2.0的官方文档中，有一节内容介绍了Flask的项目结构，我们可以窥探一番。\nMock式结构 Flask是很适合用来做Mock的，比如调试前端代码时，后端服务不一定准备好了，那么就可以先用Flask模拟后端接口返回数据。\nMock式项目结构很简单，创建一个文件夹：\n$ mkdir flask-tutorial $ cd flask-tutorial 然后在这个文件夹下面随便创建一些.py文件，来写Mock代码即可。\n比如创建一个hello.py文件：\nfrom flask import Flask app = Flask(__name__) @app.route(\u0026#39;/hello\u0026#39;) def hello(): return \u0026#39;Hello, World!\u0026#39; 再创建一个order.py文件：\nfrom flask import Flask app = Flask(__name__) @app.route(\u0026#39;/order\u0026#39;) def order(): return {\u0026#34;id\u0026#34;: 123456789} 项目结构如下所示：\n/home/user/Projects/flask-tutorial ├── hello.py ├── order.py Project式结构 Flask最正统的用法是写Web后端服务，相对于Django来说，Flask的项目结构要简洁很多，如下所示：\n/home/user/Projects/flask-tutorial ├── flaskr/ │ ├── __init__.py │ ├── db.py │ ├── schema.sql │ ├── auth.py │ ├── blog.py ├── tests/ │ ├── conftest.py │ ├── data.sql │ ├── test_factory.py │ ├── test_db.py │ ├── test_auth.py │ └── test_blog.py ├── venv/ ├── setup.py └── MANIFEST.in flaskr/ 存放项目主要源文件的包。 tests/ 存放测试代码的目录。(使用pytest框架来写) venv/ Python虚拟环境目录。 setup.py 项目构建信息的描述。 MANIFEST.in 项目包含或排除其他文件夹的说明。 可以看出来官方并没有给出flaskr/更为具体的目录结构设计了，项目规模不同，编程经验不一，自由发挥空间越大，结构差异化就越明显。这跟Flask的设计理念其实是保持一致的，Flask只提供核心功能，不限制你做什么，把选择权交给你自己。\nGit忽略文件 如果采用Git对源代码进行管理，可以配置下面的.gitignore文件：\nvenv/ *.pyc __pycache__/ instance/ .pytest_cache/ .coverage htmlcov/ dist/ build/ *.egg-info/ 把这些文件夹和文件，排除到提交的代码之外。\n参考资料：\nhttps://flask.palletsprojects.com/en/2.0.x/tutorial/layout/\nhttps://blog.csdn.net/cckavin/article/details/97945913\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/flask/000003-flaskflask%E5%AE%98%E6%96%B9%E6%8E%A8%E8%8D%90%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84%E7%AB%9F%E7%84%B6%E6%98%AF%E8%BF%99%E6%A0%B7/","summary":"Flask有个特点：千人千面。它没有硬性规定，你必须采用哪种项目结构来组织代码，这就导致每个人都按照自己的习惯和喜好来写，写出来的项目结构往往是千差万别。在Flask2.0的官方文档中，有一节内容介绍了Flask的项目结构，我们可以窥探一番。","title":"Flask官方推荐项目结构竟然是这样"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n查看本地分支\ngit branch 查看远端分支，无论是否checkout到本地\ngit branch -r 假如想比较dev和master\n命令1：比较文件\ngit diff dev master --stat 命令2：比较提交日志\ngit log dev ^master git log master..dev # since..until，包含了在dev分支而不在master分支中所有的提交，若无结果，说明dev的所有提交都合到了master 这2种等价。\ngit log dev...master dev和master的所有差异，可以加\u0026ndash;left-right参数，指明属于哪个分支的commit。\n命令3：列出合并到master的分支，master \u0026gt;= dev\ngit branch --merged master ","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%9F%A5%E8%AF%86/000003-git%E6%AF%94%E8%BE%83%E5%88%86%E6%94%AF%E5%B7%AE%E5%BC%82%E7%9A%843%E4%B8%AA%E5%91%BD%E4%BB%A4/","summary":"查看本地分支","title":"Git比较分支差异的3个命令"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在PyCharm中打开examples/httpbin/basic_test.py：\n首先映入眼帘的是左上角那个绿色小箭头，点了一下，可以直接运行，意味着HttpRunner是能够直接被pytest驱动运行的，这可就有点意思了，难道HttpRunner的底层是pytest？带着这个疑问我全局搜索了一下pytest：\n在cli.py文件中，如果参数是run，那么会执行pytest.main([\u0026quot;h\u0026quot;])，难道真是我猜测的这样？在basic_test.py最后有两行代码：\nif __name__ == \u0026#34;__main__\u0026#34;: TestCaseBasic().test_start() 试着从这里追踪，应该就能对调用链路拿捏个十拿九稳了。test_start()的源码如下：\ndef test_start(self, param: Dict = None) -\u0026gt; \u0026#34;HttpRunner\u0026#34;: \u0026#34;\u0026#34;\u0026#34;main entrance, discovered by pytest\u0026#34;\u0026#34;\u0026#34; self.__init_tests__() self.__project_meta = self.__project_meta or load_project_meta( self.__config.path ) self.__case_id = self.__case_id or str(uuid.uuid4()) self.__log_path = self.__log_path or os.path.join( self.__project_meta.RootDir, \u0026#34;logs\u0026#34;, f\u0026#34;{self.__case_id}.run.log\u0026#34; ) log_handler = logger.add(self.__log_path, level=\u0026#34;DEBUG\u0026#34;) # parse config name config_variables = self.__config.variables if param: config_variables.update(param) config_variables.update(self.__session_variables) self.__config.name = parse_data( self.__config.name, config_variables, self.__project_meta.functions ) if USE_ALLURE: # update allure report meta allure.dynamic.title(self.__config.name) allure.dynamic.description(f\u0026#34;TestCase ID: {self.__case_id}\u0026#34;) logger.info( f\u0026#34;Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}\u0026#34; ) try: return self.run_testcase( TestCase(config=self.__config, teststeps=self.__teststeps) ) finally: logger.remove(log_handler) logger.info(f\u0026#34;generate testcase log: {self.__log_path}\u0026#34;) 第一行注释就是证明了我的猜想是对的：main entrance, discovered by pytest，主程序入口，会被pytest发现。本文不去探究每行代码是什么意思，重点关注跟pytest相关的运行流程。跟着这段代码：\nreturn self.run_testcase( TestCase(config=self.__config, teststeps=self.__teststeps) ) 继续往下走，调用了self.run_testcase，它的源码如下：\ndef run_testcase(self, testcase: TestCase) -\u0026gt; \u0026#34;HttpRunner\u0026#34;: \u0026#34;\u0026#34;\u0026#34;run specified testcase Examples: \u0026gt;\u0026gt;\u0026gt; testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)]) \u0026gt;\u0026gt;\u0026gt; HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj) \u0026#34;\u0026#34;\u0026#34; self.__config = testcase.config self.__teststeps = testcase.teststeps # prepare self.__project_meta = self.__project_meta or load_project_meta( self.__config.path ) self.__parse_config(self.__config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] self.__session = self.__session or HttpSession() # save extracted variables of teststeps extracted_variables: VariablesMapping = {} # run teststeps for step in self.__teststeps: # override variables # step variables \u0026gt; extracted variables from previous steps step.variables = merge_variables(step.variables, extracted_variables) # step variables \u0026gt; testcase config variables step.variables = merge_variables(step.variables, self.__config.variables) # parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions ) # run step if USE_ALLURE: with allure.step(f\u0026#34;step: {step.name}\u0026#34;): extract_mapping = self.__run_step(step) else: extract_mapping = self.__run_step(step) # save extracted variables to session variables extracted_variables.update(extract_mapping) self.__session_variables.update(extracted_variables) self.__duration = time.time() - self.__start_at return self 跟着这段代码：\n## run step if USE_ALLURE: with allure.step(f\u0026#34;step: {step.name}\u0026#34;): extract_mapping = self.__run_step(step) else: extract_mapping = self.__run_step(step) 继续往下走，self.__run_step的源码如下：\ndef __run_step(self, step: TStep) -\u0026gt; Dict: \u0026#34;\u0026#34;\u0026#34;run teststep, teststep maybe a request or referenced testcase\u0026#34;\u0026#34;\u0026#34; logger.info(f\u0026#34;run step begin: {step.name} \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026#34;) if step.request: step_data = self.__run_step_request(step) elif step.testcase: step_data = self.__run_step_testcase(step) else: raise ParamsError( f\u0026#34;teststep is neither a request nor a referenced testcase: {step.dict()}\u0026#34; ) self.__step_datas.append(step_data) logger.info(f\u0026#34;run step end: {step.name} \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\\n\u0026#34;) return step_data.export_vars 有两个分支：\nif step.request: step_data = self.__run_step_request(step) elif step.testcase: step_data = self.__run_step_testcase(step) self.__run_step_request(step)直接调用的request：\nresp = self.__session.request(method, url, **parsed_request_dict) self.__run_step_testcase(step)直接调用的HttpRunner()：\ncase_result = ( testcase_cls() .with_session(self.__session) .with_case_id(self.__case_id) .with_variables(step_variables) .with_export(step_export) .run() ) **真相只有一个，一定在HttpRunner里面。**HttpRunner是run.py模块里面的一个类：\n刚才看到所有代码，其实都是在runner.py模块的HttpRunner类里面。看看run函数的代码：\ndef run(self) -\u0026gt; \u0026#34;HttpRunner\u0026#34;: \u0026#34;\u0026#34;\u0026#34; run current testcase Examples: \u0026gt;\u0026gt;\u0026gt; TestCaseRequestWithFunctions().run() \u0026#34;\u0026#34;\u0026#34; self.__init_tests__() testcase_obj = TestCase(config=self.__config, teststeps=self.__teststeps) return self.run_testcase(testcase_obj) 又调用了self.run_testcase，循环回去了。\n貌似陷入了死循环，实际上答案已经有了，这不就是递归么？再回头来看刚才这两个分支：\n如果是request，那么就调用self.__session.request(method, url, **parsed_request_dict)，这是递归的终止条件：\n如果是testcase，那么表示这是子用例，那么就递归下去，这是递归的子表达式：\n原来，通过TestCaseBasic().test_start()来执行测试，并没有调pytest，而是直接通过requests发送HTTP请求的，控制台和文件日志也是使用loguru库来自定义输出的。不得不对源码佩服得五体投地。\n回到开头那个问题，为什么还有pytest的相关代码呢，实际上如果是通过命令行的run来执行用例，那么就是用直接用的pytest了：\n一句话总结：如果是用命令行的run命令，那么就是通过pytest来调用的；如果是用代码里的test_start()方法，那么就是调requests作者自创的。\n最后一个问题是，为什么在PyCharm中点那个绿色的小箭头，也能运行代码呢，答案很简单，这个类TestCaseBasic是Test开头的，这个方法test_start是test_开头的，这不就是pytest的规则么。\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/httprunner/000003-httprunnerhttprunner3%E7%9A%84%E7%94%A8%E4%BE%8B%E6%98%AF%E6%80%8E%E4%B9%88%E8%BF%90%E8%A1%8C%E8%B5%B7%E6%9D%A5%E7%9A%84/","summary":"在PyCharm中打开\u003ccode\u003eexamples/httpbin/basic_test.py\u003c/code\u003e：","title":"HttpRunner3的用例是怎么运行起来的"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nServlet版本 Oracle将Java EE（Java SE还自己保留）交给开源组织，Eclipse基金会接手。但Oracle不允许开源组织使用Java名号，所以Jakarta EE名称于2018.02.26应运而生。\n正是因为组织变化，Servlet被割裂为了2个版本，javax.servlet和jakarta.servlet。\njavax.servlet已经停止维护，但它仍然是一个非常有用和重要的技术，特别是在许多现有项目中，学习和使用它将为你提供坚实的Web开发基础。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javax.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 如果希望使用Jakarta EE 9或更高版本的Servlet API，则需要切换到javax.servlet。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;jakarta.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jakarta.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;6.0.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 本文将基于javax.servlet来介绍Servlet，从Usages来看，它仍然是使用最广泛的。\nServlet API Servlet API主要有2个包，javax.servlet包含了servlet和web container使用的接口和类。javax.servlet.http包含了http相关的接口和类。\njavax.servlet接口列表：\nServlet ServletRequest ServletResponse RequestDispatcher ServletConfig ServletContext SingleThreadModel Filter FilterConfig FilterChain ServletRequestListener ServletRequestAttributeListener ServletContextListener ServletContextAttributeListener javax.servlet类列表：\nGenericServlet ServletInputStream ServletOutputStream ServletRequestWrapper ServletResponseWrapper ServletRequestEvent ServletContextEvent ServletRequestAttributeEvent ServletContextAttributeEvent ServletException UnavailableException javax.servlet.http接口列表：\nHttpServletRequest HttpServletResponse HttpSession HttpSessionListener HttpSessionAttributeListener HttpSessionBindingListener HttpSessionActivationListener HttpSessionContext (deprecated now) javax.servlet.http类列表：\nHttpServlet Cookie HttpServletRequestWrapper HttpServletResponseWrapper HttpSessionEvent HttpSessionBindingEvent HttpUtils (deprecated now) Servlet接口 Servlet Interface定义了所有servlet必须具有的行为。主要方法如下：\npublic void init(ServletConfig config) 初始化，只会被web container调用1次\npublic void service(ServletRequest request, ServletResponse response) 接收请求，返回响应，每次请求都会调用1次\npublic void destroy() 销毁，只会被web container调用1次\npublic ServletConfig getServletConfig() Servlet配置\npublic String getServletInfo() Servlet信息\n以下是代码示例：\nimport java.io.*; import javax.servlet.*; public class First implements Servlet { ServletConfig config = null; /** * 初始化 * @param config */ public void init(ServletConfig config) { this.config = config; System.out.println(\u0026#34;servlet is initialized\u0026#34;); } /** * 服务 * @param req * @param res * @throws IOException * @throws ServletException */ public void service(ServletRequest req, ServletResponse res) throws IOException, ServletException { res.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = res.getWriter(); out.print(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;b\u0026gt;hello simple servlet\u0026lt;/b\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); } /** * 销毁 */ public void destroy() { System.out.println(\u0026#34;servlet is destroyed\u0026#34;); } /** * 配置 * @return */ public ServletConfig getServletConfig() { return config; } /** * 信息 * @return */ public String getServletInfo() { return \u0026#34;copyright 2007-1010\u0026#34;; } } GenericServlet类 GenericServlet是个抽象类，实现了Servlet, ServletConfig, Serializable接口，能处理任何请求，支持任何协议。主要方法如下：\npublic void init(ServletConfig config) 初始化 public abstract void service(ServletRequest request, ServletResponse response) 接收请求，返回响应，每次请求都会调用1次 public void destroy() 销毁，只会被web container调用1次 public ServletConfig getServletConfig() Servlet配置 public String getServletInfo() Servlet信息 public void init() 无参初始化 public ServletContext getServletContext() Servlet上下文 public String getInitParameter(String name) 根据参数name返回value public Enumeration getInitParameterNames() web.xml所有参数 public String getServletName() Servlet名称 public void log(String msg) 记录Servlet日志 public void log(String msg,Throwable t) 记录Servlet日志和异常堆栈 以下是代码示例：\nimport javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.IOException; import java.io.PrintWriter; public class First extends GenericServlet { public void service(ServletRequest req, ServletResponse res) throws IOException, ServletException { res.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = res.getWriter(); out.print(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;b\u0026gt;hello generic servlet\u0026lt;/b\u0026gt;\u0026#34;); out.print(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); } } HttpServlet类 HttpServlet继承了GenericServlet抽象类。主要方法如下：\npublic void service(ServletRequest req,ServletResponse res) dispatches the request to the protected service method by converting the request and response object into http type.（这里结合英文解释比较清楚，转换类型后，调用第2个service）\nprotected void service(HttpServletRequest req, HttpServletResponse res) receives the request from the service method, and dispatches the request to the doXXX() method depending on the incoming http request type.（这里结合英文解释比较清楚，根据不同method，调用doXXX()方法）\nprotected void doGet(HttpServletRequest req, HttpServletResponse res) 处理GET请求，web container调用\nprotected void doPost(HttpServletRequest req, HttpServletResponse res) 处理POST请求，web container调用\nprotected void doHead(HttpServletRequest req, HttpServletResponse res) 处理HEAD请求，web container调用\nprotected void doOptions(HttpServletRequest req, HttpServletResponse res) 处理OPTIONS请求，web container调用\nprotected void doPut(HttpServletRequest req, HttpServletResponse res) 处理PUT请求，web container调用\nprotected void doTrace(HttpServletRequest req, HttpServletResponse res) 处理TRACE请求，web container调用\nprotected void doDelete(HttpServletRequest req, HttpServletResponse res) 处理DELETE请求，web container调用\nprotected long getLastModified(HttpServletRequest req) 上次修改时间\n如果想深入学习Servlet API，可以在Maven pom.xml引入servlet包依赖，External Libraries查看源码。\n参考资料：\nhttps://www.javatpoint.com/servlet-api\nhttps://www.javatpoint.com/Servlet-interface\nhttps://www.javatpoint.com/GenericServlet-class\nhttps://www.javatpoint.com/HttpServlet-class\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000003-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servletapi/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"servlet版本\"\u003eServlet版本\u003c/h2\u003e\n\u003cp\u003eOracle将Java EE（Java SE还自己保留）交给开源组织，Eclipse基金会接手。但Oracle不允许开源组织使用Java名号，所以Jakarta EE名称于2018.02.26应运而生。\u003c/p\u003e\n\u003cp\u003e正是因为组织变化，Servlet被割裂为了2个版本，\u003ccode\u003ejavax.servlet\u003c/code\u003e和\u003ccode\u003ejakarta.servlet\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ejavax.servlet\u003c/code\u003e已经停止维护，但它仍然是一个非常有用和重要的技术，特别是在许多现有项目中，学习和使用它将为你提供坚实的Web开发基础。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bae546f085f64d15ad93fb20a327546b.png\"\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ejavax.servlet\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003ejavax.servlet-api\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e3.1.0\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eprovided\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e如果希望使用Jakarta EE 9或更高版本的Servlet API，则需要切换到\u003ccode\u003ejavax.servlet\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e0c02834955f426aa0322d5fe2f96f71.png\"\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ejakarta.servlet\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003ejakarta.servlet-api\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e6.0.0\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eprovided\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e本文将基于\u003ccode\u003ejavax.servlet\u003c/code\u003e来介绍Servlet，从Usages来看，它仍然是使用最广泛的。\u003c/p\u003e\n\u003ch2 id=\"servlet-api\"\u003eServlet API\u003c/h2\u003e\n\u003cp\u003eServlet API主要有2个包，\u003ccode\u003ejavax.servlet\u003c/code\u003e包含了servlet和web container使用的接口和类。\u003ccode\u003ejavax.servlet.http\u003c/code\u003e包含了http相关的接口和类。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ejavax.servlet\u003c/code\u003e接口列表：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eServlet\u003c/li\u003e\n\u003cli\u003eServletRequest\u003c/li\u003e\n\u003cli\u003eServletResponse\u003c/li\u003e\n\u003cli\u003eRequestDispatcher\u003c/li\u003e\n\u003cli\u003eServletConfig\u003c/li\u003e\n\u003cli\u003eServletContext\u003c/li\u003e\n\u003cli\u003eSingleThreadModel\u003c/li\u003e\n\u003cli\u003eFilter\u003c/li\u003e\n\u003cli\u003eFilterConfig\u003c/li\u003e\n\u003cli\u003eFilterChain\u003c/li\u003e\n\u003cli\u003eServletRequestListener\u003c/li\u003e\n\u003cli\u003eServletRequestAttributeListener\u003c/li\u003e\n\u003cli\u003eServletContextListener\u003c/li\u003e\n\u003cli\u003eServletContextAttributeListener\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003ccode\u003ejavax.servlet\u003c/code\u003e类列表：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eGenericServlet\u003c/li\u003e\n\u003cli\u003eServletInputStream\u003c/li\u003e\n\u003cli\u003eServletOutputStream\u003c/li\u003e\n\u003cli\u003eServletRequestWrapper\u003c/li\u003e\n\u003cli\u003eServletResponseWrapper\u003c/li\u003e\n\u003cli\u003eServletRequestEvent\u003c/li\u003e\n\u003cli\u003eServletContextEvent\u003c/li\u003e\n\u003cli\u003eServletRequestAttributeEvent\u003c/li\u003e\n\u003cli\u003eServletContextAttributeEvent\u003c/li\u003e\n\u003cli\u003eServletException\u003c/li\u003e\n\u003cli\u003eUnavailableException\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003ccode\u003ejavax.servlet.http\u003c/code\u003e接口列表：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eHttpServletRequest\u003c/li\u003e\n\u003cli\u003eHttpServletResponse\u003c/li\u003e\n\u003cli\u003eHttpSession\u003c/li\u003e\n\u003cli\u003eHttpSessionListener\u003c/li\u003e\n\u003cli\u003eHttpSessionAttributeListener\u003c/li\u003e\n\u003cli\u003eHttpSessionBindingListener\u003c/li\u003e\n\u003cli\u003eHttpSessionActivationListener\u003c/li\u003e\n\u003cli\u003eHttpSessionContext (deprecated now)\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003ccode\u003ejavax.servlet.http\u003c/code\u003e类列表：\u003c/p\u003e","title":"JavaWeb开发基础ServletAPI"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter的界面虽然不华丽，但是使用起来还是很便利。本文将基于英文版界面对整个可视区域进行介绍，如果想看中文版，请在菜单栏点击Choose Language切换：\n启动后的界面分为3个区域：\n①是目录树，②是元件编辑区，③是工具栏。\n目录树 目录树，是个目录，树状结构，包括父节点和子节点。根节点是测试计划，可以添加子节点：线程组、配置元素、监听器、定时器、前置处理器、后置处理器、断言、Test Fragment ：\n其中，线程组是必须添加的！为什么JMeter不给默认加上？因为线程组不止一种。\n线程组的子节点，可以是取样器、逻辑控制器、前置处理器、后置处理器、断言、定时器、Test Fragment、配置元素、监听器：\n提示：取样器和逻辑控制器只能在线程组中添加，不能在测试计划中添加。\n在运行时，目录树会从上往下顺序执行。\n编辑区 编辑区跟目录树的节点是关联的，不同的节点，编辑区会显示相应节点的可编辑内容。\n这一部分会随着系列文章的更新，在涉及到某个组件如何使用的时候，再进行说明。\n工具栏 JMeter5.3版本的工具栏一共有24个按钮或图标：\n一一来看看：\n①新建测试计划。\n②基于模板创建测试计划：\nJMeter提供了常见场景的压测脚本，可以选择模板快速创建，比如Web测试。\n③打开.jmx脚本文件。\n④把当前测试计划保存为.jmx脚本文件。\n⑤删除目录树中的节点，如果选中的是父节点，那么它子节点也会一同被删除。\n⑥复制元件。\n⑦粘贴元件。\n⑧展开目录树。\n⑨收起目录树。\n⑩禁用或启用元件，如果是目录树中的父节点，那么其子节点也会一同被禁用。在调试时可以用这个功能！\n⑪运行测试计划。\n⑫运行测试计划，忽略定时器。\n⑬停止，直接把所有线程停掉，类似于“杀进程”。\n⑭关闭，等当前线程执行完成后结束线程。\n停止和关闭在点击后会弹出相同的对话框，从实际效果来看，停止比关闭的结束速度更快：\n⑮清除响应数据，比如察看结果树、聚合报告，但不会清除日志控制台。\n⑯清除全部数据，包括日志控制台。\n⑰查找对话框，支持正则：\n⑱清除查找对话框输入内容。\n⑲函数助手对话框：\n⑳帮助文档。\n21执行耗时，以秒为单位。\n22错误日志个数，点击后会打开日志控制台。\n23正在运行线程数/总线程数。\n24运行状态，运行测试计划后变绿色。\n菜单栏 菜单栏除了工具栏那些功能，还有更丰富的其他功能。\n文件 编辑 不同元件，编辑菜单内容略有不同。以下是线程组的编辑菜单：\n查找 运行 选项 工具 元件使用场景 打开JMeter后，一般会按照以下步骤在目录树中添加元件：\n添加线程组，用线程模拟用户，产生大量负载。 添加取样器，发起并发请求。 添加监听器，查看请求响应，测试结果。 添加断言，验证结果正确性。 如果取样器需要参数化，通过配置元件（自定义变量、CSV、函数）或前置处理器（用户参数）来完成。\n如果需要做关联，通过后置处理器（如正则表达式提取）来完成。\n关联：从上一条请求中获取数据，在下一条请求中使用。\n如果需要设置场景，比如模拟多少用户，运行多少时间，压测策略如何，可以修改线程组编辑区内容。\n如果需要同一时刻发送请求加压，可以使用同步定时器。\n如果需要控制业务的执行逻辑，可以使用逻辑控制器。\n小结 本文基于JMeter5.3英文版对整体界面进行了介绍，元素很简洁，功能很丰富。如果不适应英文版，那么也可以在菜单栏中切换到中文版使用。最后给出了元件常见使用场景，包括添加步骤、参数化、关联、压测场景设置、同一时刻发送请求、控制业务逻辑。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000003-jmeterjmeter%E8%8B%B1%E6%96%87%E7%89%88%E7%95%8C%E9%9D%A2%E4%BB%8B%E7%BB%8D/","summary":"JMeter的界面虽然不华丽，但是使用起来还是很便利。本文将基于英文版界面对整个可视区域进行介绍，如果想看中文版，请在菜单栏点击Choose Language切换：","title":"JMeter英文版界面介绍"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJUnit5的测试不是通过名称，而是通过注解来标识的。\n测试类与方法 Test Class：测试类，必须包含至少一个test方法，包括：\n最外层的class static member class @Nested class Test Method：测试方法，包括：\n@Test @RepeatedTest @ParameterizedTest @TestFactory @TestTemplate Lifecycle Method：生命周期方法，包括：\n@BeforeAll @AfterAll @BeforeEach @AfterEach 注意：\nTest Method和Lifecycle Method不能是abstract，也不能return。它们可以在当前测试类中声明，也可以继承自父类或接口。 Test class、Test Method和Lifecycle Method都不能是private。 示例代码：\nimport static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class StandardTests { @BeforeAll static void initAll() { } @BeforeEach void init() { } @Test void succeedingTest() { } @Test void failingTest() { fail(\u0026#34;a failing test\u0026#34;); } @Test @Disabled(\u0026#34;for demonstration purposes\u0026#34;) void skippedTest() { // not executed } @Test void abortedTest() { assumeTrue(\u0026#34;abc\u0026#34;.contains(\u0026#34;Z\u0026#34;)); fail(\u0026#34;test should have been aborted\u0026#34;); } @AfterEach void tearDown() { } @AfterAll static void tearDownAll() { } } 自定义显示名字 Test class和test method可以使用@DisplayName自定义在测试报告中的显示名字，支持空格、特殊字符和emoji表情符号。\n示例：\nimport org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName(\u0026#34;A special test case\u0026#34;) class DisplayNameDemo { @Test @DisplayName(\u0026#34;Custom test name containing spaces\u0026#34;) void testWithDisplayNameContainingSpaces() { } @Test @DisplayName(\u0026#34;╯°□°）╯\u0026#34;) void testWithDisplayNameContainingSpecialCharacters() { } @Test @DisplayName(\u0026#34;😱\u0026#34;) void testWithDisplayNameContainingEmoji() { } } 除了@DisplayName，@DisplayNameGeneration 注解能用来对显示名字做统一处理，JUnit Jupiter自带了一些生成器：\nStandard 匹配标准行为 Simple 删除没有参数的方法后面的括号 ReplaceUnderscores 用空格替换下划线 IndicativeSentences 把test class和test method名字连接起来 示例代码：\nimport org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.IndicativeSentencesGeneration; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class DisplayNameGeneratorDemo { @Nested @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_not_supported { @Test void if_it_is_zero() { } @DisplayName(\u0026#34;A negative value for year is not supported by the leap year computation.\u0026#34;) @ParameterizedTest(name = \u0026#34;For example, year {0} is not supported.\u0026#34;) @ValueSource(ints = { -1, -4 }) void if_it_is_negative(int year) { } } @Nested @IndicativeSentencesGeneration(separator = \u0026#34; -\u0026gt; \u0026#34;, generator = DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_a_leap_year { @Test void if_it_is_divisible_by_4_but_not_by_100() { } @ParameterizedTest(name = \u0026#34;Year {0} is a leap year.\u0026#34;) @ValueSource(ints = { 2016, 2020, 2048 }) void if_it_is_one_of_the_following_years(int year) { } } } @IndicativeSentencesGeneration可以自定义separator和generator。\n结果：\n+-- DisplayNameGeneratorDemo [OK] +-- A year is not supported [OK] | +-- A negative value for year is not supported by the leap year computation. [OK] | | +-- For example, year -1 is not supported. [OK] | | \u0026#39;-- For example, year -4 is not supported. [OK] | \u0026#39;-- if it is zero() [OK] \u0026#39;-- A year is a leap year [OK] +-- A year is a leap year -\u0026gt; if it is divisible by 4 but not by 100. [OK] \u0026#39;-- A year is a leap year -\u0026gt; if it is one of the following years. [OK] +-- Year 2016 is a leap year. [OK] +-- Year 2020 is a leap year. [OK] \u0026#39;-- Year 2048 is a leap year. [OK] 除了注解，也能通过配置设定全局的默认Generator，比如在 src/test/resources/junit-platform.properties文件中：\njunit.jupiter.displayname.generator.default = \\ org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores 既可以指定现有Generator，也可以指定实现了DisplayNameGenerator接口的类。\n自定义显示名字的优先级是：\n@DisplayName @DisplayNameGeneration junit.jupiter.displayname.generator.default org.junit.jupiter.api.DisplayNameGenerator.Standard 断言（Assertions） JUnit5的断言是包含在org.junit.jupiter.api.Assertions中的静态方法，比如assertTrue、assertEquals、assertNotNull、assertAll、assertThrows、assertTimeout、assertTimeoutPreemptively等。\n示例代码如下：\nimport static java.time.Duration.ofMillis; import static java.time.Duration.ofMinutes; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.CountDownLatch; import example.domain.Person; import example.util.Calculator; import org.junit.jupiter.api.Test; class AssertionsDemo { private final Calculator calculator = new Calculator(); private final Person person = new Person(\u0026#34;Jane\u0026#34;, \u0026#34;Doe\u0026#34;); @Test void standardAssertions() { assertEquals(2, calculator.add(1, 1)); assertEquals(4, calculator.multiply(2, 2), \u0026#34;The optional failure message is now the last parameter\u0026#34;); assertTrue(\u0026#39;a\u0026#39; \u0026lt; \u0026#39;b\u0026#39;, () -\u0026gt; \u0026#34;Assertion messages can be lazily evaluated -- \u0026#34; + \u0026#34;to avoid constructing complex messages unnecessarily.\u0026#34;); } @Test void groupedAssertions() { // In a grouped assertion all assertions are executed, and all // failures will be reported together. assertAll(\u0026#34;person\u0026#34;, () -\u0026gt; assertEquals(\u0026#34;Jane\u0026#34;, person.getFirstName()), () -\u0026gt; assertEquals(\u0026#34;Doe\u0026#34;, person.getLastName()) ); } @Test void dependentAssertions() { // Within a code block, if an assertion fails the // subsequent code in the same block will be skipped. assertAll(\u0026#34;properties\u0026#34;, () -\u0026gt; { String firstName = person.getFirstName(); assertNotNull(firstName); // Executed only if the previous assertion is valid. assertAll(\u0026#34;first name\u0026#34;, () -\u0026gt; assertTrue(firstName.startsWith(\u0026#34;J\u0026#34;)), () -\u0026gt; assertTrue(firstName.endsWith(\u0026#34;e\u0026#34;)) ); }, () -\u0026gt; { // Grouped assertion, so processed independently // of results of first name assertions. String lastName = person.getLastName(); assertNotNull(lastName); // Executed only if the previous assertion is valid. assertAll(\u0026#34;last name\u0026#34;, () -\u0026gt; assertTrue(lastName.startsWith(\u0026#34;D\u0026#34;)), () -\u0026gt; assertTrue(lastName.endsWith(\u0026#34;e\u0026#34;)) ); } ); } @Test void exceptionTesting() { Exception exception = assertThrows(ArithmeticException.class, () -\u0026gt; calculator.divide(1, 0)); assertEquals(\u0026#34;/ by zero\u0026#34;, exception.getMessage()); } @Test void timeoutNotExceeded() { // The following assertion succeeds. assertTimeout(ofMinutes(2), () -\u0026gt; { // Perform task that takes less than 2 minutes. }); } @Test void timeoutNotExceededWithResult() { // The following assertion succeeds, and returns the supplied object. String actualResult = assertTimeout(ofMinutes(2), () -\u0026gt; { return \u0026#34;a result\u0026#34;; }); assertEquals(\u0026#34;a result\u0026#34;, actualResult); } @Test void timeoutNotExceededWithMethod() { // The following assertion invokes a method reference and returns an object. String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting); assertEquals(\u0026#34;Hello, World!\u0026#34;, actualGreeting); } @Test void timeoutExceeded() { // The following assertion fails with an error message similar to: // execution exceeded timeout of 10 ms by 91 ms assertTimeout(ofMillis(10), () -\u0026gt; { // Simulate task that takes more than 10 ms. Thread.sleep(100); }); } @Test void timeoutExceededWithPreemptiveTermination() { // The following assertion fails with an error message similar to: // execution timed out after 10 ms assertTimeoutPreemptively(ofMillis(10), () -\u0026gt; { // Simulate task that takes more than 10 ms. new CountDownLatch(1).await(); }); } private static String greeting() { return \u0026#34;Hello, World!\u0026#34;; } } 假设（Assumptions） JUnit5的断言是包含在org.junit.jupiter.api.Assumptions中的静态方法，比如assumeTrue、assumingThat等。\n示例代码：\nimport static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.Assumptions.assumingThat; import example.util.Calculator; import org.junit.jupiter.api.Test; class AssumptionsDemo { private final Calculator calculator = new Calculator(); @Test void testOnlyOnCiServer() { assumeTrue(\u0026#34;CI\u0026#34;.equals(System.getenv(\u0026#34;ENV\u0026#34;))); // remainder of test } @Test void testOnlyOnDeveloperWorkstation() { assumeTrue(\u0026#34;DEV\u0026#34;.equals(System.getenv(\u0026#34;ENV\u0026#34;)), () -\u0026gt; \u0026#34;Aborting test: not on developer workstation\u0026#34;); // remainder of test } @Test void testInAllEnvironments() { assumingThat(\u0026#34;CI\u0026#34;.equals(System.getenv(\u0026#34;ENV\u0026#34;)), () -\u0026gt; { // perform these assertions only on the CI server assertEquals(2, calculator.divide(4, 2)); }); // perform these assertions in all environments assertEquals(42, calculator.multiply(6, 7)); } } Assertions与Assumptions区别 Assertions如果失败，test会被标记为failed。Assumptions如果失败，test会被标记为ignored，测试不会执行。\n示例：\nimport org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; public class Demo { @Test void assertTest() { assertTrue(false); } @Test void assumeTest() { assumeTrue(false); } } 结果：\n禁用测试 @Disabled能用来禁用test class或test method，建议在括号内填写上禁用理由。\n示例：\nimport org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @Disabled(\u0026#34;Disabled until bug #99 has been fixed\u0026#34;) class DisabledClassDemo { @Test void testWillBeSkipped() { } } import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class DisabledTestsDemo { @Disabled(\u0026#34;Disabled until bug #42 has been resolved\u0026#34;) @Test void testWillBeSkipped() { } @Test void testWillBeExecuted() { } } 小结 本文首先介绍了如何使用测试类与方法，来编写一个JUnit5的基本测试，然后介绍了如何自定义测试报告中的显示名字。使用断言（Assertions）可以把test标记为failed，使用假设（Assumptions）可以把test标记为ignored。最后介绍了如何禁用测试。除了基本测试，JUnit5还能编写带条件的测试。\n参考资料：\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests\nhttps://stackoverflow.com/questions/44628483/assume-vs-assert-in-junit-tests\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000003-junitjunit5%E7%BC%96%E5%86%99%E5%9F%BA%E6%9C%AC%E6%B5%8B%E8%AF%95/","summary":"JUnit5的测试不是通过名称，而是通过注解来标识的。","title":"JUnit5编写基本测试"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n系统架构 前端、后端、pytest均以Docker容器运行服务，单独的容器化执行引擎，项目环境隔离，即用即取，用完即弃，简单，高效。\n前端容器：页面交互，请求后端，展示HTML报告\n后端容器：接收前端请求，启动任务，构建镜像，触发运行pytest，挂载HTML报告\npytest容器：拉取项目代码，指定目录执行，生成HTML报告\n说明：构建镜像目前是在宿主机启动后端服务来执行docker命令的，暂未支持Kubernetes编排。宿主机安装了Docker，启动服务后，可以执行docker命令。如果采用容器部署后端，容器里面不包含Docker，无法构建，个人想法是可以借助K8S来编排，当前版本还未实现\n系统流程 支持2种运行模式配置：容器和本地。\n容器模式：判断是否支持docker，如果支持，构建pytest镜像，在构建时，通过git拉取项目代码，再运行容器，按照指定目录执行pytest，生成测试报告，并将报告文件挂载到后端。如果不支持，降级为本地运行。\n本地模式：模拟容器行为，在本地目录拉取代码，执行pytest，生成测试报告。\n效果展示 任务管理：\n容器模式：\n本地模式：\n平台大改造 pytestx平台更轻、更薄，移除了用例管理、任务关联用例相关功能代码，只保留真正的任务调度功能，backend的requirements.txt解耦，只保留后端依赖，pytest相关依赖转移到tep-project。\n那如何管理用例呢？约定大于配置，我们约定pytest项目已经通过目录维护好了一个稳定的自动化用例集，也就是说需要通过平台任务调度的用例，都统一存放在目录X下，这些用例基本不需要维护，可以每日稳定执行，然后将目录X配置到平台任务信息中，按指定目录执行用例集。对于那些不够稳定的用例，就不能放到目录X下，需要调试好以后再纳入。\n为什么不用marker？pytest的marker确实可以给测试用例打标记，也有人是手动建立任务和用例进行映射，这些方式都不如维护一个稳定的自动化用例集方便，在我们公司平台上，也是维护用例集，作为基础用例集。使用pytest项目同理。\n核心代码 一键部署\n#!/bin/bash PkgName=\u0026#39;backend\u0026#39; Dockerfile=\u0026#39;./deploy/Dockerfile.backend\u0026#39; DockerContext=./ echo \u0026#34;Start build image...\u0026#34; docker build -f $Dockerfile -t $PkgName $DockerContext if [ $? -eq 0 ] then echo \u0026#34;Build docker image success\u0026#34; echo \u0026#34;Start run image...\u0026#34; docker run -p 8000:80 $PkgName else echo \u0026#34;Build docker image failed\u0026#34; fi FROM python:3.8 ENV LANG C.UTF-8 ENV TZ=Asia/Shanghai RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \u0026amp;\u0026amp; echo \u0026#39;Asia/Shanghai\u0026#39; \u0026gt;/etc/timezone WORKDIR /app COPY ./backend . RUN pip install -r ./requirements.txt -i \\ https://pypi.tuna.tsinghua.edu.cn/simple \\ --default-timeout=3000 CMD [\u0026#34;python\u0026#34;, \u0026#34;./manage.py\u0026#34;, \u0026#34;runserver\u0026#34;, \u0026#34;0.0.0.0:80\u0026#34;] 数据库表\n更精简，只有project和task两张表，简化平台功能，聚焦任务调度：\n需要说明的是，如果多人运行任务，只会存储最后一次执行结果，这个问题不是核心，个人精力有限，不打算在开源项目中开发，更侧重于实现任务调度，供大家参考\n执行任务\nsettings配置任务模式，判断执行不同分支：\ndef run(self): logger.info(\u0026#34;任务开始执行\u0026#34;) if settings.TASK_RUN_MODE == TaskRunMode.DOCKER: # 容器模式 try: self.execute_by_docker() except Exception as e: logger.info(e) if e == TaskException.DockerNotSupportedException: logger.info(\u0026#34;降级为本地执行\u0026#34;) self.execute_by_local() if settings.TASK_RUN_MODE == TaskRunMode.LOCAL: # 本地模式 self.execute_by_local() self.save_task() 容器模式\n先根据docker -v命令判断是否支持docker，然后docker build，再docker run\ndef execute_by_docker(self): logger.info(\u0026#34;运行模式：容器\u0026#34;) output = subprocess.getoutput(\u0026#34;docker -v\u0026#34;) logger.info(output) if \u0026#34;not found\u0026#34; in output: raise TaskException.DockerNotSupportedException build_args = [ f\u0026#39;--build-arg CMD_GIT_CLONE=\u0026#34;{self.cmd_git_clone}\u0026#34;\u0026#39;, f\u0026#39;--build-arg GIT_NAME=\u0026#34;{self.git_name}\u0026#34;\u0026#39;, f\u0026#39;--build-arg EXEC_DIR=\u0026#34;{self.exec_dir}\u0026#34;\u0026#39;, f\u0026#39;--build-arg REPORT_NAME=\u0026#34;{self.report_name}\u0026#34;\u0026#39;, ] cmd = f\u0026#34;docker build {\u0026#39; \u0026#39;.join(build_args)} -f {self.dockerfile_pytest} -t {self.git_name} {BASE_DIR}\u0026#34; logger.info(cmd) output = subprocess.getoutput(cmd) logger.info(output) cmd = f\u0026#34;docker run -v {REPORT_PATH}:/app/{os.path.join(self.exec_dir, \u0026#39;reports\u0026#39;)} {self.git_name}\u0026#34; logger.info(cmd) output = subprocess.getoutput(cmd) logger.info(output) 将项目仓库、执行目录、报告名称信息，通过参数传入Dockerfile.pytest\nFROM python:3.8 ENV LANG C.UTF-8 ENV TZ=Asia/Shanghai ARG CMD_GIT_CLONE ARG GIT_NAME ARG EXEC_DIR ARG REPORT_NAME RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \u0026amp;\u0026amp; echo \u0026#39;Asia/Shanghai\u0026#39; \u0026gt;/etc/timezone WORKDIR /app RUN $CMD_GIT_CLONE RUN pip install -r ./$GIT_NAME/requirements.txt -i \\ https://pypi.tuna.tsinghua.edu.cn/simple \\ --default-timeout=3000 WORKDIR $EXEC_DIR ENV HTML_NAME=$REPORT_NAME CMD [\u0026#34;pytest\u0026#34;, \u0026#34;--html=./reports/$HTML_NAME\u0026#34;, \u0026#34;--self-contained-html\u0026#34;] docker run的-v参数将容器报告挂载在后端服务，当报告生成后，后端服务也会生成一份报告文件。再将文件内容返给前端展示：\ndef report(request, *args, **kwargs): task_id = kwargs[\u0026#34;task_id\u0026#34;] task = Task.objects.get(id=task_id) report_path = task.report_path with open(os.path.join(REPORT_PATH, report_path), \u0026#39;r\u0026#39;, encoding=\u0026#34;utf8\u0026#34;) as f: html_content = f.read() return HttpResponse(html_content, content_type=\u0026#39;text/html\u0026#39;) 测试报告使用的pytest-html，重数据内容，轻外观样式。\n本地模式\n模拟容器行为，把本地.local目录当做容器，拉代码，执行pytest，生成报告，复制报告到报告文件夹，删除本地目录：\ndef execute_by_local(self): logger.info(\u0026#34;运行模式：本地\u0026#34;) os.makedirs(self.local_path, exist_ok=True) os.chdir(self.local_path) cmd_list = [self.cmd_git_clone, self.cmd_pytest] for cmd in cmd_list: logger.info(cmd) output = subprocess.getoutput(cmd) if output: logger.info(output) os.makedirs(REPORT_PATH, exist_ok=True) shutil.copy2(self.project_report_path, REPORT_PATH) shutil.rmtree(LOCAL_PATH) 本地模式，主要用于本地调试，在缺失Docker环境时，也能调试其他功能。\n配置\nTASK_RUN_MODE = TaskRunMode.DOCKER LOCAL_PATH = os.path.join(BASE_DIR, \u0026#34;.local\u0026#34;) REPORT_PATH = os.path.join(BASE_DIR, \u0026#34;task\u0026#34;, \u0026#34;report\u0026#34;) class TaskRunner: def __init__(self, task_id, run_user_id): self.task_id = task_id self.directory = Task.objects.get(id=task_id).directory self.project_id = Task.objects.get(id=task_id).project_id self.git_repository = Project.objects.get(id=self.project_id).git_repository self.git_branch = Project.objects.get(id=self.project_id).git_branch self.git_name = re.findall(r\u0026#34;^.*/(.*).git\u0026#34;, self.git_repository)[0] self.local_path = os.path.join(LOCAL_PATH, str(uuid.uuid1()).replace(\u0026#34;-\u0026#34;, \u0026#34;\u0026#34;)) self.run_user_id = run_user_id self.current_time = time.strftime(\u0026#34;%Y-%m-%d-%H-%M-%S\u0026#34;, time.localtime(time.time())) self.report_name = f\u0026#34;{str(self.git_name)}-{self.task_id}-{self.run_user_id}-{self.current_time}.html\u0026#34; self.project_report_path = os.path.join(self.local_path, self.git_name, \u0026#34;reports\u0026#34;, self.report_name) self.dockerfile_pytest = os.path.join(DEPLOY_PATH, \u0026#34;Dockerfile.pytest\u0026#34;) self.exec_dir = os.path.join(self.git_name, self.directory) self.cmd_git_clone = f\u0026#34;git clone -b {self.git_branch} {self.git_repository}\u0026#34; self.cmd_pytest = f\u0026#34;pytest {self.exec_dir} --html={self.project_report_path} --self-contained-html\u0026#34; tep-project更新 1、整合fixture，功能类放在fixture_function模块，数据类放在其他模块，突出fixture存放数据概念，比如登录接口fixture_login存储用户名密码、数据库fixture_mysql存储连接信息、文件fixture_file_data存储文件路径\n2、改造fixture_login，数据类fixture代码更简洁\nimport pytest from loguru import logger @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login(http, file_data): logger.info(\u0026#34;----------------开始登录----------------\u0026#34;) response = http( \u0026#34;post\u0026#34;, url=file_data[\u0026#34;domain\u0026#34;] + \u0026#34;/api/users/login\u0026#34;, headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={\u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;qa123456\u0026#34;} ) assert response.status_code \u0026lt; 400 logger.info(\u0026#34;----------------登录成功----------------\u0026#34;) response = response.json() return {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;Authorization\u0026#34;: f\u0026#34;Bearer {response[\u0026#39;token\u0026#39;]}\u0026#34;} @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login_xdist(http, tep_context_manager, file_data): \u0026#34;\u0026#34;\u0026#34; 该login只会在整个运行期间执行一次 \u0026#34;\u0026#34;\u0026#34; def produce_expensive_data(variable): logger.info(\u0026#34;----------------开始登录----------------\u0026#34;) response = http( \u0026#34;post\u0026#34;, url=variable[\u0026#34;domain\u0026#34;] + \u0026#34;/api/users/login\u0026#34;, headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={\u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;qa123456\u0026#34;} ) assert response.status_code \u0026lt; 400 logger.info(\u0026#34;----------------登录成功----------------\u0026#34;) return response.json() response = tep_context_manager(produce_expensive_data, file_data) return {\u0026#34;Authorization\u0026#34;: \u0026#34;Bearer \u0026#34; + response[\u0026#34;token\u0026#34;]} 3、改造fixture_mysql，支持维护多个连接，并且保持简洁\nfixture_function.py\n@pytest.fixture(scope=\u0026#34;class\u0026#34;) def executor(): class Executor: def __init__(self, db): self.db = db self.cursor = db.cursor() def execute_sql(self, sql): try: self.cursor.execute(sql) self.db.commit() except Exception as e: print(e) self.db.rollback() return self.cursor return Executor fixture_mysql.py\n@pytest.fixture(scope=\u0026#34;class\u0026#34;) def mysql_execute(executor): db = pymysql.connect(host=\u0026#34;host\u0026#34;, port=3306, user=\u0026#34;root\u0026#34;, password=\u0026#34;password\u0026#34;, database=\u0026#34;database\u0026#34;) yield executor(db).execute_sql db.close() @pytest.fixture(scope=\u0026#34;class\u0026#34;) def mysql_execute_x(executor): db = pymysql.connect(host=\u0026#34;x\u0026#34;, port=3306, user=\u0026#34;x\u0026#34;, password=\u0026#34;x\u0026#34;, database=\u0026#34;x\u0026#34;) yield executor(db).execute_sql db.close() 4、改造fixture_file_data，并添加示例test_file_data.py\nimport os import pytest from conftest import RESOURCE_PATH @pytest.fixture(scope=\u0026#34;session\u0026#34;) def file_data(resource): file_path = os.path.join(RESOURCE_PATH, \u0026#34;demo.yaml\u0026#34;) return resource(file_path).get_data() @pytest.fixture(scope=\u0026#34;session\u0026#34;) def file_data_json(resource): file_path = os.path.join(RESOURCE_PATH, \u0026#34;demo.json\u0026#34;) return resource(file_path).get_data() 5、添加接口复用的示例代码\ntests/base就是平台调度使用的稳定自动化用例集。\n接口代码复用设计 5条用例：\ntest_search_sku.py：搜索商品，前置条件：登录\ntest_add_cart.py：添加购物车，前置条件：登录，搜索商品\ntest_order.py：下单，前置条件：登录，搜索商品，添加购物车\ntest_pay.py：支付，前置条件：登录，搜索商品，添加购物车，下单\ntest_flow.py：完整流程\n怎么设计？\n登录，每条用例前置条件都依赖，定义为fixture_login，放在fixtures目录下\n搜索商品，test_search_sku.py用例本身不需要复用，被前置条件依赖3次，可以复用\n①定义为fixture_search_sku放在fixtures❌ 弊端：导致fixtures臃肿\n②复制用例文件，允许多份代码，平行展开✅ 好处：高度解耦，不用担心依赖问题\n总结，定义为fixture需要具备底层性，足够精炼。对于业务接口用例的前置条件，尽量在用例文件内部处理，保持文件解耦，遵循独立可运行的原则。\n复制多份文件？需要修改的话要改多份文件？\n是的，但这种情况极少。我能想到的情况：一、框架设计不成熟，动了底层设计，二、接口不稳定，改了公共接口，三、用例设计不合理，不能算是自动化。接口自动化要做好的前提，其实就是框架成熟，接口稳定，用例设计合理，满足这些前提以后，沉淀下来的自动化用例，几乎不需要大批量修改，更多的是要针对每条用例，去修改内部的数据，以满足不同场景的测试需要。也就是说，针对某个用例修改这个用例的数据，是更常见的行为。\n如果项目变动实在太大，整个自动化都不能用了，不管是做封装还是平行展开，维护量都非常大，耦合度太高的话，反而还不好改。\n跟着pytestx学习接口自动化框架设计，更简单，更快速，更高效\nhttps://github.com/dongfanger/pytestx\nhttps://gitee.com/dongfanger/tep-project\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/pytestx/000003-pytestx%E5%AE%B9%E5%99%A8%E5%8C%96%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"系统架构\"\u003e系统架构\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5a917ce9454040089ddd36f52930b7b6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e前端、后端、pytest均以\u003cstrong\u003eDocker\u003c/strong\u003e容器运行服务，单独的容器化执行引擎，项目环境隔离，即用即取，用完即弃，简单，高效。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e前端容器：页面交互，请求后端，展示HTML报告\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e后端容器：接收前端请求，启动任务，构建镜像，触发运行pytest，挂载HTML报告\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003epytest容器：拉取项目代码，指定目录执行，生成HTML报告\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e说明：构建镜像目前是在宿主机启动后端服务来执行docker命令的，暂未支持Kubernetes编排。宿主机安装了Docker，启动服务后，可以执行docker命令。如果采用容器部署后端，容器里面不包含Docker，无法构建，个人想法是可以借助K8S来编排，当前版本还未实现\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"系统流程\"\u003e系统流程\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f3705e59f74a44b3b1c49d38776cf513.png\"\u003e\u003c/p\u003e\n\u003cp\u003e支持2种运行模式配置：容器和本地。\u003c/p\u003e\n\u003cp\u003e容器模式：判断是否支持docker，如果支持，构建pytest镜像，在构建时，通过git拉取项目代码，再运行容器，按照指定目录执行pytest，生成测试报告，并将报告文件挂载到后端。如果不支持，降级为本地运行。\u003c/p\u003e\n\u003cp\u003e本地模式：模拟容器行为，在本地目录拉取代码，执行pytest，生成测试报告。\u003c/p\u003e\n\u003ch1 id=\"效果展示\"\u003e效果展示\u003c/h1\u003e\n\u003cp\u003e任务管理：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3160ec51a9094badbf1543a35568a028.png\"\u003e\u003c/p\u003e\n\u003cp\u003e容器模式：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8cea72d575a34a86a29e6d21f9d792e4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本地模式：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1d2d7ccffec842b58332cf3a5ce6ffc8.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"平台大改造\"\u003e平台大改造\u003c/h1\u003e\n\u003cp\u003epytestx平台更轻、更薄，移除了用例管理、任务关联用例相关功能代码，只保留真正的任务调度功能，backend的requirements.txt解耦，只保留后端依赖，pytest相关依赖转移到tep-project。\u003c/p\u003e\n\u003cp\u003e那如何管理用例呢？约定大于配置，\u003cstrong\u003e我们约定pytest项目已经通过目录维护好了一个稳定的自动化用例集\u003c/strong\u003e，也就是说需要通过平台任务调度的用例，都统一存放在目录X下，这些用例基本不需要维护，可以每日稳定执行，然后将目录X配置到平台任务信息中，按指定目录执行用例集。对于那些不够稳定的用例，就不能放到目录X下，需要调试好以后再纳入。\u003c/p\u003e\n\u003cp\u003e为什么不用marker？pytest的marker确实可以给测试用例打标记，也有人是手动建立任务和用例进行映射，这些方式都不如维护一个稳定的自动化用例集方便，在我们公司平台上，也是维护用例集，作为基础用例集。使用pytest项目同理。\u003c/p\u003e\n\u003ch1 id=\"核心代码\"\u003e核心代码\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e一键部署\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a1cce43d2acb43cf801fd58a5a60c36d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5a392cdc2b8b4f04bf1e267f16bffff2.png\"\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e#!/bin/bash\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#953800\"\u003ePkgName\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;backend\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#953800\"\u003eDockerfile\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;./deploy/Dockerfile.backend\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#953800\"\u003eDockerContext\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e./\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6639ba\"\u003eecho\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Start build image...\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker build -f \u003cspan style=\"color:#953800\"\u003e$Dockerfile\u003c/span\u003e -t \u003cspan style=\"color:#953800\"\u003e$PkgName\u003c/span\u003e \u003cspan style=\"color:#953800\"\u003e$DockerContext\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e[\u003c/span\u003e \u003cspan style=\"color:#953800\"\u003e$?\u003c/span\u003e -eq \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003ethen\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eecho\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Build docker image success\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eecho\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Start run image...\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    docker run -p 8000:80 \u003cspan style=\"color:#953800\"\u003e$PkgName\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#6639ba\"\u003eecho\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Build docker image failed\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efi\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eFROM\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003epython:3.8\u003c/span\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eENV\u003c/span\u003e LANG C.UTF-8\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eENV\u003c/span\u003e \u003cspan style=\"color:#953800\"\u003eTZ\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eAsia/Shanghai\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eRUN\u003c/span\u003e /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \u003cspan style=\"color:#0550ae\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eecho\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Asia/Shanghai\u0026#39;\u003c/span\u003e \u0026gt;/etc/timezone\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eWORKDIR\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e/app\u003c/span\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eCOPY\u003c/span\u003e ./backend .\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eRUN\u003c/span\u003e pip install -r ./requirements.txt -i \u003cspan style=\"color:#0a3069\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    https://pypi.tuna.tsinghua.edu.cn/simple \u003cspan style=\"color:#0a3069\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    --default-timeout\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3000\u003c/span\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eCMD\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;python\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;./manage.py\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;runserver\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;0.0.0.0:80\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\u003cspan style=\"color:#f6f8fa;background-color:#82071e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e数据库表\u003c/strong\u003e\u003c/p\u003e","title":"pytestx容器化执行引擎"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nfixture是什么 看看源码\ndef fixture( callable_or_scope=None, *args, scope=\u0026#34;function\u0026#34;, params=None, autouse=False, ids=None, name=None ): \u0026#34;\u0026#34;\u0026#34;Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a fixture function. The name of the fixture function can later be referenced to cause its invocation ahead of running tests: test modules or classes can use the ``pytest.mark.usefixtures(fixturename)`` marker. Test functions can directly use fixture names as input arguments in which case the fixture instance returned from the fixture function will be injected. Fixtures can provide their values to test functions using ``return`` or ``yield`` statements. When using ``yield`` the code block after the ``yield`` statement is executed as teardown code regardless of the test outcome, and must yield exactly once. :arg scope: the scope for which this fixture is shared, one of ``\u0026#34;function\u0026#34;`` (default), ``\u0026#34;class\u0026#34;``, ``\u0026#34;module\u0026#34;``, ``\u0026#34;package\u0026#34;`` or ``\u0026#34;session\u0026#34;`` (``\u0026#34;package\u0026#34;`` is considered **experimental** at this time). This parameter may also be a callable which receives ``(fixture_name, config)`` as parameters, and must return a ``str`` with one of the values mentioned above. See :ref:`dynamic scope` in the docs for more information. :arg params: an optional list of parameters which will cause multiple invocations of the fixture function and all of the tests using it. The current parameter is available in ``request.param``. :arg autouse: if True, the fixture func is activated for all tests that can see it. If False (the default) then an explicit reference is needed to activate the fixture. :arg ids: list of string ids each corresponding to the params so that they are part of the test id. If no ids are provided they will be generated automatically from the params. :arg name: the name of the fixture. This defaults to the name of the decorated function. If a fixture is used in the same module in which it is defined, the function name of the fixture will be shadowed by the function arg that requests the fixture; one way to resolve this is to name the decorated function ``fixture_\u0026lt;fixturename\u0026gt;`` and then use ``@pytest.fixture(name=\u0026#39;\u0026lt;fixturename\u0026gt;\u0026#39;)``. \u0026#34;\u0026#34;\u0026#34; if params is not None: params = list(params) fixture_function, arguments = _parse_fixture_args( callable_or_scope, *args, scope=scope, params=params, autouse=autouse, ids=ids, name=name, ) scope = arguments.get(\u0026#34;scope\u0026#34;) params = arguments.get(\u0026#34;params\u0026#34;) autouse = arguments.get(\u0026#34;autouse\u0026#34;) ids = arguments.get(\u0026#34;ids\u0026#34;) name = arguments.get(\u0026#34;name\u0026#34;) if fixture_function and params is None and autouse is False: # direct decoration return FixtureFunctionMarker(scope, params, autouse, name=name)( fixture_function ) return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) 总结一下\n【定义】\nfixture是一个函数，在函数上添加注解@pytest.fixture来定义 定义在conftest.py中，无需import就可以调用 定义在其他文件中，import后也可以调用 定义在相同文件中，直接调用 【使用】\n第一种使用方式是@pytest.mark.usefixtures(fixturename)（如果修饰TestClass能对类中所有方法生效） 第二种使用方式是作为函数参数 第三种使用方式是autouse（不需要显示调用，自动运行） conftest.py 我们常常会把fixture定义到conftest.py文件中。\n这是pytest固定的文件名，不能自定义。\n必须放在package下，也就是目录中有__init__.py。\nconftest.py中的fixture可以用在当前目录及其子目录，不需要import，pytest会自动找。\n可以创建多个conftest.py文件，同名fixture查找时会优先用最近的。\n依赖注入 fixture实现了依赖注入。依赖注入是控制反转（IoC， Inversion of Control）的一种技术形式。\n简单理解一下什么是依赖注入和控制反转\n实在是妙啊！我们可以在不修改当前函数代码逻辑的情况下，通过fixture来额外添加一些处理。\n入门示例 ## content of ./test_smtpsimple.py import smtplib import pytest @pytest.fixture def smtp_connection(): return smtplib.SMTP(\u0026#34;smtp.gmail.com\u0026#34;, 587, timeout=5) def test_ehlo(smtp_connection): response, msg = smtp_connection.ehlo() assert response == 250 assert 0 # for demo purposes 执行后程序处理逻辑\npytest找到test_开头的函数，发现需要名字为smtp_connection的fixture，就去找 找到之后，调用smtp_connection()，return了SMTP的实例 调用test_ehlo(\u0026lt;smtp_connection instance\u0026gt;) ，入参smtp_connection等于fixture return的值 如果想看文件定义了哪些fixture，可以使用命令，_前缀的需要跟上-v\npytest --fixtures test_simplefactory.py fixture scope \u0026amp; order 既然到处都可以定义fixture，那多了岂不就乱了？\npytest规定了fxture的运行范围和运行顺序。\nfixture的范围通过参数scope来指定\n@pytest.fixture(scope=\u0026#34;module\u0026#34;) 默认是function，可以选择function, class, module, package 或 session。\nfixture都是在test第一次调用时创建，根据scope的不同有不同的运行和销毁方式\nfunction 每个函数运行一次，函数结束时销毁 class 每个类运行一次，类结束时销毁 module 每个模块运行一次，模块结束时销毁 package 每个包运行一次，包结束时销毁 session 每个会话运行一次，会话结束时销毁 fixture的顺序优先按scope从大到小，session \u0026gt; package \u0026gt; module \u0026gt; class \u0026gt; function。\n如果scope相同，就按test调用先后顺序，以及fixture之间的依赖关系。\nautouse的fixture会优先于相同scope的其他fixture。\n示例\nimport pytest ## fixtures documentation order example order = [] @pytest.fixture(scope=\u0026#34;session\u0026#34;) def s1(): order.append(\u0026#34;s1\u0026#34;) @pytest.fixture(scope=\u0026#34;module\u0026#34;) def m1(): order.append(\u0026#34;m1\u0026#34;) @pytest.fixture def f1(f3): order.append(\u0026#34;f1\u0026#34;) @pytest.fixture def f3(): order.append(\u0026#34;f3\u0026#34;) @pytest.fixture(autouse=True) def a1(): order.append(\u0026#34;a1\u0026#34;) @pytest.fixture def f2(): order.append(\u0026#34;f2\u0026#34;) def test_order(f1, m1, f2, s1): assert order == [\u0026#34;s1\u0026#34;, \u0026#34;m1\u0026#34;, \u0026#34;a1\u0026#34;, \u0026#34;f3\u0026#34;, \u0026#34;f1\u0026#34;, \u0026#34;f2\u0026#34;] 虽然test_order()是按f1, m1, f2, s1调用的，但是结果却不是按这个顺序\ns1 scope为session m1 scope为module a1 autouse，默认function，后于session、module，先于function其他fixture f3 被f1依赖 f1 test_order()参数列表第1个 f2 test_order()参数列表第3个 fixture嵌套 fixture装饰的是函数，那函数也有入参咯。\nfixture装饰的函数入参，只能是其他fixture。\n示例，f1依赖f3，如果不定义f3的话，执行会报错fixture \u0026lsquo;f3\u0026rsquo; not found\n@pytest.fixture def f1(f3): order.append(\u0026#34;f1\u0026#34;) @pytest.fixture def f3(): order.append(\u0026#34;f3\u0026#34;) def test_order(f1): pass 从test传值给fixture 借助request，可以把test中的值传递给fixture。\n示例1，smtp_connection可以使用module中的smtpserver\n## content of conftest.py import smtplib import pytest @pytest.fixture(scope=\u0026#34;module\u0026#34;) def smtp_connection(request): server = getattr(request.module, \u0026#34;smtpserver\u0026#34;, \u0026#34;smtp.gmail.com\u0026#34;) smtp_connection = smtplib.SMTP(server, 587, timeout=5) yield smtp_connection print(\u0026#34;finalizing {} ({})\u0026#34;.format(smtp_connection, server)) smtp_connection.close() ## content of test_anothersmtp.py smtpserver = \u0026#34;mail.python.org\u0026#34; # will be read by smtp fixture def test_showhelo(smtp_connection): assert 0, smtp_connection.helo() 示例2，结合request+mark，把fixt_data从test_fixt传值给了fixt\nimport pytest @pytest.fixture def fixt(request): marker = request.node.get_closest_marker(\u0026#34;fixt_data\u0026#34;) if marker is None: # Handle missing marker in some way... data = None else: data = marker.args[0] # Do something with the data return data @pytest.mark.fixt_data(42) def test_fixt(fixt): assert fixt == 42 fixture setup / teardown 其他测试框架unittest/testng，都定义了setup和teardown函数/方法，用来测试前初始化和测试后清理。\npytest也有，不过是兼容unittest等弄的，不推荐！\nfrom loguru import logger def setup(): logger.info(\u0026#34;setup\u0026#34;) def teardown(): logger.info(\u0026#34;teardown\u0026#34;) def test(): pass 建议使用fixture。\nsetup，fixture可以定义autouse来实现初始化。\n@pytest.fixture(autouse=True) autouse的fixture不需要调用，会自己运行，和test放到相同scope，就能实现setup的效果。\nautouse使用说明\nautouse遵循scope的规则，scope=\u0026ldquo;session\u0026quot;整个会话只会运行1次，其他同理 autouse定义在module中，module中的所有function都会用它（如果scope=\u0026ldquo;module\u0026rdquo;，只运行1次，如果scope=\u0026ldquo;function\u0026rdquo;，会运行多次） autouse定义在conftest.py，conftest覆盖的test都会用它 autouse定义在plugin中，安装plugin的test都会用它 在使用autouse时需要同时注意scope和定义位置 示例，transact默认scope是function，会在每个test函数执行前自动运行\n## content of test_db_transact.py import pytest class DB: def __init__(self): self.intransaction = [] def begin(self, name): self.intransaction.append(name) def rollback(self): self.intransaction.pop() @pytest.fixture(scope=\u0026#34;module\u0026#34;) def db(): return DB() class TestClass: @pytest.fixture(autouse=True) def transact(self, request, db): db.begin(request.function.__name__) yield db.rollback() def test_method1(self, db): assert db.intransaction == [\u0026#34;test_method1\u0026#34;] def test_method2(self, db): assert db.intransaction == [\u0026#34;test_method2\u0026#34;] 这个例子不用autouse，用conftest.py也能实现\n## content of conftest.py @pytest.fixture def transact(request, db): db.begin() yield db.rollback() @pytest.mark.usefixtures(\u0026#34;transact\u0026#34;) class TestClass: def test_method1(self): ... teardown，可以在fixture中使用yield关键字来实现清理。\n示例，scope为module，在module结束时，会执行yield后面的print()和smtp_connection.close()\n## content of conftest.py import smtplib import pytest @pytest.fixture(scope=\u0026#34;module\u0026#34;) def smtp_connection(): smtp_connection = smtplib.SMTP(\u0026#34;smtp.gmail.com\u0026#34;, 587, timeout=5) yield smtp_connection # provide the fixture value print(\u0026#34;teardown smtp\u0026#34;) smtp_connection.close() 可以使用with关键字进一步简化，with会自动清理上下文，执行smtp_connection.close()\n## content of test_yield2.py import smtplib import pytest @pytest.fixture(scope=\u0026#34;module\u0026#34;) def smtp_connection(): with smtplib.SMTP(\u0026#34;smtp.gmail.com\u0026#34;, 587, timeout=5) as smtp_connection: yield smtp_connection # provide the fixture value fixture参数化 后续会专门讲“pytest参数化”，这里就先跳过，请各位见谅啦。\n因为我觉得想用pytest做参数化，一定是先到参数化的文章里面找，而不是到fixture。\n把这部分放到参数化，更便于以后检索。\n简要回顾 本文开头通过源码介绍了fixture是什么，并简单总结定义和用法。然后对依赖注入进行了解释，以更好理解fixture技术的原理。入门示例给出了官网的例子，以此展开讲了范围、顺序、嵌套、传值，以及初始化和清理的知识。\n参考资料\nhttps://en.wikipedia.org/wiki/Dependency_injection\nhttps://en.wikipedia.org/wiki/Inversion_of_control\nhttps://docs.pytest.org/en/stable/contents.html#toc\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/pytest/000003-pytestpytest%E7%B2%BE%E9%80%9Afixture/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"fixture是什么\"\u003efixture是什么\u003c/h2\u003e\n\u003cp\u003e看看源码\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003efixture\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    callable_or_scope\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003eargs\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    scope\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;function\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    params\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    autouse\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eFalse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ids\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    name\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;Decorator to mark a fixture factory function.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    This decorator can be used, with or without parameters, to define a\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    fixture function.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    The name of the fixture function can later be referenced to cause its\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    invocation ahead of running tests: test\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    modules or classes can use the ``pytest.mark.usefixtures(fixturename)``\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    marker.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    Test functions can directly use fixture names as input\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    arguments in which case the fixture instance returned from the fixture\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    function will be injected.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    Fixtures can provide their values to test functions using ``return`` or ``yield``\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    statements. When using ``yield`` the code block after the ``yield`` statement is executed\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    as teardown code regardless of the test outcome, and must yield exactly once.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    :arg scope: the scope for which this fixture is shared, one of\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                ``\u0026#34;function\u0026#34;`` (default), ``\u0026#34;class\u0026#34;``, ``\u0026#34;module\u0026#34;``,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                ``\u0026#34;package\u0026#34;`` or ``\u0026#34;session\u0026#34;`` (``\u0026#34;package\u0026#34;`` is considered **experimental**\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                at this time).\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                This parameter may also be a callable which receives ``(fixture_name, config)``\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                as parameters, and must return a ``str`` with one of the values mentioned above.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                See :ref:`dynamic scope` in the docs for more information.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    :arg params: an optional list of parameters which will cause multiple\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                invocations of the fixture function and all of the tests\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                using it.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                The current parameter is available in ``request.param``.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    :arg autouse: if True, the fixture func is activated for all tests that\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                can see it.  If False (the default) then an explicit\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                reference is needed to activate the fixture.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    :arg ids: list of string ids each corresponding to the params\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                so that they are part of the test id. If no ids are provided\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                they will be generated automatically from the params.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    :arg name: the name of the fixture. This defaults to the name of the\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                decorated function. If a fixture is used in the same module in\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                which it is defined, the function name of the fixture will be\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                shadowed by the function arg that requests the fixture; one way\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                to resolve this is to name the decorated function\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                ``fixture_\u0026lt;fixturename\u0026gt;`` and then use\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e                ``@pytest.fixture(name=\u0026#39;\u0026lt;fixturename\u0026gt;\u0026#39;)``.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e params \u003cspan style=\"color:#0550ae\"\u003eis\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003enot\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        params \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elist\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eparams\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    fixture_function\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e arguments \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e _parse_fixture_args\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        callable_or_scope\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003eargs\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        scope\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003escope\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        params\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eparams\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        autouse\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eautouse\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ids\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eids\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        name\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    scope \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e arguments\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;scope\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    params \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e arguments\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;params\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    autouse \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e arguments\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;autouse\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ids \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e arguments\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;ids\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    name \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e arguments\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eget\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e fixture_function \u003cspan style=\"color:#0550ae\"\u003eand\u003c/span\u003e params \u003cspan style=\"color:#0550ae\"\u003eis\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003eand\u003c/span\u003e autouse \u003cspan style=\"color:#0550ae\"\u003eis\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eFalse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#57606a\"\u003e# direct decoration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e FixtureFunctionMarker\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003escope\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e params\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e autouse\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e name\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e)(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            fixture_function\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e FixtureFunctionMarker\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003escope\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e params\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e autouse\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e ids\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eids\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e name\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e总结一下\u003c/p\u003e","title":"pytest精通fixture"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n本文是一篇过渡，在进行用例管理模块开发之前，有必要把入门篇开发完成的代码部署到Linux系统Docker中，把部署流程走一遍，这个过程对后端设计有决定性影响。\n本地运行 通过在Vue项目执行npm run serve和在Django项目执行python manage.py runserver，我们把项目在本地跑起来了，示意图如下：\n前端在本地启了个Node服务器，后端在本地启了个Django服务器，分别使用8080和8000端口。浏览器有个同源策略：域名、端口、协议三者一致才能进行访问，否则会由于跨域访问而被浏览器拦截。图中前后端的端口不一致，出现了跨域，前端是无法直接请求后端的。解决办法是在vue.config.js中配置devServer：\n这是Node开的一个代理服务器，当前端请求后端时，会先发向Node代理服务器，Node代理服务器以相同的参数向真正的后端服务器进行请求，再把响应返回给前端。在本项目中，前端请求仍然是发给http://127.0.0.1:8080，浏览器不会拦截，Node代理服务器会帮你把请求转发给后端8000端口。\nNginx部署 搞懂了本地运行代理转发，再来看看Nginx部署。Nginx本身是个服务器，就像Node服务器一样，也可以看做Apache Tomcat。Vue项目使用npm run build命令把代码构建为dist目录静态文件，放到Nginx服务器中加载出来，结合Docker示意图如下：\n相比于本地运行，Nginx部署时，前端变化比较大，一：dist静态文件拷贝到了/usr/share/nginx/html目录中，二：对/路径来说，Nginx会监听80端口，三：对/api路径来说，Nginx会把请求转发到后端服务器端口，这也叫做反向代理。后端没有什么变化，为了和本地运行看着有点区别，把端口稍微改了下，Docker内部使用80端口。\n这里比较关键的是理解Docker teprunner-frontend、Docker teprunner-backend、Linux三者之间的关系。如果不知道Docker，那么应该听说过虚拟机，Docker从概念上理解就像是虚拟机，这三者可以看做是三台主机。Linux的IP是172.16.25.131，80端口映射到了Docker teprunner-frontend的80端口，8099端口映射到了Docker teprunner-backend的80端口，如图中下方双向箭头所示。在Linux上访问http://127.0.0.1，能打开登录页面，但是无法向后端发起请求，因为从80端口直接请求8099端口，跨域了。解决办法是在Docker teprunner-frontend借助Nginx进行反向代理，把请求先发送到Nginx服务器，再转发给Linux的8099端口。\n不能在Docker teprunner-frontend中把/api的代理设置为http://127.0.0.1:8099，因为这个Docker容器的8099端口并没有启用，启用的是Linux这台机器上的8099端口，所以需要通过IP来指定。\n整体思路明确了，接下来就开始动手操作。\n编写deploy脚本 前端\n打开teprunner-frontend文件夹，新建deploy/nginx.conf文件：\n/路径从user/share/nginx/html读文件，入口为index.html，/api转发到http://172.16.25.131:8099。这个文件会拷贝到Docker镜像中。新建Dockerfile文件：\nFROM定义了基础镜像，可以理解为操作系统，前端项目基于nginx来构建。WORKDIR定义了镜像当前工作目录，意思是在执行后面COPY操作时，镜像目录用哪一个。COPY分别把dist静态文件和nginx.conf配置文件拷贝到镜像中，COPY指令第一个参数是本机目录，第二个参数是镜像目录。镜像目录通过WORKDIR来指定，本机目录通过Docker上下文来指定，新建build.sh文件：\nDockerContext指定了Docker上下文为teprunner-frontend根目录。这里的Shell脚本有两个阶段，第1阶段是使用node编译：\ndocker run # 运行镜像 --rm # 运行后删除容器 -v $(pwd)/../:/data/src # $(pwd)指当前工作目录，把根目录挂载到data/src -v /root/.npm/_logs:/root/.npm/_logs # 挂载日志文件 -w /data/src/ # 镜像当前工作目录 $BUILDER_IMAGE # 运行镜像为node:latest，用node编译前端代码 /bin/sh -c \u0026#34;npm install \u0026amp;\u0026amp; npm run build\u0026#34; # /bin/sh是shell可执行程序，调用执行npm命令 第2阶段是打包成Docker镜像：\ndocker build # 构建镜像 -f $Dockerfile # 指定Dockerfile文件位置 -t $PkgName # 镜像包名 $DockerContext # Docker上下文 后端\n后端也是类似的，先新建deploy/Dockerfile文件：\n后端项目基于python:3.8来构建，接着设置了时区，COPY . .把Django源文件直接复制到了镜像目录/app/release中，RUN指令执行pip install命令安装依赖包，CMD和RUN有点区别，RUN指令在docker build时就执行，CMD指令在docker run时才执行，预定义启动命令。\n这里简化了迁移数据库migrate等启动命令，服务器数据库和本地用的同一个。\n再新建build.sh文件：\nPython代码不需要编译，打包成Docker镜像就可以了。\n部署到Ubuntu系统Docker Linux系统是内核版本，它有很多发行版本，比如CentOS、Ubuntu，本文采用了Ubuntu，只有一个原因，它长的好看。\n大学室友曾经冲动地把Windows系统换成了Ubuntu，还天天跟我们炫耀有多酷炫有多牛逼，过了两三天发现Office不好用，也玩不了游戏，就又换回来了。哈哈，Ubuntu平时玩玩就好了，除非是做Linux内核开发。\n下载软件：\nVMware 破解版 Ubuntu Desktop 20.04 安装过程此处不再另加赘述。打开虚拟机的Ubuntu：\n打开Terminal，输入su，输入密码，切换到root：\n发现缺少权限就su一下。\n安装curl：\napt-get install curl 安装docker：\ncurl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun 使用ifconfig查询虚拟机IP：\n不要选择.git和node_modules文件夹，把teprunner-frontend打成压缩包。不要选择.git和__pycache__文件夹，把teprunner-backend打成压缩包。复制前后端压缩包到虚拟机Documents解压：\nUbuntu Desktop的好处是提供了图像化操作界面，适合我这种小白用户。使用命令行编辑工具vi或者图形编辑工具gedit编辑teprunner-frontend/deploy/nginx.conf文件中/api转发地址为你的虚拟机实际IP地址：\n打开两个Terminal，分别cd到teprunner-frontend/deploy和teprunner-backend/deploy，执行./build.sh命令。\n如果执行提示^M之类报错，那是因为在Windows编辑后复制到Linux格式不一致，使用apt-get install dos2unix命令安装工具后进行格式转化，比如dos2unix build.sh、dos2unix Dockerfile。\n前端构建截图：\n第一次因为要下载node依赖包和拉取nginx镜像，会比较慢，第二次就快很多了。\n后端构建截图：\n第一次因为要拉取python镜像，会比较慢，第二次就快多了。\n都构建完成后，输入docker images命令就能看到打包好的Docker镜像了：\n启动前端镜像：\ndocker run -p 80:80 teprunner-frontend 启动后端镜像：\ndocker run -p 8099:80 teprunner-backend 镜像启动后就变成了Docker容器，可以理解为一台虚拟主机。-p参数用于映射Ubuntu端口和Docker端口。可以添加-d参数让容器在后台运行。docker ps -a查看容器，docker kill CONTAINER或docker stop CONTAINER退出容器。\n最后可以在虚拟机中访问http:127.0.0.1进行登录了，本地机器想要访问的话，需要把127.0.0.1改为你的虚拟机实际IP，比如http://172.16.25.131。\n小结 本文先介绍了本地运行和Nginx部署的示意图，涉及到跨域访问和反向代理。接着编写deploy脚本，编译代码，构建镜像。最后部署到Ubuntu系统的Docker中运行起来。在使用过程中，也感受到了Docker这一划时代技术的魅力，如果没有Docker，我们需要在Ubuntu上面安装nginx、node、python等软件，有了Docker，我们只需要安装Docker，其他都基于Docker镜像构建就可以了。teprunner测试平台的用例采用的是代码形式，这就涉及到了代码存放位置的问题，为了让pytest能调用执行，肯定是存放到文件里面的。本文实践给了个重要提醒，如果后端把代码直接写入磁盘文件，每次打包镜像部署后，就会把已保存的用例代码抹掉。解决这个问题的第一个办法是用K8S，第二个办法是把代码存数据库。学习版采用了第二个办法存数据库，执行时动态从数据库拿代码生成文件。第一个办法思路借鉴：\n最后，简单聊下Docker和K8S，Docker是Docker公司的，K8S是Google的，Docker是家小公司搞的，在创建之初，并没有考虑到“容器编排”这个功能，2014年 Google推出Kubernetes用于解决大规模场景下Docker容器编排的问题，2016年Kubernetes发布CRI统一接口，虽然Docker也在2016年发布了Docker Swarm，带来了Docker在多主机多容器的编排解决方案，但是已经无法阻挡K8S取得这场容器编排战争的胜利。\n参考资料：\nhttps://www.cnblogs.com/riwang/p/11883332.html\nhttps://zhuanlan.zhihu.com/p/334787180\nhttps://testerhome.com/topics/27860\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000003-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0teprunner%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E9%83%A8%E7%BD%B2%E5%88%B0linux%E7%B3%BB%E7%BB%9Fdocker/","summary":"本文是一篇过渡，在进行用例管理模块开发之前，有必要把入门篇开发完成的代码部署到Linux系统Docker中，把部署流程走一遍，这个过程对后端设计有决定性影响。","title":"teprunner测试平台部署到Linux系统Docker"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n基本功能：\n执行脚本a，打开一个chrome，脚本a执行完成，chrome未关闭。 执行脚本b，继续使用a打开的chrome，不新启浏览器。 附加：\n如果已打开的chrome未关闭，则在chrome中新建标签页来打开新的页面。 如果已打开的chrome已关闭，则新启浏览器。 最近用python+selenium+pytest，写了个测试小工具用来自动化登陆浏览器，一方面是方便管理网址、账号、密码，存放在脚本中，另一方面也省去了频繁输入登陆网站的操作，节省了不少时间。\n但这个小工具用起来存在明显问题：每次都新启一个浏览器，多了后就是这样的\n根本不知道谁是谁。\n于是就想到要实现前面提到的这些功能。\n首先要解决的第一个问题就是，怎么重新使用已打开的chrome。百度后知晓，是通过session_id。浏览器都有一个session_id，拿到这个session_id就可以通过webdriver.Remote去调chrome。\ndriver = webdriver.Chrome() params[\u0026#34;session_id\u0026#34;] = driver.session_id params[\u0026#34;server_url\u0026#34;] = driver.command_executor._url driver = webdriver.Remote(command_executor=params[\u0026#34;server_url\u0026#34;]) driver.session_id = params[\u0026#34;session_id\u0026#34;] 经过试验，python存在一个坑是每次初始化就会调start_session去新开一个空白的浏览器。网上有解决方案是继承Remote，重写start_session方法。然而，仔细看完代码就发现，何必多此一举，直接quit()就搞定。\ndriver = webdriver.Remote(command_executor=params[\u0026#34;server_url\u0026#34;]) driver.quit() # 退出start_session新开的空白浏览器 driver.session_id = params[\u0026#34;session_id\u0026#34;] quit是quit了，但driver还存在呀，所以还是多理清思路，才能避免走冤枉路。\n然后要解决的第二个问题就是，如何在执行脚本b的时候再接着用session。当然就是存本地咯。这里用到的是pickle，能很方便的在本地存取变量。\n存\nwith open(session_file, \u0026#39;wb\u0026#39;) as f: pickle.dump(params, f) 取\nwith open(session_file, \u0026#39;rb\u0026#39;) as f: params = pickle.load(f) 第三个问题就是新开标签页和切换窗口。\ndriver.execute_script(\u0026#39;window.open(\u0026#34;\u0026#34;);\u0026#39;) # 调js driver.switch_to.window(driver.window_handles[-1]) # 切换到最后一个页签 最后要解决的一个问题就是，如果已经打开的chrome关掉了，从本地文件读取的session就会过时。拿这个过时session去用，就会”chrome not reachable“。解决思路就是，捕获driver抛出的WebDriverException，重新创建新的driver。\ntry: driver = webdriver.Remote(command_executor=params[\u0026#34;server_url\u0026#34;]) driver.quit() # 退出start_session新开的空白浏览器 driver.session_id = params[\u0026#34;session_id\u0026#34;] driver.execute_script(\u0026#39;window.open(\u0026#34;\u0026#34;);\u0026#39;) driver.switch_to.window(driver.window_handles[-1]) except: driver = create_driver() 完整代码\nsession_file = \u0026#39;browser_session.data\u0026#39; def create_driver(): driver = webdriver.Chrome() with open(session_file, \u0026#39;wb\u0026#39;) as f: params = {\u0026#34;session_id\u0026#34;: driver.session_id, \u0026#34;server_url\u0026#34;: driver.command_executor._url} pickle.dump(params, f) return driver if not Path(session_file).exists(): driver = create_driver() else: with open(session_file, \u0026#39;rb\u0026#39;) as f: params = pickle.load(f) try: driver = webdriver.Remote(command_executor=params[\u0026#34;server_url\u0026#34;]) driver.quit() # 退出start_session新开的空白浏览器 driver.session_id = params[\u0026#34;session_id\u0026#34;] driver.execute_script(\u0026#39;window.open(\u0026#34;\u0026#34;);\u0026#39;) driver.switch_to.window(driver.window_handles[-1]) except: driver = create_driver() ","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E8%87%AA%E5%8A%A8%E5%8C%96/000003-ui%E8%87%AA%E5%8A%A8%E5%8C%96webdriver%E4%BD%BF%E7%94%A8%E5%B7%B2%E6%89%93%E5%BC%80%E8%BF%87%E7%9A%84chrome/","summary":"基本功能：","title":"webdriver使用已打开过的chrome"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJaCoCo全称是Java Code Coverage，Java代码覆盖率，广泛运用于各种测试平台对Java代码的全量覆盖率和增量覆盖率进行统计，分析代码行差异，度量单元测试效果。Jacoco也是精准测试的技术实现手段之一。\n入门实践的目标是写点简单代码，再加点单元测试，把JaCoCo跑起来，输出测试报告，看代码覆盖率是怎么回事，了解基本的运行流程。\n先写点简单代码：\npublic class Add { public int add(int a, int b) { if (a == 1 \u0026amp;\u0026amp; b == 1) { return a + b; } return 0; } } 再加点单元测试，这里先加一个什么都不做的单元测试，按理说代码覆盖率会是0：\nimport org.junit.Test; public class AddTest { @Test public void notCovered() { } } 如何把Jacoco跑起来呢？我先尝试了Maven插件的方式，在pom.xml中引入依赖和添加配置：\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jacoco\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jacoco-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.8.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.jacoco\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jacoco-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.8.7\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;default-prepare-agent\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;prepare-agent\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;default-report\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;report\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 引入了junit和jacoco-maven-plugin两个依赖。 插件中配置了两个execution，并指定了两个goal，prepare-agent（插桩）和report（报告）。 然后执行maven的install，如果是在IDEA中可以直接点击按钮：\n在target下就能看到html报告了：\n打开看果然代码覆盖率是0：\n修改一下单元测试：\nimport org.junit.Test; public class AddTest { @Test public void covered() { Add addDemo = new Add(); addDemo.add(1, 1); addDemo.add(1, 2); addDemo.add(2, 2); } @Test public void notCovered() { } } 先maven clean以后再install，就能看到：\n覆盖率100%了。一共有4行需要覆盖的代码行：\n至于为什么需要4行，以及我添加的单元测试为什么能够100%覆盖，文字描述有点说不清楚。可以关注我的B站，搜索dongfanger关注，我会进行相关的演示和说明。\n参考资料：\nJacoco官网Maven示例 https://www.jacoco.org/jacoco/trunk/doc/maven.html\nJacoco报告详解 https://blog.csdn.net/sayoko06/article/details/99312875\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E7%99%BD%E7%9B%92%E6%B5%8B%E8%AF%95/000003-jacoco%E4%BB%A3%E7%A0%81%E8%A6%86%E7%9B%96%E7%8E%87%E4%BB%8E0%E5%88%B0100%E7%9A%84%E5%85%A5%E9%97%A8%E5%AE%9E%E8%B7%B5/","summary":"JaCoCo全称是\u003cstrong\u003eJa\u003c/strong\u003eva \u003cstrong\u003eCo\u003c/strong\u003ede \u003cstrong\u003eCo\u003c/strong\u003everage，Java代码覆盖率，广泛运用于各种测试平台对Java代码的全量覆盖率和增量覆盖率进行统计，分析代码行差异，度量单元测试效果。Jacoco也是精准测试的技术实现手段之一。","title":"代码覆盖率从0到100的入门实践"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nFastAPI的路径操作函数，可以使用async def定义：\nfrom typing import Union from fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/\u0026#34;) async def read_root(): return {\u0026#34;Hello\u0026#34;: \u0026#34;World\u0026#34;} @app.get(\u0026#34;/items/{item_id}\u0026#34;) async def read_item(item_id: int, q: Union[str, None] = None): return {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;q\u0026#34;: q} 这算得上是FastAPI的典型特征之一。\n关于这个框架设计，有哪些技术背景和细节呢？\n技术背景 在Python语法里面，如果你想异步请求三方库，需要使用await：\nresults = await some_library() 使用了await就必须在def前面加上async：\n@app.get(\u0026#39;/\u0026#39;) async def read_results(): results = await some_library() return results 这是Python语法规定。\nFastAPI并不要求所有的路径操作函数，都必须定义为async，假如你要实时访问某些三方库，可以简单的使用def就行，不用加上await：\n@app.get(\u0026#39;/\u0026#39;) def results(): results = some_library() return results 但是无论你是否使用async，FastAPI都将异步工作，以达到\u0026quot;Fast\u0026quot;的运行速度。\n看完文章就明白这句话的意思了。\n技术细节 Python新版本已经原生支持异步代码了。所谓异步代码，指的是编程语言，会告诉计算机程序，在某个时刻停下来，等待其他任务完成后，再继续运行。在等待期间，计算机程序可以去干点别的事情，而不用一直卡在那里。这些“其他任务”，通常指的是耗时较长的IO操作，比如：\n客户端通过网络发送数据；\n服务端通过网络发送数据；\n程序从磁盘读取文件内容；\n程序将文件内容写入磁盘；\n远程API操作；\n数据库操作；\n数据库查询返回结果；\n这些操作主要阻塞在IO等待，所以又叫做IO密集型。\n并发和并行 异步有时候也叫做并发。并发(Concurrency)和并行(parallelism)是不同的概念，并发是指一个处理器同时处理多个任务，并行是指多个处理器同时处理多个不同的任务，并发是逻辑上的同时发生，并行是物理上的同时发生。\n并发\n餐厅有1个服务员和1个厨子。\n你带女朋友到餐厅排队点汉堡：\n你们给服务员说来2个汉堡：\n服务员给厨师说做2个汉堡：\n然后给了你们一个排号：\n你们开心的等，因为有排号，不需要担心别人会抢走：\n叫号了：\n取了汉堡高高兴兴的吃：\n并行\n餐厅有5个服务员兼厨子。\n你们看哪个窗口有空位：\n到餐台点2个汉堡：\n服务员自己跑到厨房做汉堡：\n你们只能站在原地等，如果走开，可能会被其他人拿走：\n汉堡做好了：\n你的女朋友不开心：\n从这个买汉堡的漫画中，可以看到并行比并发会做更多无意义的等待，并行需要5个人（5个服务员兼厨子），并发只需要2个人（1个服务员1个厨子）。这就是为什么很多Web框架要设计成异步并发了，因为很多客户端会发请求给服务端，然后服务端响应给客户端，如果有太多无用的等待，那么整个应用将慢得无法使用。而且硬件资源有限，并发也能更高效利用资源，节约成本。\n并发一定就比并行好吗？也不是，只有在出现很多等待时，并发才比并行好。比如你们要打扫房间，一间一间的打扫，没有等待，那么并发和并行就没有区别，如果你再叫3个朋友一起打扫，并行就能更快打扫完。这种执行时间完全取决于任务本身而不是等待的情况，又叫做CPU密集型。计算机里的CPU密集型操作通常需要更复杂的数据计算，比如：\n音频或图片处理；\n计算机视觉；\n机器学习；\n深度学习；\nFastAPI既支持异步并发，也支持多线程并行。\nasync和await 异步并发使用async和await来实现。\nasync定义函数：\nasync def get_burgers(number: int): # Do some asynchronous stuff to create the burgers return burgers await调用函数：\n@app.get(\u0026#39;/burgers\u0026#39;) async def read_burgers(): burgers = await get_burgers(2) return burgers 细节中的细节 FastAPI会对路径操作函数(path operation function)和依赖(dependencies)进行特殊处理。这个特殊处理是：如果你把函数定义为def而不是async def，那么FastAPI会把它放到单独的线程池中，异步执行，这就是FastAPI精彩的地方。就像官方所说，如果你不清楚你函数里面的调用是不是异步(能不能用await)，那么就把它定义为普通函数，FastAPI会采用多线程的方式处理。乱用async，在async里面有同步调用，则会变成串行，Fast秒变Slow。\n而对于其他函数，FastAPI则不会管，def就是同步调用，立马返回结果。\n现在回过头来看前面的那句话：但是无论你是否使用async，FastAPI都将异步工作，以达到\u0026quot;Fast\u0026quot;的运行速度。应该更加明白了。\n参考资料：\nConcurrency and async / await - FastAPI https://fastapi.tiangolo.com/async/\n很火的Fastapi框架，用async函数真的比普通函数快吗？https://blog.csdn.net/yyw794/article/details/108859240\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/fastapi/000003-fastapi%E5%85%B3%E4%BA%8Efastapi%E5%BC%82%E6%AD%A5%E5%B9%B6%E5%8F%91%E7%9A%84%E6%8A%80%E6%9C%AF%E8%83%8C%E6%99%AF%E5%92%8C%E7%BB%86%E8%8A%82/","summary":"FastAPI的路径操作函数，可以使用\u003ccode\u003easync def\u003c/code\u003e定义：","title":"关于FastAPI异步并发的技术背景和细节"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在上篇文章《开发你的第一个SpringBoot应用》已经对SpringBoot基本开发流程有了大体了解，本文将继续对SpringBoot官网进行学习，发现关于SpringBoot开发的更多细节。\n依赖管理 推荐使用Maven或者Gradle进行依赖管理，其他工具比如Ant，SpringBoot对它们的支持不是很好。\nSpringBoot提供了很多starter来批量添加依赖，比如spring-boot-starter-web。官方定义的starter都是spring-boot-starter-*这种命名方式，由org.springframework.boot提供：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using.build-systems.starters\n而自定义的第三方starter命名方式往往会把名字放在前面，像这样：thirdpartyproject-spring-boot-starter。\n代码目录 官方推荐的代码目录是这样：\ncom +- example +- myapplication +- MyApplication.java | +- customer | +- Customer.java | +- CustomerController.java | +- CustomerService.java | +- CustomerRepository.java | +- order +- Order.java +- OrderController.java +- OrderService.java +- OrderRepository.java 包名采用com.example.myapplication这种方式。 main启动入口类MyApplication.java放在根目录，便于查找其他文件。 其他模块跟MyApplication.java并列。 配置 尽量不使用xml进行配置，而是定义一个配置类加上@Configuration注解。如果非得用xml，那么可以用@ImportResource进行导入。配置类也不用写成一个，多个配置类可以使用多个@Configuration，也可以拆成多个使用@Import相互导入。\nxml一直被很多人吐槽，其实SpringBoot也是反对使用xml的。\n@SpringBootApplication注解已经包含了@EnableAutoConfiguration注解，会对配置进行自动导入，比如在classpath中有一个HSQLDB，如果没有手动配置数据库连接，那么SpringBoot会自动进行配置。一般只添加@SpringBootApplication就可以了，不需要再重复添加@EnableAutoConfiguration。\n通过启动应用时加上--debug可以查看有哪些自动配置类，也可以对自动配置类进行移除，比如：\n@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) public class MyApplication { } 还可以使用excludeName或spring.autoconfigure.exclude属性来指定要移除的自动配置类。\n依赖注入 SpringBoot可以添加@ComponentScan来自动查找bean，比如@Component、@Service、@Repository、@Controller会被自动注册为Spring Beans。@SpringBootApplication已经包含了@ComponentScan，所以不需要重复添加。\n比如@Service使用构造器来包含一个bean：\n@Service public class MyAccountService implements AccountService { private final RiskAssessor riskAssessor; public MyAccountService(RiskAssessor riskAssessor) { this.riskAssessor = riskAssessor; } // ... } 如果有多个构造器，那么可以使用@Autowired让Spring来自动注入：\n@Service public class MyAccountService implements AccountService { private final RiskAssessor riskAssessor; private final PrintStream out; @Autowired public MyAccountService(RiskAssessor riskAssessor) { this.riskAssessor = riskAssessor; this.out = System.out; } public MyAccountService(RiskAssessor riskAssessor, PrintStream out) { this.riskAssessor = riskAssessor; this.out = out; } // ... } @SpringBootApplication 一般在main方法上面会有这个注解，它实际上包含了3个注解：\n@EnableAutoConfiguration @ComponentScan @SpringBootConfiguration @SpringBootApplication // same as @SpringBootConfiguration @EnableAutoConfiguration // @ComponentScan public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } 如果只想包含其中部分注解，那么可以不用@SpringBootApplication：\n@SpringBootConfiguration(proxyBeanMethods = false) @EnableAutoConfiguration @Import({ SomeConfiguration.class, AnotherConfiguration.class }) public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } 启动方式 ①IDEA使用Run as Java Application。\n②命令行启动jar包。\n$ java -jar target/myapplication-0.0.1-SNAPSHOT.jar 开启远程调式\n$ java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n \\ -jar target/myapplication-0.0.1-SNAPSHOT.jar ③maven\n$ mvn spring-boot:run 参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000003-spring%E5%85%B3%E4%BA%8Espringboot%E5%BC%80%E5%8F%91%E7%9A%84%E6%9B%B4%E5%A4%9A%E7%BB%86%E8%8A%82/","summary":"在上篇文章《开发你的第一个SpringBoot应用》已经对SpringBoot基本开发流程有了大体了解，本文将继续对SpringBoot官网进行学习，发现关于SpringBoot开发的更多细节。","title":"关于SpringBoot开发的更多细节"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n什么是HAR包？ HAR（HTTP Archive format），是一种JSON格式的存档格式文件，通用扩展名为 .har。\nHAR包是JSON格式的，打开后，重点关注entries里面的request和response，包含了请求和响应信息。\n流量录制 怎么获取HAR包呢？可以网上搜索方法，浏览器F12、抓包工具（Charles、Proxyman等）都可以将HTTP请求导出为HAR包。\n生产级别的流量录制很复杂，想要了解的话可以参考阿里开源项目：https://github.com/alibaba/jvm-sandbox-repeater\n回放对比 本文重点介绍在导出HAR包后，怎么通过Python来实现回放对比。\n使用介绍 一、将HAR包转换为pytest用例\nhar_file为har包路径，profile配置开启回放，调用Har.har2case()方法将HAR包转换为pytest用例。\n转换后会生成：\ndemo_test.py 与HAR同名的pytest用例文件 demo-replay-diff 对比结果目录，暂时为空 sqlite.db 存储HAR包响应数据，标记为expect 二、执行pytest用例\nfrom tep.libraries.Diff import Diff from tep.libraries.Sqlite import Sqlite def test(HTTPRequestKeyword, BodyKeyword): var = {\u0026#39;caseId\u0026#39;: \u0026#39;023f387db398504889e0afb8dd1bc99d\u0026#39;, \u0026#39;requestOrder\u0026#39;: 1, \u0026#39;diffDir\u0026#39;: \u0026#39;/Users/wanggang424/Desktop/PycharmProjects/tep/tests/demo/case/har/demo-replay-diff\u0026#39;} url = \u0026#34;https://postman-echo.com/get?foo1=HDnY8\u0026amp;foo2=34.5\u0026#34; headers = {\u0026#39;Host\u0026#39;: \u0026#39;postman-echo.com\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;HttpRunnerPlus\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip\u0026#39;} ro = HTTPRequestKeyword(\u0026#34;get\u0026#34;, url=url, headers=headers) # user_defined_var = ro.response.jsonpath(\u0026#34;$.jsonpath\u0026#34;) assert ro.response.status_code \u0026lt; 400 Sqlite.record_actual((ro.response.text, var[\u0026#34;caseId\u0026#34;], var[\u0026#34;requestOrder\u0026#34;], \u0026#34;GET\u0026#34;, \u0026#34;https://postman-echo.com/get?foo1=HDnY8\u0026amp;foo2=34.5\u0026#34;), var) url = \u0026#34;https://postman-echo.com/post\u0026#34; headers = {\u0026#39;Host\u0026#39;: \u0026#39;postman-echo.com\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Go-http-client/1.1\u0026#39;, \u0026#39;Content-Length\u0026#39;: \u0026#39;28\u0026#39;, \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json; charset=UTF-8\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip\u0026#39;, \u0026#39;sails.sid\u0026#39;: \u0026#39;s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk\u0026#39;} body = r\u0026#34;\u0026#34;\u0026#34;{\u0026#34;foo1\u0026#34;:\u0026#34;HDnY8\u0026#34;,\u0026#34;foo2\u0026#34;:12.3}\u0026#34;\u0026#34;\u0026#34; ro = BodyKeyword(body) # ro = BodyKeyword(body, {\u0026#34;$.jsonpath\u0026#34;: \u0026#34;value\u0026#34;}) body = ro.data ro = HTTPRequestKeyword(\u0026#34;post\u0026#34;, url=url, headers=headers, json=body) # user_defined_var = ro.response.jsonpath(\u0026#34;$.jsonpath\u0026#34;) assert ro.response.status_code \u0026lt; 400 Sqlite.record_actual((ro.response.text, var[\u0026#34;caseId\u0026#34;], var[\u0026#34;requestOrder\u0026#34;], \u0026#34;POST\u0026#34;, \u0026#34;https://postman-echo.com/post\u0026#34;), var) url = \u0026#34;https://postman-echo.com/post\u0026#34; headers = {\u0026#39;Host\u0026#39;: \u0026#39;postman-echo.com\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;Go-http-client/1.1\u0026#39;, \u0026#39;Content-Length\u0026#39;: \u0026#39;20\u0026#39;, \u0026#39;Content-Type\u0026#39;: \u0026#39;application/x-www-form-urlencoded; charset=UTF-8\u0026#39;, \u0026#39;Cookie\u0026#39;: \u0026#39;sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip\u0026#39;, \u0026#39;sails.sid\u0026#39;: \u0026#39;s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw\u0026#39;} ro = HTTPRequestKeyword(\u0026#34;post\u0026#34;, url=url, headers=headers, data=body) # user_defined_var = ro.response.jsonpath(\u0026#34;$.jsonpath\u0026#34;) assert ro.response.status_code \u0026lt; 400 Sqlite.record_actual((ro.response.text, var[\u0026#34;caseId\u0026#34;], var[\u0026#34;requestOrder\u0026#34;], \u0026#34;POST\u0026#34;, \u0026#34;https://postman-echo.com/post\u0026#34;), var) Diff.make(var[\u0026#34;caseId\u0026#34;], var[\u0026#34;diffDir\u0026#34;]) 执行时会记录实际请求响应，存入sqlite数据库，标记为actual。\n同时进行JSON字段对比和文本对比，分别生成字段对比.txt和文本对比.html。\n原理解析 源码：https://github.com/dongfanger/tep.git\n一、转换\n通过haralyzer库解析HAR包，获取到request和response，再拼装成pytest用例。\n实现文件：tep/libraries/Har.py\ndef _make_case(self): var = self._prepare_var() steps = self._prepare_steps() content = Har.TEMPLATE.format(var=var, steps=steps) if self.replay: content = Har.TEMPLATE_IMPORT_REPLAY + content if not os.path.exists(self.replay_dff_dir): os.makedirs(self.replay_dff_dir) content += Har.TEMPLATE_DIFF with open(self.case_file, \u0026#34;w\u0026#34;) as f: f.write(content) def _prepare_step(self, entry) -\u0026gt; list: logger.info(\u0026#34;{} {} Convert step\u0026#34;, entry.request.method, entry.request.url) step = Step() self._make_request_method(step, entry) self._make_request_url(step, entry) self._make_request_headers(step, entry) self._make_request_body(step, entry) self._make_before_param(step, entry) self._make_after_extract(step, entry) self._make_after_assert(step, entry) if self.replay: self._make_after_replay(step, entry) self._save_replay(step, entry) # Save replay data to sqlite return self._make_statement(step, entry) def _request_param(self, step, entry) -\u0026gt; str: param = \u0026#34;\u0026#34; mime_type = entry.request.mimeType b = \u0026#39;data=body\u0026#39; if mime_type and mime_type.startswith(\u0026#34;application/x-www-form-urlencoded\u0026#34;) else \u0026#39;json=body\u0026#39; if step.request.method == \u0026#34;GET\u0026#34;: param = \u0026#39;\u0026#34;get\u0026#34;, url=url, headers=headers, params=body\u0026#39; if step.request.body else \u0026#39;\u0026#34;get\u0026#34;, url=url, headers=headers\u0026#39; if step.request.method == \u0026#34;POST\u0026#34;: param = \u0026#39;\u0026#34;post\u0026#34;, url=url, headers=headers, \u0026#39; param += b if step.request.method == \u0026#34;PUT\u0026#34;: param = \u0026#39;\u0026#34;put\u0026#34;, url=url, headers=headers, \u0026#39; param += b if step.request.method == \u0026#34;DELETE\u0026#34;: param = \u0026#39;\u0026#34;delete\u0026#34;, url=url, headers=headers\u0026#39; if self.profile.get(\u0026#34;http2\u0026#34;, False): param += \u0026#39;, http2=True\u0026#39; return param 转换过程中，将响应text存入sqlite数据库中：\ndef _save_replay(self, step, entry): Sqlite.create_table_replay() data = ( self.case_id, self.request_order, step.request.method, step.request.url, self._decode_text(entry) ) Sqlite.insert_into_replay_expect(data) self.request_order += 1 二、存储\n通过sqlite数据库存储。\n实现文件：tep/libraries/Sqlite.py\nclass Sqlite: DB_FILE = \u0026#34;sqlite3.db\u0026#34; @staticmethod def execute(sql: str, data: tuple = None): os.chdir(Config.BASE_DIR) conn = sqlite3.connect(Sqlite.DB_FILE) if data: conn.execute(sql, data) else: conn.execute(sql) conn.commit() conn.close() @staticmethod def create_table_replay(): Sqlite.execute(\u0026#34;\u0026#34;\u0026#34;CREATE TABLE IF NOT EXISTS replay (id INTEGER PRIMARY KEY AUTOINCREMENT, case_id TEXT NOT NULL, request_order INTEGER NOT NULL, method TEXT NOT NULL, url TEXT NOT NULL, expect TEXT NOT NULL, actual TEXT)\u0026#34;\u0026#34;\u0026#34;) @staticmethod def insert_into_replay_expect(data: tuple): if not Sqlite.is_replay_existed(data): Sqlite.execute(\u0026#34;INSERT INTO replay(case_id, request_order, method, url, expect) VALUES (?, ?, ?, ?, ?)\u0026#34;, data) 三、记录\n通过profile开关控制是否开启回放，对比开启前后用例差异：\n开启回放，是在每个步骤后置中，添加了Sqlite记录响应text，并在最后执行Diff。\n记录实现文件：tep/libraries/Sqlite.py\n@staticmethod def update_replay_actual(data: tuple): Sqlite.execute(\u0026#34;UPDATE replay SET actual = ? WHERE case_id = ? AND request_order = ? AND method = ? AND url = ?\u0026#34;, data) @staticmethod def record_actual(data: tuple, var: dict): Sqlite.update_replay_actual(data) var[\u0026#34;requestOrder\u0026#34;] += 1 四、对比\n做了2个对比，一是通过deepdiff库进行JSON字段对比，二是通过difflib库进行文本对比。\n实现文件：tep/libraries/Diff.py\n1、JSON字段对比，每个请求对比结果放入列表中，输出到TXT文本\n2、文本对比，从数据库取出expect和actual并格式化，所有响应text拼接到一个字符串进行对比，输出到HTML文件\n@staticmethod def make(case_id: str, diff_dir: str): results = Sqlite.get_expect_actual(case_id) diffs = [] expect_text = \u0026#34;\u0026#34; actual_text = \u0026#34;\u0026#34; for row in results: expect, actual, method, url = row diff = Diff.make_deepdiff(expect, actual) diffs.append(diff) expect_text += Diff._format_json_str(expect, method, url) actual_text += Diff._format_json_str(actual, method, url) diff_html = Diff.make_difflib(expect_text, actual_text) Diff._output_file(diffs, diff_html, diff_dir) @staticmethod def make_deepdiff(expect: str, actual: str) -\u0026gt; str: diff = DeepDiff(expect, actual) return str(diff) @staticmethod def make_difflib(lines1: str, lines2: str) -\u0026gt; str: diff = difflib.HtmlDiff().make_file(lines1.splitlines(), lines2.splitlines()) return diff @staticmethod def _format_json_str(text: str, method: str, url: str) -\u0026gt; str: s = json.dumps(json.loads(text), indent=4, ensure_ascii=False) s = \u0026#34;{} {}\\n{}\\n\\n\u0026#34;.format(method, url, s) return s @staticmethod def _output_file(diffs: list, diff_html: str, diff_dir: str): with open(os.path.join(diff_dir, Diff.DIFF_TXT_FILE), \u0026#34;w\u0026#34;) as f: f.write(\u0026#34;\\n\\n\u0026#34;.join(diffs)) styled_html = f\u0026#34;\u0026lt;style\u0026gt;td {{width: 50%;}}\u0026lt;/style\u0026gt;\\n{diff_html}\u0026#34; with open(os.path.join(diff_dir, Diff.DIFF_HTML_FILE), \u0026#34;w\u0026#34;) as f: f.write(styled_html) 流量录制回放已经成为一种重要的测试手段，既可以快速生成自动化用例，也可以回放对比开发和线上差异，学习起来吧。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%B5%81%E9%87%8F%E5%9B%9E%E6%94%BE/000003-%E5%BD%95%E5%88%B6%E5%9B%9E%E6%94%BE%E5%9F%BA%E4%BA%8Ehar%E5%8C%85%E7%9A%84%E6%B5%81%E9%87%8F%E5%BD%95%E5%88%B6%E5%9B%9E%E6%94%BE/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"什么是har包\"\u003e什么是HAR包？\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eHAR（HTTP Archive format）\u003c/code\u003e，是一种\u003ccode\u003eJSON\u003c/code\u003e格式的存档格式文件，通用扩展名为 \u003ccode\u003e.har\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8f720806d7c44e50810f54a676360e7d.png\"\u003e\u003c/p\u003e\n\u003cp\u003eHAR包是JSON格式的，打开后，重点关注\u003ccode\u003eentries\u003c/code\u003e里面的\u003ccode\u003erequest\u003c/code\u003e和\u003ccode\u003eresponse\u003c/code\u003e，包含了请求和响应信息。\u003c/p\u003e\n\u003ch2 id=\"流量录制\"\u003e流量录制\u003c/h2\u003e\n\u003cp\u003e怎么获取HAR包呢？可以网上搜索方法，浏览器F12、抓包工具（Charles、Proxyman等）都可以将HTTP请求导出为HAR包。\u003c/p\u003e\n\u003cp\u003e生产级别的流量录制很复杂，想要了解的话可以参考阿里开源项目：https://github.com/alibaba/jvm-sandbox-repeater\u003c/p\u003e\n\u003ch2 id=\"回放对比\"\u003e回放对比\u003c/h2\u003e\n\u003cp\u003e本文重点介绍在导出HAR包后，怎么通过Python来实现回放对比。\u003c/p\u003e\n\u003ch3 id=\"使用介绍\"\u003e使用介绍\u003c/h3\u003e\n\u003cp\u003e一、将HAR包转换为pytest用例\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fa6977fdad54413b91359c8a37fc111a.png\"\u003e\u003c/p\u003e\n\u003cp\u003ehar_file为har包路径，profile配置开启回放，调用Har.har2case()方法将HAR包转换为pytest用例。\u003c/p\u003e\n\u003cp\u003e转换后会生成：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003edemo_test.py 与HAR同名的pytest用例文件\u003c/li\u003e\n\u003cli\u003edemo-replay-diff 对比结果目录，暂时为空\u003c/li\u003e\n\u003cli\u003esqlite.db 存储HAR包响应数据，标记为expect\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e二、执行pytest用例\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etep.libraries.Diff\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e Diff\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etep.libraries.Sqlite\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e Sqlite\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eHTTPRequestKeyword\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e BodyKeyword\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    var \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;caseId\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;023f387db398504889e0afb8dd1bc99d\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;requestOrder\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;diffDir\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;/Users/wanggang424/Desktop/PycharmProjects/tep/tests/demo/case/har/demo-replay-diff\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://postman-echo.com/get?foo1=HDnY8\u0026amp;foo2=34.5\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    headers \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Host\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;postman-echo.com\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;User-Agent\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;HttpRunnerPlus\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Accept-Encoding\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;gzip\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ro \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e HTTPRequestKeyword\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# user_defined_var = ro.response.jsonpath(\u0026#34;$.jsonpath\u0026#34;)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e ro\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eresponse\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Sqlite\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erecord_actual\u003cspan style=\"color:#1f2328\"\u003e((\u003c/span\u003ero\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eresponse\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etext\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;caseId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;requestOrder\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;GET\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://postman-echo.com/get?foo1=HDnY8\u0026amp;foo2=34.5\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://postman-echo.com/post\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    headers \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Host\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;postman-echo.com\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;User-Agent\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Go-http-client/1.1\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Content-Length\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;28\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Content-Type\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;application/json; charset=UTF-8\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Cookie\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Accept-Encoding\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;gzip\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;sails.sid\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003er\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;{\u0026#34;foo1\u0026#34;:\u0026#34;HDnY8\u0026#34;,\u0026#34;foo2\u0026#34;:12.3}\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ro \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e BodyKeyword\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# ro = BodyKeyword(body, {\u0026#34;$.jsonpath\u0026#34;: \u0026#34;value\u0026#34;})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e ro\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003edata\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ro \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e HTTPRequestKeyword\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# user_defined_var = ro.response.jsonpath(\u0026#34;$.jsonpath\u0026#34;)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e ro\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eresponse\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Sqlite\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erecord_actual\u003cspan style=\"color:#1f2328\"\u003e((\u003c/span\u003ero\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eresponse\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etext\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;caseId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;requestOrder\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;POST\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://postman-echo.com/post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://postman-echo.com/post\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    headers \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Host\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;postman-echo.com\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;User-Agent\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Go-http-client/1.1\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Content-Length\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;20\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Content-Type\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;application/x-www-form-urlencoded; charset=UTF-8\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Cookie\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e%2F\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003eAw\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;Accept-Encoding\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;gzip\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;sails.sid\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e%2F\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003eAw\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ro \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e HTTPRequestKeyword\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eheaders\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e data\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# user_defined_var = ro.response.jsonpath(\u0026#34;$.jsonpath\u0026#34;)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e ro\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eresponse\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Sqlite\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003erecord_actual\u003cspan style=\"color:#1f2328\"\u003e((\u003c/span\u003ero\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eresponse\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003etext\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;caseId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;requestOrder\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;POST\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;https://postman-echo.com/post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e),\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Diff\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003emake\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003evar\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;caseId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e],\u003c/span\u003e var\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;diffDir\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e执行时会记录实际请求响应，存入sqlite数据库，标记为actual。\u003c/p\u003e","title":"基于HAR包的流量录制回放"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n针对“pytest搭建接口自动化框架”，谈谈对框架设计的当前认知。\n简约至上。选择pytest就是选择Python，Python的设计理念是Simple is better than complex，不能让初学者直接上手的框架设计，都是在反其道而行之。所谓具备编程思想的自动化框架，并不值得追求。\n原生用法。Beautiful is better than ugly，能不封装就不封装，不改变依赖库的函数声明，函数名、入参列表、返回类型。通过可省参数追加入参，通过装饰器添加代码，通过猴子补丁更改行为。\n数据用例一体。Flat is better than nested，平铺比嵌套更容易编写，阅读，维护。将数据放在用例文件中，在单个文件中编写用例。数据驱动时，可从外部读取。变量管理亦是如此。\npytest提供了测试框架的基础骨架，Python库提供了各式各样的组装零件，我们要做的是拼凑，搭建适用于接口自动化测试的框架。\n宜轻不宜重。挑选Python库，优先选择轻量级的，比如pytest-html既能满足使用需要，又能定制化样式，就不用安装依赖Java环境的Allure。比如Python内置logging就能打印日志，就没必要非得使用依赖visual c++的loguru。\n用例独立。用例相互之间没有依赖，随便拉出一条用例就能执行。多接口场景用例，把每个接口视为一个测试步骤，排列在用例里面。无上游依赖、出参稳定的接口抽取为公共函数。简单来说，用例可以只包含一个接口，也可以包含多个接口。接口可以写在用例里面，也可以写在用例外面作为公共函数，再导入到用例里面。接口参数不同验证不同场景，复制用例文件，命名为新用例。\n中文命名。用代码编写pytest，有个缺点是文件命名晦涩难懂。在“用例独立”这条设计原则之上，可以采用中文命名用例集（文件夹）和用例名称（文件名）。不存在用例相互依赖，就不需要import，代码中就不会出现中文，不影响代码执行和“专业性”。用中文写注释没问题，不要用中文作为对象名。\n标记不如目录。pytest支持marker给测试用例打标，执行时按标记筛选用例执行。用例多了以后，维护标记变得麻烦。将用例集按照某种特性分组，比如基础自动化用例集、每日巡检用例集、联调用例集。按目录维护用例，按目录批量执行用例。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000003-%E6%8E%A5%E5%8F%A3%E8%87%AA%E5%8A%A8%E5%8C%96%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%BC%AB%E8%B0%88v1.0/","summary":"针对“pytest搭建接口自动化框架”，谈谈对框架设计的当前认知。","title":"接口自动化框架设计漫谈V1.0"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n背景 在2022年7月6日我曾组织过一次开源测试平台开发报名，有50+人报名，到2023年1月28日项目暂停，如大家在Gittee看到的，除我以外，只有1个人真正提交了代码：奉孝。\n原因如其中一位同学所说：“感觉我们大部份还是缺少从0到1的经验，负责的内容有时候不要知道从哪下手，又少了点积极性”。描述得很贴切，可能刚开始动力很足，但因为缺少经验，不够积极，最后实践不了。说的很好，就是做不到。也很无奈。\n二次启动，分为2个阶段，第一个阶段是学习阶段，我带着大家一起把teprunner平台从0到1开发一遍，把需要掌握的知识先学一遍，保证有一定基础，挑战能够独立完成模块开发。第二个阶段是开发阶段，共同完成EasyPytest测试平台开发，学以致用，加深对前后端分离项目开发的理解，具有真实的平台开发实践。\n学习阶段 你都不维护了，我为什么还要学teprunner？Django没意思？我想学FastAPI？\nteprunner有一套完整教程，包括前后端源码、11篇电子书教程、2小时视频讲解，虽然我因为工作原因没有继续维护teprunner，但是它仍然非常适合初学者入门学习测试开发，掌握里面的知识，对于后续学习其他技术，都有很大帮助。我的技术提升除了二刷《流畅的Python》和编写了tep开源工具，最大的提升就是完成了teprunner测试平台开发，如果你也能自己动手完成一遍，一定有极大提升。\nDjango、Flask、FastAPI、SpringBoot，我认为从入门者角度来说，Django是最适合的，它属于开箱即用的框架，很多东西都给你准备好了，你用就行，避免刚开始就陷入环境搭建，三方库拼装、数据库维护的麻烦中，降低理解门槛和操作难度。学会了Django，再学其他的，也很简单，一通百通。刚开始学习，学哪个不是最重要的，最重要的是先学会一个，迈出第一步。以我的经验来说，学习成熟框架Django是个正确选择。\n开发阶段 EasyPytest的后端把Django切换为了FastAPI，掌握了Django，再对比FastAPI，就能对后端开发框架有更深入的理解。就像我学Python和Java一样，只学一门百思不得其解，对比一看，豁然开朗。目前已经完成了登录、首页、用户管理的开发，Gitee也有相应源码。未来的规划，是做teprunner的升级版，实现pytest平台化，80%工作本地开发脚本，20%工作交给平台编排运行。\n报名方式 添加微信cekaigang，备注参加平台开发，进群。\n现开启学习阶段报名：\n按需选择，10元/节，录播+直播集中答疑。报名后会获得一份录播+一次集中答疑直播+一个每节课源码。每节课源码跟GitHub上源码不同，只会包含当前这节课进度的源码，一步一步的看到测试平台是如何开发出来的。\n课表链接： https://eqgvpqzl6c.feishu.cn/mindnotes/bmncndM8oei2B0MIFE1RMrGuKjd\n开发阶段报名需至少参与一节学习课程，从优秀学员或积极学员中选择，时间另行通知。\n当然，如果你已经具备奉孝这样的技术能力和积极性，欢迎直接与我联系，请提供你的开源项目地址或在Gitee提交Pull Requests，以证明能够独立完成模块开发，直接参与EP平台共建。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/japi/000003-%E5%BC%80%E6%BA%90%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E5%BC%80%E5%8F%91%E6%9C%9F%E5%BE%85%E4%BD%A0%E7%9A%84%E5%8A%A0%E5%85%A5/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"背景\"\u003e背景\u003c/h1\u003e\n\u003cp\u003e在2022年7月6日我曾组织过一次开源测试平台开发报名，有50+人报名，到2023年1月28日项目暂停，如大家在Gittee看到的，除我以外，只有1个人真正提交了代码：奉孝。\u003c/p\u003e\n\u003cp\u003e原因如其中一位同学所说：“感觉我们大部份还是缺少从0到1的经验，负责的内容有时候不要知道从哪下手，又少了点积极性”。描述得很贴切，可能刚开始动力很足，但因为缺少经验，不够积极，最后实践不了。说的很好，就是做不到。也很无奈。\u003c/p\u003e\n\u003cp\u003e二次启动，分为2个阶段，第一个阶段是学习阶段，我带着大家一起把teprunner平台从0到1开发一遍，把需要掌握的知识先学一遍，保证有一定基础，挑战能够独立完成模块开发。第二个阶段是开发阶段，共同完成EasyPytest测试平台开发，学以致用，加深对前后端分离项目开发的理解，具有真实的平台开发实践。\u003c/p\u003e\n\u003ch1 id=\"学习阶段\"\u003e学习阶段\u003c/h1\u003e\n\u003cp\u003e你都不维护了，我为什么还要学teprunner？Django没意思？我想学FastAPI？\u003c/p\u003e\n\u003cp\u003eteprunner有一套完整教程，包括前后端源码、11篇电子书教程、2小时视频讲解，虽然我因为工作原因没有继续维护teprunner，但是它仍然非常适合初学者入门学习测试开发，掌握里面的知识，对于后续学习其他技术，都有很大帮助。我的技术提升除了二刷《流畅的Python》和编写了tep开源工具，最大的提升就是完成了teprunner测试平台开发，如果你也能自己动手完成一遍，一定有极大提升。\u003c/p\u003e\n\u003cp\u003eDjango、Flask、FastAPI、SpringBoot，我认为从入门者角度来说，Django是最适合的，它属于开箱即用的框架，很多东西都给你准备好了，你用就行，避免刚开始就陷入环境搭建，三方库拼装、数据库维护的麻烦中，降低理解门槛和操作难度。学会了Django，再学其他的，也很简单，一通百通。刚开始学习，学哪个不是最重要的，最重要的是先学会一个，迈出第一步。以我的经验来说，学习成熟框架Django是个正确选择。\u003c/p\u003e\n\u003ch1 id=\"开发阶段\"\u003e开发阶段\u003c/h1\u003e\n\u003cp\u003eEasyPytest的后端把Django切换为了FastAPI，掌握了Django，再对比FastAPI，就能对后端开发框架有更深入的理解。就像我学Python和Java一样，只学一门百思不得其解，对比一看，豁然开朗。目前已经完成了登录、首页、用户管理的开发，Gitee也有相应源码。未来的规划，是做teprunner的升级版，实现pytest平台化，80%工作本地开发脚本，20%工作交给平台编排运行。\u003c/p\u003e\n\u003ch1 id=\"报名方式\"\u003e报名方式\u003c/h1\u003e\n\u003cp\u003e添加微信\u003cstrong\u003ecekaigang\u003c/strong\u003e，备注参加平台开发，进群。\u003c/p\u003e\n\u003cp\u003e现开启\u003cstrong\u003e学习阶段\u003c/strong\u003e报名：\u003c/p\u003e\n\u003cp\u003e按需选择，10元/节，录播+直播集中答疑。报名后会获得一份录播+一次集中答疑直播+一个每节课源码。每节课源码跟GitHub上源码不同，只会包含当前这节课进度的源码，一步一步的看到测试平台是如何开发出来的。\u003c/p\u003e\n\u003cp\u003e课表链接： \u003ca href=\"https://eqgvpqzl6c.feishu.cn/mindnotes/bmncndM8oei2B0MIFE1RMrGuKjd\"\u003ehttps://eqgvpqzl6c.feishu.cn/mindnotes/bmncndM8oei2B0MIFE1RMrGuKjd\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e开发阶段报名需至少参与一节学习课程，从优秀学员或积极学员中选择，时间另行通知。\u003c/p\u003e\n\u003cp\u003e当然，如果你已经具备奉孝这样的技术能力和积极性，欢迎直接与我联系，请提供你的开源项目地址或在Gitee提交Pull Requests，以证明能够独立完成模块开发，直接参与EP平台共建。\u003c/p\u003e","title":"开源测试平台开发期待你的加入"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n温馨提示：开发指南，根据V2.0.0编写，不随版本更新维护，仅供入门参考。最新实现请结合教程和源码阅读，教程会在版本发布后维护。\n环境准备 安装Python，3.8以上版本 安装poetry包管理工具，pip install poetry 克隆代码，git clone ``https://github.com/dongfanger/tep 准备就绪，撸起袖子干！\n目录结构 dist poetry build生成目标文件，用于发布pypi tep 核心代码 tests 测试代码 utils 工具包 venv 虚拟环境 .gitignore 忽略上传git LICENSE 证书 poetry.lock 版本锁定 pyproject.toml 配置信息 README.md 说明文件 poetry命令 初始化：poetry init\n添加包：poetry add pytest\n移除包：poetry remove pytest\n安装包：poetry install --only main\n构建包：poetry build\n发布包：poetry publish\npoetry管理包信息可以在pyproject.toml文件中查看：\n[tool.poetry.dependencies] python = \u0026#34;^3.8\u0026#34; faker = \u0026#34;^4.1.1\u0026#34; urllib3 = \u0026#34;^2.0.7\u0026#34; requests = \u0026#34;^2.22.0\u0026#34; pyyaml = \u0026#34;^5.4.1\u0026#34; pytest-assume = \u0026#34;^2.4.2\u0026#34; loguru = \u0026#34;^0.4.1\u0026#34; fastapi = \u0026#34;^0.72.0\u0026#34; uvicorn = \u0026#34;^0.17.0\u0026#34; pydantic = \u0026#34;^1.9.0\u0026#34; pytest = \u0026#34;^7.1.1\u0026#34; pytest-xdist = \u0026#34;^3.1.0\u0026#34; filelock = \u0026#34;^3.8.2\u0026#34; jsonpath = \u0026#34;^0.82\u0026#34; pymysql = \u0026#34;^1.1.0\u0026#34; pytest-html = \u0026#34;^4.0.2\u0026#34; allure-pytest = \u0026#34;^2.13.2\u0026#34; allure-python-commons = \u0026#34;^2.13.2\u0026#34; 指定国内镜像：\n[[tool.poetry.source]] name = \u0026#34;tsinghua\u0026#34; priority = \u0026#34;default\u0026#34; url = \u0026#34;https://pypi.tuna.tsinghua.edu.cn/simple\u0026#34; 注册命令行：\n[tool.poetry.scripts] tep = \u0026#34;tep.cli:main\u0026#34; 添加插件以调用pytest hook：\n[tool.poetry.plugins.\u0026#34;pytest11\u0026#34;] \u0026#34;tep\u0026#34; = \u0026#34;tep.plugin:Plugin\u0026#34; 框架设计 框架内核是pytest，为框架提供了用例识别、组织运行、IDE集成等基础能力，以及pytest框架稳定性和强劲扩展能力。同时集成了requests等三方库，支持接口测试等。并实现了项目脚手架。\n关键字驱动是通过pytest fixture特性来实现的，主要借助它实现：①测试前后置处理，②无需import就能使用，③PyCharm语法提示。这是fixture函数相比于普通函数的优势。关键字分为定义层和实现层，定义层是关键字契约，实现层负责具体逻辑实现。\n适配层做了向下兼容，通过参数转换确保用例层使用的关键字，不会受迭代升级变化影响，使用者无感知，所有变化都有框架内部处理和兼容。\n在项目内通过conftest.py跟框架进行连接，比如路径查找，插件加载等，同时定义run.py执行入口。也可以在项目中自定义关键字。\n命令行实现 通过poetry注册在pyproject.toml\n[tool.poetry.scripts] tep = \u0026#34;tep.cli:main\u0026#34; tep/cli.py的main函数与之对接。\n#!/usr/bin/python ## encoding=utf-8 import argparse import sys from tep import __description__, __version__ from tep.scaffold import scaffold def main(): parser = argparse.ArgumentParser(description=__description__) parser.add_argument(\u0026#34;-v\u0026#34;, \u0026#34;--version\u0026#34;, dest=\u0026#34;version\u0026#34;, action=\u0026#34;store_true\u0026#34;, help=\u0026#34;show version\u0026#34;) parser.add_argument(\u0026#34;-s\u0026#34;, \u0026#34;--startproject\u0026#34;, metavar=\u0026#39;project_name\u0026#39;, type=str, help=\u0026#34;Create a new project with template structure\u0026#34;) parser.add_argument(\u0026#34;-venv\u0026#34;, dest=\u0026#34;create_venv\u0026#34;, action=\u0026#34;store_true\u0026#34;, help=\u0026#34;Create virtual environment in the project, and install tep\u0026#34;) if len(sys.argv) == 1: # tep parser.print_help() sys.exit(0) elif len(sys.argv) == 2: if sys.argv[1] in [\u0026#34;-v\u0026#34;, \u0026#34;--version\u0026#34;]: print(f\u0026#34;Current Version: V{__version__}\u0026#34;) print(r\u0026#34;\u0026#34;\u0026#34; ____o__ __o____ o__ __o__/_ o__ __o / \\ / \\ \u0026lt;| v \u0026lt;| v\\ \\o/ \u0026lt; \u0026gt; / \\ \u0026lt;\\ | | \\o/ o/ \u0026lt; \u0026gt; o__/_ |__ _\u0026lt;|/ | | | o \u0026lt;o\u0026gt; \u0026lt;o\u0026gt; \u0026lt;| | | / \\ / \\ _\\o__/_ / \\ \u0026#34;\u0026#34;\u0026#34;) elif sys.argv[1] in [\u0026#34;-h\u0026#34;, \u0026#34;--help\u0026#34;]: parser.print_help() elif sys.argv[1] in [\u0026#34;-s\u0026#34;, \u0026#34;--startproject\u0026#34;]: parser.print_help() sys.exit(0) args = parser.parse_args() if sys.argv[1] in [\u0026#34;-s\u0026#34;, \u0026#34;--startproject\u0026#34;]: scaffold(args) 通过argparse库实现命名行参数。判断是-s时，调用scaffold(args)创建脚手架。\n脚手架实现 tep/scaffold.py 创建文件夹和创建文件：\ndef create_folder(path): os.makedirs(path) msg = f\u0026#34;Created folder: {path}\u0026#34; print(msg) def create_file(path, file_content=\u0026#34;\u0026#34;): with open(path, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: f.write(file_content) msg = f\u0026#34;Created file: {path}\u0026#34; print(msg) 根据文件内容，通过字符串填充。\n识别到-venv参数时创建虚拟环境：\nif Config.CREATE_ENV: # Create Python virtual Environment os.chdir(project_name) print(\u0026#34;\\nCreating virtual environment\u0026#34;) os.system(\u0026#34;python -m venv .venv\u0026#34;) print(\u0026#34;Created virtual environment: .venv\u0026#34;) # Install tep in the Python virtual Environment print(\u0026#34;Installing tep\u0026#34;) if platform.system().lower() == \u0026#39;windows\u0026#39;: os.chdir(\u0026#34;.venv\u0026#34;) os.chdir(\u0026#34;Scripts\u0026#34;) os.system(\u0026#34;pip install tep\u0026#34;) elif platform.system().lower() == \u0026#39;linux\u0026#39;: os.chdir(\u0026#34;.venv\u0026#34;) os.chdir(\u0026#34;bin\u0026#34;) os.system(\u0026#34;pip install tep\u0026#34;) 关键字实现 tep/keywords目录下，定义在api.py，实现在impl里面：\napi是关键字契约，以HTTPRequestKeyword为例：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def HTTPRequestKeyword(): def _function(*args, **kwargs) -\u0026gt; Result: method, url, kwargs = Args.parse([\u0026#34;method\u0026#34;, \u0026#34;url\u0026#34;], args, kwargs) return HTTPRequestImpl(method, url, **kwargs) return _function 关键字是一个fixture函数，在函数内部定义了另外一个函数，然后把内部函数的函数名return了，当调用这个fixture函数时，使用使用的是fixture的return，也就是内部函数名，就相当于是在调内部函数了。这是pytest fixture的特性，不用管为什么，就这么用就对了。\napi也是适配层，在内部函数中，对参数做了转换，用到了Args类：\nclass Args: @classmethod def parse(cls, fields: list, args: tuple, kwargs: dict) -\u0026gt; tuple: # Parse fixed args results = [] for i, field in enumerate(fields): if i \u0026lt; len(args): results.append(args[i]) else: value = kwargs.get(field, None) if value: results.append(value) # Args comes from kwargs, pop the key kwargs.pop(field) results.append(kwargs) return tuple(results) 根据fields，从args和kwargs中解析出入参，然后传入关键字实现，比如这里解析了method和url两个入参，传入HTTPRequestImpl函数。\n同时内部函数返回类型都是Result对象：\nclass Result: # Http request, response response: TepResponse = None # Any data data: Any = None # Connect database, connection conn = None # Connect database, cursor cursor = None 所有关键字的返回类型都封装在这里，基本类型就传入data，特殊类型就显式定义，比如接口请求响应就定义为response: TepResponse。确保后续如果关键字要新增返回值，也不会影响老代码。\n关键字实现在impl包里面，有的关键字实现复杂，有的关键字实现简单。\n复杂的：HTTPRequestImpl、BodyImpl\n简单的：UserDefinedVariablesImpl、DataImpl、DbcImpl\ndef UserDefinedVariablesImpl(*args, **kwargs) -\u0026gt; Result: file_path = os.path.join(Config().DATA_DIR, \u0026#34;UserDefinedVariables.yaml\u0026#34;) result = Result() result.data = File(file_path).load() return result 篇幅有限，关键字实现细节请阅读源码。所有关键字都在tests/demo/case编写了测试代码：\n参数化实现 先看测试代码，tests/demo/case/test_body.py：\nimport json from loguru import logger def test(BodyKeyword): body = r\u0026#34;\u0026#34;\u0026#34;{\u0026#34;id\u0026#34;:1,\u0026#34;param\u0026#34;:\u0026#34;[{\\\u0026#34;page\\\u0026#34;: 1, \\\u0026#34;pinList\\\u0026#34;:[\\\u0026#34;cekaigang\\\u0026#34;]}]\u0026#34;,\u0026#34;ext1\u0026#34;:{\u0026#34;a\u0026#34;:1,\u0026#34;b\u0026#34;:1},\u0026#34;ext2\u0026#34;:[1,1,1],\u0026#34;ext3\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;pytest\u0026#34;}}\u0026#34;\u0026#34;\u0026#34; ro = BodyKeyword(body, {\u0026#34;$.id\u0026#34;: 9, \u0026#34;$.param[0].page\u0026#34;: 9, \u0026#34;$.param[0].pinList[0]\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;$.ext1.a\u0026#34;: 9, \u0026#34;$.ext2[0]\u0026#34;: 9, \u0026#34;$.ext2[2]\u0026#34;: 9, \u0026#34;$.ext3.name\u0026#34;: \u0026#34;tep\u0026#34;}) body = ro.data logger.info(json.dumps(body, ensure_ascii=False)) 将JSON字符串按照JSONPath匹配后修改值。\nJMeter是直接在字符串中通过${}这种语法来做的，在写Python代码时这样做会有点复杂，难以处理。比如，可以用format或者f-string来做，如果%s和{}跟JSON内容不冲突是可以的，冲突了就参数化失败了。所以这里采用JSONPath来实现。\ntep/keywords/impl/BodyImpl.py，比较复杂，实现思路：\nJSONPath转换为字典中括号取值 递归遍历JSON，如果识别到是str类型，那么尝试转换为JSON继续遍历 遍历到最后一层时，将值进行替换 #!/usr/bin/python ## encoding=utf-8 import json import re from typing import Any from tep.libraries.Result import Result def BodyImpl(json_str: str, expr: dict) -\u0026gt; Result: json_obj = json.loads(json_str) for json_path, value in expr.items(): _assign(json_obj, json_path, value) result = Result() result.data = json_obj return result def _jsonpath_to_dict_expr(jsonpath: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; Input: $.store.book[0].title Output: \u0026#39;[\u0026#34;store\u0026#34;][\u0026#34;book\u0026#34;][0][\u0026#34;title\u0026#34;]\u0026#39; \u0026#34;\u0026#34;\u0026#34; tokens = re.findall(r\u0026#39;\\.(\\w+)|\\[(\\d+)\\]\u0026#39;, jsonpath) expr = \u0026#39;\u0026#39; for token in tokens: if token[0]: expr += \u0026#39;[\u0026#34;{}\u0026#34;]\u0026#39;.format(token[0]) else: expr += \u0026#39;[{}]\u0026#39;.format(token[1]) return expr def _parse_dict_expr(expr: str) -\u0026gt; list: \u0026#34;\u0026#34;\u0026#34; Input: \u0026#39;[\u0026#34;store\u0026#34;][\u0026#34;book\u0026#34;][0][\u0026#34;title\u0026#34;]\u0026#39; Output: [\u0026#39;store\u0026#39;, \u0026#39;book\u0026#39;, 0, \u0026#39;title\u0026#39;] \u0026#34;\u0026#34;\u0026#34; tokens = re.findall(r\u0026#39;\\[\u0026#34;(.*?)\u0026#34;\\]|\\[(\\d+)\\]\u0026#39;, expr) result = [int(index) if index.isdigit() else name for name, index in tokens] return result def _nested_modify(json_obj: [dict, list], keys: list, value: Any, current_level: int = 0): if current_level == len(keys) - 1: json_obj[keys[current_level]] = value else: current_key = keys[current_level] # Nested string json {\u0026#34;id\u0026#34;: 1, \u0026#34;param\u0026#34;: \u0026#34;{\\\u0026#34;page\\\u0026#34;: 1}\u0026#34;} if isinstance(json_obj[current_key], str): # str to json current_value = json.loads(json_obj[current_key]) if isinstance(current_value, dict) or isinstance(current_value, list): nested_string_json_obj = current_value _nested_modify(nested_string_json_obj, keys[current_level + 1:], value) # json to str json_obj[current_key] = json.dumps(nested_string_json_obj, ensure_ascii=False) else: _nested_modify(json_obj[current_key], keys, value, current_level + 1) def _assign(json_obj: [dict, list], json_path: str, value: Any): dict_expr = _jsonpath_to_dict_expr(json_path) keys = _parse_dict_expr(dict_expr) _nested_modify(json_obj, keys, value) 执行入口 from tep.libraries.Run import Run if __name__ == \u0026#39;__main__\u0026#39;: settings = { \u0026#34;path\u0026#34;: [\u0026#34;test_demo.py\u0026#34;], # Path to run, relative path to case \u0026#34;report\u0026#34;: False, # Output test report or not \u0026#34;report_type\u0026#34;: \u0026#34;pytest-html\u0026#34; # \u0026#34;pytest-html\u0026#34; \u0026#34;allure\u0026#34; } Run(settings) 通过Run类实现：\nclass Run: def __init__(self, *args, **kwargs): os.system(Cmd(*args).pytest()) 也就是os.system执行命令。命令由Cmd类拼装：\nclass Cmd: template = \u0026#34;pytest -s {where_to_run} {tep_report}\u0026#34; def __init__(self, *args, **kwargs): settings = args[0] self.RUN_PATH = [os.path.join(Config().CASE_DIR, path) for path in settings[\u0026#34;path\u0026#34;]] self.RUN_REPORT = settings[\u0026#34;report\u0026#34;] self.RUN_REPORT_TYPE = settings[\u0026#34;report_type\u0026#34;] def pytest(self) -\u0026gt; str: cmd = self.template.format( where_to_run=\u0026#34; \u0026#34;.join(self.RUN_PATH), tep_report=self.tep_report() ) return cmd def tep_report(self) -\u0026gt; str: if self.RUN_REPORT: if self.RUN_REPORT_TYPE == \u0026#34;pytest-html\u0026#34;: return f\u0026#34;--html={Config().HTML_REPORT_PATH}.html --self-contained-html\u0026#34; elif self.RUN_REPORT_TYPE == \u0026#34;allure\u0026#34;: return \u0026#34;--tep-reports\u0026#34; return \u0026#34;\u0026#34; 根据settings解析出运行配置，拼装到pytest命令行。\n路径查找 做框架必须要解决的一个问题是，怎么在框架查找到项目路径。因为通过pip install安装后，框架代码是放在site-packages里面的：\n跟项目本地目录没在一块，框架要查找case、data、report目录就要先知道项目根目录。\ntep框架是基于pytest的，pytest会先加载conftest.py，所以在这个文件将项目根目录告诉框架。\nfrom tep.plugin import tep_plugins pytest_plugins = tep_plugins() tep_plugins()在tep/plugin.py中实现：\ndef tep_plugins(): \u0026#34;\u0026#34;\u0026#34; Must be placed at the top, execute first to initialize base dir \u0026#34;\u0026#34;\u0026#34; caller = inspect.stack()[1] Config.BASE_DIR = os.path.abspath(os.path.dirname(caller.filename)) plugins = _keyword_path() + _fixture_path() # +[other plugins] return plugins 通过inpect反查调用者，从而获取到conftest.py的路径，再查到项目根目录。再将路径存入Config类：\nclass Config: # Class variable initialize first BASE_DIR = \u0026#34;\u0026#34; # Constant CREATE_ENV = False # The temporary directory of the allure source file, which is a pile of JSON files, # will be deleted when generating HTML reports ALLURE_SOURCE_PATH = \u0026#34;.allure.source.temp\u0026#34; def __init__(self): # Instance variable initialize after class variable assigned self.CASE_DIR = os.path.join(self.BASE_DIR, \u0026#34;case\u0026#34;) self.DATA_DIR = os.path.join(self.BASE_DIR, \u0026#34;data\u0026#34;) self.REPORT_DIR = os.path.join(self.BASE_DIR, \u0026#34;report\u0026#34;) current_time = time.strftime(\u0026#34;%Y-%m-%d-%H-%M-%S\u0026#34;, time.localtime(time.time())) self.HTML_REPORT_PATH = os.path.join(self.REPORT_DIR, \u0026#34;report-\u0026#34; + current_time) Config类包含了tep框架本身的配置信息。\n需要特别注意类变量和实例变量的区别，这里将BASE_DIR定义为类变量，也就是一开始就初始化。而将CASE_DIR、DATA_DIR、REPORT_DIR定义为实例变量，一开始不初始化，等到类变量初始化以后，在实例化对象时赋值。也是就说，Config.BASE_DIR类变量赋值，Config().CASE_DIR实例为对象后取值。否则可能出现这样的问题：假如将CASE_DIR也定义为类变量，在某个地方先于tep_plugins()时调用了Config.CASE_DIR，那么此时BASE_DIR是空的，就拿不到项目路径。毕竟Python的import也会执行代码，然后是从上往下执行，保不齐哪里会出问题。\n为了代码健壮，一是按照类变量和实例变量分别定义，二是将tep_plugins()定义放在文件最上面。\nfixture识别 同样是在tep_plugins()中加载的，返回import路径列表传入conftest.py中的pytest_plugins，这是pytest语法，能加载到fixture。\nimport路径列表：\ndef _keyword_path() -\u0026gt; list: return [\u0026#34;tep.keywords.api\u0026#34;] def _fixture_path(): _fixture_dir = os.path.join(Config.BASE_DIR, \u0026#34;fixture\u0026#34;) paths = [] # 项目下的fixtures for root, _, files in os.walk(_fixture_dir): for file in files: if file.startswith(\u0026#34;fixture_\u0026#34;) and file.endswith(\u0026#34;.py\u0026#34;): full_path = os.path.join(root, file) import_path = full_path.replace(_fixture_dir, \u0026#34;\u0026#34;).replace(\u0026#34;\\\\\u0026#34;, \u0026#34;.\u0026#34;) import_path = import_path.replace(\u0026#34;/\u0026#34;, \u0026#34;.\u0026#34;).replace(\u0026#34;.py\u0026#34;, \u0026#34;\u0026#34;) paths.append(\u0026#34;fixture\u0026#34; + import_path) return paths 一个是tep自身路径tep.keywords.api模块，一个是项目路径fixture下以fixture_开头模块。\nAllure报告 在pyproject.toml中配置：\n[tool.poetry.plugins.\u0026#34;pytest11\u0026#34;] \u0026#34;tep\u0026#34; = \u0026#34;tep.plugin:Plugin\u0026#34; Plugin中就能写pytest hook：\nclass Plugin: @staticmethod def pytest_addoption(parser): \u0026#34;\u0026#34;\u0026#34; Allure test report, command line parameters \u0026#34;\u0026#34;\u0026#34; parser.addoption( \u0026#34;--tep-reports\u0026#34;, action=\u0026#34;store_const\u0026#34;, const=True, help=\u0026#34;Create tep allure HTML reports.\u0026#34; ) @staticmethod def pytest_configure(config): \u0026#34;\u0026#34;\u0026#34; Reference: https://github.com/allure-framework/allure-python/blob/master/allure-pytest/src/plugin.py In order to generate an allure source file for generating HTML reports \u0026#34;\u0026#34;\u0026#34; if _tep_reports(config): if os.path.exists(Config.ALLURE_SOURCE_PATH): shutil.rmtree(Config.ALLURE_SOURCE_PATH) test_listener = AllureListener(config) config.pluginmanager.register(test_listener) allure_commons.plugin_manager.register(test_listener) config.add_cleanup(cleanup_factory(test_listener)) clean = config.option.clean_alluredir file_logger = AllureFileLogger(Config.ALLURE_SOURCE_PATH, clean) # allure_source allure_commons.plugin_manager.register(file_logger) config.add_cleanup(cleanup_factory(file_logger)) @staticmethod def pytest_sessionfinish(session): \u0026#34;\u0026#34;\u0026#34; Generate an allure report after the test run ends \u0026#34;\u0026#34;\u0026#34; reports_path = os.path.join(Config.BASE_DIR, \u0026#34;reports\u0026#34;) if _tep_reports(session.config): if _is_master(session.config): # Generate reports only at the master node # Historical data from the latest report, filling in the allure trend chart if os.path.exists(reports_path): his_reports = os.listdir(reports_path) if his_reports: latest_report_history = os.path.join(reports_path, his_reports[-1], \u0026#34;history\u0026#34;) shutil.copytree(latest_report_history, os.path.join(Config.ALLURE_SOURCE_PATH, \u0026#34;history\u0026#34;)) os.system(f\u0026#34;allure generate {Config.ALLURE_SOURCE_PATH} -o {Config().HTML_REPORT_PATH} --clean\u0026#34;) shutil.rmtree(Config.ALLURE_SOURCE_PATH) pytest_addoption添加了--tep-reports参数。\npytest_configure生成allure源文件。\npytest_sessionfinish在测试结束后将源文件转成HTML报告。\n额外做了2个增强：一是根据历史报告填充趋势图，二是在pytest-xdist分布式执行时只生成一份报告。\n内部库 其他内部库一览。\nTepResponse，封装了requests.Response，添加了jsonpath方法\n#!/usr/bin/python ## encoding=utf-8 import jsonpath from requests import Response class TepResponse(Response): \u0026#34;\u0026#34;\u0026#34; Inherit on requests.Response, adding additional methods \u0026#34;\u0026#34;\u0026#34; def __init__(self, response): super().__init__() for k, v in response.__dict__.items(): self.__dict__[k] = v def jsonpath(self, expr: str): \u0026#34;\u0026#34;\u0026#34; Force the first value here for simple values If complex values are taken, it is recommended to use jsonpath native directly \u0026#34;\u0026#34;\u0026#34; return jsonpath.jsonpath(self.json(), expr)[0] File，读取YAML/JSON文件：\n#!/usr/bin/python ## encoding=utf-8 import json import os import yaml class File: def __init__(self, path: str): self.path = path def load(self) -\u0026gt; [dict, list]: file_type = self._file_type() if file_type in [\u0026#34;.yml\u0026#34;, \u0026#34;.yaml\u0026#34;, \u0026#34;.YML\u0026#34;, \u0026#34;YAML\u0026#34;]: return self._yaml_load() if file_type in [\u0026#34;.json\u0026#34;, \u0026#34;.JSON\u0026#34;]: return self._json_load() def _file_type(self) -\u0026gt; str: return os.path.splitext(self.path)[-1] def _yaml_load(self) -\u0026gt; [dict, list]: with open(self.path, encoding=\u0026#34;utf8\u0026#34;) as f: return yaml.load(f.read(), Loader=yaml.FullLoader) def _json_load(self) -\u0026gt; [dict, list]: with open(self.path, encoding=\u0026#34;utf8\u0026#34;) as f: return json.load(f) DB，执行数据库sql\nfrom loguru import logger class DB: @classmethod def pymysql_execute(cls, conn, cursor, sql): try: cursor.execute(sql) conn.commit() except Exception as e: logger.error(f\u0026#34;Database execute error: {e}\u0026#34;) conn.rollback() 数据库连接是在自定义关键字mysql_execute中实现的：\ntests/demo/fixture/fixture_mysql.py import pytest from tep.libraries.DB import DB from tep.libraries.Result import Result @pytest.fixture(scope=\u0026#34;session\u0026#34;) def mysql_execute(DbcKeyword): ro = DbcKeyword(host=\u0026#34;127.0.0.1\u0026#34;, port=3306, user=\u0026#34;root\u0026#34;, password=\u0026#34;12345678\u0026#34;, database=\u0026#34;sys\u0026#34;) conn = ro.conn def _function(sql: str) -\u0026gt; Result: cursor = conn.cursor() DB.pymysql_execute(conn, cursor, sql) ro = Result() ro.cursor = cursor return ro yield _function conn.close() # After test, close connection 这里就利用了fixture的前后置特性，yield前是测试前置操作，yield后是测试后置操作。测试前连接数据库，测试后关闭数据库连接。scope=\u0026quot;session\u0026quot;可以配置是整个会话期间都只连接一次，还是按其他维度进行连接和关闭。\nMock服务 tests/scripts/mock.py 使用FastAPI实现了简单后端服务，Mock从登录到下单接口：\nimport uvicorn from fastapi import FastAPI, Request app = FastAPI() @app.post(\u0026#34;/login\u0026#34;) async def login(req: Request): body = await req.json() if body[\u0026#34;username\u0026#34;] == \u0026#34;dongfanger\u0026#34; and body[\u0026#34;password\u0026#34;] == \u0026#34;123456\u0026#34;: return {\u0026#34;Cookie\u0026#34;: \u0026#34;de2e3ffu29\u0026#34;} return \u0026#34;\u0026#34; @app.get(\u0026#34;/searchSku\u0026#34;) def search_sku(req: Request): if req.headers.get(\u0026#34;Cookie\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and req.query_params.get(\u0026#34;skuName\u0026#34;) == \u0026#34;book\u0026#34;: return {\u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;2.3\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/addCart\u0026#34;) async def add_cart(req: Request): body = await req.json() if req.headers.get(\u0026#34;Cookie\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;skuId\u0026#34;] == \u0026#34;222\u0026#34;: return {\u0026#34;skuId\u0026#34;: \u0026#34;222\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;2.3\u0026#34;, \u0026#34;skuNum\u0026#34;: 3, \u0026#34;totalPrice\u0026#34;: \u0026#34;6.9\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/order\u0026#34;) async def order(req: Request): body = await req.json() if req.headers.get(\u0026#34;Cookie\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;skuId\u0026#34;] == \u0026#34;222\u0026#34;: return {\u0026#34;orderId\u0026#34;: \u0026#34;333\u0026#34;} return \u0026#34;\u0026#34; @app.post(\u0026#34;/pay\u0026#34;) async def pay(req: Request): body = await req.json() if req.headers.get(\u0026#34;Cookie\u0026#34;) == \u0026#34;de2e3ffu29\u0026#34; and body[\u0026#34;orderId\u0026#34;] == \u0026#34;333\u0026#34;: return {\u0026#34;success\u0026#34;: \u0026#34;true\u0026#34;} return \u0026#34;\u0026#34; if __name__ == \u0026#39;__main__\u0026#39;: uvicorn.run(\u0026#34;mock:app\u0026#34;, host=\u0026#34;127.0.0.1\u0026#34;, port=5000) 工具包 Pairwise.py，根据多个条件生成两两组合过滤后的结果集，适用于查询条件组合验证。\nimport copy import itertools from sys import stdout from loguru import logger def parewise(option: list) -\u0026gt; list: \u0026#34;\u0026#34;\u0026#34; Automatically generate composite use cases \u0026#34;\u0026#34;\u0026#34; cp = [] # Cartesian product s = [] # Split in pairs for x in eval(\u0026#39;itertools.product\u0026#39; + str(tuple(option))): cp.append(x) s.append([i for i in itertools.combinations(x, 2)]) logger.info(\u0026#39;Cartesian product:%s\u0026#39; % len(cp)) del_row = [] print_progress_bar(0) s2 = copy.deepcopy(s) for i in range(len(s)): # Match each line of use cases if (i % 100) == 0 or i == len(s) - 1: print_progress_bar(int(100 * i / (len(s) - 1))) t = 0 # Judge whether the pairwise splitting of each line of use cases appears in other lines for j in range(len(s[i])): flag = False for i2 in [x for x in range(len(s2)) if s2[x] != s[i]]: # Find the same column if s[i][j] == s2[i2][j]: t = t + 1 flag = True break # The same column was not found, so there\u0026#39;s no need to search for the remaining columns if not flag: break if t == len(s[i]): del_row.append(i) s2.remove(s[i]) res = [cp[i] for i in range(len(cp)) if i not in del_row] logger.info(\u0026#39;After filtering:%s\u0026#39; % len(res)) return res def print_progress_bar(i): c = int(i / 10) progress = \u0026#39;\\r %2d%% [%s%s]\u0026#39; a = \u0026#39;■\u0026#39; * c b = \u0026#39;□\u0026#39; * (10 - c) msg = progress % (i, a, b) stdout.write(msg) stdout.flush() if __name__ == \u0026#39;__main__\u0026#39;: pl = [[\u0026#39;M\u0026#39;, \u0026#39;O\u0026#39;, \u0026#39;P\u0026#39;], [\u0026#39;W\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;I\u0026#39;], [\u0026#39;C\u0026#39;, \u0026#39;E\u0026#39;]] res = parewise(pl) print() for x in res: print(x) SimplifyJSON.py，简化JSON为一行，因为tep是把JSON放文件里面的，为了看起来清爽，代码规范是尽量放一行，JSON可以使用此工具压缩。\nimport json def simplify_json(json_str: str) -\u0026gt; str: data = json.loads(json_str) return json.dumps(data, separators=(\u0026#39;,\u0026#39;, \u0026#39;:\u0026#39;)) if __name__ == \u0026#39;__main__\u0026#39;: json_str = r\u0026#34;\u0026#34;\u0026#34; { \u0026#34;orderId\u0026#34;: 1, \u0026#34;payAmount\u0026#34;: \u0026#34;0.2\u0026#34; } \u0026#34;\u0026#34;\u0026#34; print(simplify_json(json_str)) 压缩后如果JSON还特别长，可以格式化代码，PyCharm会根据宽度设置自动换行。如果觉得压缩了不好看，想查看JSON展开效果，使用json.cn网站更快。关于压缩JSON这点，也算是在代码阅读性和JSON阅读性之间做的妥协，可以视情况调整。\n本地调试 tests/demo就是一个示例项目，项目脚手架生成的项目是子集。该示例项目包含了所有关键字测试代码，工具测试代码，和其他测试场景代码。项目脚手架只包含最基础的结构文件。\n可以在示例项目中调试框架代码，它能正确识别到tep内各模块代码。\n如果想调试pip install安装后的效果，可以执行tests/scripts/install.py脚本：\n#!/usr/bin/python ## encoding=utf-8 import os import shutil import subprocess from tep import __version__ if __name__ == \u0026#39;__main__\u0026#39;: tep_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.chdir(tep_path) dist_path = os.path.join(tep_path, \u0026#34;dist\u0026#34;) if os.path.exists(dist_path): shutil.rmtree(dist_path) os.system(\u0026#34;poetry install --only main\u0026#34;) os.system(\u0026#34;poetry build\u0026#34;) proc = subprocess.Popen([\u0026#34;pip\u0026#34;, \u0026#34;uninstall\u0026#34;, \u0026#34;tep\u0026#34;], stdin=subprocess.PIPE) proc.communicate(input=\u0026#34;y\u0026#34;.encode()) os.chdir(r\u0026#34;/Users/wanggang888/Desktop/PycharmProjects/tep/venv/lib/python3.8/site-packages\u0026#34;) for dir_name in os.listdir(): if dir_name.startswith(\u0026#34;tep\u0026#34;): shutil.rmtree(dir_name) os.chdir(dist_path) os.system(f\u0026#34;pip install tep-{__version__}-py3-none-any.whl\u0026#34;) 它会自动执行poetry命令安装和构建，然后pip install安装dist目录下的包到Python环境中。接着就能调试cli、脚手架、路径查找等功能。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000003-%E5%BC%80%E6%BA%90%E6%A1%86%E6%9E%B6%E6%A1%86%E6%9E%B6v2.0.0%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/","summary":"\u003cstrong\u003e温馨提示：开发指南，根据V2.0.0编写，不随版本更新维护，仅供入门参考。最新实现请结合教程和源码阅读，教程会在版本发布后维护。\u003c/strong\u003e","title":"框架V2.0.0开发指南"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n连续调用两次，第一次提交成功，第二次会被驳回——串行重复，系统是防住了的。\n但这个BUG是在任务调度时触发的，任务高并发模式下，并行调用了提交申请接口，两个请求几乎同时进来，最终出现了两个都提交成功的申请单。\n一、幂等测试是什么 重复：连续请求、超时重试、消息重发\n并发：极短时间内，1毫秒，同时有多个请求\n重复和并发的区别在于，两个请求的时间间隔，重复的请求之间有间隔，并发的请求之间几乎没有间隔。\n很多代码只能防住重复，却防不了并发。在制定测试策略时，要考虑并发幂等的处理。\n二、两种实现方式的漏洞 方式1：先查后写的空窗 这是最经典的一种“假幂等”。下面用支付回调来举例，订单支付成功后给用户发优惠券。\ndef pay_callback(order_no): if db.get(order_no) is None: # 并发时两个请求都查到 None db.insert(order_no) grant_coupon(order_no) # 于是券发了两张 return \u0026#34;success\u0026#34; db.get(order_no)，两个并发请求都查到空，都会进到if分支，造成重复执行，幂等失效。支付成功后发了两张券。\n换成状态机也是一样的问题。数据库已经存在记录，靠状态值来判断该不该发券：\ndef pay_callback(order_no): record = db.query(\u0026#34;SELECT status FROM pay_record WHERE order_no = ?\u0026#34;, order_no) if record.status == \u0026#39;INIT\u0026#39;: # ← 应用层判断 db.execute(\u0026#34;UPDATE pay_record SET status = \u0026#39;PAID\u0026#39; WHERE order_no = ?\u0026#34;, order_no) grant_coupon(order_no) # 发券 两个并发请求同时读到\u0026rsquo;INIT\u0026rsquo;，都能通过if判断，都能执行完，券还是发了两张。\n根因是同一个：先查后写，查和写分成两步，中间有个时间空窗。并发的两个请求都挤在空窗里通过了检查，于是都往下执行。\n方式2：分布式锁的释放 def pay_callback(order_no): with lock(order_no): exists = db.get(order_no) is not None # 锁只盖住这一句 # ← 锁在这里就释放了! if not exists: db.insert(order_no) grant_coupon(order_no) # 发券在锁外面 return \u0026#34;success\u0026#34; 加了分布式锁，但是锁的粒度没有覆盖整个业务，两个并发请求还是都能执行完，也防不住幂等。\n三、幂等怎么测 幂等测试的本质，同一个请求，不管发几次、怎么发，实际结果都跟发一次一样。\n哪些场景涉及到幂等？\n串行重复请求，比如用户连续点击多次 并行请求 消息重试，超时或异常（最容易忽略） 有哪些幂等测试手段？\n相同参数连续调多次 使用多线程/JMeter并发压测 在“检查”和“执行”之间插延迟，把毫秒级竞争窗口加大 第1次超时或报错后再调 执行到一半中断再重试 重复发送相同的消息体 幂等标识的值边界，比如requestId空值/超长/特殊字符 幂等验证哪些结果？\n重复请求返回相同结果 结果数据不翻倍（重点） 不同幂等key视为不同请求 四、幂等处理的正确姿势 核心：数据库。\nRedis分布式锁会超时释放、进程崩溃丢锁、主从切换失效，只能减少冲突，无法保证真的幂等。\n真正的幂等是数据库的原子性。\n# 方式：把判断放进 SQL，一条语句的原子性 rows = db.execute(\u0026#34;\u0026#34;\u0026#34; UPDATE pay_record SET status = \u0026#39;PAID\u0026#39; WHERE order_no = ? AND status = \u0026#39;INIT\u0026#39; \u0026#34;\u0026#34;\u0026#34;, order_no) if rows == 0: return \u0026#34;已处理\u0026#34; # 别人改过了，我不是第一个 grant_coupon(order_no) # 只有改成功的那个才发券 return \u0026#34;success\u0026#34; 要说明的是，这条SQL解决的是并发下的重复执行——同一时刻只有一个请求能把INIT改成PAID，从根上杜绝了发两张券。\n但它没解决另一个问题：改状态成功、grant_coupon却失败了。这属于「后置动作失败」，要靠事务、本地消息表、或发券失败的补偿重试来兜底，跟幂等没关系。\n在做幂等测试时，多从并发角度考虑。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%90%8E%E7%AB%AF%E6%B5%8B%E8%AF%95/000003-%E5%B9%82%E7%AD%89%E6%B5%8B%E8%AF%95/","summary":"连续调用两次，第一次提交成功，第二次会被驳回——串行重复，系统是防住了的。","title":"幂等测试"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n成年人最大的底气是你拥有一技之长，人生才不会被动。从事测试开发岗位，需要精通一门编程语言，是核心，是大招，是招牌技能。我内心一直在Java和Python之间选择，反复横跳。\n在《2020年度总结，似乎没有什么大的长进，似乎也得到了一些收获》中写到： “Python 编程。我并不是没有编程基础，汇编语言、C 语言、C++、数据结构、算法导论是大学必修课，我自己学过 JavaScript、Java（大学有选修课我没报） 和 Golang。我的问题有 2 个，一个是同学对比下的挫败感导致对编程的畏惧，另一个是就业方向太多，我想的也太多，徘徊在十字路口，没有真正磨炼过。直到 Python 大火，我找到了适合我的路。它解决了我这 2 个问题，因为简单，所以我很轻松就能获得编程的成就感，Python 的生态虽然不是最好，但也还算不错。与其来回纠结，不如选择一条路走下去。精通 Python，也许能为我在工作上带来还没有出现过的那种兴奋。”\n在《2021非正式年度总结》中写到： “在任何行业都存在金字塔原理，从下往上，越往上门槛越高，人越少，但是所获更多。对于测试来说，功能测试人员无疑是最多的，然后是自动化性能之类会写代码的，在写代码中，肯定是会Python的人是最多的，会Java的人少。而我身边除了前面提到的两位同事，以前听说拿高薪，以及我在招聘网站看到高薪岗位的要求，都是要求会Java的。甚至我还在TesterHome上研究了一些大佬，这些大佬有9成都是会Java的。在国内，Java程序员是最多的，会Java的测试理所当然会更加吃香。忘了说，去初创公司的那位同事，这家公司的创始人是阿里P10。”\n很显然，最近这两三年，就是在犹豫中度过的。躁动的原因无非以下几点：\n1、Python简单，Java太难；\n2、只会Python好像还差点意思；\n3、想学Java一直没找到门路；\n直到我偶然遇见了它。\nJava官方教程 我花了2周时间，像看小说一样，迫不及待的把这个教程一口气看完了。最震撼的，反复品味的一个认知点是，Java8大数据类型：byte、short、int、long、float、double、boolean、char。它们都是小写。\nJava中有严格的大小写约定，大写字母开头的都是类名，基本类型都有对应的包装类，封装了更多对基本类型的方法，如Integer。\n还有2个特殊的小写关键字：void和array(数组)。而大写的String其实是个类，而不是基本类型。\n联想到Python的基本类型：Number(数字)、String(字符串)、List(列表)、Tuple(元组)、Set(集合)、Dictionary(字典)，一下就明白为啥Python是“万物皆对象”这句话的含义了。Python没有Java的元类型，Python的基本类型就已经是类维度的了。所以Python的变量都是指向的类的对象。Python中出现的int()、str()等小写的其实都是函数而已，不是基本类型。\n语言果然是相通的！打通了这一点，很多之前卡住的概念顺理成章的就融会贯通了。对象、类、接口、包、继承、集合、泛化、Lambda表达式、注解、异常等概念很轻松就吃下来了。最难理解的还是Stream流那一块，以后在实践中再慢慢掌握吧。\n另外一个感触很深的点是Java的包管理机制。它的命名是公司域名倒置如com.jd + 一级目录 + 二级目录 + 三级目录 + 以此类推的形式。Java提供了几个默认的包，比如java.lang，Java本身的很多类都在这个包里面，隐式已经import了，不需要再单独import。跟Python不一样的是，Python每个文件都是一个module，引用其他文件里面的对象必须import；而Java每个文件是一个class，文件夹是package，同一个package下面的文件，想引用其他文件里面的类不需要import。并且，Java只有相同层级的才是一个包，父文件夹和子文件夹不是同一个包，也就是说，子文件夹的class想引用父文件夹的class，还是需要import的。\n理解了这一层，再看公司后端的代码，就要清晰很多了。所谓的分层设计，都要用到Java的包管理机制。\nSpringBoot官方教程 刷完Java官方教程后，我又马不停蹄的刷了SpringBoot官方教程。说实话体验很不好，我印象最深刻的就是spring-boot-start开头的是SpringBoot官方提供的封装好的库，而以它结尾的是三方自定义的库。这在看pom.xml的时候能有眉目一点。后面的知识点很多都是各种配置，加上全英文，理解难度很大。不过还是囫囵吞枣式的完整刷了一遍，说不定万一以后想到了，有点印象，能帮助检索呢。\nPS：后来明白了，SpringBoot本身就是约定大于配置，帮助简化开发的。真正要学习的框架是Spring。\nJava全栈知识体系 这不是官方教程了，是大佬整理的博客。建议大家重点看下Spring栏目下的内容。\n我刷完了Spring、框架|中间件、架构3个栏目下的全部内容，大部分是略过，重点看了Spring演进过程、缓存、限流、降级和熔断、负载均衡几块内容。有三大收获：\n一、Spring是由bean构成的。 Spring中的bean相当于功能组件，我们通过组装这些bean来实现一个系统应用。IOC容器就是来存放这些bean的容器，IOC是控制反转，它的意思是，我们不需要自己用new来创建bean，而是由Spring来帮我们创建bean。把创建后的bean注入到程序代码里面，就叫做依赖注入，有3种方式：XML配置、代码配置、注解。理解到这个底层逻辑后，再往上看就能看懂设计理念了。Spring的演进过程是：J2EE (Java to 企业版) → Spring (SpringMVC是遵循MVC规范的Web开发框架) → SpringBoot (简化Spring开发)。\n二、接触Java才能拓展视野。 在看架构栏目的时候，好多概念是在学习Python时无法体会的，比如缓存、限流、降级和熔断、负载均衡、分布式系统、全局唯一ID、数据库Sharding等。这些都是很多公司的后端技术，了解这些技术，有助于进行更深层次的测试。\n三、要善于画图。 文章包含了很多图，流程图，结构图，时序图，架构图，各种图。也有淘宝、京东、闲鱼、美团等公司的案例分享文章，也是各种图。这说明在汇报时，图是最有表现力的一种形式，这是社会共识。我在快速浏览过程中，也是会优先看图，再看字。画图、会写PPT是很重要的一项软技能。\n回想起左耳朵耗子的那段话：“离开Java的世界，基本上离开了做架构的世界 注：我以为用Java适合做架构这事应该是常识了，但是评论中有很多人非常反对这个事。那我解释一下吧：首先，小型的项目用什么语言都行，爱用什么用什么。但是，真正的企业级架构就不一样了，其中并不仅仅只是RESTful API或RPC，还有各种配套设施和控制系统，比如：应用网关，服务发现、配置中心、健康检查、服务监控、服务治理（熔断、限流、幂等、重试、隔离、事务补偿）、Tracing监控、SOA/ESB、CQRS、EDA……这些东西在非Java的技术栈体系内，很难看到全貌，Java强大的生态环境，就是让你把注意力放到更高层次的架构和业务上来的。（千万不要觉得，整几个服务RPC一下，加个缓存，加个队列，就能叫架构，那只是系统集成罢了）”。\n以【努力成为一名测试架构师】为目标来看，精通Java是必经之路了。\n参考资料：\nJava官方教程 https://dev.java/learn/\nSpringBoot官方教程 https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#getting-started\nJava全栈知识体系 https://pdai.tech/\n别让自己“墙”了自己 https://coolshell.cn/articles/20276.html\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000003-%E8%BF%9B%E9%98%B6%E5%88%B7%E5%AE%8C3%E4%B8%AAjava%E6%95%99%E7%A8%8B%E5%90%8E%E4%B8%8D%E5%86%8D%E7%8A%B9%E8%B1%AB/","summary":"成年人最大的底气是你拥有一技之长，人生才不会被动。从事测试开发岗位，需要精通一门编程语言，是核心，是大招，是招牌技能。我内心一直在Java和Python之间选择，反复横跳。","title":"刷完3个Java教程后不再犹豫"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n编写测试用例，是我做测试工作最痛苦的一件事情，没有之一。最难受的是把测试点写成“标准”用例。先要写一个让人一目了然的用例标题，还要讲究什么主谓宾造句，曾经为了让标题好看，我还硬加“验证”2个字前缀。前提条件要把账号数据这些罗列出来。步骤要写清楚每一步干啥，预期也要具体化和量化，不能只写正确。文字写起来啰里啰嗦，写多了头都炸了。\n我承认，那些很详尽的测试用例是好的。但是我写不出来，也完全不想写。没有意义。还不如写代码来得痛快。可这是客观要求。只能硬着头皮干。向榜样用例学习。\n现在AI拯救了我。我已经3个月没有手写过用例了。以前花半天才能写完的用例，现在AI写好后，10分钟就优化成可用的了。甚至让AI写用例的SKILL，也是让AI写的。\nAI写用例关键是看写出来的效果好不好，这就要看采纳率。采纳率是100条用例，你采纳的用例是80条，那么采纳率就是80%，采纳率越高，生成的用例质量就越高。\n刚开始用最通用的方式，①prd、②trd、③代码diff，扔给AI让它写。prd和trd如果质量很差，会直接影响用例效果。\n这就需要建设知识库。知识是蒸馏来的，包括代码仓库、测试用例、自动化脚本等。知识库的设计可以采用分层模式，下图是阿里技术文章里面的研发视角知识库：\n测试的知识库更加侧重于测试分析、测试用例等。还有向量数据库、CRG、知识图谱等高级玩法。这些词语看不懂没关系，发给AI让它给你解释，直接让它来实现，我就是这么玩的，Vibe Coding全程不用写一句代码。\n知识库怎么用呢？在生成用例SKILL时使用。分析代码diff时阅读系统架构和系统功能，设计用例时阅读系统流程和业务规则，评估回归范围时参考历史用例。这些都让AI来做。\n做好以上这些，生成的用例有些需求采纳率很高，换个需求可能又牛头不对马嘴了。这时需要人工调优。不是我去手改用例，而是在对话框持续交互，XXX场景怎么设计的，把XXX场景的用例补充，用例描述不要涉及太多代码细节。如何提高生成的初始用例的采纳率和稳定性，仍然是一个持续解决的困难问题。 AI拯救了我，也代替我的工作，除了编写测试用例，以后还会自动执行测试。个人大胆预测，以后测试岗位会越来越少，现在有些公司已经撤掉了测试团队，有些公司已经让测试转开发了。\n但淘汰的是公司岗位，并不是淘汰的人。\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E6%B5%8B%E8%AF%95/000003-%E6%88%913%E4%B8%AA%E6%9C%88%E6%B2%A1%E6%89%8B%E5%86%99%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B%E4%BA%86%E4%BD%86%E6%88%91%E7%9A%84%E7%94%A8%E4%BE%8B%E9%87%87%E7%BA%B3%E7%8E%87%E6%98%AF80/","summary":"编写测试用例，是我做测试工作最痛苦的一件事情，没有之一。最难受的是把测试点写成“标准”用例。先要写一个让人一目了然的用例标题，还要讲究什么主谓宾造句，曾经为了让标题好看，我还硬加“验证”2个字前缀。前提条件要把账号数据这些罗列出来。步骤要写清楚每一步干啥，预期也要具体化和量化，不能只写正确。文字写起来啰里啰嗦，写多了头都炸了。","title":"我3个月没手写测试用例了，但我的用例采纳率是80%"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n如果没有这次全量数据对比工具，那么也许这个历史问题会继续隐藏着，直到发生线上事故才暴露出来，毕竟人工抽样验证发现的概率只有5.8%。\n背景是发票系统有18500个电子发票订单被财务系统驳回了，驳回原因是财务系统上线了全电发票需求，上线后电子发票枚举被误删，无法处理电子发票。需要我们发票系统对这18500电子发票订单，重新触发提票，让发票能正常开出来。也就是，我们需要刷数。刷数是个高危操作，极易引发线上问题。\n经验教训告诉我们，刷数虽然是一种处理线上问题的方法，但是也特别容易引起二次事故。对于刷数，我们需要像新需求一样对待，经过完备的需求分析、设计评审、代码评审。主要考虑以下3点：\n刷数范围，怎么筛选问题数据，评审过滤条件；\n刷数程序，怎么修复问题数据，评审代码逻辑；\n验证方法，怎么验证修复数据，分析测试场景；\n在刷数实施前，群里报备，周知相关方及相关人员。先试刷，验证无问题后，再全刷，最后验证问题数据已经得以修复。这一套刷数流程，通过加强前期评审，能很好地预防缺陷，增强刷数成功的信心。但是人工评估可能遗漏场景，可能对真实数据情况把握不全，刷数的关键还在于最后的数据验证。\n抽样验证还是全量验证，这是一个问题。抽样验证是人工随机挑几个数据进行验证，我们通常倾向于使用抽样验证，一是抽样验证是一种科学的有效的验证方法，虽然它存在一定概率的遗漏，但是很多时候是可以接受的风险；二是抽样验证也是无奈之举，找不到办法进行全量验证。我们遇到的困难是，数据存在ES中，批量把所有数据查出来很麻烦，也无法直接编写校验逻辑，全量验证似乎是不可能的。\n全量验证有2个思路：\n如果能直连库，那么先提数，再写程序对比；\n如果只能WEB页面查数，那么使用Python爬虫提数，再写程序对比；\n后者适用于我们的情况。ES能通过WEB页面查询数据，只要是WEB页面，即使有Cookie，也能爬取到接口数据。F12抓包到查询接口的URL、Cookie、入参后，使用Python的requests库可以爬取查询结果数据：\nurl = \u0026#39;http://xxx\u0026#39; headers = { \u0026#39;Cookie\u0026#39;: \u0026#39;xxx\u0026#39;, \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39; } response = request(\u0026#39;get\u0026#39;, url=url, headers=headers) result = response.json() 我们使用这种方式，传入订单号，查询到了申请单数据，以便进行对比校验逻辑。而订单号，研发打在了日志里，需要下载日志文件后，进行解析，可以使用Python切片：\norders = set() with open(\u0026#39;some.log\u0026#39;) as f: for line in f.read().splitlines(): if \u0026#39;xxx\u0026#39; in line: orders.add(line[line.index(\u0026#39;orderId=\u0026#39;):line.index(\u0026#39;,已执行\u0026#39;)].replace(\u0026#39;orderId=\u0026#39;, \u0026#39;\u0026#39;)) print(len(orders)) with open(\u0026#39;orders.txt\u0026#39;, \u0026#39;w\u0026#39;) as f: f.writelines(\u0026#39;,\u0026#39;.join(orders)) .index可以定位到关键词的索引，然后[:]切片获取指定内容。\n日志解析订单号+爬虫获取申请单+编写对比校验逻辑，全量数据对比工具就完成了。可是18500单，上万级别数据，要全部对比完，至少要几个小时。是时候使用多线程了。\n多线程第一步，拆解数据，将18500单拆成以100单为一组的列表：\ndef split_list(lst, size): \u0026#34;\u0026#34;\u0026#34; 将列表 lst 拆分成每份 size 个元素的子列表，并返回一个包含所有子列表的列表。 \u0026#34;\u0026#34;\u0026#34; return [lst[i:i + size] for i in range(0, len(lst), size)] 多线程第二步，队列提数，让多个线程依次从列表中取出数据，每个线程每次取不同的数据：\nimport threading ### 待处理的数据列表 data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ### 创建锁对象 lock = threading.Lock() ### 定义线程函数 def process_data(): global data while True: # 加锁 lock.acquire() # 如果列表为空，说明所有数据已被处理完毕，退出循环 if len(data) == 0: lock.release() break # 取出列表中的第一个数据 num = data.pop(0) # 释放锁 lock.release() # 对数据进行处理 print(\u0026#34;Processing data:\u0026#34;, num) ### 创建多个线程 threads = [] for i in range(5): t = threading.Thread(target=process_data) threads.append(t) ### 启动所有线程 for t in threads: t.start() ### 等待所有线程结束 for t in threads: t.join() print(\u0026#34;All data processed.\u0026#34;) 阻塞队列是通过加锁来实现的，每个线程在取数前先加锁，然后pop(0)取出列表中的第一个数据，再释放锁。上述程序修改①data②数据处理逻辑③线程数即可使用。\n对比工具使用多线程后，运行时间从小时级别降到了分钟级别。当天研发本来以为要跑很久，准备第二天再来看，就先撤了。我执着了一下多线程实现，在ChatGPT帮助下，很快就把结果跑出来。赶紧打电话摇人，让研发回来看问题，研发那时刚到家，掏出钥匙把门打开。在全量对比前，我们也都做了一轮抽样验证，均没有发现任何问题。18500单全量对比，发现有1064单存在问题，能抽样发现的概率只有5.8%。\n总结，分析这些问题原因：\n遗漏了1种数据情况，评估不到位\n未考虑到刷数环境影响，在预发环境刷数，上下游环境都是预发，可能跟线上版本不一样，尤其是做写操作时，格外需要注意\n刷数程序本身缺陷，这个缺陷隐藏在一段用了很多次刷数的历史代码里面，不是100%会导致问题\n可以发现，大数据量验证，人工无法百分百保证数据准确性，抽样检查，94.2%概率发现不了问题。最稳妥的办法，还是全量对比，让每条数据，都经过对比规则的检验。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/%E5%B0%8F%E5%B7%A5%E5%85%B7/000003-%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7%E4%B8%80%E6%AC%A1%E5%85%A8%E9%87%8F%E6%95%B0%E6%8D%AE%E5%AF%B9%E6%AF%94%E5%B7%A5%E5%85%B7%E5%8F%91%E7%8E%B0%E9%97%AE%E9%A2%98%E7%9A%84%E8%BF%87%E7%A8%8B%E4%B8%8E%E6%80%9D%E8%80%83/","summary":"如果没有这次全量数据对比工具，那么也许这个历史问题会继续隐藏着，直到发生线上事故才暴露出来，毕竟人工抽样验证发现的概率只有\u003cstrong\u003e5.8%\u003c/strong\u003e。","title":"一次全量数据对比工具发现问题的过程与思考"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPyCharm 是什么 PyCharm（读作“拍恰姆”）是 JetBrains 全家桶中的一员，专门用来写 Python 的：\n官方网址是：\nhttps://www.jetbrains.com/pycharm/\nfor Professional Developers，顾名思义，它是为专业开发者而生的。\n版本选择 PyCharm 有 2 个版本：\n左边的 Professional 是专业版，需要付（ po ）费（ jie ）。右边的 Community 是社区版，永久免费。一般社区版够用了。我用的是专业版，看着齐全。\n打开 PyCharm 安装完成后，打开：\n不需要导入配置，点击 OK 按钮：\n有 2 个主题，个人偏好左边这种，酷酷的。点击左下角 Skip Remaining and Set Defaults 跳过插件安装，进入主界面（专业版破解后才能看到这个界面，具体破解方法网上贼多）：\n首次打开，还没有创建项目，会出现引导页。选择 New Project：\n这是专业版的界面，社区版会简略一点。选择好存放目录后，还需要配置解释器。点击解释器右边的 ... ，切换到 System Interpreter，选择已经安装好的 Python：\nOK 后再点击 Create，来了：\n老手可以勾选 Tip of the Day 左小角 Don't show tips。\n界面说明一张图 运行结果 右上角的按钮区可以点击运行，代码编辑区域的行号那如果出现了绿色小箭头，也可以点击运行，甚至是代码编辑区里面右键，文件名右键，都可以运行！看看 PyCharm 想的多周到，生怕你不会运行。运行后结果会从下方弹出来：\n防止中文乱码两张图 hello 开篇我们用的是 Python 自带的 IDLE，现在试试在 PyCharm 中 say hello。先在项目目录中右键创建 .py 文件：\n输入代码：\nprint(\u0026#34;hello\u0026#34;) 在代码编辑器任意位置右键 Run：\nPython 同样会回应一句 hello：\n没错，是的，你已经会用 PyCharm 写 Python 了！\n小结 本文介绍了 PyCharm 的基本使用，更多技巧请阅读知乎问答 你有哪些想要分享的 PyCharm 使用技巧？，比我写的好太多了。如果使用过程中遇到问题，百度一般能搜出来答案。不行的话就找 Google，安装浏览器插件 ghelper 即可访问，谷歌浏览器和火狐浏览器都支持。还解决不了，有一个办法，就是加入我们的高质量核心交流“Python互助讨论群”，技术面前无大佬，无惧提问，我们都是一群相互取暖的小白，交流经验，共同进步，每周六分享PyCharm使用技巧等知识点，每周日一起刷LeetCode编程题。公众号后台回复”加群“即可。\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000003-python%E7%94%A8pycharm%E6%89%93%E4%B8%AA%E4%B8%93%E4%B8%9A%E7%9A%84%E6%8B%9B%E5%91%BC/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"pycharm-是什么\"\u003ePyCharm 是什么\u003c/h1\u003e\n\u003cp\u003ePyCharm（读作“拍恰姆”）是 JetBrains 全家桶中的一员，专门用来写 Python 的：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c44188b99ce64b79b8110b5ba73e8243.png\"\u003e\u003c/p\u003e\n\u003cp\u003e官方网址是：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.jetbrains.com/pycharm/\"\u003ehttps://www.jetbrains.com/pycharm/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b33f7efb2c65436e8377e120c06c370d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003efor Professional Developers\u003c/code\u003e，顾名思义，它是为专业开发者而生的。\u003c/p\u003e\n\u003ch1 id=\"版本选择\"\u003e版本选择\u003c/h1\u003e\n\u003cp\u003ePyCharm 有 2 个版本：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ef79b1a013e3402797f37c2960579179.png\"\u003e\u003c/p\u003e\n\u003cp\u003e左边的 Professional 是专业版，需要付（ po ）费（ jie ）。右边的 Community 是社区版，永久免费。一般社区版够用了。我用的是专业版，看着齐全。\u003c/p\u003e\n\u003ch1 id=\"打开-pycharm\"\u003e打开 PyCharm\u003c/h1\u003e\n\u003cp\u003e安装完成后，打开：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/95d96ab8ba794accbfb59f876e782a71.png\"\u003e\u003c/p\u003e\n\u003cp\u003e不需要导入配置，点击 OK 按钮：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c675af1e627548e8b982e54ada04e0fe.png\"\u003e\u003c/p\u003e\n\u003cp\u003e有 2 个主题，个人偏好左边这种，酷酷的。点击左下角 \u003ccode\u003eSkip Remaining and Set Defaults\u003c/code\u003e 跳过插件安装，进入主界面（专业版破解后才能看到这个界面，具体破解方法网上贼多）：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1aa1138889b04c4e81063482cd2519d4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e首次打开，还没有创建项目，会出现引导页。选择 \u003ccode\u003eNew Project\u003c/code\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/066c88f9d9a34ca59719f243ae14a12c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这是专业版的界面，社区版会简略一点。选择好存放目录后，还需要配置解释器。点击解释器右边的 \u003ccode\u003e...\u003c/code\u003e ，切换到 \u003ccode\u003eSystem Interpreter\u003c/code\u003e，选择已经安装好的 Python：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d8524f598a0849538fa8194535fd3d64.png\"\u003e\u003c/p\u003e\n\u003cp\u003eOK 后再点击 Create，来了：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a310612292d2450c85fdc30c81342d40.png\"\u003e\u003c/p\u003e\n\u003cp\u003e老手可以勾选 \u003ccode\u003eTip of the Day\u003c/code\u003e 左小角 \u003ccode\u003eDon't show tips\u003c/code\u003e。\u003c/p\u003e","title":"用PyCharm打个专业的招呼"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n80道常考算法题，逢考必过，剑指offer。\nimport java.util.*; /** * 80道算法题 */ public class AQ80 { //1大数加法 public String t1_bigNumberAdd(String s, String t) { // write code here Stack\u0026lt;Integer\u0026gt; stack = new Stack\u0026lt;\u0026gt;(); StringBuilder result = new StringBuilder(); int i = s.length() - 1; int j = t.length() - 1; int carry = 0; while (i \u0026gt;= 0 || j \u0026gt;= 0 || carry != 0) { carry += i \u0026gt;= 0 ? s.charAt(i--) - \u0026#39;0\u0026#39; : 0; carry += j \u0026gt;= 0 ? t.charAt(j--) - \u0026#39;0\u0026#39; : 0; stack.push(carry % 10); carry /= 10; } while (!stack.isEmpty()) { result.append(stack.pop()); } return result.toString(); } //2链表中环的入口结点 public static class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } } public ListNode t2_entryNodeOfLoop(ListNode pHead) { if (pHead == null) return null; ListNode slow = pHead; ListNode fast = pHead; while (fast != null \u0026amp;\u0026amp; fast.next != null) { fast = fast.next.next; slow = slow.next; if (slow == fast) { break; } } if (fast == null || fast.next == null) { return null; } fast = pHead; while (fast != slow) { fast = fast.next; slow = slow.next; } return fast; } //3判断链表中是否有环 public boolean t3_hasCycle(ListNode head) { ListNode pos = head; Set\u0026lt;ListNode\u0026gt; visited = new HashSet\u0026lt;\u0026gt;(); while (pos != null) { if (visited.contains(pos)) { return true; } else { visited.add(pos); } pos = pos.next; } return false; } //4二叉树中的最大路径和 public static class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int i) { val = i; } } int maxSum = Integer.MIN_VALUE; public int t4_maxPathSum(TreeNode root) { // write code here maxGain(root); return maxSum; } public int maxGain(TreeNode node) { if (node == null) { return 0; } int leftGain = Math.max(maxGain(node.left), 0); int rightGain = Math.max(maxGain(node.right), 0); int priceNewPath = node.val + leftGain + rightGain; maxSum = Math.max(maxSum, priceNewPath); return node.val + Math.max(leftGain, rightGain); } //5将升序数组转化为平衡二叉搜索树 public TreeNode t5_sortedArrayToBST(int[] num) { // write code here if (num == null || num.length == 0) { return null; } return process(num, 0, num.length - 1); } public TreeNode process(int[] num, int left, int right) { if (left \u0026gt; right) { return null; } if (left == right) { return new TreeNode(num[left]); } int len = right - left + 1; int mid = left + len / 2; TreeNode root = new TreeNode(num[mid]); root.left = process(num, left, mid - 1); root.right = process(num, mid + 1, right); return root; } //6重建二叉树 public TreeNode t6_reConstructBinaryTree(int[] pre, int[] vin) { return reConstructBinaryTree(pre, 0, pre.length - 1, vin, 0, vin.length - 1); } private TreeNode reConstructBinaryTree(int[] pre, int startPre, int endPre, int[] vin, int startVin, int endVin) { if (startPre \u0026gt; endPre || startVin \u0026gt; endVin) { return null; } TreeNode root = new TreeNode(pre[startPre]); for (int i = startVin; i \u0026lt;= endVin; i++) { if (vin[i] == pre[startPre]) { root.left = reConstructBinaryTree(pre, startPre + 1, startPre + i - startVin, vin, startVin, i - 1); root.right = reConstructBinaryTree(pre, i - startVin + startPre + 1, endPre, vin, i + 1, endVin); break; } } return root; } //7按之字形顺序打印二叉树 public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; t7_print(TreeNode pRoot) { LinkedList\u0026lt;TreeNode\u0026gt; deque = new LinkedList\u0026lt;\u0026gt;(); ArrayList res = new ArrayList\u0026lt;\u0026gt;(); if (pRoot != null) { deque.add(pRoot); } while (!deque.isEmpty()) { ArrayList tmp = new ArrayList\u0026lt;\u0026gt;(); for (int i = deque.size(); i \u0026gt; 0; i--) { TreeNode node = deque.removeFirst(); tmp.add(node.val); if (node.left != null) { deque.addLast(node.left); } if (node.right != null) { deque.addLast(node.right); } } res.add(tmp); if (deque.isEmpty()) { break; } tmp = new ArrayList\u0026lt;\u0026gt;(); for (int i = deque.size(); i \u0026gt; 0; i--) { TreeNode node = deque.removeLast(); tmp.add(node.val); if (node.right != null) { deque.addFirst(node.right); } if (node.left != null) { deque.addFirst(node.left); } } res.add(tmp); } return res; } //8求二叉树的层序遍历 public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; t8_levelOrder(TreeNode root) { // write code here ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); if (root == null) { return res; } dfs(root, res, 0); return res; } public void dfs(TreeNode root, ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; list, int depth) { if (root == null) { return; } if (list.size() == depth) { list.add(new ArrayList\u0026lt;\u0026gt;()); } dfs(root.left, list, depth + 1); list.get(depth).add(root.val); dfs(root.right, list, depth + 1); } //9对称的二叉树 boolean t9_isSymmetrical(TreeNode pRoot) { return isSame(pRoot, pRoot); } public boolean isSame(TreeNode root1, TreeNode root2) { if (root1 == null \u0026amp;\u0026amp; root2 == null) { return true; } if (root1 == null || root2 == null) { return false; } return root1.val == root2.val \u0026amp;\u0026amp; isSame(root1.left, root2.right) \u0026amp;\u0026amp; isSame(root1.right, root2.left); } //10最长回文子串 public int t10_getLongestPalindrome(String A) { // write code here int maxLen = 0; int n = A.length(); for (int i = 0; i \u0026lt; n; i++) { for (int j = i + 1; j \u0026lt;= n; j++) { String now = A.substring(i, j); if (isHuiWen(now) \u0026amp;\u0026amp; now.length() \u0026gt; maxLen) { maxLen = now.length(); } } } return maxLen; } public boolean isHuiWen(String s) { int len = s.length(); for (int i = 0; i \u0026lt; len / 2; i++) { if (s.charAt(i) != s.charAt(len - i - 1)) { return false; } } return true; } //11顺时针旋转矩阵 public int[][] t11_rotateMatrix(int[][] mat, int n) { // write code here for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; i; j++) { int tmp = mat[i][j]; mat[i][j] = mat[j][i]; mat[j][i] = tmp; } } for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; n / 2; j++) { int tmp = mat[i][n - 1 - j]; mat[i][n - 1 - j] = mat[i][j]; mat[i][j] = tmp; } } return mat; } //12连续子数组的最大和 public int t12_findGreatestSumOfSubArray(int[] array) { int res = array[0]; int max = array[0]; for (int i = 1; i \u0026lt; array.length; i++) { max = Math.max(max + array[i], array[i]); res = Math.max(max, res); } return res; } //13合并两个有序的数组 public void t13_merge(int A[], int m, int B[], int n) { int i = 0, j = 0, p = 0; int[] c = new int[m + n]; while (i \u0026lt; m \u0026amp;\u0026amp; j \u0026lt; n) { c[p++] = A[i] \u0026lt;= B[j] ? A[i++] : B[j++]; } while (i \u0026lt; m) { c[p++] = A[i++]; } while (j \u0026lt; n) { c[p++] = B[j++]; } for (int x = 0; x \u0026lt; p; x++) { A[x] = c[x]; } } //14删除有序链表中重复的元素-I public ListNode t14_deleteDuplicates(ListNode head) { // write code here ListNode cur = head; while (cur != null) { while (cur.next != null \u0026amp;\u0026amp; cur.val == cur.next.val) { cur.next = cur.next.next; } cur = cur.next; } return head; } //15删除有序链表中重复的元素-II public ListNode t15_deleteDuplicates(ListNode head) { // write code here ListNode node = new ListNode(-1); node.next = head; ListNode p = node; ListNode cur = head; while (cur != null \u0026amp;\u0026amp; cur.next != null) { if (cur.val != cur.next.val) { p = cur; } else { while (cur.next != null \u0026amp;\u0026amp; cur.val == cur.next.val) { cur = cur.next; } p.next = cur.next; } cur = cur.next; } return node.next; } //16括号生成 public ArrayList\u0026lt;String\u0026gt; t16_generateParenthesis(int n) { // write code here ArrayList\u0026lt;String\u0026gt; result = new ArrayList\u0026lt;\u0026gt;(10); backtrack(\u0026#34;\u0026#34;, 0, 0, n, result); return result; } private void backtrack(String string, int open, int close, int n, List\u0026lt;String\u0026gt; result) { if (string.length() == n \u0026lt;\u0026lt; 1) { result.add(string); return; } if (open \u0026lt; n) { backtrack(string + \u0026#34;(\u0026#34;, open + 1, close, n, result); } if (close \u0026lt; open) { backtrack(string + \u0026#34;)\u0026#34;, open, close + 1, n, result); } } //17集合的所有子集(一) ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; result = new ArrayList\u0026lt;\u0026gt;(); public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; t17_subsets(int[] nums) { ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); Arrays.sort(nums); for (int j = 0; j \u0026lt;= nums.length; j++) { backtracking(nums, j, 0, list); } return result; } public void backtracking(int[] nums, int k, int start, ArrayList\u0026lt;Integer\u0026gt; list) { if (k \u0026lt; 0) { return; } else if (k == 0) { result.add(new ArrayList(list)); } else { for (int i = start; i \u0026lt; nums.length; i++) { list.add(nums[i]); backtracking(nums, k - 1, i + 1, list); list.remove(list.size() - 1); } } } //18最小覆盖子串 public String t18_minWindow(String S, String T) { // write code here int[] map = new int[128]; for (int i = 0; i \u0026lt; T.length(); i++) { map[T.charAt(i)]++; } int begin = 0, end = 0, d = Integer.MAX_VALUE, counter = T.length(), head = 0; while (end \u0026lt; S.length()) { if (map[S.charAt(end++)]-- \u0026gt; 0) { counter--; } while (counter == 0) { if (end - begin \u0026lt; d) { d = end - (head = begin); } if (map[S.charAt(begin++)]++ == 0) { counter++; } } } return d == Integer.MAX_VALUE ? \u0026#34;\u0026#34; : S.substring(head, head + d); } //19缺失的第一个正整数 public int t19_minNumberDisappeared(int[] nums) { // write code here for (int i = 0; i \u0026lt; nums.length; i++) { if (nums[i] \u0026lt;= 0) { nums[i] = nums.length + 1; } } for (int i = 0; i \u0026lt; nums.length; i++) { int x = Math.abs(nums[i]); if (x \u0026lt;= nums.length) { nums[x - 1] = (-1) * Math.abs(nums[x - 1]); } } for (int i = 0; i \u0026lt; nums.length; i++) { if (nums[i] \u0026gt; 0) { return i + 1; } } return nums.length + 1; } //20第一个只出现一次的字符 public int t20_FirstNotRepeatingChar(String str) { int[] dp = new int[123]; for (int i = 0; i \u0026lt; str.length(); i++) { dp[str.charAt(i)]++; } for (int i = 0; i \u0026lt; str.length(); i++) { if (dp[str.charAt(i)] == 1) { return i; } } return -1; } //21合并两个排序的链表 public ListNode t21_Merge(ListNode list1, ListNode list2) { if (list1 == null) { return list2; } if (list2 == null) { return list1; } if (list1.val \u0026lt;= list2.val) { list1.next = t21_Merge(list1.next, list2); return list1; } else { list2.next = t21_Merge(list1, list2.next); return list2; } } //22编辑距离(二) public int t22_minEditCost(String str1, String str2, int ic, int dc, int rc) { // write code here if (str1.length() == 0) { return str2.length() * ic; } if (str2.length() == 0) { return str1.length() * dc; } int n1 = str1.length(), n2 = str2.length(); int[][] dp = new int[n1 + 1][n2 + 1]; for (int i = 0; i \u0026lt;= n1; i++) { dp[i][0] = i * dc; } for (int i = 0; i \u0026lt;= n2; i++) { dp[0][i] = i * ic; } for (int i = 1; i \u0026lt;= n1; i++) { for (int j = 1; j \u0026lt;= n2; j++) { if (str1.charAt(i - 1) == str2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = Math.min(dp[i - 1][j] + dc, Math.min(dp[i][j - 1] + ic, dp[i - 1][j - 1] + rc)); } } } return dp[n1][n2]; } //23在两个长度相等的排序数组中找到上中位数 public int t23_findMedianinTwoSortedAray(int[] arr1, int[] arr2) { // write code here if (arr1 == null || arr2 == null || arr1.length != arr2.length) { return 0; } int left1 = 0; int right1 = arr1.length - 1; int left2 = 0; int right2 = arr2.length - 1; int mid1 = 0; int mid2 = 0; int offset = 0; while (left1 \u0026lt; right1) { mid1 = left1 + (right1 - left1) / 2; mid2 = left2 + (right2 - left2) / 2; offset = ((right1 - left1 + 1) \u0026amp; 1) ^ 1; if (arr1[mid1] \u0026gt; arr2[mid2]) { right1 = mid1; left2 = mid2 + offset; } else if (arr1[mid1] \u0026lt; arr2[mid2]) { right2 = mid2; left1 = mid1 + offset; } else { return arr1[mid1]; } } return Math.min(arr1[left1], arr2[left2]); } //24合并区间 public class Interval { int start; int end; Interval() { start = 0; end = 0; } Interval(int s, int e) { start = s; end = e; } } public ArrayList\u0026lt;Interval\u0026gt; t24_merge(ArrayList\u0026lt;Interval\u0026gt; intervals) { ArrayList\u0026lt;Interval\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); Collections.sort(intervals, (a, b) -\u0026gt; a.start - b.start); int len = intervals.size(); int i = 0; while (i \u0026lt; len) { int left = intervals.get(i).start; int right = intervals.get(i).end; while (i \u0026lt; len - 1 \u0026amp;\u0026amp; intervals.get(i + 1).start \u0026lt;= right) { right = Math.max(right, intervals.get(i + 1).end); i++; } res.add(new Interval(left, right)); i++; } return res; } //25两个链表生成相加链表 public ListNode t25_addInList(ListNode head1, ListNode head2) { // write code here if (head1 == null) { return head2; } if (head2 == null) { return head1; } head1 = reverse(head1); head2 = reverse(head2); ListNode head = new ListNode(-1); ListNode nHead = head; int tmp = 0; while (head1 != null || head2 != null) { int val = tmp; if (head1 != null) { val += head1.val; head1 = head1.next; } if (head2 != null) { val += head2.val; head2 = head2.next; } tmp = val / 10; nHead.next = new ListNode(val % 10); nHead = nHead.next; } if (tmp \u0026gt; 0) { nHead.next = new ListNode(tmp); } return reverse(head.next); } ListNode reverse(ListNode head) { if (head == null) { return head; } ListNode cur = head; ListNode node = null; while (cur != null) { ListNode tail = cur.next; cur.next = node; node = cur; cur = tail; } return node; } //26最长无重复子数组 public int t26_maxLength(int[] arr) { // write code here if (arr.length == 0) { return 0; } HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); int max = 0; for (int i = 0, j = 0; i \u0026lt; arr.length; ++i) { if (map.containsKey(arr[i])) { j = Math.max(j, map.get(arr[i]) + 1); } map.put(arr[i], i); max = Math.max(max, i - j + 1); } return max; } //27有重复项数字的全排列 boolean[] mark; public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; t27_permuteUnique(int[] num) { ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); LinkedList\u0026lt;Integer\u0026gt; track = new LinkedList\u0026lt;\u0026gt;(); mark = new boolean[num.length]; Arrays.sort(num); backtrack(num, res, track); return res; } public void backtrack(int[] num, ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; res, LinkedList\u0026lt;Integer\u0026gt; track) { if (track.size() == num.length) { res.add(new ArrayList\u0026lt;Integer\u0026gt;(track)); return; } for (int i = 0; i \u0026lt; num.length; i++) { if (mark[i] || i \u0026gt; 0 \u0026amp;\u0026amp; num[i] == num[i - 1] \u0026amp;\u0026amp; !mark[i - 1]) { continue; } track.add(num[i]); mark[i] = true; backtrack(num, res, track); track.removeLast(); mark[i] = false; } } //28通配符匹配 public boolean t28_isMatch(String s, String p) { int row = s.length(); int col = p.length(); boolean[][] dp = new boolean[row + 1][col + 1]; dp[0][0] = true; for (int j = 1; j \u0026lt; col + 1; j++) { if (dp[0][j - 1]) { if (p.charAt(j - 1) == \u0026#39;*\u0026#39;) { dp[0][j] = true; } else { break; } } } for (int i = 0; i \u0026lt; row; i++) { for (int j = 0; j \u0026lt; col; j++) { if (p.charAt(j) == s.charAt(i) || p.charAt(j) == \u0026#39;?\u0026#39;) { dp[i + 1][j + 1] = dp[i][j]; } else if (p.charAt(j) == \u0026#39;*\u0026#39;) { dp[i + 1][j + 1] = dp[i][j] || dp[i + 1][j] || dp[i][j + 1]; } } } return dp[row][col]; } //29实现二叉树先序，中序和后序遍历 public int[][] t29_threeOrders(TreeNode root) { // write code here List\u0026lt;Integer\u0026gt; list1 = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;Integer\u0026gt; list2 = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;Integer\u0026gt; list3 = new ArrayList\u0026lt;\u0026gt;(); preOrder(root, list1); inOrder(root, list2); postOrder(root, list3); int[][] res = new int[3][list1.size()]; for (int i = 0; i \u0026lt; list1.size(); i++) { res[0][i] = list1.get(i); res[1][i] = list2.get(i); res[2][i] = list3.get(i); } return res; } public void preOrder(TreeNode root, List\u0026lt;Integer\u0026gt; list) { if (root == null) { return; } list.add(root.val); preOrder(root.left, list); preOrder(root.right, list); } public void inOrder(TreeNode root, List\u0026lt;Integer\u0026gt; list) { if (root == null) { return; } inOrder(root.left, list); list.add(root.val); inOrder(root.right, list); } public void postOrder(TreeNode root, List\u0026lt;Integer\u0026gt; list) { if (root == null) { return; } postOrder(root.left, list); postOrder(root.right, list); list.add(root.val); } //30加起来和为目标值的组合(二) public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; t30_combinationSum2(int[] num, int target) { ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; res = new ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt;(); ArrayList\u0026lt;Integer\u0026gt; arr = new ArrayList\u0026lt;Integer\u0026gt;(); if (num == null || num.length == 0 || target \u0026lt; 0) { return res; } Arrays.sort(num); dfs(num, target, res, arr, 0); return res; } void dfs(int[] num, int target, ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; res, ArrayList\u0026lt;Integer\u0026gt; arr, int start) { if (target == 0) { res.add(new ArrayList\u0026lt;Integer\u0026gt;(arr)); return; } if (start \u0026gt;= num.length) { return; } for (int i = start; i \u0026lt; num.length; i++) { if (i \u0026gt; start \u0026amp;\u0026amp; num[i] == num[i - 1]) { continue; } if (num[i] \u0026lt;= target) { arr.add(num[i]); dfs(num, target - num[i], res, arr, i + 1); arr.remove(arr.size() - 1); } } return; } //31最长的括号子串 public int t31_longestValidParentheses(String s) { // write code here int maxans = 0; int[] dp = new int[s.length()]; for (int i = 1; i \u0026lt; s.length(); i++) { if (s.charAt(i) == \u0026#39;)\u0026#39;) { if (s.charAt(i - 1) == \u0026#39;(\u0026#39;) { dp[i] = (i \u0026gt;= 2 ? dp[i - 2] : 0) + 2; } else if (i - dp[i - 1] \u0026gt; 0 \u0026amp;\u0026amp; s.charAt(i - dp[i - 1] - 1) == \u0026#39;(\u0026#39;) { dp[i] = dp[i - 1] + ((i - dp[i - 1]) \u0026gt;= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2; } maxans = Math.max(maxans, dp[i]); } } return maxans; } //32链表中的节点每k个一组翻转 public ListNode t32_reverseKGroup(ListNode head, int k) { // write code here if (k \u0026lt;= 1) { return head; } if (head == null) { return head; } ListNode node = head; int len = length(head); head = node; int sx = len / k; ListNode result = new ListNode(0); ListNode now = result; int cnt = 0; for (int i = 0; i \u0026lt; sx; i++) { ListNode tmp = null; for (int j = 0; j \u0026lt; k; j++) { ListNode bl = head.next; head.next = tmp; tmp = head; head = bl; } now.next = tmp; while (now.next != null) { now = now.next; } } now.next = head; return result.next; } public int length(ListNode now) { int cnt = 0; if (now != null) { cnt = 1; } while (now.next != null) { cnt++; now = now.next; } return cnt; } //33合并k个已排序的链表 public ListNode t33_mergeKLists(ArrayList\u0026lt;ListNode\u0026gt; lists) { return mergeList(lists, 0, lists.size() - 1); } public ListNode mergeList(ArrayList\u0026lt;ListNode\u0026gt; lists, int left, int right) { if (left == right) { return lists.get(left); } if (left \u0026gt; right) { return null; } int mid = left + ((right - left) \u0026gt;\u0026gt; 1); return merge(mergeList(lists, left, mid), mergeList(lists, mid + 1, right)); } public ListNode merge(ListNode l1, ListNode l2) { if (l1 == null || l2 == null) { return l1 == null ? l2 : l1; } ListNode dummy = new ListNode(-1); ListNode cur = dummy; while (l1 != null \u0026amp;\u0026amp; l2 != null) { if (l1.val \u0026lt; l2.val) { cur.next = l1; l1 = l1.next; } else { cur.next = l2; l2 = l2.next; } cur = cur.next; } cur.next = (l1 == null ? l2 : l1); return dummy.next; } //34有效括号序列 public boolean t34_isValid(String s) { // write code here Stack\u0026lt;Character\u0026gt; stk = new Stack\u0026lt;\u0026gt;(); for (char c : s.toCharArray()) { if (c == \u0026#39;(\u0026#39;) { stk.push(\u0026#39;)\u0026#39;); } else if (c == \u0026#39;[\u0026#39;) { stk.push(\u0026#39;]\u0026#39;); } else if (c == \u0026#39;{\u0026#39;) { stk.push(\u0026#39;}\u0026#39;); } else { if (stk.isEmpty() || c != stk.pop()) { return false; } } } return stk.isEmpty(); } //35删除链表的倒数第n个节点 public ListNode t55_removeNthFromEnd(ListNode head, int n) { // write code here int length = 0; ListNode p = head; ListNode q = head; while (head != null) { length++; head = head.next; } if (length \u0026lt; 2) { return null; } if (n == length) { return q.next; } int i = 0; while (p != null) { i++; if (i == length - n) { p.next = p.next.next; } p = p.next; } return q; } //36三数之和 public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; t36_threeSum(int[] num) { ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); if (num == null || num.length \u0026lt; 3) { return res; } Arrays.sort(num);// 排序 for (int i = 0; i \u0026lt; num.length - 2; i++) { if (num[i] \u0026gt; 0) { break;// 如果当前数字大于0，则三数之和一定大于0，所以结束循环 } if (i \u0026gt; 0 \u0026amp;\u0026amp; num[i] == num[i - 1]) { continue;// 去重 } int L = i + 1; int R = num.length - 1; while (L \u0026lt; R) { int sum = num[i] + num[L] + num[R]; if (sum == 0) { ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); list.add(num[i]); list.add(num[L]); list.add(num[R]); res.add(list); while (L \u0026lt; R \u0026amp;\u0026amp; num[L] == num[L + 1]) { L++; } while (L \u0026lt; R \u0026amp;\u0026amp; num[R] == num[R - 1]) { R--; } L++; R--; } else if (sum \u0026gt; 0) { R--; } else if (sum \u0026lt; 0) { L++; } } } return res; } //37最长公共前缀 public String t37_longestCommonPrefix(String[] strs) { // //纵向扫描 if (strs.length == 0 || strs == null) { return \u0026#34;\u0026#34;; } int rows = strs.length; int cols = strs[0].length(); //开始扫描 for (int i = 0; i \u0026lt; cols; i++) { char firstChar = strs[0].charAt(i); for (int j = 1; j \u0026lt; rows; j++) { if (strs[j].length() == i || strs[j].charAt(i) != firstChar) { return strs[0].substring(0, i); } } } return strs[0]; } //38反转数字 public int t38_reverse(int x) { int res = 0; while (x != 0) { int t = x % 10; int newRes = res * 10 + t; //如果数字溢出，直接返回0 if ((newRes - t) / 10 != res) return 0; res = newRes; x = x / 10; } return res; } //39判断一棵二叉树是否为搜索二叉树和完全二叉树 long pre = Long.MIN_VALUE; public boolean[] t39_judgeIt(TreeNode root) { // write code here return new boolean[]{isSBT(root), isCBT(root)}; } public boolean isSBT(TreeNode root) { if (root == null) { return true; } if (!isSBT(root.left)) { return false; } if (root.val \u0026lt;= pre) { return false; } pre = root.val; return isSBT(root.right); } public boolean isCBT(TreeNode root) { if (root == null) { return true; } Queue\u0026lt;TreeNode\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); queue.offer(root); boolean leaf = false; while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i \u0026lt; size; i++) { TreeNode node = queue.poll(); if (leaf \u0026amp;\u0026amp; (node.left != null || node.right != null)) { return false; } if (node.left == null \u0026amp;\u0026amp; node.right != null) { return false; } if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } else { leaf = true; } } } return true; } //40两数之和 public int[] t40_twoSum(int[] numbers, int target) { // write code here HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; numbers.length; i++) { if (map.containsKey(target - numbers[i])) return new int[]{map.get(target - numbers[i]) + 1, i + 1}; else map.put(numbers[i], i); } throw new IllegalArgumentException(\u0026#34;No solution\u0026#34;); } //41判断是不是平衡二叉树 public int depth(TreeNode root) { if (root == null) return 0; int left = depth(root.left); if (left == -1) return -1; int right = depth(root.right); if (right == -1) return -1; if (left - right \u0026lt; (-1) || left - right \u0026gt; 1) return -1; else return 1 + (left \u0026gt; right ? left : right); } public boolean t41_IsBalanced_Solution(TreeNode root) { return depth(root) != -1; } //42扑克牌顺子 public boolean t42_IsContinuous(int[] numbers) { int[] d = new int[14]; d[0] = -5; int len = numbers.length; int max = -1; int min = 14; for (int i = 0; i \u0026lt; len; i++) { d[numbers[i]]++; if (numbers[i] == 0) { continue; } if (d[numbers[i]] \u0026gt; 1) { return false; } if (numbers[i] \u0026gt; max) { max = numbers[i]; } if (numbers[i] \u0026lt; min) { min = numbers[i]; } } if (max - min \u0026lt; 5) { return true; } return false; } //43斐波那契数列 public int t43_Fibonacci(int n) { int[] ans = new int[50]; ans[0] = 0; ans[1] = 1; for (int i = 2; i \u0026lt;= n; i++) { ans[i] = ans[i - 1] + ans[i - 2]; } return ans[n]; } //44两个链表的第一个公共结点 public ListNode t44_FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { ListNode l1 = pHead1, l2 = pHead2; while (l1 != l2) { l1 = (l1 == null) ? pHead2 : l1.next; l2 = (l2 == null) ? pHead1 : l2.next; } return l1; } //45跳台阶 public int t45_jumpFloor(int target) { if (target \u0026lt; 2) { return target; } int p = 0, q = 1, r = 2; for (int i = 3; i \u0026lt;= target; i++) { p = q; q = r; r = p + q; } return r; } //46单链表的排序 public ListNode t46_sortInList(ListNode head) { // write code here if (head == null || head.next == null) return head; ListNode slow = head, fast = head.next; while (fast != null \u0026amp;\u0026amp; fast.next != null) { slow = slow.next; fast = fast.next.next; } ListNode newList = slow.next; slow.next = null; ListNode left = t46_sortInList(head); ListNode right = t46_sortInList(newList); ListNode lhead = new ListNode(-1); ListNode res = lhead; while (left != null \u0026amp;\u0026amp; right != null) { if (left.val \u0026lt; right.val) { lhead.next = left; left = left.next; } else { lhead.next = right; right = right.next; } lhead = lhead.next; } lhead.next = left != null ? left : right; return res.next; } //47二叉树的镜像 public TreeNode t47_Mirror(TreeNode pRoot) { if (pRoot == null) return pRoot; if (pRoot.left == null \u0026amp;\u0026amp; pRoot.right == null) return pRoot; TreeNode temp = pRoot.left; pRoot.left = pRoot.right; pRoot.right = temp; t47_Mirror(pRoot.left); t47_Mirror(pRoot.right); return pRoot; } //48数组中出现次数超过一半的数字 public int t48_MoreThanHalfNum_Solution(int[] array) { if (array == null || array.length == 0) return 0; int preValue = array[0]; int count = 1; for (int i = 1; i \u0026lt; array.length; i++) { if (array[i] == preValue) count++; else { count--; if (count == 0) { preValue = array[i]; count = 1; } } } int num = 0; for (int i = 0; i \u0026lt; array.length; i++) if (array[i] == preValue) num++; return (num \u0026gt; array.length / 2) ? preValue : 0; } //49数字在升序数组中出现的次数 public int t49_GetNumberOfK(int[] array, int k) { int index = Arrays.binarySearch(array, k); if (index \u0026lt; 0) return 0; int cnt = 1; for (int i = index + 1; i \u0026lt; array.length \u0026amp;\u0026amp; array[i] == k; i++) cnt++; for (int i = index - 1; i \u0026gt;= 0 \u0026amp;\u0026amp; array[i] == k; i--) cnt++; return cnt; } //50用两个栈实现队列 Stack\u0026lt;Integer\u0026gt; stack1 = new Stack\u0026lt;Integer\u0026gt;(); Stack\u0026lt;Integer\u0026gt; stack2 = new Stack\u0026lt;Integer\u0026gt;(); public void t50_push(int node) { stack1.push(node); } public int pop() { if (stack2.size() \u0026lt;= 0) { while (stack1.size() != 0) { stack2.push(stack1.pop()); } } return stack2.pop(); } //51反转链表 public ListNode t51_ReverseList(ListNode head) { Stack\u0026lt;ListNode\u0026gt; stack = new Stack\u0026lt;\u0026gt;(); //把链表节点全部摘掉放到栈中 while (head != null) { stack.push(head); head = head.next; } if (stack.isEmpty()) return null; ListNode node = stack.pop(); ListNode dummy = node; //栈中的结点全部出栈，然后重新连成一个新的链表 while (!stack.isEmpty()) { ListNode tempNode = stack.pop(); node.next = tempNode; node = node.next; } //最后一个结点就是反转前的头结点，一定要让他的next //等于空，否则会构成环 node.next = null; return dummy; } //52滑动窗口的最大值 public ArrayList\u0026lt;Integer\u0026gt; t52_maxInWindows(int[] num, int size) { ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;Integer\u0026gt;(); int max = 0; if (num.length == 0 || size \u0026gt; num.length || size == 0) { return list; } for (int i = 0; i \u0026lt;= num.length - size; i++) { max = num[i]; for (int j = i; j \u0026lt; size + i; j++) { if (max \u0026lt; num[j]) { max = num[j]; } } list.add(max); } return list; } //53矩阵元素查找 public int[] t53_findElement(int[][] mat, int n, int m, int x) { // write code here int nn = n - 1; int mm = 0; while (nn \u0026gt;= 0 \u0026amp;\u0026amp; mm \u0026lt;= m - 1) { if (mat[nn][mm] == x) return new int[]{nn, mm}; else if (mat[nn][mm] \u0026gt; x) nn--; else mm++; } return new int[]{}; } //54字符串变形 public String t54_trans(String s, int n) { StringBuffer ss = new StringBuffer(); StringBuffer str = new StringBuffer(); for (int i = n - 1; i \u0026gt;= 0; i--) { if (s.charAt(i) == \u0026#39; \u0026#39;) { ss.append(str.toString() + \u0026#34; \u0026#34;); str = new StringBuffer(); } else { //大小写反转 char sss = (char) (s.charAt(i) \u0026lt; 97 ? s.charAt(i) + 32 : s.charAt(i) - 32); //将字符添加到str的首个位置保证单词不被反转 str.insert(0, sss); } } return ss.append(str.toString()).toString(); } //55最长上升子序列(三) public static int[] t55_LIS(int[] arr) { // write code here List\u0026lt;Integer\u0026gt; result = new ArrayList\u0026lt;\u0026gt;(); int[] maxLength = new int[arr.length]; for (int i = 0; i \u0026lt; arr.length; i++) { if (result.size() \u0026gt; 0) { if (result.get(result.size() - 1) \u0026lt; arr[i]) { result.add(arr[i]); maxLength[i] = result.size(); } else { for (int j = result.size() - 1; j \u0026gt;= 0; j--) { if (result.get(j) \u0026lt; arr[i]) { result.set(j + 1, arr[i]); maxLength[i] = j + 2; break; } if (j == 0) { result.set(0, arr[i]); maxLength[i] = 1; } } } } else { result.add(arr[i]); maxLength[i] = 1; } } int[] resultArray = new int[result.size()]; for (int i = arr.length - 1, j = result.size(); j \u0026gt; 0; i--) { if (maxLength[i] == j) { resultArray[--j] = arr[i]; } } return resultArray; } //56最长公共子序列(二) public String t56_LCS(String s1, String s2) { int len1 = s1.length(); int len2 = s2.length(); if (len1 == 0 || len2 == 0) return \u0026#34;-1\u0026#34;; int[][] dp = new int[len1 + 1][len2 + 1]; for (int i = 0; i \u0026lt; len1 + 1; i++) { for (int j = 0; j \u0026lt; len2 + 1; j++) { if (i == 0 || j == 0) { dp[i][j] = 0; continue; } if (s1.charAt(i - 1) == s2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } StringBuilder sb = new StringBuilder(); int s1L = len1, s2L = len2; while (s1L != 0 \u0026amp;\u0026amp; s2L != 0) { if (s1.charAt(s1L - 1) == s2.charAt(s2L - 1)) { sb.append(s1.charAt(s1L - 1)); s1L--; s2L--; } else { if (dp[s1L - 1][s2L] \u0026gt; dp[s1L][s2L - 1]) { s1L--; } else { s2L--; } } } if (sb.length() == 0) return \u0026#34;-1\u0026#34;; return sb.reverse().toString(); } //57设计LRU缓存结构 static class Node { int key, value; Node prev, next; public Node(int key, int value) { this.key = key; this.value = value; } } private Map\u0026lt;Integer, Node\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); private Node head = new Node(-1, -1); private Node tail = new Node(-1, -1); private int k; public int[] t57_LRU(int[][] operators, int k) { // write code here this.k = k; head.next = tail; tail.prev = head; int len = (int) Arrays.stream(operators).filter(x -\u0026gt; x[0] == 2).count(); int[] ans = new int[len]; int cnt = 0; for (int i = 0; i \u0026lt; operators.length; i++) { if (operators[i][0] == 1) { set(operators[i][1], operators[i][2]); } else { ans[cnt++] = get(operators[i][1]); } } return ans; } public void set(int key, int value) { if (get(key) \u0026gt; -1) { map.get(key).value = value; } else { if (map.size() == k) { int rk = tail.prev.key; tail.prev.prev.next = tail; tail.prev = tail.prev.prev; map.remove(rk); } Node node = new Node(key, value); map.put(key, node); removeToHead(node); } } public int get(int key) { if (map.containsKey(key)) { Node node = map.get(key); node.prev.next = node.next; node.next.prev = node.prev; removeToHead(node); return node.value; } return -1; } public void removeToHead(Node node) { node.next = head.next; head.next.prev = node; head.next = node; node.prev = head; } //58设计LFU缓存结构 public int[] t58_LFU(int[][] operators, int k) { HashMap\u0026lt;Integer, Nodex\u0026gt; count = new HashMap\u0026lt;\u0026gt;(); HashMap\u0026lt;Integer, int[]\u0026gt; temp = new HashMap\u0026lt;\u0026gt;(); PriorityQueue\u0026lt;Integer\u0026gt; minHeap = new PriorityQueue\u0026lt;\u0026gt;((o1, o2) -\u0026gt; count.get(o1).count.equals(count.get(o2).count) ? count.get(o1).time.compareTo(count.get(o2).time) : count.get(o1).count.compareTo(count.get(o2).count)); ArrayList\u0026lt;Integer\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); int time = 0; for (int i = 0; i \u0026lt; operators.length; i++) { if (operators[i][0] == 1) { if (temp.size() == k) { int poll = minHeap.poll(); count.remove(poll); temp.remove(poll); } temp.put(operators[i][1], operators[i]); if (count.containsKey(operators[i][1])) { Nodex node = new Nodex(count.get(operators[i][1]).count + 1, time++); count.put(operators[i][1], node); minHeap.remove(operators[i][1]); minHeap.offer(operators[i][1]); } else { Nodex node1 = new Nodex(1, time++); count.put(operators[i][1], node1); minHeap.offer(operators[i][1]); } } else if (operators[i][0] == 2) { if (temp.get(operators[i][1]) == null) { res.add(-1); } else { int[] operator = temp.get(operators[i][1]); res.add(operator[2]); if (count.containsKey(operators[i][1])) { Nodex node2 = new Nodex(count.get(operators[i][1]).count + 1, time++); count.put(operators[i][1], node2); minHeap.remove(operators[i][1]); minHeap.offer(operators[i][1]); } else { Nodex node3 = new Nodex(1, time++); count.put(operators[i][1], node3); minHeap.offer(operators[i][1]); } } } } int[] result = new int[res.size()]; for (int i = 0; i \u0026lt; res.size(); i++) { result[i] = res.get(i); } return result; } class Nodex { public Integer count; public Integer time; public Nodex(Integer count, Integer time) { this.count = count; this.time = time; } } //59数组中的最长连续子序列 public int t59_MLS(int[] arr) { Set\u0026lt;Integer\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); for (int num : arr) set.add(num); int longest = 0; for (int num : arr) { if (set.contains(num - 1)) continue; int currentNum = num; int count = 1; while (set.contains(currentNum + 1)) { currentNum++; count++; } longest = Math.max(longest, count); } return longest; } //60判断一个链表是否为回文结构 public class ListNodex { int val; ListNodex next = null; } public boolean t60_isPail(ListNodex head) { ListNodex temp = head; Stack\u0026lt;Integer\u0026gt; stack = new Stack(); while (temp != null) { stack.push(temp.val); temp = temp.next; } while (head != null) { if (head.val != stack.pop()) { return false; } head = head.next; } return true; } //61判断t1树中是否有与t2树完全相同的子树 public boolean t61_isContains(TreeNode root1, TreeNode root2) { if (root1 == null) return false; return t61_isContains(root1.left, root2) || t61_isContains(root1.right, root2) || check(root1, root2); } public boolean check(TreeNode root1, TreeNode root2) { if (root1 == null \u0026amp;\u0026amp; root2 == null) return true; if (root1 == null || root2 == null || root1.val != root2.val) return false; return check(root1.left, root2.left) \u0026amp;\u0026amp; check(root1.right, root2.right); } //62把字符串转换成整数(atoi) public int t62_StrToInt(String s) { int len = s.length(); if (len == 0) return 0; int sign = 1;//默认为正数 long num = 0; int i = 0; while (i \u0026lt; len \u0026amp;\u0026amp; s.charAt(i) == \u0026#39; \u0026#39;) i++; if (i \u0026lt; len) { if (s.charAt(i) == \u0026#39;-\u0026#39;) { sign = -1; i++; } else if (s.charAt(i) == \u0026#39;+\u0026#39;) i++; } while (i \u0026lt; len) { if (s.charAt(i) \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; s.charAt(i) \u0026lt;= \u0026#39;9\u0026#39;) { num = num * 10 + (s.charAt(i) - \u0026#39;0\u0026#39;); if (sign == -1 \u0026amp;\u0026amp; num * (-1) \u0026lt; Integer.MIN_VALUE) return Integer.MIN_VALUE; else if (sign == 1 \u0026amp;\u0026amp; num \u0026gt; Integer.MAX_VALUE) return Integer.MAX_VALUE; i++; } else break; } int res = (int) num; res *= sign; return res; } //63反转字符串 public String t63_solve(String str) { char[] cstr = str.toCharArray(); int len = str.length(); for (int i = 0; i \u0026lt; len / 2; i++) { char t = cstr[i]; cstr[i] = cstr[len - 1 - i]; cstr[len - 1 - i] = t; } return new String(cstr); } //64二分查找-II public int t64_search(int[] nums, int target) { //边界条件判断 if (nums == null || nums.length == 0) return -1; int left = 0; int right = nums.length - 1; while (left \u0026lt; right) { int mid = left + (right - left) / 2; if (nums[mid] \u0026lt; target) { left = mid + 1; } else { right = mid; } } return nums[left] == target ? left : -1; } //65三个数的最大乘积 public long t65_solve(int[] A) { // write code here int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE; int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; for (int num : A) { if (num \u0026gt; max1) { max3 = max2; max2 = max1; max1 = num; } else if (num \u0026gt; max2) { max3 = max2; max2 = num; } else if (num \u0026gt; max3) max3 = num; if (num \u0026lt; min1) { min2 = min1; min1 = num; } else if (num \u0026lt; min2) min2 = num; } return Math.max((long) max1 * max2 * max3, (long) max1 * min1 * min2); } //66寻找峰值 public int t66_findPeakElement(int[] nums) { int left = 0; int right = nums.length - 1; while (left \u0026lt; right) { int mid = left + (right - left) / 2; if (nums[mid] \u0026gt; nums[mid + 1]) { right = mid; } else { left = mid + 1; } } return right; } //67最大数 public String t67_solve(int[] nums) { // write code here ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; nums.length; i++) { list.add(String.valueOf(nums[i])); } Collections.sort(list, new Comparator\u0026lt;String\u0026gt;() { @Override public int compare(String a, String b) { return (b + a).compareTo(a + b); } }); if (list.get(0).equals(\u0026#34;0\u0026#34;)) return \u0026#34;0\u0026#34;; StringBuilder res = new StringBuilder(); for (int i = 0; i \u0026lt; list.size(); i++) { res.append(list.get(i)); } return res.toString(); } //68验证IP地址 public String t68_solve(String IP) { return validIPv4(IP) ? \u0026#34;IPv4\u0026#34; : (validIPv6(IP) ? \u0026#34;IPv6\u0026#34; : \u0026#34;Neither\u0026#34;); } private boolean validIPv4(String IP) { String[] strs = IP.split(\u0026#34;\\\\.\u0026#34;, -1); if (strs.length != 4) { return false; } for (String str : strs) { if (str.length() \u0026gt; 1 \u0026amp;\u0026amp; str.startsWith(\u0026#34;0\u0026#34;)) { return false; } try { int val = Integer.parseInt(str); if (!(val \u0026gt;= 0 \u0026amp;\u0026amp; val \u0026lt;= 255)) { return false; } } catch (NumberFormatException numberFormatException) { return false; } } return true; } private boolean validIPv6(String IP) { String[] strs = IP.split(\u0026#34;:\u0026#34;, -1); if (strs.length != 8) { return false; } for (String str : strs) { if (str.length() \u0026gt; 4 || str.length() == 0) { return false; } try { int val = Integer.parseInt(str, 16); } catch (NumberFormatException numberFormatException) { return false; } } return true; } //69把数字翻译成字符串 public int t69_solve(String nums) { if (nums.length() == 0 || nums.charAt(0) == \u0026#39;0\u0026#39;) return 0; int[] dp = new int[nums.length()]; dp[0] = 1; for (int i = 1; i \u0026lt; dp.length; i++) { if (nums.charAt(i) != \u0026#39;0\u0026#39;) { dp[i] = dp[i - 1]; } // 3 2 4 int num = (nums.charAt(i - 1) - \u0026#39;0\u0026#39;) * 10 + (nums.charAt(i) - \u0026#39;0\u0026#39;); if (num \u0026gt;= 10 \u0026amp;\u0026amp; num \u0026lt;= 26) { if (i == 1) { dp[i] += 1; } else { dp[i] += dp[i - 2]; } } } return dp[nums.length() - 1]; } //70合并二叉树 public TreeNode t70_mergeTrees(TreeNode t1, TreeNode t2) { if (t1 == null) return t2; if (t2 == null) return t1; t1.val += t2.val; t1.left = t70_mergeTrees(t1.left, t2.left); t1.right = t70_mergeTrees(t1.right, t2.right); return t1; } //71最小的K个数 public ArrayList\u0026lt;Integer\u0026gt; t71_GetLeastNumbers_Solution(int[] input, int k) { ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;Integer\u0026gt;(); if (input == null || k \u0026gt; input.length || k \u0026lt;= 0) return list; PriorityQueue\u0026lt;Integer\u0026gt; queue = new PriorityQueue\u0026lt;Integer\u0026gt;(new Comparator\u0026lt;Integer\u0026gt;() { public int compare(Integer i1, Integer i2) { return i2.compareTo(i1); } }); int len = input.length; for (int i = 0; i \u0026lt; len; ++i) { if (queue.size() != k) { queue.offer(input[i]); } else if (queue.peek() \u0026gt; input[i]) { queue.poll(); queue.offer(input[i]); } } Iterator\u0026lt;Integer\u0026gt; it = queue.iterator(); while (it.hasNext()) { list.add(it.next()); } return list; } //72字符串的排列 HashSet\u0026lt;String\u0026gt; res = new HashSet\u0026lt;String\u0026gt;(); ArrayList\u0026lt;String\u0026gt; r = new ArrayList\u0026lt;String\u0026gt;(); int length; char[] array; public ArrayList\u0026lt;String\u0026gt; t72_Permutation(String str) { if (str.length() == 0) return new ArrayList\u0026lt;String\u0026gt;(); init(str); perm(0); r = new ArrayList\u0026lt;String\u0026gt;(res); Collections.sort(r); return r; } public void init(String str) { length = str.length(); array = str.toCharArray(); Arrays.sort(array); res = new HashSet\u0026lt;String\u0026gt;(); } public void perm(int k) { if (k == length) { String s = new String(array); if (!res.contains(s)) res.add(s); } else { int j = k; for (; j \u0026lt; length; j++) { swap(k, j); perm(k + 1); swap(k, j); } } } public void swap(int i, int j) { char tmp = array[i]; array[i] = array[j]; array[j] = tmp; } //73最长公共子串 public String t73_LCS(String str1, String str2) { int maxLenth = 0; int maxLastIndex = 0; int[][] dp = new int[str1.length() + 1][str2.length() + 1]; for (int i = 0; i \u0026lt; str1.length(); i++) { for (int j = 0; j \u0026lt; str2.length(); j++) { if (str1.charAt(i) == str2.charAt(j)) { dp[i + 1][j + 1] = dp[i][j] + 1; if (dp[i + 1][j + 1] \u0026gt; maxLenth) { maxLenth = dp[i + 1][j + 1]; maxLastIndex = i; } } else { dp[i + 1][j + 1] = 0; } } } return str1.substring(maxLastIndex - maxLenth + 1, maxLastIndex + 1); } //74接雨水问题 public static long t74_maxWater(int[] arr) { if (arr == null || arr.length \u0026lt;= 2) { return 0; } int left = 0, right = arr.length - 1; long sum = 0; int mark = Math.min(arr[left], arr[right]); while (left \u0026lt; right) { if (arr[left] \u0026lt; arr[right]) { left++; if (arr[left] \u0026lt; mark) { sum += mark - arr[left]; } else { mark = Math.min(arr[left], arr[right]); } } else { right--; if (arr[right] \u0026lt; mark) { sum += mark - arr[right]; } else { mark = Math.min(arr[right], arr[left]); } } } return sum; } //75环形链表的约瑟夫问题 public int t75_ysf(int n, int m) { // write code here ListNode head = new ListNode(1); ListNode tail = head; for (int i = 2; i \u0026lt;= n; i++) { tail.next = new ListNode(i); tail = tail.next; } tail.next = head; ListNode index = head; ListNode pre = tail; int k = 0; while (index.next != null \u0026amp;\u0026amp; index.next != index) { k++; ListNode next = index.next; if (k == m) { pre.next = pre.next.next; k = 0; } pre = index; index = next; } return index.val; } //76链表的奇偶重排 public ListNode t76_oddEvenList(ListNode head) { if (head == null) { return head; } ListNode evenHead = head.next; ListNode odd = head, even = evenHead; while (even != null \u0026amp;\u0026amp; even.next != null) { odd.next = even.next; odd = odd.next; even.next = odd.next; even = even.next; } odd.next = evenHead; return head; } //77买卖股票的最好时机(二) public int t77_maxProfit(int[] prices) { int ans = 0; for (int i = 1; i \u0026lt; prices.length; i++) { if (prices[i] \u0026gt; prices[i - 1]) { ans += prices[i] - prices[i - 1]; } } return ans; } //78排序 public int[] t78_MySort(int[] arr) { quickSort(arr, 0, arr.length - 1); return arr; } public void quickSort(int[] list, int left, int right) { if (left \u0026lt; right) { int point = partition(list, left, right); quickSort(list, left, point - 1); quickSort(list, point + 1, right); } } public int partition(int[] list, int left, int right) { int first = list[left]; while (left \u0026lt; right) { while (left \u0026lt; right \u0026amp;\u0026amp; list[right] \u0026gt;= first) { right--; } swap(list, left, right); while (left \u0026lt; right \u0026amp;\u0026amp; list[left] \u0026lt;= first) { left++; } swap(list, left, right); } return left; } private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } //79判断是否为回文字符串 public boolean t79_judge(String str) { if (str.length() == 0) return true; //两个指针，一个从左边开始，一个从右边开始，每次两个 //指针都同时往中间挪，只要两个指针指向的字符不一样就返回false int left = 0; int right = str.length() - 1; while (left \u0026lt; right) { if (str.charAt(left++) != str.charAt(right--)) return false; } return true; } //80数组中只出现一次的数（其它数出现k次） public int t80_foundOnceNumber(int[] arr, int k) { // write code here HashMap\u0026lt;Integer, Boolean\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); for (int num : arr) { if (map.containsKey(num)) { map.put(num, true); } else { map.put(num, false); } } Set\u0026lt;Integer\u0026gt; set = map.keySet(); for (int num : set) { if (map.get(num) == false) { return num; } } return 0; } //冒泡排序 public int[] bubbleSort(int[] nums) { for (int i = 0; i \u0026lt; nums.length; i++) { for (int j = 0; j \u0026lt; nums.length - i - 1; j++) { if (nums[j] \u0026gt; nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; } } } return nums; } } ","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E7%AE%97%E6%B3%95/000002-80%E9%81%93%E9%AB%98%E9%A2%91%E7%AE%97%E6%B3%95%E9%A2%98java%E7%89%88/","summary":"80道常考算法题，逢考必过，剑指offer。","title":"80道高频算法题Java版"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在对安卓进行测试时，我们需要把 apk 传到安卓机上，对请求抓包，同时监控应用日志。本文就来讲讲具体操作。\n安卓机是指基于安卓的机器 ，如手机、POS 机、电视盒子等。\n传文件 我们拒绝用 U 盘传文件。直接用 WIFI 传。别问，问就是想装逼。哈哈哈。\n① 先把装逼神器 adb 下载下来，打开以下网址：\nhttps://adb.clockworkmod.com/\n下载后得到 UniversalAdbDriverSetup.msi 文件，只有 16.2 M，相比有些人让安装 Android Studio，轻巧了太多。\n② 安装之后打开 cmd，输入 adb：\n出现如图所示就算安装成功了。\n③ 接着继续装逼，把安卓机掏出来，打开设置 | 系统 | 关于手机，对着“版本号”一顿猛戳，戳到开发者模式为止，启用它（不同安卓机可能方式不同）。\n④ 装，接着装。在 WIFI 设置中，找到已经和电脑处于同一网络的 WIFI，查看高级选项，获取 IP 地址。\n⑤ 在电脑的 cmd 中，输入以下命令使用 adb 连接安卓机：\nadb connect 192.168.x.x 出现如图所示就算连接成功了：\n⑥ 好，一切就绪，开始骚操作，电脑 → 手机：\nadb push D:\\test.apk /storage/emulated/0/ 电脑 ← 手机：\nadb pull /storage/emulated/0/test.apk D:\\ 抓包 抓包用到的工具是 Charles。下载地址：\nhttps://www.charlesproxy.com/\n下载后直接安装即可。建议找下破解，不然用着会有点卡。\n抓包很简单，只需要两步。第一步在电脑端启动代理。第二步在安卓机上手动设置代理。\n第一步在电脑端启动代理。Charles 中打开 Proxy | Proxy Settings 启用代理，默认端口 8888：\n第二步在安卓机上手动设置代理。打开安卓机 WIFI 设置，编辑网络（不同手机可能方式不同），添加手动代理，填写电脑 IP 和 Charles 代理端口：\n保存后 Charles 会提示有连接请求：\n允许即可。\n看日志 电脑端 cmd 一行命令搞定：\nadb logcat 不过这样打出来的日志会比较多。可以做下过滤。\nWindows：\nadb logcat | find \u0026#34;some\u0026#34; Linux：\nadb locat | grep \u0026#34;some\u0026#34; 抓不到包的问题 有可能会抓不到包。不同机器原因各有不同。这里我不能给出完美的解决办法。只能分享下我的经验。如果无法解决，还是找下百度。\n一种可能就是 HTTPS 证书问题，在 Help | SSL Proxying 有对应功能：\n有些时候电脑端安装完证书就可以了，有些时候还需要在安卓机上下载证书：\nhttp://charlesproxy.com/getssl\n有一次我访问这个链接无响应，重启 Charles 又可以了下载了。\n还有一种可能是没有添加白名单，Proxy | SSL Proxying setttings：\n简要回顾 本文介绍了安卓机测试环境的准备，用到了 adb、Charles 软件，进行传文件、抓包、看日志。操作还是比较简单，不过实际过程中，可能会由于机器不同导致遇到奇奇怪怪的问题。通过百度，一般是能够解决的。最后温馨提示，如果第二天安卓机提示无法连接网络，那么看看是不是电脑的 Charles 关掉了。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E7%A7%BB%E5%8A%A8%E7%AB%AF/000002-android%E6%B5%8B%E8%AF%95%E4%B8%89%E4%BB%B6%E5%A5%97%E4%BC%A0%E6%96%87%E4%BB%B6%E6%8A%93%E5%8C%85%E7%9C%8B%E6%97%A5%E5%BF%97/","summary":"在对安卓进行测试时，我们需要把 apk 传到安卓机上，对请求抓包，同时监控应用日志。本文就来讲讲具体操作。","title":"Android测试三件套：传文件、抓包、看日志"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nClaude Code是Anthropic公司的产品，跟字节的豆包、阿里的千问和腾讯的元宝一样，只不过它更擅长于跟代码打交道。用Claude Code来写代码，就是通常说的Vibe Coding，跟着感觉编程。\nClaude Code交互 一般有3种交互方式，命令行、Pycharm / IDEA插件、Visual Studio Code，我最喜欢第三种，界面设计、操作丝滑、多模态。\nVibe Coding，是以对话框的形式为主的交互方式。我们习惯在界面左边放代码区，然后右侧边小窗口放claude对话框。现在，需要反过来：\n我们不再需要写代码，只需要在对话框告诉AI我们的想法，它来写。\n重点说下VS交互，可以同时开多个Claude窗口，查看历史对话，复制粘贴图片等，视觉效果和操作流畅度，都是体验最好的。毫不夸张地说，正是Visual Studio Code让我把Claude Code玩得溜。\n最重要的是输入 Claude可以写代码，写文档，配置环境，排查问题。前提是你要准确告诉它你的想法。如果实在描述不清楚，可以切换到Plan模式，让它来采访你。记住Claude是有上下文的，一次没说清楚，可以持续补充。熟能生巧，你的想法只有你知道，在使用中持续提升。现在AI越来越高级了，当你说的不清楚，它还会找你确认细节的。不用太担心。\nAI不是人 你不用跟ai说，请你帮我XXX、你能不能XXX、看一下XXX。AI不是人，不用那么焦虑的和它对话，它只是一个机器，一个普通的工具，直接命令它干活。\n把你的电脑交给Claude Code 跟龙虾类似，Claude Code也可以直接操作你电脑的所有东西，比如VS的快捷键配置直接告诉ai把ctrl + c设置为打开claude新窗口，它会自动给你完成。其他的安装软件、解决环境问题和本地文件操作，它都可以做。\n写脚本还是写平台 刚开始我发现claude可以写web平台，一股脑把现在的小工具全部做成了网页，实际使用下来发现，有些工具还是Python脚本用起来顺手。当你使用Claude写Python脚本的时候，你就能感受到自动生成代码的快乐。\nSKILL和MCP 不要自己写skill，不要自己写mcp，这些都交给claude。Vibe Coding的精髓就在于，不要自己动手，只管验收最终效果。也可以让对话框A来写，对话框B再来检查和优化。\n对话框 最关键的我认为是交互方式的思想转变。很多时候我们不需要在页面点点点，也不需要执行Python脚本，而是让claude去完成这些事情，在对话框里跟ai交互。我80%的时间都是在对话框用自然语言和ai交互，让它生成和执行某些脚本，打开浏览器抓一些数据，汇总某些信息。“少动手，多说话”，你就能事半功倍。\n发现问题，解决问题，工作效率自然就能提高。\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E6%B5%8B%E8%AF%95/000002-claude-code%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%E5%B8%AE%E4%BD%A0%E6%8F%90%E6%95%8880/","summary":"Claude Code是Anthropic公司的产品，跟字节的豆包、阿里的千问和腾讯的元宝一样，只不过它更擅长于跟代码打交道。用Claude Code来写代码，就是通常说的Vibe Coding，跟着感觉编程。","title":"Claude Code使用技巧帮你提效80%"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDjango 开发项目是很快的，有多快？看完本篇文章，你就知道了。\n安装 Django 前提条件：已安装 Python。\nDjango 使用 pip 命令直接就可以安装：\npip install django 如果安装失败，很可能是因为网络连接超时了，试试国内镜像：\npip install --default-timeout=6000 -i https://pypi.tuna.tsinghua.edu.cn/simple django 这条命令同时延长了超时时间，提高成功率。\n安装完成后，通过 django-admin --version 这条命令验证。\n创建 project 按住 Windows 键 + R，输入 cmd 回车，接着 cd 到任意目录：\n或者先打开目录窗口，在地址栏输入 cmd 回车，不需要 cd（推荐）：\n在命令行窗口输入命令创建 project：\ndjango-admin startproject mysite 目录结构如下（注意有 2 个 mysite 哦）：\nmysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py 创建 app Django 项目由 1 个 project 和多个 app 组成。project 存放项目配置，app 是网站各个功能的具体实现。\napp 的放置目录没有限制，这里先放在和 mysite 的同级目录中，先进入外层的 mysite 文件夹：\ncd mysite 输入命令创建：\ndjango-admin startapp polls 目录结构如下：\nmysite/ polls/ __init__.py admin.py apps.py migrations/ __init__.py models.py tests.py views.py mysite/ manage.py 使用 cmd 创建完 project 和 app 以后，就可以打开 PyCharm，再 Open mysite 项目了：\n为了让 app 和 project 关联起来，需要在 mysite/settings.py 文件中添加配置：\nINSTALLED_APPS = [ \u0026#39;django.contrib.admin\u0026#39;, \u0026#39;django.contrib.auth\u0026#39;, \u0026#39;django.contrib.contenttypes\u0026#39;, \u0026#39;django.contrib.sessions\u0026#39;, \u0026#39;django.contrib.messages\u0026#39;, \u0026#39;django.contrib.staticfiles\u0026#39;, \u0026#39;polls.apps.PollsConfig\u0026#39;, # 添加 ] 在 polls/models.py 文件中添加数据模型：\nfrom django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField(\u0026#39;date published\u0026#39;) class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) 连接 MySQL 前提条件：已安装 MySQL，创建好数据库。\nDjango 默认使用轻量级数据库 SQLite，我们切换为常用的 MySQL。\n修改 mysite/settings.py 中的数据库配置：\nDATABASES = { \u0026#39;default\u0026#39;: { \u0026#39;ENGINE\u0026#39;: \u0026#39;django.db.backends.mysql\u0026#39;, \u0026#39;HOST\u0026#39;: \u0026#39;主机\u0026#39;, \u0026#39;PORT\u0026#39;: 端口, \u0026#39;NAME\u0026#39;: \u0026#39;数据库名\u0026#39;, \u0026#39;USER\u0026#39;: \u0026#39;用户名\u0026#39;, \u0026#39;PASSWORD\u0026#39;: \u0026#39;密码\u0026#39; } } Django 默认通过底层调用 mysqlclient 这个库和 MySQL 交互。但是 mysqlclient 非常不好安装！我们改用 pymysql。\n先安装：\npip install pymysql 然后在与 mysite/settings.py 文件同级的 __init__ 文件中添加：\nimport pymysql pymysql.version_info = (1, 4, 0, \u0026#34;final\u0026#34;, 0) pymysql.install_as_MySQLdb() 数据迁移 数据迁移，指的是把代码中的数据模型，迁移到数据库中，创建相应的表结构。\n第一步，告诉 Django 数据模型有更新：\npython manage.py makemigrations polls 执行后输出：\nMigrations for \u0026#39;polls\u0026#39;: polls/migrations/0001_initial.py - Create model Question - Create model Choice 第二步，执行数据迁移同步：\npython manage.py migrate 执行后输出：\nOperations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Rendering model states... DONE Applying polls.0001_initial... OK 数据库表就创建好了：\n启动项目 万事俱备，只差 1 条启动命令，在根目录执行：\npython manage.py runserver 访问 http://127.0.0.1:8000/，就可以看到来自 Django 老铁的这发火箭了：\n小结 本文是对《匆匆一眼》文章的补充。介绍了如何安装 Django，创建 Django 的 project 和 app，连接 MySQL，执行数据迁移，最后启动项目的实际操作步骤。思路很清晰。整体流程清楚了，可这些文件都是干嘛的呢？\n参考资料：\nhttps://docs.djangoproject.com/en/3.1/intro/tutorial01/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000002-djangodjango%E6%90%AD%E5%BB%BA%E7%A4%BA%E4%BE%8B%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E4%B8%8E%E9%81%BF%E5%9D%91%E7%BB%86%E8%8A%82/","summary":"Django 开发项目是很快的，有多快？看完本篇文章，你就知道了。","title":"Django搭建示例项目实战与避坑细节"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n响应状态码 在@app.post()方法中添加status_code参数：\nfrom fastapi import FastAPI app = FastAPI() @app.post(\u0026#34;/items/\u0026#34;, status_code=201) async def create_item(name: str): return {\u0026#34;name\u0026#34;: name} status_code也可以是IntEnum，比如Python的http.HTTPStatus。\n常见响应状态码：\n100以上，信息；很少直接使用；\n200以上，成功；200是OK，201是Created，204是No Content；\n300以上，重定向；304是Not Modified；\n400以上，客户端错误；404是Not Found；\n500以上，服务器错误；\nFastAPI引入了status，可以方便的录入这些状态：\nfrom fastapi import FastAPI, status app = FastAPI() @app.post(\u0026#34;/items/\u0026#34;, status_code=status.HTTP_201_CREATED) async def create_item(name: str): return {\u0026#34;name\u0026#34;: name} 表单数据 为了使用表单，首先需要安装python-multipart：\npip install python-multipart 示例：\nfrom fastapi import FastAPI, Form app = FastAPI() @app.post(\u0026#34;/login/\u0026#34;) async def login(username: str = Form(), password: str = Form()): return {\u0026#34;username\u0026#34;: username} 表单由HTML中的\u0026lt;form\u0026gt;\u0026lt;/form\u0026gt;发送，请求头的content-type一般是application/x-www-form-urlencoded，当为文件时multipart/form-data。\n请求文件 示例：\nfrom fastapi import FastAPI, File, UploadFile app = FastAPI() @app.post(\u0026#34;/files/\u0026#34;) async def create_file(file: bytes = File()): return {\u0026#34;file_size\u0026#34;: len(file)} @app.post(\u0026#34;/uploadfile/\u0026#34;) async def create_upload_file(file: UploadFile): return {\u0026#34;filename\u0026#34;: file.filename} create_file()的类型为bytes，接收到的文件内容也是bytes，数据都存在于内存中，适用于小文件。create_upload_file()的类型为UploadFile，它会在内存设置一个最大存储，超出最大存储，就会把数据转存到磁盘，适用于大文件。\nUploadFile有以下属性：\nfilename，文件名，比如myimage.jpg；\ncontent_type，文件类型，比如image/jpeg；\nfile，SpooledTemporaryFile实例，一个file-like对象。\nUploadFile有以下方法：\nwrite(data)：写数据(str或bytes)到文件；\nread(size)：从文件读size(int)大小的bytes或character；\nseek(offset)：定位到文件中offset(int)的位置，比如await myfile.seek(0)会定位到文件开始；\nclose()：关闭文件；\n所有这些方法都是async的，需要await：\ncontents = await myfile.read() 不想await就使用其中的file对象：\ncontents = myfile.file.read() 文件可选非必传：\nfrom typing import Union from fastapi import FastAPI, File, UploadFile app = FastAPI() @app.post(\u0026#34;/files/\u0026#34;) async def create_file(file: Union[bytes, None] = File(default=None)): if not file: return {\u0026#34;message\u0026#34;: \u0026#34;No file sent\u0026#34;} else: return {\u0026#34;file_size\u0026#34;: len(file)} @app.post(\u0026#34;/uploadfile/\u0026#34;) async def create_upload_file(file: Union[UploadFile, None] = None): if not file: return {\u0026#34;message\u0026#34;: \u0026#34;No upload file sent\u0026#34;} else: return {\u0026#34;filename\u0026#34;: file.filename} UploadFile的metadata：\nfrom fastapi import FastAPI, File, UploadFile app = FastAPI() @app.post(\u0026#34;/files/\u0026#34;) async def create_file(file: bytes = File(description=\u0026#34;A file read as bytes\u0026#34;)): return {\u0026#34;file_size\u0026#34;: len(file)} @app.post(\u0026#34;/uploadfile/\u0026#34;) async def create_upload_file( file: UploadFile = File(description=\u0026#34;A file read as UploadFile\u0026#34;), ): return {\u0026#34;filename\u0026#34;: file.filename} 多文件上传：\nfrom typing import List from fastapi import FastAPI, File, UploadFile from fastapi.responses import HTMLResponse app = FastAPI() @app.post(\u0026#34;/files/\u0026#34;) async def create_files(files: List[bytes] = File()): return {\u0026#34;file_sizes\u0026#34;: [len(file) for file in files]} @app.post(\u0026#34;/uploadfiles/\u0026#34;) async def create_upload_files(files: List[UploadFile]): return {\u0026#34;filenames\u0026#34;: [file.filename for file in files]} @app.get(\u0026#34;/\u0026#34;) async def main(): content = \u0026#34;\u0026#34;\u0026#34; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026#34;/files/\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;files\u0026#34; type=\u0026#34;file\u0026#34; multiple\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;form action=\u0026#34;/uploadfiles/\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;files\u0026#34; type=\u0026#34;file\u0026#34; multiple\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026#34;\u0026#34;\u0026#34; return HTMLResponse(content=content) 同时请求表单和文件 示例：\nfrom fastapi import FastAPI, File, Form, UploadFile app = FastAPI() @app.post(\u0026#34;/files/\u0026#34;) async def create_file( file: bytes = File(), fileb: UploadFile = File(), token: str = Form() ): return { \u0026#34;file_size\u0026#34;: len(file), \u0026#34;token\u0026#34;: token, \u0026#34;fileb_content_type\u0026#34;: fileb.content_type, } 错误处理 FastAPI提供了HTTPException：\nfrom fastapi import FastAPI, HTTPException app = FastAPI() items = {\u0026#34;foo\u0026#34;: \u0026#34;The Foo Wrestlers\u0026#34;} @app.get(\u0026#34;/items/{item_id}\u0026#34;) async def read_item(item_id: str): if item_id not in items: raise HTTPException(status_code=404, detail=\u0026#34;Item not found\u0026#34;) return {\u0026#34;item\u0026#34;: items[item_id]} HTTPException不是return而是raise的，抛出异常。\n对于抛出的异常，可以使用@app.exception_handler自定义handler进行处理：\nfrom fastapi import FastAPI, Request from fastapi.responses import JSONResponse class UnicornException(Exception): def __init__(self, name: str): self.name = name app = FastAPI() @app.exception_handler(UnicornException) async def unicorn_exception_handler(request: Request, exc: UnicornException): return JSONResponse( status_code=418, content={\u0026#34;message\u0026#34;: f\u0026#34;Oops! {exc.name} did something. There goes a rainbow...\u0026#34;}, ) @app.get(\u0026#34;/unicorns/{name}\u0026#34;) async def read_unicorn(name: str): if name == \u0026#34;yolo\u0026#34;: raise UnicornException(name=name) return {\u0026#34;unicorn_name\u0026#34;: name} 在抛出HTTPException异常时，FastAPI有很多默认的handler，比如RequestValidationError，可以使用此方法重写默认的handler：\nfrom fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import PlainTextResponse from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI() @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc): return PlainTextResponse(str(exc.detail), status_code=exc.status_code) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): return PlainTextResponse(str(exc), status_code=400) @app.get(\u0026#34;/items/{item_id}\u0026#34;) async def read_item(item_id: int): if item_id == 3: raise HTTPException(status_code=418, detail=\u0026#34;Nope! I don\u0026#39;t like 3.\u0026#34;) return {\u0026#34;item_id\u0026#34;: item_id} 默认handler会返回：\n{ \u0026#34;detail\u0026#34;: [ { \u0026#34;loc\u0026#34;: [ \u0026#34;path\u0026#34;, \u0026#34;item_id\u0026#34; ], \u0026#34;msg\u0026#34;: \u0026#34;value is not a valid integer\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;type_error.integer\u0026#34; } ] } 而重写handler后会返回字符串：\n1 validation error path -\u0026gt; item_id value is not a valid integer (type=type_error.integer) 如果不想改动默认handler，只是补充点信息，可以导入http_exception_handler和request_validation_exception_handler：\nfrom fastapi import FastAPI, HTTPException from fastapi.exception_handlers import ( http_exception_handler, request_validation_exception_handler, ) from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI() @app.exception_handler(StarletteHTTPException) async def custom_http_exception_handler(request, exc): print(f\u0026#34;OMG! An HTTP error!: {repr(exc)}\u0026#34;) return await http_exception_handler(request, exc) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): print(f\u0026#34;OMG! The client sent invalid data!: {exc}\u0026#34;) return await request_validation_exception_handler(request, exc) @app.get(\u0026#34;/items/{item_id}\u0026#34;) async def read_item(item_id: int): if item_id == 3: raise HTTPException(status_code=418, detail=\u0026#34;Nope! I don\u0026#39;t like 3.\u0026#34;) return {\u0026#34;item_id\u0026#34;: item_id} 路径操作配置 响应状态码：\nfrom typing import Set, Union from fastapi import FastAPI, status from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() @app.post(\u0026#34;/items/\u0026#34;, response_model=Item, status_code=status.HTTP_201_CREATED) async def create_item(item: Item): return item 标签：\nfrom typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() @app.post(\u0026#34;/items/\u0026#34;, response_model=Item, tags=[\u0026#34;items\u0026#34;]) async def create_item(item: Item): return item @app.get(\u0026#34;/items/\u0026#34;, tags=[\u0026#34;items\u0026#34;]) async def read_items(): return [{\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;price\u0026#34;: 42}] @app.get(\u0026#34;/users/\u0026#34;, tags=[\u0026#34;users\u0026#34;]) async def read_users(): return [{\u0026#34;username\u0026#34;: \u0026#34;johndoe\u0026#34;}] 标签枚举：\nfrom enum import Enum from fastapi import FastAPI app = FastAPI() class Tags(Enum): items = \u0026#34;items\u0026#34; users = \u0026#34;users\u0026#34; @app.get(\u0026#34;/items/\u0026#34;, tags=[Tags.items]) async def get_items(): return [\u0026#34;Portal gun\u0026#34;, \u0026#34;Plumbus\u0026#34;] @app.get(\u0026#34;/users/\u0026#34;, tags=[Tags.users]) async def read_users(): return [\u0026#34;Rick\u0026#34;, \u0026#34;Morty\u0026#34;] 概要和描述：\nfrom typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() @app.post( \u0026#34;/items/\u0026#34;, response_model=Item, summary=\u0026#34;Create an item\u0026#34;, description=\u0026#34;Create an item with all the information, name, description, price, tax and a set of unique tags\u0026#34;, ) async def create_item(item: Item): return item 文档字符串：\nfrom typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() @app.post(\u0026#34;/items/\u0026#34;, response_model=Item, summary=\u0026#34;Create an item\u0026#34;) async def create_item(item: Item): \u0026#34;\u0026#34;\u0026#34; Create an item with all the information: - **name**: each item must have a name - **description**: a long description - **price**: required - **tax**: if the item doesn\u0026#39;t have tax, you can omit this - **tags**: a set of unique tag strings for this item \u0026#34;\u0026#34;\u0026#34; return item 响应描述：\nfrom typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() @app.post( \u0026#34;/items/\u0026#34;, response_model=Item, summary=\u0026#34;Create an item\u0026#34;, response_description=\u0026#34;The created item\u0026#34;, ) async def create_item(item: Item): \u0026#34;\u0026#34;\u0026#34; Create an item with all the information: - **name**: each item must have a name - **description**: a long description - **price**: required - **tax**: if the item doesn\u0026#39;t have tax, you can omit this - **tags**: a set of unique tag strings for this item \u0026#34;\u0026#34;\u0026#34; return item 标记为deprecated：\nfrom fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;, tags=[\u0026#34;items\u0026#34;]) async def read_items(): return [{\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;price\u0026#34;: 42}] @app.get(\u0026#34;/users/\u0026#34;, tags=[\u0026#34;users\u0026#34;]) async def read_users(): return [{\u0026#34;username\u0026#34;: \u0026#34;johndoe\u0026#34;}] @app.get(\u0026#34;/elements/\u0026#34;, tags=[\u0026#34;items\u0026#34;], deprecated=True) async def read_elements(): return [{\u0026#34;item_id\u0026#34;: \u0026#34;Foo\u0026#34;}] JSON兼容编码器 jsonable_encoder()函数的作用是把Pydantic model转换成JSON兼容的类型比如dict、list等。\nfrom datetime import datetime from typing import Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder from pydantic import BaseModel fake_db = {} class Item(BaseModel): title: str timestamp: datetime description: Union[str, None] = None app = FastAPI() @app.put(\u0026#34;/items/{id}\u0026#34;) def update_item(id: str, item: Item): json_compatible_item_data = jsonable_encoder(item) fake_db[id] = json_compatible_item_data Body-更新 使用PUT：\nfrom typing import List, Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: Union[str, None] = None description: Union[str, None] = None price: Union[float, None] = None tax: float = 10.5 tags: List[str] = [] items = { \u0026#34;foo\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;price\u0026#34;: 50.2}, \u0026#34;bar\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Bar\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The bartenders\u0026#34;, \u0026#34;price\u0026#34;: 62, \u0026#34;tax\u0026#34;: 20.2}, \u0026#34;baz\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Baz\u0026#34;, \u0026#34;description\u0026#34;: None, \u0026#34;price\u0026#34;: 50.2, \u0026#34;tax\u0026#34;: 10.5, \u0026#34;tags\u0026#34;: []}, } @app.get(\u0026#34;/items/{item_id}\u0026#34;, response_model=Item) async def read_item(item_id: str): return items[item_id] @app.put(\u0026#34;/items/{item_id}\u0026#34;, response_model=Item) async def update_item(item_id: str, item: Item): update_item_encoded = jsonable_encoder(item) items[item_id] = update_item_encoded return update_item_encoded 输入数据使用了jsonable_encoder()函数转换为JSON兼容类型。\n使用PATCH：\nexclude_unset=True\nfrom typing import List, Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: Union[str, None] = None description: Union[str, None] = None price: Union[float, None] = None tax: float = 10.5 tags: List[str] = [] items = { \u0026#34;foo\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;price\u0026#34;: 50.2}, \u0026#34;bar\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Bar\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The bartenders\u0026#34;, \u0026#34;price\u0026#34;: 62, \u0026#34;tax\u0026#34;: 20.2}, \u0026#34;baz\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Baz\u0026#34;, \u0026#34;description\u0026#34;: None, \u0026#34;price\u0026#34;: 50.2, \u0026#34;tax\u0026#34;: 10.5, \u0026#34;tags\u0026#34;: []}, } @app.get(\u0026#34;/items/{item_id}\u0026#34;, response_model=Item) async def read_item(item_id: str): return items[item_id] @app.patch(\u0026#34;/items/{item_id}\u0026#34;, response_model=Item) async def update_item(item_id: str, item: Item): stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) update_data = item.dict(exclude_unset=True) updated_item = stored_item_model.copy(update=update_data) items[item_id] = jsonable_encoder(updated_item) return updated_item .copy(update=update_data)\nfrom typing import List, Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: Union[str, None] = None description: Union[str, None] = None price: Union[float, None] = None tax: float = 10.5 tags: List[str] = [] items = { \u0026#34;foo\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;price\u0026#34;: 50.2}, \u0026#34;bar\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Bar\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The bartenders\u0026#34;, \u0026#34;price\u0026#34;: 62, \u0026#34;tax\u0026#34;: 20.2}, \u0026#34;baz\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Baz\u0026#34;, \u0026#34;description\u0026#34;: None, \u0026#34;price\u0026#34;: 50.2, \u0026#34;tax\u0026#34;: 10.5, \u0026#34;tags\u0026#34;: []}, } @app.get(\u0026#34;/items/{item_id}\u0026#34;, response_model=Item) async def read_item(item_id: str): return items[item_id] @app.patch(\u0026#34;/items/{item_id}\u0026#34;, response_model=Item) async def update_item(item_id: str, item: Item): stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) update_data = item.dict(exclude_unset=True) updated_item = stored_item_model.copy(update=update_data) items[item_id] = jsonable_encoder(updated_item) return updated_item PUT和PATCH都可以用来部分更新，PUT用的更多。\n依赖 什么是依赖注入？在FastAPI里面，你可以在路径操作函数中添加依赖的声明，然后FastAPI会自动加载这些依赖。\n依赖注入的好处有：\n复用代码； 复用数据库连接； 增强安全、认证、角色； 等等等； 依赖注入示例：\nfrom typing import Union from fastapi import Depends, FastAPI app = FastAPI() async def common_parameters( q: Union[str, None] = None, skip: int = 0, limit: int = 100 ): return {\u0026#34;q\u0026#34;: q, \u0026#34;skip\u0026#34;: skip, \u0026#34;limit\u0026#34;: limit} @app.get(\u0026#34;/items/\u0026#34;) async def read_items(commons: dict = Depends(common_parameters)): return commons @app.get(\u0026#34;/users/\u0026#34;) async def read_users(commons: dict = Depends(common_parameters)): return commons common_parameters()函数是个简单的依赖； Depends引入依赖； FastAPI就会自动调用common_parameters()函数并把结果返回给commons，而无需任何其他代码。\n依赖也可以使用class，把common_parameters()函数改为CommonQueryParams类：\nfrom typing import Union from fastapi import Depends, FastAPI app = FastAPI() fake_items_db = [{\u0026#34;item_name\u0026#34;: \u0026#34;Foo\u0026#34;}, {\u0026#34;item_name\u0026#34;: \u0026#34;Bar\u0026#34;}, {\u0026#34;item_name\u0026#34;: \u0026#34;Baz\u0026#34;}] class CommonQueryParams: def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): self.q = q self.skip = skip self.limit = limit @app.get(\u0026#34;/items/\u0026#34;) async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)): response = {} if commons.q: response.update({\u0026#34;q\u0026#34;: commons.q}) items = fake_items_db[commons.skip : commons.skip + commons.limit] response.update({\u0026#34;items\u0026#34;: items}) return response Depends会创建一个CommonQueryParams的实例然后赋值给commons。\n更一步简化，只写1次CommonQueryParams：\ncommons: CommonQueryParams = Depends() Depends()里面的CommonQueryParams可以省略掉。\nFastAPI支持子依赖，也就是Depends嵌套：\nfrom typing import Union from fastapi import Cookie, Depends, FastAPI app = FastAPI() def query_extractor(q: Union[str, None] = None): return q def query_or_cookie_extractor( q: str = Depends(query_extractor), last_query: Union[str, None] = Cookie(default=None), ): if not q: return last_query return q @app.get(\u0026#34;/items/\u0026#34;) async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)): return {\u0026#34;q_or_cookie\u0026#34;: query_or_default} 如果使用同一个依赖多次，FastAPI默认会只注入一次。可以按以下设置让FastAPI注入多次：\nasync def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): return {\u0026#34;fresh_value\u0026#34;: fresh_value} 多个依赖可以用dependencies的list：\nfrom fastapi import Depends, FastAPI, Header, HTTPException app = FastAPI() async def verify_token(x_token: str = Header()): if x_token != \u0026#34;fake-super-secret-token\u0026#34;: raise HTTPException(status_code=400, detail=\u0026#34;X-Token header invalid\u0026#34;) async def verify_key(x_key: str = Header()): if x_key != \u0026#34;fake-super-secret-key\u0026#34;: raise HTTPException(status_code=400, detail=\u0026#34;X-Key header invalid\u0026#34;) return x_key @app.get(\u0026#34;/items/\u0026#34;, dependencies=[Depends(verify_token), Depends(verify_key)]) async def read_items(): return [{\u0026#34;item\u0026#34;: \u0026#34;Foo\u0026#34;}, {\u0026#34;item\u0026#34;: \u0026#34;Bar\u0026#34;}] 如果给FastAPI的构造函数传入dependencies，那么就是全局依赖：\nfrom fastapi import Depends, FastAPI, Header, HTTPException async def verify_token(x_token: str = Header()): if x_token != \u0026#34;fake-super-secret-token\u0026#34;: raise HTTPException(status_code=400, detail=\u0026#34;X-Token header invalid\u0026#34;) async def verify_key(x_key: str = Header()): if x_key != \u0026#34;fake-super-secret-key\u0026#34;: raise HTTPException(status_code=400, detail=\u0026#34;X-Key header invalid\u0026#34;) return x_key app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) @app.get(\u0026#34;/items/\u0026#34;) async def read_items(): return [{\u0026#34;item\u0026#34;: \u0026#34;Portal Gun\u0026#34;}, {\u0026#34;item\u0026#34;: \u0026#34;Plumbus\u0026#34;}] @app.get(\u0026#34;/users/\u0026#34;) async def read_users(): return [{\u0026#34;username\u0026#34;: \u0026#34;Rick\u0026#34;}, {\u0026#34;username\u0026#34;: \u0026#34;Morty\u0026#34;}] 如果在依赖函数中使用yield，它后面的代码就相当于teardown，这点用法跟pytest的fixture类似：\nasync def get_db(): db = DBSession() try: yield db finally: db.close() 另外，借助yield和with可以创建一个上下文管理器（实现__enter__和__exit__）：\nclass MySuperContextManager: def __init__(self): self.db = DBSession() def __enter__(self): return self.db def __exit__(self, exc_type, exc_value, traceback): self.db.close() async def get_db(): with MySuperContextManager() as db: yield db 安全 FastAPI支持OAuth2协议：\nfrom fastapi import Depends, FastAPI from fastapi.security import OAuth2PasswordBearer app = FastAPI() oauth2_scheme = OAuth2PasswordBearer(tokenUrl=\u0026#34;token\u0026#34;) @app.get(\u0026#34;/items/\u0026#34;) async def read_items(token: str = Depends(oauth2_scheme)): return {\u0026#34;token\u0026#34;: token} 备注：需要提前安装 pip install python-multipart，因为OAuth2使用表单来发送username和password。虽然这个接口已经加上鉴权了。但这些入参都没有生效，因为我们还没有添加相应的处理代码。为了让鉴权实际生效，我们继续添加代码：\nfrom typing import Union from fastapi import Depends, FastAPI from fastapi.security import OAuth2PasswordBearer from pydantic import BaseModel app = FastAPI() oauth2_scheme = OAuth2PasswordBearer(tokenUrl=\u0026#34;token\u0026#34;) class User(BaseModel): username: str email: Union[str, None] = None full_name: Union[str, None] = None disabled: Union[bool, None] = None def fake_decode_token(token): return User( username=token + \u0026#34;fakedecoded\u0026#34;, email=\u0026#34;john@example.com\u0026#34;, full_name=\u0026#34;John Doe\u0026#34; ) async def get_current_user(token: str = Depends(oauth2_scheme)): user = fake_decode_token(token) return user @app.get(\u0026#34;/users/me\u0026#34;) async def read_users_me(current_user: User = Depends(get_current_user)): return current_user 定义模型User； 创建依赖get_current_user； fake_decode_token接收token，返回模拟的假用户； read_users_me注入依赖； 然后实现username和password：\nfrom typing import Union from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from pydantic import BaseModel fake_users_db = { \u0026#34;johndoe\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;johndoe\u0026#34;, \u0026#34;full_name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;johndoe@example.com\u0026#34;, \u0026#34;hashed_password\u0026#34;: \u0026#34;fakehashedsecret\u0026#34;, \u0026#34;disabled\u0026#34;: False, }, \u0026#34;alice\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;alice\u0026#34;, \u0026#34;full_name\u0026#34;: \u0026#34;Alice Wonderson\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;alice@example.com\u0026#34;, \u0026#34;hashed_password\u0026#34;: \u0026#34;fakehashedsecret2\u0026#34;, \u0026#34;disabled\u0026#34;: True, }, } app = FastAPI() def fake_hash_password(password: str): return \u0026#34;fakehashed\u0026#34; + password oauth2_scheme = OAuth2PasswordBearer(tokenUrl=\u0026#34;token\u0026#34;) class User(BaseModel): username: str email: Union[str, None] = None full_name: Union[str, None] = None disabled: Union[bool, None] = None class UserInDB(User): hashed_password: str def get_user(db, username: str): if username in db: user_dict = db[username] return UserInDB(**user_dict) def fake_decode_token(token): # This doesn\u0026#39;t provide any security at all # Check the next version user = get_user(fake_users_db, token) return user async def get_current_user(token: str = Depends(oauth2_scheme)): user = fake_decode_token(token) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=\u0026#34;Invalid authentication credentials\u0026#34;, headers={\u0026#34;WWW-Authenticate\u0026#34;: \u0026#34;Bearer\u0026#34;}, ) return user async def get_current_active_user(current_user: User = Depends(get_current_user)): if current_user.disabled: raise HTTPException(status_code=400, detail=\u0026#34;Inactive user\u0026#34;) return current_user @app.post(\u0026#34;/token\u0026#34;) async def login(form_data: OAuth2PasswordRequestForm = Depends()): user_dict = fake_users_db.get(form_data.username) if not user_dict: raise HTTPException(status_code=400, detail=\u0026#34;Incorrect username or password\u0026#34;) user = UserInDB(**user_dict) hashed_password = fake_hash_password(form_data.password) if not hashed_password == user.hashed_password: raise HTTPException(status_code=400, detail=\u0026#34;Incorrect username or password\u0026#34;) return {\u0026#34;access_token\u0026#34;: user.username, \u0026#34;token_type\u0026#34;: \u0026#34;bearer\u0026#34;} @app.get(\u0026#34;/users/me\u0026#34;) async def read_users_me(current_user: User = Depends(get_current_active_user)): return current_user 现在就可以测试一下了http://127.0.0.1:8000/docs：\n授权以后：\n访问/users/me会返回：\n{ \u0026#34;username\u0026#34;: \u0026#34;johndoe\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;johndoe@example.com\u0026#34;, \u0026#34;full_name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;disabled\u0026#34;: false, \u0026#34;hashed_password\u0026#34;: \u0026#34;fakehashedsecret\u0026#34; } 如果logout再访问会出现：\n{ \u0026#34;detail\u0026#34;: \u0026#34;Not authenticated\u0026#34; } 输入错误的用户会出现：\n{ \u0026#34;detail\u0026#34;: \u0026#34;Inactive user\u0026#34; } 如果想使用JWT，那么先安装python-jose。为了给密码加密，需要安装passlib。\n示例代码：\nfrom datetime import datetime, timedelta from typing import Union from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel ## to get a string like this run: ## openssl rand -hex 32 SECRET_KEY = \u0026#34;09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7\u0026#34; ALGORITHM = \u0026#34;HS256\u0026#34; ACCESS_TOKEN_EXPIRE_MINUTES = 30 fake_users_db = { \u0026#34;johndoe\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;johndoe\u0026#34;, \u0026#34;full_name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;johndoe@example.com\u0026#34;, \u0026#34;hashed_password\u0026#34;: \u0026#34;$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW\u0026#34;, \u0026#34;disabled\u0026#34;: False, } } class Token(BaseModel): access_token: str token_type: str class TokenData(BaseModel): username: Union[str, None] = None class User(BaseModel): username: str email: Union[str, None] = None full_name: Union[str, None] = None disabled: Union[bool, None] = None class UserInDB(User): hashed_password: str pwd_context = CryptContext(schemes=[\u0026#34;bcrypt\u0026#34;], deprecated=\u0026#34;auto\u0026#34;) oauth2_scheme = OAuth2PasswordBearer(tokenUrl=\u0026#34;token\u0026#34;) app = FastAPI() def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password): return pwd_context.hash(password) def get_user(db, username: str): if username in db: user_dict = db[username] return UserInDB(**user_dict) def authenticate_user(fake_db, username: str, password: str): user = get_user(fake_db, username) if not user: return False if not verify_password(password, user.hashed_password): return False return user def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({\u0026#34;exp\u0026#34;: expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt async def get_current_user(token: str = Depends(oauth2_scheme)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=\u0026#34;Could not validate credentials\u0026#34;, headers={\u0026#34;WWW-Authenticate\u0026#34;: \u0026#34;Bearer\u0026#34;}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get(\u0026#34;sub\u0026#34;) if username is None: raise credentials_exception token_data = TokenData(username=username) except JWTError: raise credentials_exception user = get_user(fake_users_db, username=token_data.username) if user is None: raise credentials_exception return user async def get_current_active_user(current_user: User = Depends(get_current_user)): if current_user.disabled: raise HTTPException(status_code=400, detail=\u0026#34;Inactive user\u0026#34;) return current_user @app.post(\u0026#34;/token\u0026#34;, response_model=Token) async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=\u0026#34;Incorrect username or password\u0026#34;, headers={\u0026#34;WWW-Authenticate\u0026#34;: \u0026#34;Bearer\u0026#34;}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={\u0026#34;sub\u0026#34;: user.username}, expires_delta=access_token_expires ) return {\u0026#34;access_token\u0026#34;: access_token, \u0026#34;token_type\u0026#34;: \u0026#34;bearer\u0026#34;} @app.get(\u0026#34;/users/me/\u0026#34;, response_model=User) async def read_users_me(current_user: User = Depends(get_current_active_user)): return current_user @app.get(\u0026#34;/users/me/items/\u0026#34;) async def read_own_items(current_user: User = Depends(get_current_active_user)): return [{\u0026#34;item_id\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;owner\u0026#34;: current_user.username}] 其中的SECRET_KEY通过openssl生成：\nopenssl rand -hex 32 中间件 FastAPI这里的中间件，指的是一个函数，它在请求处理前被调用，在响应返回前调用。有点类似于Spring的过滤器filter。\n创建中间件：\nimport time from fastapi import FastAPI, Request app = FastAPI() @app.middleware(\u0026#34;http\u0026#34;) async def add_process_time_header(request: Request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers[\u0026#34;X-Process-Time\u0026#34;] = str(process_time) return response CORS Cross-Origin Resource Sharing，跨域访问。\n同域包括协议、域名、端口，以下均是不同域：\nhttp://localhost https://localhost http://localhost:8080 使用CORSMiddleware可以实现跨域访问：\nfrom fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [ \u0026#34;http://localhost.tiangolo.com\u0026#34;, \u0026#34;https://localhost.tiangolo.com\u0026#34;, \u0026#34;http://localhost\u0026#34;, \u0026#34;http://localhost:8080\u0026#34;, ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=[\u0026#34;*\u0026#34;], allow_headers=[\u0026#34;*\u0026#34;], ) @app.get(\u0026#34;/\u0026#34;) async def main(): return {\u0026#34;message\u0026#34;: \u0026#34;Hello World\u0026#34;} allow_origins，允许域名，[*]代表所有； allow_origin_regex，允许域名的正则匹配； allow_methods，允许请求方法，[*]代表所有； allow_headers，允许请求头，[*]代表所有； allow_credentials，跨域访问时是否需要cookie，默认False，设置为True时allow_origins不能设置为[*]； expose_headers，暴露给浏览器的响应头，默认[]； max_age，浏览器最大缓存CORS 响应的时间，默认60s； SQL关系型数据库 官方教程使用的是SQLAlchemy。\n示例：\n. └── sql_app ├── __init__.py ├── crud.py ├── database.py ├── main.py ├── models.py └── schemas.py 安装：\npip install sqlalchemy 创建数据库：\nfrom sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker SQLALCHEMY_DATABASE_URL = \u0026#34;sqlite:///./sql_app.db\u0026#34; ## SQLALCHEMY_DATABASE_URL = \u0026#34;postgresql://user:password@postgresserver/db\u0026#34; engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={\u0026#34;check_same_thread\u0026#34;: False} ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() 创建数据库模型：\nfrom sqlalchemy import Boolean, Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship from .database import Base class User(Base): __tablename__ = \u0026#34;users\u0026#34; id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) items = relationship(\u0026#34;Item\u0026#34;, back_populates=\u0026#34;owner\u0026#34;) class Item(Base): __tablename__ = \u0026#34;items\u0026#34; id = Column(Integer, primary_key=True, index=True) title = Column(String, index=True) description = Column(String, index=True) owner_id = Column(Integer, ForeignKey(\u0026#34;users.id\u0026#34;)) owner = relationship(\u0026#34;User\u0026#34;, back_populates=\u0026#34;items\u0026#34;) 创建Pydantic模型：\nfrom typing import List, Union from pydantic import BaseModel class ItemBase(BaseModel): title: str description: Union[str, None] = None class ItemCreate(ItemBase): pass class Item(ItemBase): id: int owner_id: int class Config: orm_mode = True class UserBase(BaseModel): email: str class UserCreate(UserBase): password: str class User(UserBase): id: int is_active: bool items: List[Item] = [] class Config: orm_mode = True 注意，SQLAlchemy使用=赋值，Pydantic使用:赋值。\n增删改查：\nfrom sqlalchemy.orm import Session from . import models, schemas def get_user(db: Session, user_id: int): return db.query(models.User).filter(models.User.id == user_id).first() def get_user_by_email(db: Session, email: str): return db.query(models.User).filter(models.User.email == email).first() def get_users(db: Session, skip: int = 0, limit: int = 100): return db.query(models.User).offset(skip).limit(limit).all() def create_user(db: Session, user: schemas.UserCreate): fake_hashed_password = user.password + \u0026#34;notreallyhashed\u0026#34; db_user = models.User(email=user.email, hashed_password=fake_hashed_password) db.add(db_user) db.commit() db.refresh(db_user) return db_user def get_items(db: Session, skip: int = 0, limit: int = 100): return db.query(models.Item).offset(skip).limit(limit).all() def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): db_item = models.Item(**item.dict(), owner_id=user_id) db.add(db_item) db.commit() db.refresh(db_item) return db_item 主程序：\nfrom typing import List from fastapi import Depends, FastAPI, HTTPException from sqlalchemy.orm import Session from . import crud, models, schemas from .database import SessionLocal, engine models.Base.metadata.create_all(bind=engine) app = FastAPI() ## Dependency def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.post(\u0026#34;/users/\u0026#34;, response_model=schemas.User) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): db_user = crud.get_user_by_email(db, email=user.email) if db_user: raise HTTPException(status_code=400, detail=\u0026#34;Email already registered\u0026#34;) return crud.create_user(db=db, user=user) @app.get(\u0026#34;/users/\u0026#34;, response_model=List[schemas.User]) def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): users = crud.get_users(db, skip=skip, limit=limit) return users @app.get(\u0026#34;/users/{user_id}\u0026#34;, response_model=schemas.User) def read_user(user_id: int, db: Session = Depends(get_db)): db_user = crud.get_user(db, user_id=user_id) if db_user is None: raise HTTPException(status_code=404, detail=\u0026#34;User not found\u0026#34;) return db_user @app.post(\u0026#34;/users/{user_id}/items/\u0026#34;, response_model=schemas.Item) def create_item_for_user( user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) ): return crud.create_user_item(db=db, item=item, user_id=user_id) @app.get(\u0026#34;/items/\u0026#34;, response_model=List[schemas.Item]) def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): items = crud.get_items(db, skip=skip, limit=limit) return items 大应用-多文件 示例目录结构：\n. ├── app # \u0026#34;app\u0026#34; is a Python package │ ├── __init__.py # this file makes \u0026#34;app\u0026#34; a \u0026#34;Python package\u0026#34; │ ├── main.py # \u0026#34;main\u0026#34; module, e.g. import app.main │ ├── dependencies.py # \u0026#34;dependencies\u0026#34; module, e.g. import app.dependencies │ └── routers # \u0026#34;routers\u0026#34; is a \u0026#34;Python subpackage\u0026#34; │ │ ├── __init__.py # makes \u0026#34;routers\u0026#34; a \u0026#34;Python subpackage\u0026#34; │ │ ├── items.py # \u0026#34;items\u0026#34; submodule, e.g. import app.routers.items │ │ └── users.py # \u0026#34;users\u0026#34; submodule, e.g. import app.routers.users │ └── internal # \u0026#34;internal\u0026#34; is a \u0026#34;Python subpackage\u0026#34; │ ├── __init__.py # makes \u0026#34;internal\u0026#34; a \u0026#34;Python subpackage\u0026#34; │ └── admin.py # \u0026#34;admin\u0026#34; submodule, e.g. import app.internal.admin APIRouter用于定义子模块的路由：\nfrom fastapi import APIRouter router = APIRouter() @router.get(\u0026#34;/users/\u0026#34;, tags=[\u0026#34;users\u0026#34;]) async def read_users(): return [{\u0026#34;username\u0026#34;: \u0026#34;Rick\u0026#34;}, {\u0026#34;username\u0026#34;: \u0026#34;Morty\u0026#34;}] @router.get(\u0026#34;/users/me\u0026#34;, tags=[\u0026#34;users\u0026#34;]) async def read_user_me(): return {\u0026#34;username\u0026#34;: \u0026#34;fakecurrentuser\u0026#34;} @router.get(\u0026#34;/users/{username}\u0026#34;, tags=[\u0026#34;users\u0026#34;]) async def read_user(username: str): return {\u0026#34;username\u0026#34;: username} from fastapi import APIRouter, Depends, HTTPException from ..dependencies import get_token_header router = APIRouter( prefix=\u0026#34;/items\u0026#34;, tags=[\u0026#34;items\u0026#34;], dependencies=[Depends(get_token_header)], responses={404: {\u0026#34;description\u0026#34;: \u0026#34;Not found\u0026#34;}}, ) fake_items_db = {\u0026#34;plumbus\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Plumbus\u0026#34;}, \u0026#34;gun\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Portal Gun\u0026#34;}} @router.get(\u0026#34;/\u0026#34;) async def read_items(): return fake_items_db @router.get(\u0026#34;/{item_id}\u0026#34;) async def read_item(item_id: str): if item_id not in fake_items_db: raise HTTPException(status_code=404, detail=\u0026#34;Item not found\u0026#34;) return {\u0026#34;name\u0026#34;: fake_items_db[item_id][\u0026#34;name\u0026#34;], \u0026#34;item_id\u0026#34;: item_id} @router.put( \u0026#34;/{item_id}\u0026#34;, tags=[\u0026#34;custom\u0026#34;], responses={403: {\u0026#34;description\u0026#34;: \u0026#34;Operation forbidden\u0026#34;}}, ) async def update_item(item_id: str): if item_id != \u0026#34;plumbus\u0026#34;: raise HTTPException( status_code=403, detail=\u0026#34;You can only update the item: plumbus\u0026#34; ) return {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;name\u0026#34;: \u0026#34;The great Plumbus\u0026#34;} 在主程序中引入子模块路由：\nfrom fastapi import Depends, FastAPI from .dependencies import get_query_token, get_token_header from .internal import admin from .routers import items, users app = FastAPI(dependencies=[Depends(get_query_token)]) app.include_router(users.router) app.include_router(items.router) app.include_router( admin.router, prefix=\u0026#34;/admin\u0026#34;, tags=[\u0026#34;admin\u0026#34;], dependencies=[Depends(get_token_header)], responses={418: {\u0026#34;description\u0026#34;: \u0026#34;I\u0026#39;m a teapot\u0026#34;}}, ) @app.get(\u0026#34;/\u0026#34;) async def root(): return {\u0026#34;message\u0026#34;: \u0026#34;Hello Bigger Applications!\u0026#34;} 后台任务 使用BackgroundTasks定义后台任务：\nfrom fastapi import BackgroundTasks, FastAPI app = FastAPI() def write_notification(email: str, message=\u0026#34;\u0026#34;): with open(\u0026#34;log.txt\u0026#34;, mode=\u0026#34;w\u0026#34;) as email_file: content = f\u0026#34;notification for {email}: {message}\u0026#34; email_file.write(content) @app.post(\u0026#34;/send-notification/{email}\u0026#34;) async def send_notification(email: str, background_tasks: BackgroundTasks): background_tasks.add_task(write_notification, email, message=\u0026#34;some notification\u0026#34;) return {\u0026#34;message\u0026#34;: \u0026#34;Notification sent in the background\u0026#34;} BackgroundTasks也能支持依赖注入：\nfrom typing import Union from fastapi import BackgroundTasks, Depends, FastAPI app = FastAPI() def write_log(message: str): with open(\u0026#34;log.txt\u0026#34;, mode=\u0026#34;a\u0026#34;) as log: log.write(message) def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): if q: message = f\u0026#34;found query: {q}\\n\u0026#34; background_tasks.add_task(write_log, message) return q @app.post(\u0026#34;/send-notification/{email}\u0026#34;) async def send_notification( email: str, background_tasks: BackgroundTasks, q: str = Depends(get_query) ): message = f\u0026#34;message to {email}\\n\u0026#34; background_tasks.add_task(write_log, message) return {\u0026#34;message\u0026#34;: \u0026#34;Message sent\u0026#34;} 元数据和文档URL 设置应用元数据：\nfrom fastapi import FastAPI description = \u0026#34;\u0026#34;\u0026#34; ChimichangApp API helps you do awesome stuff. 🚀 ### Items You can **read items**. ### Users You will be able to: * **Create users** (_not implemented_). * **Read users** (_not implemented_). \u0026#34;\u0026#34;\u0026#34; app = FastAPI( title=\u0026#34;ChimichangApp\u0026#34;, description=description, version=\u0026#34;0.0.1\u0026#34;, terms_of_service=\u0026#34;http://example.com/terms/\u0026#34;, contact={ \u0026#34;name\u0026#34;: \u0026#34;Deadpoolio the Amazing\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;http://x-force.example.com/contact/\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;dp@x-force.example.com\u0026#34;, }, license_info={ \u0026#34;name\u0026#34;: \u0026#34;Apache 2.0\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://www.apache.org/licenses/LICENSE-2.0.html\u0026#34;, }, ) @app.get(\u0026#34;/items/\u0026#34;) async def read_items(): return [{\u0026#34;name\u0026#34;: \u0026#34;Katana\u0026#34;}] 效果：\n设置tag元数据：\nfrom fastapi import FastAPI tags_metadata = [ { \u0026#34;name\u0026#34;: \u0026#34;users\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Operations with users. The **login** logic is also here.\u0026#34;, }, { \u0026#34;name\u0026#34;: \u0026#34;items\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Manage items. So _fancy_ they have their own docs.\u0026#34;, \u0026#34;externalDocs\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Items external docs\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://fastapi.tiangolo.com/\u0026#34;, }, }, ] app = FastAPI(openapi_tags=tags_metadata) @app.get(\u0026#34;/users/\u0026#34;, tags=[\u0026#34;users\u0026#34;]) async def get_users(): return [{\u0026#34;name\u0026#34;: \u0026#34;Harry\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;Ron\u0026#34;}] @app.get(\u0026#34;/items/\u0026#34;, tags=[\u0026#34;items\u0026#34;]) async def get_items(): return [{\u0026#34;name\u0026#34;: \u0026#34;wand\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;flying broom\u0026#34;}] 添加tag：\nfrom fastapi import FastAPI tags_metadata = [ { \u0026#34;name\u0026#34;: \u0026#34;users\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Operations with users. The **login** logic is also here.\u0026#34;, }, { \u0026#34;name\u0026#34;: \u0026#34;items\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Manage items. So _fancy_ they have their own docs.\u0026#34;, \u0026#34;externalDocs\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Items external docs\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://fastapi.tiangolo.com/\u0026#34;, }, }, ] app = FastAPI(openapi_tags=tags_metadata) @app.get(\u0026#34;/users/\u0026#34;, tags=[\u0026#34;users\u0026#34;]) async def get_users(): return [{\u0026#34;name\u0026#34;: \u0026#34;Harry\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;Ron\u0026#34;}] @app.get(\u0026#34;/items/\u0026#34;, tags=[\u0026#34;items\u0026#34;]) async def get_items(): return [{\u0026#34;name\u0026#34;: \u0026#34;wand\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;flying broom\u0026#34;}] 效果：\nOpenAPI的URL默认是/openapi.json，设置/api/v1/openapi.json：\nfrom fastapi import FastAPI app = FastAPI(openapi_url=\u0026#34;/api/v1/openapi.json\u0026#34;) @app.get(\u0026#34;/items/\u0026#34;) async def read_items(): return [{\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;}] 文档的URL默认是/docs，设置为/documentation：\nfrom fastapi import FastAPI app = FastAPI(docs_url=\u0026#34;/documentation\u0026#34;, redoc_url=None) @app.get(\u0026#34;/items/\u0026#34;) async def read_items(): return [{\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;}] 静态文件 使用StaticFiles：\nfrom fastapi import FastAPI from fastapi.staticfiles import StaticFiles app = FastAPI() app.mount(\u0026#34;/static\u0026#34;, StaticFiles(directory=\u0026#34;static\u0026#34;), name=\u0026#34;static\u0026#34;) FastAPI会自动挂载静态文件。\n单元测试 使用pytest和TestClient：\nfrom fastapi import FastAPI from fastapi.testclient import TestClient app = FastAPI() @app.get(\u0026#34;/\u0026#34;) async def read_main(): return {\u0026#34;msg\u0026#34;: \u0026#34;Hello World\u0026#34;} client = TestClient(app) def test_read_main(): response = client.get(\u0026#34;/\u0026#34;) assert response.status_code == 200 assert response.json() == {\u0026#34;msg\u0026#34;: \u0026#34;Hello World\u0026#34;} 单元测试文件拆出来：\n. ├── app │ ├── __init__.py │ ├── main.py │ └── test_main.py from fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/\u0026#34;) async def read_main(): return {\u0026#34;msg\u0026#34;: \u0026#34;Hello World\u0026#34;} from fastapi.testclient import TestClient from .main import app client = TestClient(app) def test_read_main(): response = client.get(\u0026#34;/\u0026#34;) assert response.status_code == 200 assert response.json() == {\u0026#34;msg\u0026#34;: \u0026#34;Hello World\u0026#34;} 调试 通常借助PyCharm打断点调试：\n参考资料：\n官方教程 https://fastapi.tiangolo.com/tutorial/\n示例项目 https://fastapi.tiangolo.com/project-generation/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/fastapi/000002-fastapifastapi%E5%AE%98%E6%96%B9%E6%95%99%E7%A8%8B%E5%A4%AA%E6%A3%92%E4%BA%86%E4%B8%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"响应状态码\"\u003e响应状态码\u003c/h1\u003e\n\u003cp\u003e在\u003ccode\u003e@app.post()\u003c/code\u003e方法中添加status_code参数：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003efastapi\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e FastAPI\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapp \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.post\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/items/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e status_code\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e201\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003easync\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ecreate_item\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003estr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e name\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003estatus_code也可以是\u003ccode\u003eIntEnum\u003c/code\u003e，比如Python的\u003ccode\u003ehttp.HTTPStatus\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e常见响应状态码：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e100以上，信息；很少直接使用；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e200以上，成功；200是OK，201是Created，204是No Content；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e300以上，重定向；304是Not Modified；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e400以上，客户端错误；404是Not Found；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e500以上，服务器错误；\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFastAPI引入了status，可以方便的录入这些状态：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003efastapi\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e status\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapp \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.post\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/items/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e status_code\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003estatus\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003eHTTP_201_CREATED\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003easync\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ecreate_item\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003estr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e name\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"表单数据\"\u003e表单数据\u003c/h1\u003e\n\u003cp\u003e为了使用表单，首先需要安装python-multipart：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epip install python\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003emultipart\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e示例：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003efastapi\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e Form\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapp \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.post\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/login/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003easync\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elogin\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eusername\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003estr\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e Form\u003cspan style=\"color:#1f2328\"\u003e(),\u003c/span\u003e password\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003estr\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e Form\u003cspan style=\"color:#1f2328\"\u003e()):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;username\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e username\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e表单由HTML中的\u003ccode\u003e\u0026lt;form\u0026gt;\u0026lt;/form\u0026gt;\u003c/code\u003e发送，请求头的content-type一般是\u003ccode\u003eapplication/x-www-form-urlencoded\u003c/code\u003e，当为文件时\u003ccode\u003emultipart/form-data\u003c/code\u003e。\u003c/p\u003e\n\u003ch1 id=\"请求文件\"\u003e请求文件\u003c/h1\u003e\n\u003cp\u003e示例：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003efastapi\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e File\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e UploadFile\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapp \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.post\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/files/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003easync\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ecreate_file\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efile\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ebytes\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e File\u003cspan style=\"color:#1f2328\"\u003e()):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;file_size\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efile\u003cspan style=\"color:#1f2328\"\u003e)}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.post\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/uploadfile/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003easync\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ecreate_upload_file\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003efile\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e UploadFile\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;filename\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e file\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003efilename\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003ecreate_file()\u003c/code\u003e的类型为bytes，接收到的文件内容也是bytes，数据都存在于内存中，适用于小文件。\u003ccode\u003ecreate_upload_file()\u003c/code\u003e的类型为UploadFile，它会在内存设置一个最大存储，超出最大存储，就会把数据转存到磁盘，适用于大文件。\u003c/p\u003e","title":"FastAPI官方教程太棒了（下）"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在HttpRunner3的示例代码中，发送HTTP请求的代码是这样写的：\nfrom httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseBasic(HttpRunner): config = Config(\u0026#34;basic test with httpbin\u0026#34;).base_url(\u0026#34;https://httpbin.org/\u0026#34;) teststeps = [ Step( RunRequest(\u0026#34;headers\u0026#34;) .get(\u0026#34;/headers\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) .assert_equal(\u0026#34;body.headers.Host\u0026#34;, \u0026#34;httpbin.org\u0026#34;) ), # 省略 Step( RunRequest(\u0026#34;post data\u0026#34;) .post(\u0026#34;/post\u0026#34;) .with_headers(**{\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}) .with_data(\u0026#34;abc\u0026#34;) .validate() .assert_equal(\u0026#34;status_code\u0026#34;, 200) ), # 省略 ] if __name__ == \u0026#34;__main__\u0026#34;: TestCaseBasic().test_start() 类TestCaseBasic继承了类HttpRunner。 在类TestCaseBasic的内部定义了teststeps列表，由多个Step类的实例对象组成。 类Step初始化传入类RunRequest的方法get和post就把HTTP请求发出去了。 这到底是怎么实现的？\n先看下RunRequest的源码：\nclass RunRequest(object): def __init__(self, name: Text): self.__step_context = TStep(name=name) def with_variables(self, **variables) -\u0026gt; \u0026#34;RunRequest\u0026#34;: self.__step_context.variables.update(variables) return self def setup_hook(self, hook: Text, assign_var_name: Text = None) -\u0026gt; \u0026#34;RunRequest\u0026#34;: if assign_var_name: self.__step_context.setup_hooks.append({assign_var_name: hook}) else: self.__step_context.setup_hooks.append(hook) return self def get(self, url: Text) -\u0026gt; RequestWithOptionalArgs: self.__step_context.request = TRequest(method=MethodEnum.GET, url=url) return RequestWithOptionalArgs(self.__step_context) def post(self, url: Text) -\u0026gt; RequestWithOptionalArgs: self.__step_context.request = TRequest(method=MethodEnum.POST, url=url) return RequestWithOptionalArgs(self.__step_context) def put(self, url: Text) -\u0026gt; RequestWithOptionalArgs: self.__step_context.request = TRequest(method=MethodEnum.PUT, url=url) return RequestWithOptionalArgs(self.__step_context) def head(self, url: Text) -\u0026gt; RequestWithOptionalArgs: self.__step_context.request = TRequest(method=MethodEnum.HEAD, url=url) return RequestWithOptionalArgs(self.__step_context) def delete(self, url: Text) -\u0026gt; RequestWithOptionalArgs: self.__step_context.request = TRequest(method=MethodEnum.DELETE, url=url) return RequestWithOptionalArgs(self.__step_context) def options(self, url: Text) -\u0026gt; RequestWithOptionalArgs: self.__step_context.request = TRequest(method=MethodEnum.OPTIONS, url=url) return RequestWithOptionalArgs(self.__step_context) def patch(self, url: Text) -\u0026gt; RequestWithOptionalArgs: self.__step_context.request = TRequest(method=MethodEnum.PATCH, url=url) return RequestWithOptionalArgs(self.__step_context) 里面定义了get、post等HTTP请求的Method。方法内部：\nself.__step_context.request = TRequest(method=MethodEnum.GET, url=url) 有个TRequest类：\nclass TRequest(BaseModel): \u0026#34;\u0026#34;\u0026#34;requests.Request model\u0026#34;\u0026#34;\u0026#34; method: MethodEnum url: Url params: Dict[Text, Text] = {} headers: Headers = {} req_json: Union[Dict, List, Text] = Field(None, alias=\u0026#34;json\u0026#34;) data: Union[Text, Dict[Text, Any]] = None cookies: Cookies = {} timeout: float = 120 allow_redirects: bool = True verify: Verify = False upload: Dict = {} # used for upload files 它继承了pydantic.BaseModel，是用来做数据验证的，比如这里的url指定了Url类型，如果传一个str类型，就会校验失败。简而言之，这是给代码规范用的，没有实际的业务功能。\n下面有一行注释：requests.Request mode，看来这个跟requests有点关系。\n回过头来看看self.__step_context.request，也就是self.__step_context对象有个request属性，它的定义是：\nself.__step_context = TStep(name=name) 答案应该就在TStep中了：\nclass TStep(BaseModel): name: Name request: Union[TRequest, None] = None testcase: Union[Text, Callable, None] = None variables: VariablesMapping = {} setup_hooks: Hooks = [] teardown_hooks: Hooks = [] # used to extract request\u0026#39;s response field extract: VariablesMapping = {} # used to export session variables from referenced testcase export: Export = [] validators: Validators = Field([], alias=\u0026#34;validate\u0026#34;) validate_script: List[Text] = [] 还是个Model，里面的request定义是：\nrequest: Union[TRequest, None] = None 又绕回TRequest了。这个Union是typing模块里面的：Union[X, Y] means either X or Y. 意思就是request的类型要么是TRequest要么是None。\n在刚才get的方法中，还有一句return RequestWithOptionalArgs(self.__step_context)，RequestWithOptionalArgs的定义如下：\nclass RequestWithOptionalArgs(object): def __init__(self, step_context: TStep): self.__step_context = step_context def with_params(self, **params) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.params.update(params) return self def with_headers(self, **headers) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.headers.update(headers) return self def with_cookies(self, **cookies) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.cookies.update(cookies) return self def with_data(self, data) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.data = data return self def with_json(self, req_json) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.req_json = req_json return self def set_timeout(self, timeout: float) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.timeout = timeout return self def set_verify(self, verify: bool) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.verify = verify return self def set_allow_redirects(self, allow_redirects: bool) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.allow_redirects = allow_redirects return self def upload(self, **file_info) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: self.__step_context.request.upload.update(file_info) return self def teardown_hook( self, hook: Text, assign_var_name: Text = None ) -\u0026gt; \u0026#34;RequestWithOptionalArgs\u0026#34;: if assign_var_name: self.__step_context.teardown_hooks.append({assign_var_name: hook}) else: self.__step_context.teardown_hooks.append(hook) return self def extract(self) -\u0026gt; StepRequestExtraction: return StepRequestExtraction(self.__step_context) def validate(self) -\u0026gt; StepRequestValidation: return StepRequestValidation(self.__step_context) def perform(self) -\u0026gt; TStep: return self.__step_context 可以给HTTP请求添加params、headers等可选项。\n看到这里，仍然不知道HTTP请求到底发出去的，因为没有调用呀。\n只能往上层找，看调用RunRequest的Step类：\nclass Step(object): def __init__( self, step_context: Union[ StepRequestValidation, StepRequestExtraction, RequestWithOptionalArgs, RunTestCase, StepRefCase, ], ): self.__step_context = step_context.perform() @property def request(self) -\u0026gt; TRequest: return self.__step_context.request @property def testcase(self) -\u0026gt; TestCase: return self.__step_context.testcase def perform(self) -\u0026gt; TStep: return self.__step_context Step类的__init__方法也用Union做了类型校验，其中RequestWithOptionalArgs就是RunRequest的gei等方法会返回的，这倒是匹配上了。它还有个request属性。有点眉目了。\n再往上层找，看HttpRunner类，有个__run_step_request的方法：\ndef __run_step_request(self, step: TStep) -\u0026gt; StepData: \u0026#34;\u0026#34;\u0026#34;run teststep: request\u0026#34;\u0026#34;\u0026#34; step_data = StepData(name=step.name) # parse prepare_upload_step(step, self.__project_meta.functions) request_dict = step.request.dict() request_dict.pop(\u0026#34;upload\u0026#34;, None) parsed_request_dict = parse_data( request_dict, step.variables, self.__project_meta.functions ) parsed_request_dict[\u0026#34;headers\u0026#34;].setdefault( \u0026#34;HRUN-Request-ID\u0026#34;, f\u0026#34;HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}\u0026#34;, ) step.variables[\u0026#34;request\u0026#34;] = parsed_request_dict # setup hooks if step.setup_hooks: self.__call_hooks(step.setup_hooks, step.variables, \u0026#34;setup request\u0026#34;) # prepare arguments method = parsed_request_dict.pop(\u0026#34;method\u0026#34;) url_path = parsed_request_dict.pop(\u0026#34;url\u0026#34;) url = build_url(self.__config.base_url, url_path) parsed_request_dict[\u0026#34;verify\u0026#34;] = self.__config.verify parsed_request_dict[\u0026#34;json\u0026#34;] = parsed_request_dict.pop(\u0026#34;req_json\u0026#34;, {}) # request resp = self.__session.request(method, url, **parsed_request_dict) resp_obj = ResponseObject(resp) step.variables[\u0026#34;response\u0026#34;] = resp_obj # teardown hooks if step.teardown_hooks: self.__call_hooks(step.teardown_hooks, step.variables, \u0026#34;teardown request\u0026#34;) def log_req_resp_details(): err_msg = \u0026#34;\\n{} DETAILED REQUEST \u0026amp; RESPONSE {}\\n\u0026#34;.format(\u0026#34;*\u0026#34; * 32, \u0026#34;*\u0026#34; * 32) # log request err_msg += \u0026#34;====== request details ======\\n\u0026#34; err_msg += f\u0026#34;url: {url}\\n\u0026#34; err_msg += f\u0026#34;method: {method}\\n\u0026#34; headers = parsed_request_dict.pop(\u0026#34;headers\u0026#34;, {}) err_msg += f\u0026#34;headers: {headers}\\n\u0026#34; for k, v in parsed_request_dict.items(): v = utils.omit_long_data(v) err_msg += f\u0026#34;{k}: {repr(v)}\\n\u0026#34; err_msg += \u0026#34;\\n\u0026#34; # log response err_msg += \u0026#34;====== response details ======\\n\u0026#34; err_msg += f\u0026#34;status_code: {resp.status_code}\\n\u0026#34; err_msg += f\u0026#34;headers: {resp.headers}\\n\u0026#34; err_msg += f\u0026#34;body: {repr(resp.text)}\\n\u0026#34; logger.error(err_msg) # extract extractors = step.extract extract_mapping = resp_obj.extract(extractors) step_data.export_vars = extract_mapping variables_mapping = step.variables variables_mapping.update(extract_mapping) # validate validators = step.validators session_success = False try: resp_obj.validate( validators, variables_mapping, self.__project_meta.functions ) session_success = True except ValidationFailure: session_success = False log_req_resp_details() # log testcase duration before raise ValidationFailure self.__duration = time.time() - self.__start_at raise finally: self.success = session_success step_data.success = session_success if hasattr(self.__session, \u0026#34;data\u0026#34;): # httprunner.client.HttpSession, not locust.clients.HttpSession # save request \u0026amp; response meta data self.__session.data.success = session_success self.__session.data.validators = resp_obj.validation_results # save step data step_data.data = self.__session.data return step_data 就是这里了，它的函数名用了双下划线开头：双下划线前缀会让Python解释器重写属性名称，以避免子类中的命名冲突。 这也称为名称改写（name mangling），即解释器会更改变量的名称，以便在稍后扩展这个类时避免命名冲突。说人话就是，类的私有成员，只能在类的内部调用，不对外暴露。它只在__run_step()方法中调用了1次：step_data = self.__run_step_request(step)。\n中间有一段：\n## request resp = self.__session.request(method, url, **parsed_request_dict) resp_obj = ResponseObject(resp) step.variables[\u0026#34;response\u0026#34;] = resp_obj 好家伙，self.__session.request()，跟reqeusts那个有点像了。点进去。\n一下就跳转到了httprunner.client.py，众里寻他千百度，默然回首，它竟然就在client。\nclass HttpSession(requests.Session): \u0026#34;\u0026#34;\u0026#34; Class for performing HTTP requests and holding (session-) cookies between requests (in order to be able to log in and out of websites). Each request is logged so that HttpRunner can display statistics. This is a slightly extended version of `python-request \u0026lt;http://python-requests.org\u0026gt;`_\u0026#39;s :py:class:`requests.Session` class and mostly this class works exactly the same. \u0026#34;\u0026#34;\u0026#34; def __init__(self): super(HttpSession, self).__init__() self.data = SessionData() def update_last_req_resp_record(self, resp_obj): \u0026#34;\u0026#34;\u0026#34; update request and response info from Response() object. \u0026#34;\u0026#34;\u0026#34; # TODO: fix self.data.req_resps.pop() self.data.req_resps.append(get_req_resp_record(resp_obj)) def request(self, method, url, name=None, **kwargs): 继承了requests.Session然后进行了重写。\n果然，还是用到了requests库。\n参考资料：\nhttps://github.com/httprunner/httprunner\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/httprunner/000002-httprunnerhttprunner3%E7%9A%84http%E8%AF%B7%E6%B1%82%E6%98%AF%E6%80%8E%E4%B9%88%E5%8F%91%E5%87%BA%E5%8E%BB%E7%9A%84/","summary":"在HttpRunner3的示例代码中，发送HTTP请求的代码是这样写的：","title":"HttpRunner3的HTTP请求是怎么发出去的"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nServlet是什么？ 现在的Java Web开发，很少直接接触到Servlet。在Spring项目中，Spring Boot基于Spring Framework，简化了Spring应用程序开发，Spring Framework是一个全面的Java应用程序框架，其中包含Spring MVC模块，Spring MVC封装了Servlet，以实现Web功能。Servlet不属于Spirng项目，而是Java EE（后改名为Jakarta EE）的一部分。\nOracle将Java EE（Java SE还自己保留）交给开源组织，Eclipse基金会接手。但Oracle不允许开源组织使用Java名号，所以Jakarta EE名称于2018.02.26应运而生。\nServlet可以有很多种描述：\nServlet是一个技术，用来创建Web应用程序\nServlet是一个API，提供了很多接口、类和文档\nServlet是一个接口，必须实现该接口来创建Servlet\nServlet是一个类，位于服务端，接收请求，返回响应\nServlet是一个Web组件，用来创建动态Web页面\nCGI与Servlet 在Servlet技术出现以前，Web应用程序是通过CGI（Common Gateway Interface），一种脚本语言，来实现的。\n但是CGI存在很多缺点：\n如果客户端增多，服务端响应会变慢\n对于每个请求，CGI都会创建一个进程，进程数有限\n依赖C、C++、perl等编程语言\nServlet技术出现以后，解决了很多CGI问题：\n性能：每个请求创建一个线程，而不是进程\n可移植性：因为使用了Java语言\n健壮性：JVM管理Servlet，无需关心内存泄漏、垃圾回收等\n安全性：因为使用了Java语言\n总的来说，Servlet是Server Applet，服务端小程序。它是单例多线程的，每个Servlet类在Web容器中只有一个实例，每次客户端请求，Web Server都会从线程池中分配一个线程去创建Servlet，每个请求由一个独立的线程处理，这些线程共享同一个Servlet实例，通过HTTP协议接收和响应来自Web Client的请求。\n参考资料：\nhttps://www.javatpoint.com/servlet-tutorial\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000002-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80servlet%E7%AE%80%E4%BB%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"servlet是什么\"\u003eServlet是什么？\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0863f0b88a9b4244b59573ab6e6655bd.png\"\u003e\u003c/p\u003e\n\u003cp\u003e现在的Java Web开发，很少直接接触到Servlet。在Spring项目中，Spring Boot基于Spring Framework，简化了Spring应用程序开发，Spring Framework是一个全面的Java应用程序框架，其中包含Spring MVC模块，Spring MVC封装了Servlet，以实现Web功能。Servlet不属于Spirng项目，而是Java EE（后改名为Jakarta EE）的一部分。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eOracle将Java EE（Java SE还自己保留）交给开源组织，Eclipse基金会接手。但Oracle不允许开源组织使用Java名号，所以Jakarta EE名称于2018.02.26应运而生。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eServlet可以有很多种描述：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eServlet是一个技术，用来创建Web应用程序\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServlet是一个API，提供了很多接口、类和文档\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServlet是一个接口，必须实现该接口来创建Servlet\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServlet是一个类，位于服务端，接收请求，返回响应\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServlet是一个Web组件，用来创建动态Web页面\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/524e589b55a04b2795c7331558a7ee61.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"cgi与servlet\"\u003eCGI与Servlet\u003c/h1\u003e\n\u003cp\u003e在Servlet技术出现以前，Web应用程序是通过CGI（Common Gateway Interface），一种脚本语言，来实现的。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/495c5d1c208d4ad7a9debb06e0dc9cbe.png\"\u003e\u003c/p\u003e\n\u003cp\u003e但是CGI存在很多缺点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e如果客户端增多，服务端响应会变慢\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e对于每个请求，CGI都会创建一个进程，进程数有限\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e依赖C、C++、perl等编程语言\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eServlet技术出现以后，解决了很多CGI问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e性能：每个请求创建一个线程，而不是进程\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e可移植性：因为使用了Java语言\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e健壮性：JVM管理Servlet，无需关心内存泄漏、垃圾回收等\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e安全性：因为使用了Java语言\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f63bbfd6ec814adcbea9122bff9d7c82.png\"\u003e\u003c/p\u003e\n\u003cp\u003e总的来说，Servlet是Server Applet，服务端小程序。它是单例多线程的，每个Servlet类在Web容器中只有一个实例，每次客户端请求，Web Server都会从线程池中分配一个线程去创建Servlet，每个请求由一个独立的线程处理，这些线程共享同一个Servlet实例，通过HTTP协议接收和响应来自Web Client的请求。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.javatpoint.com/servlet-tutorial\"\u003ehttps://www.javatpoint.com/servlet-tutorial\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"JavaWeb开发基础Servlet简介"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n学习路线 本文是Java高级编程精华笔记，是对菜鸟教程中Java面向对象和Java高级编程的重要章节进行了学习，整理形成的学习笔记，知识图谱如下图所示：\n如果想对Java深入学习，可以参考阿里巴巴的技术图谱：\nhttps://developer.aliyun.com/graph/java\n继承 Java使用extends关键字来声明继承：\nclass 父类 { } class 子类 extends 父类 { } 示例，子类企鹅和子类老鼠继承父类动物：\n//父类：动物 public class Animal { private String name; private int id; public Animal(String myName, int myid) { name = myName; id = myid; } public void eat() { System.out.println(name + \u0026#34;正在吃\u0026#34;); } public void sleep() { System.out.println(name + \u0026#34;正在睡\u0026#34;); } public void introduction() { System.out.println(\u0026#34;大家好！我是\u0026#34; + id + \u0026#34;号\u0026#34; + name + \u0026#34;.\u0026#34;); } } //子类：企鹅 public class Penguin extends Animal { public Penguin(String myName, int myid) { super(myName, myid); } } //子类：老鼠 public class Mouse extends Animal { public Mouse(String myName, int myid) { super(myName, myid); } } Java只支持单继承，不支持多继承，但可以多重继承：\n子类会继承父类的非private属性、方法。\n子类可以扩展自己的属性、方法。\n子类可以覆盖父类的方法，用自己的方式实现。\n也就是，你的是我的，我的还是我的，我还可以改变你的。\n构造器，子类不会继承父类，而且如果父类的构造器有参数，子类必须通过super关键字显式调用。 也就是，你老子始终是你老子。\nimplements关键字\n前面说到了Java使用extends声明继承类，而implements用来声明实现接口，并且可以同时实现多个接口。\npublic interface A { public void eat(); public void sleep(); } public interface B { public void show(); } public class C implements A,B { } super和this关键字\nsuper：用来实现对父类成员的访问。\nthis：指向自己的引用。\n示例：\nclass Animal { void eat() { System.out.println(\u0026#34;animal : eat\u0026#34;); } } class Dog extends Animal { void eat() { System.out.println(\u0026#34;dog : eat\u0026#34;); } void eatTest() { this.eat(); // this 调用自己的方法 super.eat(); // super 调用父类方法 } } public class Test { public static void main(String[] args) { Animal a = new Animal(); a.eat(); Dog d = new Dog(); d.eatTest(); } } final关键字\n如果修饰类，则表示类不能被继承。如果修饰方法，则表示方法不能被子类重写。\n重写与重载 重写与重载的区别可以通过下面这张图来看：\n重写就是子类覆盖父类，方法名和参数都一样：\n重载就是在一个类里面的多个方法，方法名相同，参数不同。调用时根据入参匹配到不同的方法：\n多态 多态是同一个行为具有多个不同表现形式或形态的能力。重写可以看做是父类和子类之间多态性的一种表现（重载可以理解成多态的具体表现形式），如图所示：\nclass Shape { void draw() {} } class Circle extends Shape { void draw() { System.out.println(\u0026#34;Circle.draw()\u0026#34;); } } class Square extends Shape { void draw() { System.out.println(\u0026#34;Square.draw()\u0026#34;); } } class Triangle extends Shape { void draw() { System.out.println(\u0026#34;Triangle.draw()\u0026#34;); } } 多态存在有三个必要条件：\n继承\n重写\n父类引用指向子类对象：Parent p = new Child();\n当使用多态方式调用方法时，首先检查父类中是否有该方法，如果没有，则编译错误；如果有，再去调用子类的同名方法。\n多态有三种实现方式：\n重写\n接口\n抽象类和抽象方法\n抽象类 抽象类更普通类的区别在于，它不能实例化对象，只能被继承使用。抽象类使用abstract关键字定义：\npublic abstract class Employee { private String name; private String address; private int number; public Employee(String name, String address, int number) { System.out.println(\u0026#34;Constructing an Employee\u0026#34;); this.name = name; this.address = address; this.number = number; } public double computePay() { System.out.println(\u0026#34;Inside Employee computePay\u0026#34;); return 0.0; } public void mailCheck() { System.out.println(\u0026#34;Mailing a check to \u0026#34; + this.name + \u0026#34; \u0026#34; + this.address); } public String toString() { return name + \u0026#34; \u0026#34; + address + \u0026#34; \u0026#34; + number; } public String getName() { return name; } public String getAddress() { return address; } public void setAddress(String newAddress) { address = newAddress; } public int getNumber() { return number; } } 抽象方法\n抽象类里面既可以定义普通方法，也可以定义抽象方法：\npublic abstract class Employee { private String name; private String address; private int number; public abstract double computePay(); } 抽象方法也是通过abstract关键字定义，只有方法名，没有方法体（方法名后面是分号而不是花括号），具体实现由它的子类确定。\n抽象方法最终必须被重写了才能在实例化对象中使用。如果一个类继承了带有抽象方法的抽象类，那么这个类要么也是抽象类，要么就必须重写抽象方法。\n接口 接口不是类，它们属于不同的概念。接口通过interface关键字来定义：\npublic interface Animal { public void eat(); public void travel(); } 接口和接口中的方法是隐式抽象的，不必使用abstract关键字。\n接口中的方法都是public。\n类使用implements关键字实现接口：\npublic class MammalInt implements Animal{ public void eat(){ System.out.println(\u0026#34;Mammal eats\u0026#34;); } public void travel(){ System.out.println(\u0026#34;Mammal travels\u0026#34;); } public int noOfLegs(){ return 0; } public static void main(String args[]){ MammalInt m = new MammalInt(); m.eat(); m.travel(); } } 类必须实现接口里面的所有方法，否则会编译失败。只有抽象类才可以不实现接口的方法。\n一个接口能继承另一个接口，接口的继承也是使用extends关键字：\n// 文件名: Sports.java public interface Sports { public void setHomeTeam(String name); public void setVisitingTeam(String name); } // 文件名: Football.java public interface Football extends Sports { public void homeTeamScored(int points); public void visitingTeamScored(int points); public void endOfQuarter(int quarter); } // 文件名: Hockey.java public interface Hockey extends Sports { public void homeGoalScored(); public void visitingGoalScored(); public void endOfPeriod(int period); public void overtimePeriod(int ot); } 并且接口支持多继承：\npublic interface Hockey extends Sports, Event 实现了子接口的类，需要同时实现所有父接口中的方法。\n接口里面也可以没有方法：\npackage java.util; public interface EventListener {} 它的主要目的是：\n建立一个公共的父接口。\n让实现它的类属于一个特定的类型。\n枚举 枚举是一种特殊的类，一般用来表示一组常量。枚举使用enum关键字来定义，常量使用逗号,分隔：\nenum Color { RED, GREEN, BLUE; } public class Test { // 执行输出结果 public static void main(String[] args) { Color c1 = Color.RED; System.out.println(c1); } } 枚举也可以声明在内部类中：\npublic class Test { enum Color { RED, GREEN, BLUE; } // 执行输出结果 public static void main(String[] args) { Color c1 = Color.RED; System.out.println(c1); } } 枚举可以使用for语句遍历：\nenum Color { RED, GREEN, BLUE; } public class MyClass { public static void main(String[] args) { for (Color myVar : Color.values()) { System.out.println(myVar); } } } 也可以用在switch语句中：\nenum Color { RED, GREEN, BLUE; } public class MyClass { public static void main(String[] args) { Color myVar = Color.BLUE; switch(myVar) { case RED: System.out.println(\u0026#34;红色\u0026#34;); break; case GREEN: System.out.println(\u0026#34;绿色\u0026#34;); break; case BLUE: System.out.println(\u0026#34;蓝色\u0026#34;); break; } } } enum定义的枚举默认继承了java.lang.Enum类，具有3个方法：\nvalues()，返回枚举中所有常量。\nordinal()，返回常量的索引。\nvalueOf()，返回指定字符串值的常量。\nenum Color { RED, GREEN, BLUE; } public class Test { public static void main(String[] args) { // 调用 values() Color[] arr = Color.values(); // 迭代枚举 for (Color col : arr) { // 查看索引 System.out.println(col + \u0026#34; at index \u0026#34; + col.ordinal()); } // 使用 valueOf() 返回枚举常量，不存在的会报错 IllegalArgumentException System.out.println(Color.valueOf(\u0026#34;RED\u0026#34;)); } } 枚举跟普通类一样可以定义变量、方法和构造函数：\nenum Color { RED, GREEN, BLUE; // 构造函数 private Color() { System.out.println(\u0026#34;Constructor called for : \u0026#34; + this.toString()); } public void colorInfo() { System.out.println(\u0026#34;Universal Color\u0026#34;); } } public class Test { // 输出 public static void main(String[] args) { Color c1 = Color.RED; System.out.println(c1); c1.colorInfo(); } } 输出：\nConstructor called for : RED Constructor called for : GREEN Constructor called for : BLUE RED Universal Color 包 包可以理解为类的目录，使用package关键字定义：\npackage com.runoob; public class Runoob { } 一个公司一般使用它互联网域名的倒序形式来作为它的包名，比如runoob.com，所有的包名都以com.runoob开头。\n同一个包中的类可以直接访问，而访问不同的包需要先import。\n//导入包下所有类 import payroll.*; //导入包下某个类 import payroll.Employee; 集合 集合（Collections）和数组（Arrays）的区别：\n长度区别：数组固定；集合可变。 内容区别：数组既可以是基本类型，也可以是引用类型；集合只能是引用类型。 存储类型：数组只能存储一种类型；集合可以存储不同类型（但一般也只存储一种类型） 集合中只能使用基本类型的包装类：\n集合主要包括两种类型的容器：Collection和Map，Collection是元素集合，Map是键值对。\n集合有3个层次：\n接口代表集合的抽象数据类型，我们重点关注实现类和算法，尤其是4种常用实现类：ArrayList、LinkedList、HashSet、HashMap。\nArrayList ArrayList相当于可以动态修改的数组。它继承了AbstractList，并实现了List接口：\n定义\nimport java.util.ArrayList; // 引入 ArrayList 类 ArrayList\u0026lt;E\u0026gt; objectName = new ArrayList\u0026lt;\u0026gt;();　// 初始化 添加元素\n使用add()方法：\nimport java.util.ArrayList; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); System.out.println(sites); } } 访问元素\n使用get()方法：\nimport java.util.ArrayList; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); System.out.println(sites.get(1)); // 访问第二个元素 } } 修改元素\n使用set()方法：\nimport java.util.ArrayList; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); sites.set(2, \u0026#34;Wiki\u0026#34;); // 第一个参数为索引位置，第二个为要修改的值 System.out.println(sites); } } 删除元素\n使用remove()方法：\nimport java.util.ArrayList; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); sites.remove(3); // 删除第四个元素 System.out.println(sites); } } 计算大小\n使用size()方法：\nimport java.util.ArrayList; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); System.out.println(sites.size()); } } 迭代遍历\nfor循环：\nimport java.util.ArrayList; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); for (int i = 0; i \u0026lt; sites.size(); i++) { System.out.println(sites.get(i)); } } } for-each语句：\nimport java.util.ArrayList; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); for (String i : sites) { System.out.println(i); } } } 排序\n使用Collections.sort()方法：\nimport java.util.ArrayList; import java.util.Collections; // 引入 Collections 类 public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Wiki\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); sites.add(\u0026#34;Google\u0026#34;); Collections.sort(sites); // 字母排序 for (String i : sites) { System.out.println(i); } } } LinkedList LinkedList是链表，分为单向链表和双向链表。\n单向链表包含2个值，①当前节点的值，②下一个节点的链接：\n双向链表包含3个值，①当前节点的值，②向前的节点链接，③向后的节点链接：\n与ArrayList相比，LinkedList的增加和删除的效率更高，而修改和查找的效率更低。\n快速记忆法：\n增删：LinkedList\n改查：ArrayList\nLinkedList继承了AbstractSequentialList，实现了很多接口：\n定义\n// 引入 LinkedList 类 import java.util.LinkedList; LinkedList\u0026lt;E\u0026gt; list = new LinkedList\u0026lt;E\u0026gt;(); // 普通创建方法 或者 LinkedList\u0026lt;E\u0026gt; list = new LinkedList(Collection\u0026lt;? extends E\u0026gt; c); // 使用集合创建链表 添加元素\nimport java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); System.out.println(sites); } } 在列表开头添加元素\n使用addFirst()方法：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); // 使用 addFirst() 在头部添加元素 sites.addFirst(\u0026#34;Wiki\u0026#34;); System.out.println(sites); } } 在列表结尾添加元素\n使用addLast()方法：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); // 使用 addLast() 在尾部添加元素 sites.addLast(\u0026#34;Wiki\u0026#34;); System.out.println(sites); } } 在列表开头移除元素\n使用removeFirst()方法：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); // 使用 removeFirst() 移除头部元素 sites.removeFirst(); System.out.println(sites); } } 在列表结果移除元素\n使用removeLast()方法：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); // 使用 removeLast() 移除尾部元素 sites.removeLast(); System.out.println(sites); } } 获取列表开头的元素\n使用getFirst()方法：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); // 使用 getFirst() 获取头部元素 System.out.println(sites.getFirst()); } } 获取列表结尾的元素\n使用getLast()方法：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); // 使用 getLast() 获取尾部元素 System.out.println(sites.getLast()); } } 迭代元素\n可以使用for循环：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); for (int size = sites.size(), i = 0; i \u0026lt; size; i++) { System.out.println(sites.get(i)); } } } 也可以使用for-each语句：\n// 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main(String[] args) { LinkedList\u0026lt;String\u0026gt; sites = new LinkedList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Weibo\u0026#34;); for (String i : sites) { System.out.println(i); } } } HashMap HashMap叫做哈希表，相当于键值对(key-value)的字典。它继承于AbstractMap，实现了Map、Cloneable、java.io.Serializable接口：\n定义\nimport java.util.HashMap; // 引入 HashMap 类 HashMap\u0026lt;Integer, String\u0026gt; Sites = new HashMap\u0026lt;\u0026gt;(); HashMap中只能使用基本类型的包装类，因为HashMap也是集合一种，在集合中只能使用包装类。\n添加元素\n使用put()方法：\n// 引入 HashMap 类 import java.util.HashMap; public class RunoobTest { public static void main(String[] args) { // 创建 HashMap 对象 Sites HashMap\u0026lt;Integer, String\u0026gt; Sites = new HashMap\u0026lt;Integer, String\u0026gt;(); // 添加键值对 Sites.put(1, \u0026#34;Google\u0026#34;); Sites.put(2, \u0026#34;Runoob\u0026#34;); Sites.put(3, \u0026#34;Taobao\u0026#34;); Sites.put(4, \u0026#34;Zhihu\u0026#34;); System.out.println(Sites); } } 访问元素\n使用get()方法：\n// 引入 HashMap 类 import java.util.HashMap; public class RunoobTest { public static void main(String[] args) { // 创建 HashMap 对象 Sites HashMap\u0026lt;Integer, String\u0026gt; Sites = new HashMap\u0026lt;Integer, String\u0026gt;(); // 添加键值对 Sites.put(1, \u0026#34;Google\u0026#34;); Sites.put(2, \u0026#34;Runoob\u0026#34;); Sites.put(3, \u0026#34;Taobao\u0026#34;); Sites.put(4, \u0026#34;Zhihu\u0026#34;); System.out.println(Sites.get(3)); } } 删除元素\n使用remove()方法：\n// 引入 HashMap 类 import java.util.HashMap; public class RunoobTest { public static void main(String[] args) { // 创建 HashMap 对象 Sites HashMap\u0026lt;Integer, String\u0026gt; Sites = new HashMap\u0026lt;Integer, String\u0026gt;(); // 添加键值对 Sites.put(1, \u0026#34;Google\u0026#34;); Sites.put(2, \u0026#34;Runoob\u0026#34;); Sites.put(3, \u0026#34;Taobao\u0026#34;); Sites.put(4, \u0026#34;Zhihu\u0026#34;); Sites.remove(4); System.out.println(Sites); } } 删除所有键值对\n使用clear()方法：\n// 引入 HashMap 类 import java.util.HashMap; public class RunoobTest { public static void main(String[] args) { // 创建 HashMap 对象 Sites HashMap\u0026lt;Integer, String\u0026gt; Sites = new HashMap\u0026lt;Integer, String\u0026gt;(); // 添加键值对 Sites.put(1, \u0026#34;Google\u0026#34;); Sites.put(2, \u0026#34;Runoob\u0026#34;); Sites.put(3, \u0026#34;Taobao\u0026#34;); Sites.put(4, \u0026#34;Zhihu\u0026#34;); Sites.clear(); System.out.println(Sites); } } 计算大小\n使用size()方法：\n// 引入 HashMap 类 import java.util.HashMap; public class RunoobTest { public static void main(String[] args) { // 创建 HashMap 对象 Sites HashMap\u0026lt;Integer, String\u0026gt; Sites = new HashMap\u0026lt;Integer, String\u0026gt;(); // 添加键值对 Sites.put(1, \u0026#34;Google\u0026#34;); Sites.put(2, \u0026#34;Runoob\u0026#34;); Sites.put(3, \u0026#34;Taobao\u0026#34;); Sites.put(4, \u0026#34;Zhihu\u0026#34;); System.out.println(Sites.size()); } } 迭代\n推荐使用entrySet：\nMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;Integer, Integer\u0026gt;(); for(Map.Entry\u0026lt;Integer, Integer\u0026gt; entry : map.entrySet()){ System.out.println(\u0026#34;key = \u0026#34; + entry.getKey() + \u0026#34;, value = \u0026#34; + entry.getValue()) } 也可以用其他方式：\n// 引入 HashMap 类 import java.util.HashMap; public class RunoobTest { public static void main(String[] args) { // 创建 HashMap 对象 Sites HashMap\u0026lt;Integer, String\u0026gt; Sites = new HashMap\u0026lt;Integer, String\u0026gt;(); // 添加键值对 Sites.put(1, \u0026#34;Google\u0026#34;); Sites.put(2, \u0026#34;Runoob\u0026#34;); Sites.put(3, \u0026#34;Taobao\u0026#34;); Sites.put(4, \u0026#34;Zhihu\u0026#34;); // 输出 key 和 value for (Integer i : Sites.keySet()) { System.out.println(\u0026#34;key: \u0026#34; + i + \u0026#34; value: \u0026#34; + Sites.get(i)); } // 返回所有 value 值 for(String value: Sites.values()) { // 输出每一个value System.out.print(value + \u0026#34;, \u0026#34;); } } } HashSet HashSet是一个不允许有重复元素的集合。它实现了Set接口：\n定义\nimport java.util.HashSet; // 引入 HashSet 类 HashSet\u0026lt;String\u0026gt; sites = new HashSet\u0026lt;String\u0026gt;(); 添加元素\n// 引入 HashSet 类 import java.util.HashSet; public class RunoobTest { public static void main(String[] args) { HashSet\u0026lt;String\u0026gt; sites = new HashSet\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); // 重复的元素不会被添加 System.out.println(sites); } } 判断元素是否存在\n使用contains()方法：\n// 引入 HashSet 类 import java.util.HashSet; public class RunoobTest { public static void main(String[] args) { HashSet\u0026lt;String\u0026gt; sites = new HashSet\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); // 重复的元素不会被添加 System.out.println(sites.contains(\u0026#34;Taobao\u0026#34;)); } } 删除元素\n使用remove()方法：\n// 引入 HashSet 类 import java.util.HashSet; public class RunoobTest { public static void main(String[] args) { HashSet\u0026lt;String\u0026gt; sites = new HashSet\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); // 重复的元素不会被添加 sites.remove(\u0026#34;Taobao\u0026#34;); // 删除元素，删除成功返回 true，否则为 false System.out.println(sites); } } 删除所有元素\n使用clear()方法：\n// 引入 HashSet 类 import java.util.HashSet; public class RunoobTest { public static void main(String[] args) { HashSet\u0026lt;String\u0026gt; sites = new HashSet\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); // 重复的元素不会被添加 sites.clear(); System.out.println(sites); } } 计算大小\n使用size()方法：\n// 引入 HashSet 类 import java.util.HashSet; public class RunoobTest { public static void main(String[] args) { HashSet\u0026lt;String\u0026gt; sites = new HashSet\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); // 重复的元素不会被添加 System.out.println(sites.size()); } } 迭代\n使用for-each语句：\n// 引入 HashSet 类 import java.util.HashSet; public class RunoobTest { public static void main(String[] args) { HashSet\u0026lt;String\u0026gt; sites = new HashSet\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); // 重复的元素不会被添加 for (String i : sites) { System.out.println(i); } } } Iterator Iterator是迭代器，可以用来迭代ArrayList等集合。迭代器主要有3个方法：\nnext()：返回下一个元素，并更新迭代器状态。 hasNext()：检查是否还有元素。 remove()：删除元素。 获取集合的迭代器\n// 引入 ArrayList 和 Iterator 类 import java.util.ArrayList; import java.util.Iterator; public class RunoobTest { public static void main(String[] args) { // 创建集合 ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); // 获取迭代器 Iterator\u0026lt;String\u0026gt; it = sites.iterator(); // 输出集合中的第一个元素 System.out.println(it.next()); } } 迭代器遍历集合\n// 引入 ArrayList 和 Iterator 类 import java.util.ArrayList; import java.util.Iterator; public class RunoobTest { public static void main(String[] args) { // 创建集合 ArrayList\u0026lt;String\u0026gt; sites = new ArrayList\u0026lt;String\u0026gt;(); sites.add(\u0026#34;Google\u0026#34;); sites.add(\u0026#34;Runoob\u0026#34;); sites.add(\u0026#34;Taobao\u0026#34;); sites.add(\u0026#34;Zhihu\u0026#34;); // 获取迭代器 Iterator\u0026lt;String\u0026gt; it = sites.iterator(); // 输出集合中的所有元素 while(it.hasNext()) { System.out.println(it.next()); } } } 删除元素\n// 引入 ArrayList 和 Iterator 类 import java.util.ArrayList; import java.util.Iterator; public class RunoobTest { public static void main(String[] args) { ArrayList\u0026lt;Integer\u0026gt; numbers = new ArrayList\u0026lt;Integer\u0026gt;(); numbers.add(12); numbers.add(8); numbers.add(2); numbers.add(23); Iterator\u0026lt;Integer\u0026gt; it = numbers.iterator(); while(it.hasNext()) { Integer i = it.next(); if(i \u0026lt; 10) { it.remove(); // 删除小于 10 的元素 } } System.out.println(numbers); } } 泛型 泛型可以理解为通用类型，不是具体某个类型，而是泛指某些类型。\nJava中的泛型标记符如下所示：\nE - Element (在集合中使用，因为集合中存放的是元素) T - Type（Java 类） K - Key（键） V - Value（值） N - Number（数值类型） ？ - 表示不确定的 java 类型 示例：\npublic class GenericMethodTest { // 泛型方法 printArray public static \u0026lt;E\u0026gt; void printArray(E[] inputArray) { // 输出数组元素 for (E element : inputArray) { System.out.printf(\u0026#34;%s \u0026#34;, element); } System.out.println(); } public static void main(String args[]) { // 创建不同类型数组： Integer, Double 和 Character Integer[] intArray = {1, 2, 3, 4, 5}; Double[] doubleArray = {1.1, 2.2, 3.3, 4.4}; Character[] charArray = {\u0026#39;H\u0026#39;, \u0026#39;E\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;O\u0026#39;}; System.out.println(\u0026#34;整型数组元素为:\u0026#34;); printArray(intArray); // 传递一个整型数组 System.out.println(\u0026#34;\\n双精度型数组元素为:\u0026#34;); printArray(doubleArray); // 传递一个双精度型数组 System.out.println(\u0026#34;\\n字符型数组元素为:\u0026#34;); printArray(charArray); // 传递一个字符型数组 } } 其中\u0026lt;E\u0026gt;放在方法前表明这是一个泛型方法。可以通过extends限制类型的范围：\npublic class MaximumTest { // 比较三个值并返回最大值 //java.lang.Comparable是个接口，包含一个compareTo()方法 public static \u0026lt;T extends Comparable\u0026lt;T\u0026gt;\u0026gt; T maximum(T x, T y, T z) { T max = x; // 假设x是初始最大值 if ( y.compareTo( max ) \u0026gt; 0 ){ max = y; //y 更大 } if ( z.compareTo( max ) \u0026gt; 0 ){ max = z; // 现在 z 更大 } return max; // 返回最大对象 } public static void main( String args[] ) { System.out.printf( \u0026#34;%d, %d 和 %d 中最大的数为 %d\\n\\n\u0026#34;, 3, 4, 5, maximum( 3, 4, 5 ) ); System.out.printf( \u0026#34;%.1f, %.1f 和 %.1f 中最大的数为 %.1f\\n\\n\u0026#34;, 6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) ); System.out.printf( \u0026#34;%s, %s 和 %s 中最大的数为 %s\\n\u0026#34;,\u0026#34;pear\u0026#34;, \u0026#34;apple\u0026#34;, \u0026#34;orange\u0026#34;, maximum( \u0026#34;pear\u0026#34;, \u0026#34;apple\u0026#34;, \u0026#34;orange\u0026#34; ) ); } } 除了泛型、泛型方法，还可以定义泛型类：\npublic class Box\u0026lt;T\u0026gt; { private T t; public void add(T t) { this.t = t; } public T get() { return t; } public static void main(String[] args) { Box\u0026lt;Integer\u0026gt; integerBox = new Box\u0026lt;Integer\u0026gt;(); Box\u0026lt;String\u0026gt; stringBox = new Box\u0026lt;String\u0026gt;(); integerBox.add(new Integer(10)); stringBox.add(new String(\u0026#34;菜鸟教程\u0026#34;)); System.out.printf(\u0026#34;整型值为 :%d\\n\\n\u0026#34;, integerBox.get()); System.out.printf(\u0026#34;字符串为 :%s\\n\u0026#34;, stringBox.get()); } } ?是类型通配符，比如List\u0026lt;?\u0026gt;在逻辑上是List\u0026lt;String\u0026gt;、List\u0026lt;Integer\u0026gt;等的父类：\nimport java.util.*; public class GenericTest { public static void main(String[] args) { List\u0026lt;String\u0026gt; name = new ArrayList\u0026lt;String\u0026gt;(); List\u0026lt;Integer\u0026gt; age = new ArrayList\u0026lt;Integer\u0026gt;(); List\u0026lt;Number\u0026gt; number = new ArrayList\u0026lt;Number\u0026gt;(); name.add(\u0026#34;icon\u0026#34;); age.add(18); number.add(314); getData(name); getData(age); getData(number); } public static void getData(List\u0026lt;?\u0026gt; data) { System.out.println(\u0026#34;data :\u0026#34; + data.get(0)); } } ?也可以通过extends关键字来限定类型范围：\nimport java.util.*; public class GenericTest { public static void main(String[] args) { List\u0026lt;String\u0026gt; name = new ArrayList\u0026lt;String\u0026gt;(); List\u0026lt;Integer\u0026gt; age = new ArrayList\u0026lt;Integer\u0026gt;(); List\u0026lt;Number\u0026gt; number = new ArrayList\u0026lt;Number\u0026gt;(); name.add(\u0026#34;icon\u0026#34;); age.add(18); number.add(314); //getUperNumber(name);//1 getUperNumber(age);//2 getUperNumber(number);//3 } public static void getData(List\u0026lt;?\u0026gt; data) { System.out.println(\u0026#34;data :\u0026#34; + data.get(0)); } //只接受Number及其子类 public static void getUperNumber(List\u0026lt;? extends Number\u0026gt; data) { System.out.println(\u0026#34;data :\u0026#34; + data.get(0)); } } List\u0026lt;? extends Number表示只接受Number及其子类（指定上限）。此外还能通过List\u0026lt;? super Number\u0026gt;来表示只能接受Number及其父类（指定下限）。\n参考资料：\nJava面向对象 https://www.runoob.com/java/java-inheritance.html\nJava高级编程 https://www.runoob.com/java/java-data-structures.html\nJava集合超详解 https://blog.csdn.net/feiyanaffection/article/details/81394745\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000002-%E8%BF%9B%E9%98%B6java%E9%AB%98%E7%BA%A7%E7%BC%96%E7%A8%8B%E7%B2%BE%E5%8D%8E%E7%AC%94%E8%AE%B0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"学习路线\"\u003e学习路线\u003c/h1\u003e\n\u003cp\u003e本文是Java高级编程精华笔记，是对菜鸟教程中Java面向对象和Java高级编程的重要章节进行了学习，整理形成的学习笔记，知识图谱如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d4f54842487343bf91808fbd87cf726a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果想对Java深入学习，可以参考\u003cstrong\u003e阿里巴巴的技术图谱\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://developer.aliyun.com/graph/java\"\u003ehttps://developer.aliyun.com/graph/java\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c395f78abe43469391ee0049ab4e3590.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"继承\"\u003e继承\u003c/h1\u003e\n\u003cp\u003eJava使用\u003ccode\u003eextends\u003c/code\u003e关键字来声明继承：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eclass 父类 {\n}\n\nclass 子类 extends 父类 {\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e示例，子类企鹅和子类老鼠继承父类动物：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//父类：动物\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eAnimal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eAnimal\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyName\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyid\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003ename\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyName\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eid\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyid\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eeat\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ename\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;正在吃\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003esleep\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ename\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;正在睡\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eintroduction\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;大家好！我是\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eid\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;号\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ename\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;.\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//子类：企鹅\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003ePenguin\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eAnimal\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003ePenguin\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyName\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyid\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emyName\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyid\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e//子类：老鼠\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMouse\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eextends\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eAnimal\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eMouse\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyName\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyid\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003esuper\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003emyName\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emyid\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJava只支持单继承，不支持多继承，但可以多重继承：\u003c/p\u003e","title":"Java高级编程精华笔记"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n运行文件 JMeter下载地址为：\nhttps://jmeter.apache.org/download_jmeter.cgi\n最新版本为5.4.1，依赖于Java8+，下载后无需安装，解压即用。解压后的目录和文件如下：\nbin 二进制文件目录，包括配置文件、启动文件、启动jar包、示例脚本。比如Windows和Linux系统的两个启动脚本： docs API离线帮助文档。 extras 辅助功能，可用于Ant、Jenkins集成。 lib lib根目录下存放JMeter基础jar包，lib/ext目录下存放第三方或自定义扩展jar包。 licenses 证书文本文件。 printable_docs 离线帮助文档。 LICENSE 证书说明。 NOTICE 版权声明。 README 简介。 测试计划 JMeter以图形界面方式启动后，首先映入眼帘的是左侧的TestPlan，测试计划：\nJMeter的UI界面只能同时编辑一个测试计划，如果想再编辑另外一个测试计划，必须再打开一个JMeter界面。这是因为测试计划对应的是.jmx文件！不同的.jmx文件只能在不同的界面中编辑。\nJMeter界面是通过组件拖拽的方式来编写脚本的，脚本的根目录是测试计划。根节点只能有一个，作为根节点，测试计划很适合来做全局配置，比如在User Defined Variables处添加键值对格式的全局变量。\n测试计划是默认节点，JMeter还有另外一个不可缺少的次节点：Thread Group，线程组。\n多线程 性能测试工具的目的是用少量电脑产生成百上千个并发请求。多进程和多线程正好能用来实现这个需求，书上有句关于进程和线程的至理名言：“进程是操作系统分配资源的最小单位，线程是程序执行的最小单位”。该如何理解呢？前一句的意思是说，每个应用程序都有对应的进程，按照进程来分配资源，可以使得资源利用最大化，避免浪费。后一句的理解得从一个故事讲起，很久以前，进程拿着资源找CPU执行。CPU一看进程太大，说我处理不了。进程只能拆成多个线程给CPU，不但能执行了，还能在线程之间灵活切换。\n多进程和多线程都可以用来模拟并发请求。JMeter是用Java语言写的程序，运行在JVM虚拟机之上，进程的开销比线程大很多，JVM为了保证足够性能，控制了进程的数量。如果用多线程，单台电脑能提供了负载有限，那么就只能用多线程。JMeter只支持多线程！\n另外一个性能测试工具LoadRunner支持多进程和多线程。\n用户即线程 JMeter除了Thread Group，其他地方都是用的另外一个词：User。实际上它们是一个意思，用户就是线程。JMeter的运行原理就是用多个线程来模拟多个用户！以下是简单示例：\n从图中可以看出，有两个线程组，Thread Group 1和Thread Group 2，它们分别定义了自己组内的用户要做的事情。不同线程组做的事情是不一样的，但是同一线程组内部做的事情是一模一样的。我们在写JMeter脚本的时候，实际上是在定义每个用户要做哪些事。当运行脚本的时候，JMeter会启动多个线程，都来执行这个脚本，按照我们写的进行执行。\n由于每个线程启动时间不一样，线程执行时资源环境也有区别，导致线程的执行进度并不完全一致，比如一些线程已经登录完了，另一些线程可能才刚开始登录。后面的文章会给出一些解决办法。\n远程运行 一台机器提供了线程数量有限，如果想要施压更大的并发，那么就需要多台机器。JMeter天然支持多台机器并发：\n控制机和负载机本质上没有区别，同一台机器既可以做控制机，也可以做负载机，取决于你的负载设计。控制机的作用是管理多台远程负载机，控制远程负载机脚本运行，收集远程负载机测试结果。负载机是实际产生并发的机器，它们向被测应用系统发起负载。\n运行逻辑如下：\n远程负载机启动Agent客户端，等待控制机连接。 控制机连接上远程负载机。 控制机发送指令到远程负载机启动线程。 远程负载机运行脚本，回传结果数据。 控制机收集结果并显示。 操作步骤如下：\n远程负载机启动jmeter-server.bat。\n在控制机bin\\JMeter.properties文件中，修改remote_hosts，添加远程负载机的ip和port，多个远程负载机ip 用逗号分隔，如remote_hosts=192.168.0.1:1664,192.168.0.2:1664。1664是默认RMI端口号（Remote Method Invocation，远程方法调用）。\n在控制机菜单栏Run执行Remote命令：\n如果远程负载机需要参数或jar包，可以手动先把环境准备好，也可以使用自动化工具在脚本运行前从控制机发送到远程负载机。\n小结 本文先从JMeter运行文件讲起，启动后默认有测试计划，建线程组，线程即用户，用多线程来模拟多用户，这就是JMeter运行起来施压的原理。为了解决单台机器负载能力有限的问题，JMeter还提供了远程运行方式，既可以在局域网用多台机器来控制，也可以部署到云服务器上，进行云压测。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000002-jmeterjmeter%E6%98%AF%E5%A6%82%E4%BD%95%E8%BF%90%E8%A1%8C%E6%96%BD%E5%8E%8B%E7%9A%84/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"运行文件\"\u003e运行文件\u003c/h1\u003e\n\u003cp\u003eJMeter下载地址为：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://jmeter.apache.org/download_jmeter.cgi\"\u003ehttps://jmeter.apache.org/download_jmeter.cgi\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e最新版本为5.4.1，依赖于Java8+，下载后无需安装，解压即用。解压后的目录和文件如下：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8882ce2b3e0f46aab689527e19940f07.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ebin 二进制文件目录，包括配置文件、启动文件、启动jar包、示例脚本。比如Windows和Linux系统的两个启动脚本：\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/90a79cc617a44ecb991bec7a4c75c2cf.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003edocs API离线帮助文档。\u003c/li\u003e\n\u003cli\u003eextras 辅助功能，可用于Ant、Jenkins集成。\u003c/li\u003e\n\u003cli\u003elib lib根目录下存放JMeter基础jar包，lib/ext目录下存放第三方或自定义扩展jar包。\u003c/li\u003e\n\u003cli\u003elicenses 证书文本文件。\u003c/li\u003e\n\u003cli\u003eprintable_docs 离线帮助文档。\u003c/li\u003e\n\u003cli\u003eLICENSE 证书说明。\u003c/li\u003e\n\u003cli\u003eNOTICE 版权声明。\u003c/li\u003e\n\u003cli\u003eREADME 简介。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"测试计划\"\u003e测试计划\u003c/h1\u003e\n\u003cp\u003eJMeter以图形界面方式启动后，首先映入眼帘的是左侧的TestPlan，测试计划：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/08391c0919354b15aaf9807d7d60e07f.png\"\u003e\u003c/p\u003e\n\u003cp\u003eJMeter的UI界面只能同时编辑一个测试计划，如果想再编辑另外一个测试计划，必须再打开一个JMeter界面。这是因为测试计划对应的是.jmx文件！不同的.jmx文件只能在不同的界面中编辑。\u003c/p\u003e\n\u003cp\u003eJMeter界面是通过组件拖拽的方式来编写脚本的，脚本的根目录是测试计划。根节点只能有一个，作为根节点，测试计划很适合来做全局配置，比如在User Defined Variables处添加键值对格式的全局变量。\u003c/p\u003e\n\u003cp\u003e测试计划是默认节点，JMeter还有另外一个不可缺少的次节点：Thread Group，线程组。\u003c/p\u003e\n\u003ch1 id=\"多线程\"\u003e多线程\u003c/h1\u003e\n\u003cp\u003e性能测试工具的目的是用少量电脑产生成百上千个并发请求。多进程和多线程正好能用来实现这个需求，书上有句关于进程和线程的至理名言：“进程是操作系统分配资源的最小单位，线程是程序执行的最小单位”。该如何理解呢？前一句的意思是说，每个应用程序都有对应的进程，按照进程来分配资源，可以使得资源利用最大化，避免浪费。后一句的理解得从一个故事讲起，很久以前，进程拿着资源找CPU执行。CPU一看进程太大，说我处理不了。进程只能拆成多个线程给CPU，不但能执行了，还能在线程之间灵活切换。\u003c/p\u003e\n\u003cp\u003e多进程和多线程都可以用来模拟并发请求。JMeter是用Java语言写的程序，运行在JVM虚拟机之上，进程的开销比线程大很多，JVM为了保证足够性能，控制了进程的数量。如果用多线程，单台电脑能提供了负载有限，那么就只能用多线程。JMeter只支持多线程！\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e另外一个性能测试工具LoadRunner支持多进程和多线程。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"用户即线程\"\u003e用户即线程\u003c/h1\u003e\n\u003cp\u003eJMeter除了Thread Group，其他地方都是用的另外一个词：User。实际上它们是一个意思，用户就是线程。JMeter的运行原理就是用多个线程来模拟多个用户！以下是简单示例：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/aa6bb9b7be474396862a0b81a3c68463.png\"\u003e\u003c/p\u003e\n\u003cp\u003e从图中可以看出，有两个线程组，Thread Group 1和Thread Group 2，它们分别定义了自己组内的用户要做的事情。不同线程组做的事情是不一样的，但是同一线程组内部做的事情是一模一样的。我们在写JMeter脚本的时候，实际上是在定义每个用户要做哪些事。当运行脚本的时候，JMeter会启动多个线程，都来执行这个脚本，按照我们写的进行执行。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e由于每个线程启动时间不一样，线程执行时资源环境也有区别，导致线程的执行进度并不完全一致，比如一些线程已经登录完了，另一些线程可能才刚开始登录。后面的文章会给出一些解决办法。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"远程运行\"\u003e远程运行\u003c/h1\u003e\n\u003cp\u003e一台机器提供了线程数量有限，如果想要施压更大的并发，那么就需要多台机器。JMeter天然支持多台机器并发：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/cca61040161c43e8a0d833613f1aa247.png\"\u003e\u003c/p\u003e\n\u003cp\u003e控制机和负载机本质上没有区别，同一台机器既可以做控制机，也可以做负载机，取决于你的负载设计。控制机的作用是管理多台远程负载机，控制远程负载机脚本运行，收集远程负载机测试结果。负载机是实际产生并发的机器，它们向被测应用系统发起负载。\u003c/p\u003e\n\u003cp\u003e运行逻辑如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e远程负载机启动Agent客户端，等待控制机连接。\u003c/li\u003e\n\u003cli\u003e控制机连接上远程负载机。\u003c/li\u003e\n\u003cli\u003e控制机发送指令到远程负载机启动线程。\u003c/li\u003e\n\u003cli\u003e远程负载机运行脚本，回传结果数据。\u003c/li\u003e\n\u003cli\u003e控制机收集结果并显示。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e操作步骤如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e远程负载机启动jmeter-server.bat。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e在控制机bin\\JMeter.properties文件中，修改remote_hosts，添加远程负载机的ip和port，多个远程负载机ip 用逗号分隔，如remote_hosts=192.168.0.1:1664,192.168.0.2:1664。1664是默认RMI端口号（Remote Method Invocation，远程方法调用）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e在控制机菜单栏Run执行Remote命令：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/70903ad24ce243368980c6b411ecd767.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如果远程负载机需要参数或jar包，可以手动先把环境准备好，也可以使用自动化工具在脚本运行前从控制机发送到远程负载机。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"小结\"\u003e小结\u003c/h1\u003e\n\u003cp\u003e本文先从JMeter运行文件讲起，启动后默认有测试计划，建线程组，线程即用户，用多线程来模拟多用户，这就是JMeter运行起来施压的原理。为了解决单台机器负载能力有限的问题，JMeter还提供了远程运行方式，既可以在局域网用多台机器来控制，也可以部署到云服务器上，进行云压测。\u003c/p\u003e","title":"JMeter是如何运行施压的"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n注解（Annotations）是JUnit的标志性技术，本文就来对它的20个注解，以及元注解和组合注解进行学习。\n20个注解 在org.junit.jupiter.api包中定义了这些注解，它们分别是：\n@Test 测试方法，可以直接运行。\n@ParameterizedTest 参数化测试，比如：\n@ParameterizedTest @ValueSource(strings = { \u0026#34;racecar\u0026#34;, \u0026#34;radar\u0026#34;, \u0026#34;able was I ere I saw elba\u0026#34; }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); } @RepeatedTest 重复测试，比如：\n@RepeatedTest(10) void repeatedTest() { // ... } @TestFactory 测试工厂，专门生成测试方法，比如：\nimport org.junit.jupiter.api.DynamicTest; @TestFactory Collection\u0026lt;DynamicTest\u0026gt; dynamicTestsFromCollection() { return Arrays.asList( dynamicTest(\u0026#34;1st dynamic test\u0026#34;, () -\u0026gt; assertTrue(isPalindrome(\u0026#34;madam\u0026#34;))), dynamicTest(\u0026#34;2nd dynamic test\u0026#34;, () -\u0026gt; assertEquals(4, calculator.multiply(2, 2))) ); } @TestTemplate 测试模板，比如：\nfinal List\u0026lt;String\u0026gt; fruits = Arrays.asList(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;, \u0026#34;lemon\u0026#34;); @TestTemplate @ExtendWith(MyTestTemplateInvocationContextProvider.class) void testTemplate(String fruit) { assertTrue(fruits.contains(fruit)); } public class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider { @Override public boolean supportsTestTemplate(ExtensionContext context) { return true; } @Override public Stream\u0026lt;TestTemplateInvocationContext\u0026gt; provideTestTemplateInvocationContexts( ExtensionContext context) { return Stream.of(invocationContext(\u0026#34;apple\u0026#34;), invocationContext(\u0026#34;banana\u0026#34;)); } } @TestTemplate必须注册一个TestTemplateInvocationContextProvider，它的用法跟@Test类似。\n@TestMethodOrder 指定测试顺序，比如：\nimport org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @TestMethodOrder(OrderAnnotation.class) class OrderedTestsDemo { @Test @Order(1) void nullValues() { // perform assertions against null values } @Test @Order(2) void emptyValues() { // perform assertions against empty values } @Test @Order(3) void validValues() { // perform assertions against valid values } } @TestInstance 是否生成多个测试实例，默认JUnit每个测试方法生成一个实例，使用这个注解能让每个类只生成一个实例，比如：\n@TestInstance(Lifecycle.PER_CLASS) class TestMethodDemo { @Test void test1() { } @Test void test2() { } @Test void test3() { } } @DisplayName 自定义测试名字，会体现在测试报告中，比如：\nimport org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName(\u0026#34;A special test case\u0026#34;) class DisplayNameDemo { @Test @DisplayName(\u0026#34;Custom test name containing spaces\u0026#34;) void testWithDisplayNameContainingSpaces() { } @Test @DisplayName(\u0026#34;╯°□°）╯\u0026#34;) void testWithDisplayNameContainingSpecialCharacters() { } @Test @DisplayName(\u0026#34;😱\u0026#34;) void testWithDisplayNameContainingEmoji() { } } @DisplayNameGeneration 测试名字统一处理，比如：\nimport org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.IndicativeSentencesGeneration; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class DisplayNameGeneratorDemo { @Nested @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_not_supported { @Test void if_it_is_zero() { } @DisplayName(\u0026#34;A negative value for year is not supported by the leap year computation.\u0026#34;) @ParameterizedTest(name = \u0026#34;For example, year {0} is not supported.\u0026#34;) @ValueSource(ints = { -1, -4 }) void if_it_is_negative(int year) { } } @Nested @IndicativeSentencesGeneration(separator = \u0026#34; -\u0026gt; \u0026#34;, generator = DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_a_leap_year { @Test void if_it_is_divisible_by_4_but_not_by_100() { } @ParameterizedTest(name = \u0026#34;Year {0} is a leap year.\u0026#34;) @ValueSource(ints = { 2016, 2020, 2048 }) void if_it_is_one_of_the_following_years(int year) { } } } @BeforeEach 在每个@Test, @RepeatedTest, @ParameterizedTest, or @TestFactory之前执行。\n@AfterEach 在每个@Test, @RepeatedTest, @ParameterizedTest, or @TestFactory之后执行。\n@BeforeAll 在所有的@Test, @RepeatedTest, @ParameterizedTest, and @TestFactory之前执行。\n@AfterAll 在所有的@Test, @RepeatedTest, @ParameterizedTest, and @TestFactory之后执行。\n@Nested 嵌套测试，一个类套一个类，例子参考上面那个。\n@Tag 打标签，相当于分组，比如：\nimport org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @Tag(\u0026#34;fast\u0026#34;) @Tag(\u0026#34;model\u0026#34;) class TaggingDemo { @Test @Tag(\u0026#34;taxes\u0026#34;) void testingTaxCalculation() { } } @Disabled 禁用测试，比如：\nimport org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @Disabled(\u0026#34;Disabled until bug #99 has been fixed\u0026#34;) class DisabledClassDemo { @Test void testWillBeSkipped() { } } @Timeout 对于test, test factory, test template, or lifecycle method，如果超时了就认为失败了，比如：\nclass TimeoutDemo { @BeforeEach @Timeout(5) void setUp() { // fails if execution time exceeds 5 seconds } @Test @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) void failsIfExecutionTimeExceeds100Milliseconds() { // fails if execution time exceeds 100 milliseconds } } @ExtendWith 注册扩展，比如：\n@ExtendWith(RandomParametersExtension.class) @Test void test(@Random int i) { // ... } JUnit5提供了标准的扩展机制来允许开发人员对JUnit5的功能进行增强。JUnit5提供了很多的标准扩展接口，第三方可以直接实现这些接口来提供自定义的行为。\n@RegisterExtension 通过字段注册扩展，比如：\nclass WebServerDemo { @RegisterExtension static WebServerExtension server = WebServerExtension.builder() .enableSecurity(false) .build(); @Test void getProductList() { WebClient webClient = new WebClient(); String serverUrl = server.getServerUrl(); // Use WebClient to connect to web server using serverUrl and verify response assertEquals(200, webClient.get(serverUrl + \u0026#34;/products\u0026#34;).getResponseStatus()); } } @TempDir 临时目录，比如：\n@Test void writeItemsToFile(@TempDir Path tempDir) throws IOException { Path file = tempDir.resolve(\u0026#34;test.txt\u0026#34;); new ListWriter(file).write(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;); assertEquals(singletonList(\u0026#34;a,b,c\u0026#34;), Files.readAllLines(file)); } 元注解和组合注解 JUnit Jupiter支持元注解，能继承后实现自定义注解，比如自定义@Fast注解：\nimport java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Tag; @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Tag(\u0026#34;fast\u0026#34;) public @interface Fast { } 使用：\n@Fast @Test void myFastTest() { // ... } 这个@Fast注解也是组合注解，甚至可以更进一步和@Test组合：\nimport java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Tag(\u0026#34;fast\u0026#34;) @Test public @interface FastTest { } 只用@FastTest就可以了：\n@FastTest void myFastTest() { // ... } 小结 本文对JUnit20个主要的注解进行了介绍和示例演示，JUnit Jupiter支持元注解，可以自定义注解，也可以把多个注解组合起来。\n参考资料：\nhttps://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations\nhttps://vitzhou.gitbooks.io/junit5/content/junit/extension_model.html#%E6%A6%82%E8%BF%B0\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000002-junitjunit5%E6%B3%A8%E8%A7%A3%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%BC%95/","summary":"注解（Annotations）是JUnit的标志性技术，本文就来对它的20个注解，以及元注解和组合注解进行学习。","title":"JUnit5注解学习指引"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n大家好，我是刚哥。\nPostman我们都会用，调接口无比强大，最近我们部门有同学分享了一个《如何快速自动化》专题，我一听这名字，呵呵，估计又是讲些空洞的理论。然而，当我敲打着键盘，耳边传来，Postman，录制流量，这几个字的时候，发现事情并没有这么简单。\nPostman确实是可以录制流量的。前提是使用Chrome并安装Postman Interceptor插件：\n安装好插件以后，就可以在Postman客户端中进行连接了：\n点击右上角那个雷达图标 选择Interceptor CONNECTED表示已经连上了 选择Collections，最好先创建再选择 然后在浏览器插件中开启录制：\n准备工作就绪。接着就开始尽情点点点，所有的接口流量都会保存到Postman的Collections里面：\n流量就录制好了。可以在Postman使用这些录制好的接口。也可以导出成JSON文件：\n文件内容如下：\n{ \u0026#34;info\u0026#34;: { \u0026#34;_postman_id\u0026#34;: \u0026#34;2335bec9-31ed-4eb5-bcd5-e7a145a7c5e8\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;case\u0026#34;, \u0026#34;schema\u0026#34;: \u0026#34;https://schema.getpostman.com/json/collection/v2.1.0/collection.json\u0026#34; }, \u0026#34;item\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;http://localhost:8080/hello\u0026#34;, \u0026#34;request\u0026#34;: { \u0026#34;method\u0026#34;: \u0026#34;GET\u0026#34;, \u0026#34;header\u0026#34;: [ { \u0026#34;key\u0026#34;: \u0026#34;User-Agent\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;Accept\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\u0026#34; } ], \u0026#34;url\u0026#34;: { \u0026#34;raw\u0026#34;: \u0026#34;http://localhost:8080/hello\u0026#34;, \u0026#34;protocol\u0026#34;: \u0026#34;http\u0026#34;, \u0026#34;host\u0026#34;: [ \u0026#34;localhost\u0026#34; ], \u0026#34;port\u0026#34;: \u0026#34;8080\u0026#34;, \u0026#34;path\u0026#34;: [ \u0026#34;hello\u0026#34; ] } }, \u0026#34;response\u0026#34;: [] } ] } 里面url、method、header、body等应有尽有。\n我给的示例只包括部分，大家可以自己亲自试试，说不定会有惊喜发现。\n有了这个JSON文件，用处就大了，可以：\n写个Python脚本自动生成用例 把JSON文件导入到接口测试平台的（像我司的接口测试平台就支持这种导入方式）。 未来tep和teprunner会支持这个功能。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%B5%81%E9%87%8F%E5%9B%9E%E6%94%BE/000002-%E5%BD%95%E5%88%B6%E5%9B%9E%E6%94%BEpostman%E7%AB%9F%E7%84%B6%E8%BF%98%E8%83%BD%E5%BD%95%E5%88%B6%E6%B5%81%E9%87%8F/","summary":"大家好，我是刚哥。","title":"Postman竟然还能录制流量"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n在Shell执行pytest -h可以看到pytest的命令行参数有这10大类，共132个\n序号 类别 中文名 包含命令行参数数量 1 positional arguments 形参 1 2 general 通用 31 3 reporting 报告 18 4 collection 收集 15 5 test session debugging and configuration 测试session调试和配置 11 6 pytest-warnings pytest警告 1 7 logging 日志 11 8 reporting-allure allure测试报告 3 9 ini-options pytest.ini/tox.ini/setup.cfg\n配置文件 37 10 environment variables 环境变量 4 1.positional arguments file_or_dir\n指定一个或多个文件/目录\npytest [file_or_dir] [file_or_dir] [...] 2.general -k EXPRESSION\n名字包含test_method或test_other的函数或类\n-k \u0026#34;test_method or test_other\u0026#34; 名字不包含test_method\n-k \u0026#34;not test_method\u0026#34; 名字不包含test_method不包含test_other\n-k \u0026#39;not test_method and not test_other\u0026#39; 大小写敏感。\n源码这里不是很明白，先放这，以后再补充解释吧\n@classmethod def from_item(cls, item: \u0026#34;Item\u0026#34;) -\u0026gt; \u0026#34;KeywordMatcher\u0026#34;: mapped_names = set() # Add the names of the current item and any parent items. import pytest for node in item.listchain(): if not isinstance(node, (pytest.Instance, pytest.Session)): mapped_names.add(node.name) # Add the names added as extra keywords to current or parent items. mapped_names.update(item.listextrakeywords()) # Add the names attached to the current function through direct assignment. function_obj = getattr(item, \u0026#34;function\u0026#34;, None) if function_obj: mapped_names.update(function_obj.__dict__) # Add the markers to the keywords as we no longer handle them correctly. mapped_names.update(mark.name for mark in item.iter_markers()) return cls(mapped_names) -m MARKEXPR\n包含mark1，不包含mark2\n-m \u0026#39;mark1 and not mark2\u0026#39; \u0026ndash;markers\n显示markers\npytest --markers -x, \u0026ndash;exitfirst\n第一个error或failed的test就退出，2个参数等价\npytest -x pytest --exitfirst \u0026ndash;maxfail=num\n2个errors或failures就退出\npytest --maxfail=2 \u0026ndash;strict-config\n解析配置文件中pytest部分时，遇到warning就抛出error\npytest --strict-config -c file\n从my.ini文件加载配置\npytest -c my.ini \u0026ndash;continue-on-collection-errors\n收集test失败，仍然强制继续执行\npytest --continue-on-collection-errors \u0026ndash;rootdir=ROOTDIR\ntests根目录，相对路径\npytest --rootdir=\u0026#34;root_dir\u0026#34; pytest --rootdir=\u0026#34;./root_dir\u0026#34; pytest --rootdir=\u0026#34;root_dir/another_dir/\u0026#34; 绝对路径\npytest --rootdir=\u0026#34;/home/user/root_dir\u0026#34; 带变量\npytest --rootdir=\u0026#34;$HOME/root_dir\u0026#34; \u0026ndash;fixtures, \u0026ndash;funcargs\n显示fixtures，以下等价\npytest --fixtures pytest --funcargs 显示以_开头的fixture\npytest --fixtures -v \u0026ndash;fixtures-per-test\n显示每个test用到的fixture\npytest --fixtures-per-test \u0026ndash;pdb\n在errors或KeyboardInterrupt时，启用默认Python debugger\npytest --pdb \u0026ndash;pdbcls=modulename:classname\n启用自定义Python debugger，由IPython.terminal.debugger module下的TerminalPdb class定义\npytest --pdbcls=IPython.terminal.debugger:TerminalPdb \u0026ndash;trace\nrun每个test时break，进入debugger交互\npytest --trace \u0026ndash;capture=method\n文件描述符（默认）\npytest --capture=fd stdout/stderr 内存\npytest --capture=sys 显示print\npytest --capture=no tee-sys\npytest --capture=tee-sys -s\n等价于--capture=no\n\u0026ndash;runxfail\n强制运行xfail标记的test\npytest --runxfail \u0026ndash;lf, \u0026ndash;last-failed\n重跑上次失败的tests，如果没有失败就重跑全部，以下等价\npytest -lf pytest --last-failed \u0026ndash;ff, \u0026ndash;failed-first\n优先跑上次失败的test，tests的顺序会被打乱，以下等价\npytest -ff pytest --failed-first \u0026ndash;nf, \u0026ndash;new-first\n优先跑新添加的tests，剩余的按文件mtime顺序，以下等价\npytest --nf pytest --new-first \u0026ndash;cache-show=[CACHESHOW]\n显示缓存，默认*\npytest --cache-show 显示缓存，带参数cache\\nodeids\npytest --cache-show=cache\\nodeids \u0026ndash;cache-clear\n运行开始时清缓存\npytest --cache-clear \u0026ndash;lfnf={all,none}, \u0026ndash;last-failed-no-failures={all,none}\n没有last-failed缓存数据，或上次没有失败时，\n跑全部tests\npytest --lfnf=all pytest --last-failed-no-failures=all 不运行\npytest --lfnf=none pytest --last-failed-no-failures=none \u0026ndash;sw, \u0026ndash;stepwise\n逐步运行，在失败时退出，下次运行时从失败的用例开始\npytest -sw pytest --stepwise \u0026ndash;stepwise-skip\n跳过第一个失败的test，如果再遇到失败就退出\npytest --stepwise-skip \u0026ndash;allure-severities=SEVERITIES_SET\n指定allure severities运行\npytest --allure-severities=blocker, critical, normal, minor, trivial \u0026ndash;allure-epics=EPICS_SET\n指定allure epics运行\npytest --allure-epics=my_epic \u0026ndash;allure-features=FEATURES_SET\n指定allure features运行\npytest --allure-features=my_feature \u0026ndash;allure-stories=STORIES_SET\n指定allure stories运行\npytest --allure-stories=my_story \u0026ndash;allure-link-pattern=LINK_TYPE:LINK_PATTERN\n不知道怎么用，溜了溜了\npytest --allure-link-pattern= 3.reporting \u0026ndash;durations=N\n显示2个最慢的setup/test的耗时\npytest --durations=2 显示所有耗时\npytest --durations=0 -v, \u0026ndash;verbose\n输出详细信息\npytest -v pytest --verbose -q, \u0026ndash;quiet\n输出简要信息\npytest -q pytest --quiet \u0026ndash;verbosity=VERBOSE\n设置信息显示等级为2\npytest --verbosity=2 -r chars\n默认\u0026quot;fE\u0026quot;\n显示failed的信息\npytest -r f 显示Error的信息\npytest -r E 显示skipped的信息\npytest -r s 显示xfailed的信息\npytest -r x 显示Xpassed的信息\npytest -r X 显示passed的信息\npytest -r p 显示Passed with output的信息\npytest -r P 显示all except passed的信息\npytest -r a pytest -r A 显示warnings are enabled by default (\u0026ndash;disable-warnings)的信息\npytest -r w 重置list\npytest -r N -l, \u0026ndash;showlocals\n在tracebacks中显示局部变量，默认不显示\npytest -l pytest --showlocals \u0026ndash;tb=style\ntraceback打印模式\npytest --tb=auto pytest --tb=long pytest --tb=short pytest --tb=line pytest --tb=native pytest --tb=no \u0026ndash;show-capture\n失败的tests如何显示，默认\u0026quot;all\u0026quot;\npytest --show-capture=no pytest --show-capture=stdout pytest --show-capture=stderr pytest --show-capture=log pytest --show-capture=all \u0026ndash;full-trace\n不截取traceback，默认会截断\npytest --full-trace \u0026ndash;color=color\n显示颜色\npytest --color=yes 不显示颜色\npytest --color=no 自动\npytest --color=auto \u0026ndash;pastebin=mode\n没什么用\npytest --pastebin=mode \u0026ndash;junit-xml=path\n创建junit-xml风格的测试报告\npytest --junit-xml=path \u0026ndash;junit-prefix=str\njunit-xml输出中的classnames添加前缀hello\npytest --junit-prefix=\u0026#34;hello\u0026#34; \u0026ndash;result-log=path\n不建议使用\npytest --result-log=path 4.collection \u0026ndash;collect-only, \u0026ndash;co\n只收集，不执行。可以用来统计写了多少条自动化用例\npytest --collect-only pytest --co \u0026ndash;pyargs\n把所有参数解释为python包(package)\npytest --pyargs \u0026ndash;ignore=path\n忽略不收集，可以多个(逗号隔开)\npytest --ignore=path1,path2,path3 \u0026ndash;ignore-glob=path\npath匹配，可以多个(逗号隔开)\npytest --ignore-glob=\u0026#34;*_01.py\u0026#34; \u0026ndash;deselect=nodeid_prefix\n通过node id prefix反选。可以多个(逗号隔开)\n取消选择tests/foobar/test_foobar_01.py::test_a\n--deselect=\u0026#34;tests/foobar/test_foobar_01.py::test_a\u0026#34; \u0026ndash;confcutdir=dir\n只加载相对于tests/foobar/目录的conftest.py文件\npytest --confcutdir=\u0026#34;tests/foobar/\u0026#34; \u0026ndash;noconftest\n不加载conftest.py文件\npytest --noconftest \u0026ndash;keep-duplicates\n收集重复的test文件，默认只会收集1item，加参数后会收集2items\npytest test.py test.py --keep-duplicates \u0026ndash;collect-in-virtualenv\n收集本地虚拟环境目录的tests\npytest --collect-in-virtualenv \u0026ndash;doctest-modules\ndoctest没啥用\npytest --doctest-modules \u0026ndash;doctest-report={none,cdiff,ndiff,udiff,only_first_failure}\ndoctest没啥用\npytest --doctest-report={none,cdiff,ndiff,udiff,only_first_failure} \u0026ndash;doctest-glob=pat\ndoctest没啥用\npytest --doctest-glob=pat \u0026ndash;doctest-ignore-import-errors\ndoctest没啥用\npytest --doctest-ignore-import-errors \u0026ndash;doctest-continue-on-failure\ndoctest没啥用\npytest --doctest-continue-on-failure 5.test session debugging and configuration \u0026ndash;basetemp=dir\ntest run的base临时目录（如果存在会先删除）\npytest --basetemp=dir -V, \u0026ndash;version\npytest版本\npytest -V pytest --version -h, \u0026ndash;help\npytest帮助\npytest -h pytest --help -p name\n加载plugin module或 entry point\npytest -p name 不加载doctest\npytest -p no:doctest \u0026ndash;trace-config\n查看本地安装好的第三方插件\npytest --trace-config \u0026ndash;debug\n保存debug信息到\u0026rsquo;pytestdebug.log\u0026rsquo;文件\npytest --debug -o OVERRIDE_INI, \u0026ndash;override-ini=OVERRIDE_INI\n覆盖ini文件配置\npytest -o xfail_strict=True -o cache_dir=cache pytest --override-ini=OVERRIDE_INI \u0026ndash;assert=MODE\n默认rewrite\npytest --assert=rewrite 无assertion debugging\npytest --assert=plain \u0026ndash;setup-only\n只setup fixtures，不执行tests\npytest --setup-only \u0026ndash;setup-show\n执行tests的时候显示fixture setup\npytest --setup-show \u0026ndash;setup-plan\n显示fixtures和tests计划会执行什么，但是不执行\n也可以用来统计自动化用例\npytest --setup-plan 6.pytest-warnings -W PYTHONWARNINGS, \u0026ndash;pythonwarnings=PYTHONWARNINGS\n设置报告哪些warnings\npytest -W PYTHONWARNINGS pytest --pythonwarnings=PYTHONWARNINGS 7.logging 推荐直接使用loguru第三方库。\n\u0026ndash;log-level=LEVEL\n默认没有设置，依赖log handler\nWARNING DEBUG INFO ERROR\npytest --log-level=LEVEL \u0026ndash;log-format=LOG_FORMAT\n日志格式\npytest --log-format=\u0026#34;%(asctime)s %(levelname)s %(message)s\u0026#34; \u0026ndash;log-date-format=LOG_DATE_FORMAT\n日期格式\npytest --log-date-format=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34; \u0026ndash;log-cli-level=LOG_CLI_LEVEL\ncli日志级别\npytest --log-cli-level=LOG_CLI_LEVEL \u0026ndash;log-cli-format=LOG_CLI_FORMAT\ncli日志格式\npytest --log-cli-format=\u0026#34;%(asctime)s %(levelname)s %(message)s\u0026#34; \u0026ndash;log-cli-date-format=LOG_CLI_DATE_FORMAT\ncli日志级别\npytest --log-cli-date-format=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34; \u0026ndash;log-file=LOG_FILE\n日志文件路径\npytest --log-file=LOG_FILE \u0026ndash;log-file-level=LOG_FILE_LEVEL\n日志文件级别\npytest --log-file-level=LOG_FILE_LEVEL \u0026ndash;log-file-format=LOG_FILE_FORMAT\n日志文件格式\npytest --log-file-format=\u0026#34;%(asctime)s %(levelname)s %(message)s\u0026#34; \u0026ndash;log-file-date-format=LOG_FILE_DATE_FORMAT\n日志文件日期\npytest --log-file-date-format=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34; \u0026ndash;log-auto-indent=LOG_AUTO_INDENT\n自动换行\ntrue|flase on|off\npytest --log-auto-indent=LOG_AUTO_INDENT 8.reporting-allure \u0026ndash;alluredir=DIR\nallure数据生成目录，注意不是html哦，而是json文件，需要allure generate data_dir -o html_dir才能生成html\npytest --alluredir=DIR \u0026ndash;clean-alluredir\n如果存在alluredir，先清除\npytest --clean-alluredir \u0026ndash;allure-no-capture\n报告不捕获pytest的logging/stdout/stderr信息\npytest --allure-no-capture 9.ini-options ini文件用例设置一些初始化默认值。\n部分内容其实质跟参数是一样用法。\nmarkers (linelist)\n自定义marker\n## pytest.ini [pytest] markers = webtest: Run the webtest case hello: Run the hello case empty_parameter_set_mark (string)\n默认情况下，如果@pytest.mark.parametrize的argnames中的参数没有接收到任何的实参的话，用例的结果将会被置为SKIPPED；empty_parameter_set_mark可以设置为skip、xfail、fail_at_collect。\nnorecursedirs (args)\n忽略一些目录\n## pytest.ini [pytest] norecursedirs = .* build dist CVS _darcs {arch} *.egg venv src testpaths (args)\n指定目录\n## pytest.ini [pytest] testpaths = test_path usefixtures (args)\n默认使用fixtures。\npython_files (args)\nglob文件匹配模式的python test modules。\npython_classes (args)\n前缀/glob文件匹配模式的python test classes。\npython_functions (args)\n前缀/glob文件匹配模式的python test functions。\nty_support (bool)\n有风险，没用。\nconsole_output_style (string)\n控制台输出样式\nclassic 经典样式 progress: 带进度百分比 count 计数而不是百分比 xfail_strict (bool)\n默认false，true时@pytest.mark.xfail的test，会被强制失败，即使是成功的。\nenable_assertion_pass_hook (bool)\n确保删除之前生成的pyc缓存文件。\njunit_suite_name (string)\n不用学。\njunit_logging (string)\n不用学。\njunit_log_passing_tests (bool)\n不用学。\njunit_duration_report (string)\n不用学。\njunit_family (string)\n不用学。\ndoctest_optionflags (args)\n不用学。\ndoctest_encoding (string)\n不用学。\ncache_dir (string)\n缓存目录。\nfilterwarnings (linelist)\n同 -W/\u0026ndash;pythonwarnings。\nlog_level (string)\n同命令行参数。\nlog_format (string)\n同命令行参数。\nlog_date_format (string)\n同命令行参数。\nlog_cli (bool)\ntrue，test run的时候，实时显示日志。\nlog_cli_level (string)\n同命令行参数。\nlog_cli_format (string)\n同命令行参数。\nlog_cli_date_format (string)\n同命令行参数。\nlog_file (string)\n同命令行参数。\nlog_file_level (string)\n同命令行参数。\nlog_file_format (string)\n同命令行参数。\nlog_file_date_format (string)\n同命令行参数。\nlog_auto_indent (string)\n同命令行参数。\nfaulthandler_timeout (string)\n如果test的运行时间超过设定的时间（超时），会打印相关traceback。\naddopts (args)\n执行时带的默认参数，可以避免每次都要输入一遍\naddopts = -rsxX -v --reruns=1 --count=2 minversion (string)\npytest最小版本号。如果pytest低于这个版本，运行会报错。\nrequired_plugins (args)\n必须的插件。\n10.environment variables PYTEST_ADDOPTS\n命令行选项\nexport PYTEST_ADDOPTS= PYTEST_PLUGINS\n包含应作为插件加载的以逗号分隔的模块列表\nexport PYTEST_PLUGINS=mymodule.plugin,xdist PYTEST_DISABLE_PLUGIN_AUTOLOAD\n禁用插件自动加载\nexport PYTEST_DISABLE_PLUGIN_AUTOLOAD= PYTEST_DEBUG\n启用pytest调试\nexport PYTEST_DEBUG= ","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/pytest/000002-pytestpytest132%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0%E7%94%A8%E6%B3%95/","summary":"在Shell执行\u003ccode\u003epytest -h\u003c/code\u003e可以看到pytest的命令行参数有这10大类，共132个","title":"pytest132个命令行参数用法"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n概览 脚手架：\n目录：\n用例代码：\n\u0026#34;\u0026#34;\u0026#34; 测试登录到下单流程，需要先启动后端服务 \u0026#34;\u0026#34;\u0026#34; test_data = { \u0026#34;查询SKU\u0026#34;: { \u0026#34;skuName\u0026#34;: \u0026#34;电子书\u0026#34; }, \u0026#34;添加购物车\u0026#34;: { \u0026#34;skuId\u0026#34;: 123, \u0026#34;skuNum\u0026#34;: 2 }, \u0026#34;下单\u0026#34;: { \u0026#34;orderId\u0026#34;: 222, \u0026#34;payAmount\u0026#34;: \u0026#34;0.2\u0026#34; }, \u0026#34;支付\u0026#34;: { \u0026#34;skuId\u0026#34;: 123, \u0026#34;price\u0026#34;: 0.1, \u0026#34;skuNum\u0026#34;: 2, \u0026#34;totalPrice\u0026#34;: 0.2 }, } case_vars = dict() def test(http, login_headers, file_data): # 搜索商品 url = file_data[\u0026#34;domain\u0026#34;] + \u0026#34;/api/tasks/mock/searchSku\u0026#34; body = test_data[\u0026#34;查询SKU\u0026#34;] response = http(\u0026#34;get\u0026#34;, url=url, headers=login_headers, params=body) assert response.status_code \u0026lt; 400 case_vars[\u0026#34;skuId\u0026#34;] = response.jsonpath(\u0026#34;$.skuId\u0026#34;) case_vars[\u0026#34;skuPrice\u0026#34;] = response.jsonpath(\u0026#34;$.price\u0026#34;) # 添加购物车 url = file_data[\u0026#34;domain\u0026#34;] + \u0026#34;/api/tasks/mock/addCart\u0026#34; body = test_data[\u0026#34;添加购物车\u0026#34;] body[\u0026#34;skuId\u0026#34;] = case_vars[\u0026#34;skuId\u0026#34;] response = http(\u0026#34;post\u0026#34;, url=url, headers=login_headers, json=body) assert response.status_code \u0026lt; 400 case_vars[\u0026#34;skuNum\u0026#34;] = response.jsonpath(\u0026#34;$.skuNum\u0026#34;) case_vars[\u0026#34;totalPrice\u0026#34;] = response.jsonpath(\u0026#34;$.totalPrice\u0026#34;) # 下单 url = file_data[\u0026#34;domain\u0026#34;] + \u0026#34;/api/tasks/mock/order\u0026#34; body = test_data[\u0026#34;下单\u0026#34;] body[\u0026#34;skuId\u0026#34;] = case_vars[\u0026#34;skuId\u0026#34;] body[\u0026#34;price\u0026#34;] = case_vars[\u0026#34;skuPrice\u0026#34;] body[\u0026#34;skuNum\u0026#34;] = case_vars[\u0026#34;skuNum\u0026#34;] body[\u0026#34;totalPrice\u0026#34;] = case_vars[\u0026#34;totalPrice\u0026#34;] response = http(\u0026#34;post\u0026#34;, url=url, headers=login_headers, json=body) assert response.status_code \u0026lt; 400 case_vars[\u0026#34;orderId\u0026#34;] = response.jsonpath(\u0026#34;$.orderId\u0026#34;) # 支付 url = file_data[\u0026#34;domain\u0026#34;] + \u0026#34;/api/tasks/mock/pay\u0026#34; body = test_data[\u0026#34;支付\u0026#34;] body[\u0026#34;orderId\u0026#34;] = case_vars[\u0026#34;orderId\u0026#34;] response = http(\u0026#34;post\u0026#34;, url=url, headers=login_headers, json=body) assert response.status_code \u0026lt; 400 assert response.jsonpath(\u0026#34;$.success\u0026#34;) == \u0026#34;true\u0026#34; 页面下载脚手架 启动平台前后端服务后，从页面下载脚手架，平台会拉取开源项目tep-project最新代码，打成压缩包，生成下载文件，弹窗下载。\n备注：tep startproject demo使用的已封版的1.0.0版本，新框架请访问开源项目tep-project，或者开源平台pytestx\n精简目录 目录直观上非常精简，得益于去掉了环境变量、函数等目录，聚焦三大目录：\nfixtures\nresources\ntests\n重度使用fixture fixture原本只能在conftest定义，借助pytest插件扩展识别fixtures目录：\n#!/usr/bin/python ## encoding=utf-8 \u0026#34;\u0026#34;\u0026#34; @Author : dongfanger @Date : 8/14/2020 9:16 AM @Desc : 插件 \u0026#34;\u0026#34;\u0026#34; import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) RESOURCE_PATH = os.path.join(BASE_DIR, \u0026#34;resources\u0026#34;) def fixture_paths(): \u0026#34;\u0026#34;\u0026#34; fixture路径，1、项目下的fixtures；2、tep下的fixture； :return: \u0026#34;\u0026#34;\u0026#34; _fixtures_dir = os.path.join(BASE_DIR, \u0026#34;fixtures\u0026#34;) paths = [] # 项目下的fixtures for root, _, files in os.walk(_fixtures_dir): for file in files: if file.startswith(\u0026#34;fixture_\u0026#34;) and file.endswith(\u0026#34;.py\u0026#34;): full_path = os.path.join(root, file) import_path = full_path.replace(_fixtures_dir, \u0026#34;\u0026#34;).replace(\u0026#34;\\\\\u0026#34;, \u0026#34;.\u0026#34;) import_path = import_path.replace(\u0026#34;/\u0026#34;, \u0026#34;.\u0026#34;).replace(\u0026#34;.py\u0026#34;, \u0026#34;\u0026#34;) paths.append(\u0026#34;fixtures\u0026#34; + import_path) return paths pytest_plugins = fixture_paths() # +[其他插件] conftest.py的fixture全部转移至fixtures目录定义。\n公共函数消失，统统通过fixture来实现，依赖注入。\n包括requests.request封装\n#!/usr/bin/python ## encoding=utf-8 import decimal import json import time import jsonpath import pytest import requests import urllib3 from loguru import logger from requests import Response urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class TepResponse(Response): \u0026#34;\u0026#34;\u0026#34; 二次封装requests.Response，添加额外方法 \u0026#34;\u0026#34;\u0026#34; def __init__(self, response): super().__init__() for k, v in response.__dict__.items(): self.__dict__[k] = v def jsonpath(self, expr): \u0026#34;\u0026#34;\u0026#34; 此处强制取第一个值，便于简单取值 如果复杂取值，建议直接jsonpath原生用法 \u0026#34;\u0026#34;\u0026#34; return jsonpath.jsonpath(self.json(), expr)[0] @pytest.fixture(scope=\u0026#34;session\u0026#34;) def http(): def inner(method, url, **kwargs): template = \u0026#34;\u0026#34;\u0026#34;\\n Request URL: {} Request Method: {} Request Headers: {} Request Payload: {} Status Code: {} Response: {} Elapsed: {} \u0026#34;\u0026#34;\u0026#34; start = time.process_time() response = requests.request(method, url, **kwargs) # requests.request原生用法 end = time.process_time() elapsed = str(decimal.Decimal(\u0026#34;%.3f\u0026#34; % float(end - start))) + \u0026#34;s\u0026#34; headers = kwargs.get(\u0026#34;headers\u0026#34;, {}) kwargs.pop(\u0026#34;headers\u0026#34;) payload = kwargs log = template.format(url, method, json.dumps(headers), json.dumps(payload), response.status_code, response.text, elapsed) logger.info(log) return TepResponse(response) return inner 只是名字换成了http：\nhttp(\u0026#34;post\u0026#34;, url=url, headers=login_headers, json=body) 因为request是fixture保留关键字。\n数据分离 数据支持从文件读取，当然这也是一个fixture：\nimport json import os import pytest import yaml from conftest import RESOURCE_PATH class Resource: def __init__(self, path): self.path = path def get_data(self): file_type = self._get_file_type() if file_type in [\u0026#34;.yml\u0026#34;, \u0026#34;.yaml\u0026#34;, \u0026#34;.YML\u0026#34;, \u0026#34;YAML\u0026#34;]: return self._get_yaml_file_data() if file_type in [\u0026#34;.json\u0026#34;, \u0026#34;.JSON\u0026#34;]: return self._get_json_file_data() def _get_file_type(self): return os.path.splitext(self.path)[-1] def _get_yaml_file_data(self): with open(self.path, encoding=\u0026#34;utf8\u0026#34;) as f: return yaml.load(f.read(), Loader=yaml.FullLoader) def _get_json_file_data(self): with open(self.path, encoding=\u0026#34;utf8\u0026#34;) as f: return json.load(f) @pytest.fixture(scope=\u0026#34;session\u0026#34;) def file_data(): file_path = os.path.join(RESOURCE_PATH, \u0026#34;demo.yaml\u0026#34;) return Resource(file_path).get_data() 也可以放在用例文件中。为什么？“只改数据不动用例代码”，如果没有这种情况，请毫不犹豫将数据放到用例文件中，不要从excel、yaml读取数据，增加无意义的中间转换。从流量回放替代自动化的趋势来看，数据和用例作为整体来维护和运行，会越来越普遍。在使用低代码平台时，测试数据也是写在用例里面，只有少量的公共信息，会抽出来作为变量。测试技术在发展，只有符合当前实际使用需要的，才是最好的。\n用例设计 约定大于配置：\n数据区域、用例区域分离\n用例由步骤组成\n步骤分为前置条件、用例体、断言、数据提取\n数据区域，接口入参、用例中间变量等：\ntest_data = { \u0026#34;查询SKU\u0026#34;: { \u0026#34;skuName\u0026#34;: \u0026#34;电子书\u0026#34; }, \u0026#34;添加购物车\u0026#34;: { \u0026#34;skuId\u0026#34;: 123, \u0026#34;skuNum\u0026#34;: 2 }, \u0026#34;下单\u0026#34;: { \u0026#34;orderId\u0026#34;: 222, \u0026#34;payAmount\u0026#34;: \u0026#34;0.2\u0026#34; }, \u0026#34;支付\u0026#34;: { \u0026#34;skuId\u0026#34;: 123, \u0026#34;price\u0026#34;: 0.1, \u0026#34;skuNum\u0026#34;: 2, \u0026#34;totalPrice\u0026#34;: 0.2 }, } case_vars = dict() 用例定义，test函数，fixture引用：\ndef test(http, login_headers, file_data): 步骤：\n## 搜索商品 url = file_data[\u0026#34;domain\u0026#34;] + \u0026#34;/api/tasks/mock/searchSku\u0026#34; body = test_data[\u0026#34;查询SKU\u0026#34;] response = http(\u0026#34;get\u0026#34;, url=url, headers=login_headers, params=body) assert response.status_code \u0026lt; 400 case_vars[\u0026#34;skuId\u0026#34;] = response.jsonpath(\u0026#34;$.skuId\u0026#34;) case_vars[\u0026#34;skuPrice\u0026#34;] = response.jsonpath(\u0026#34;$.price\u0026#34;) 每个用例文件单独可运行。不存在用例依赖，复用步骤封装为fixture，以依赖注入方式在各用例中复用。用例一定要解耦，这在任务调度时非常重要。\n总结，重新定义目录，重新定义用例组织，重新定义fixture，减少过程代码，专注于用例编写，轻松上手pytest。\n跟着pytestx学习接口自动化框架设计，更简单，更稳定，更高效。\nhttps://github.com/dongfanger/pytestx\nhttps://gitee.com/dongfanger/tep-project\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/pytestx/000002-pytestx%E9%87%8D%E6%96%B0%E5%AE%9A%E4%B9%89%E6%8E%A5%E5%8F%A3%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"概览\"\u003e概览\u003c/h1\u003e\n\u003cp\u003e脚手架：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/aefec433fde14d39abde74206ab83341.png\"\u003e\u003c/p\u003e\n\u003cp\u003e目录：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d271b2542dab494595723669cc50dd81.png\"\u003e\u003c/p\u003e\n\u003cp\u003e用例代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e测试登录到下单流程，需要先启动后端服务\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etest_data \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;查询SKU\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuName\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;电子书\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;添加购物车\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e123\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;下单\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;orderId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e222\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;payAmount\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;0.2\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;支付\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e123\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;price\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0.1\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e0.2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecase_vars \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003edict\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003ehttp\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e login_headers\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e file_data\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 搜索商品\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e file_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/api/tasks/mock/searchSku\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e test_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;查询SKU\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e http\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;get\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003elogin_headers\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e params\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.price\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 添加购物车\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e file_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/api/tasks/mock/addCart\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e test_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;添加购物车\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e http\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003elogin_headers\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 下单\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e file_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/api/tasks/mock/order\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e test_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;下单\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;price\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;skuNum\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;totalPrice\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e http\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003elogin_headers\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;orderId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.orderId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e# 支付\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    url \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e file_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;domain\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/api/tasks/mock/pay\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e test_data\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;支付\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    body\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;orderId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e case_vars\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;orderId\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    response \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e http\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;post\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e url\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003eurl\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e headers\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003elogin_headers\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e json\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003ebody\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003estatus_code \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e400\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003eassert\u003c/span\u003e response\u003cspan style=\"color:#0550ae\"\u003e.\u003c/span\u003ejsonpath\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;$.success\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;true\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"页面下载脚手架\"\u003e页面下载脚手架\u003c/h1\u003e\n\u003cp\u003e启动平台前后端服务后，从页面下载脚手架，平台会拉取开源项目tep-project最新代码，打成压缩包，生成下载文件，弹窗下载。\u003c/p\u003e","title":"pytestx重新定义接口框架设计"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nFlask是什么 如果说Django是一个开箱即用的自带完善功能的Web框架，那么Flask就是一个只提供基础结构的微型框架。这一点从它的GitHub源码就能看出端倪：\n它的核心代码就只有这么多。微型意味着Flask本身只提供了简单的核心的功能，但它具备了强大的可扩展性。比如，它不会强制你使用某个ORM框架，而是把选择权交给你，让你自己决定集成哪个扩展。\n无论如何，都应该学习一下Flask，因为它让你从零开始，了解一个Web框架是如何诞生的：\n安装Flask Flask需要Python3.6及以上才能使用，最新的async异步支持需要Python3.7及以上。\n安装Flask前可以先创建一个虚拟环境，这样相关的包都放在这个虚拟环境中。\nMac用户：\n## 创建 $ mkdir myproject $ cd myproject $ python3 -m venv venv ## 激活 $ . venv/bin/activate Windows用户：\n## 创建 \u0026gt; mkdir myproject \u0026gt; cd myproject \u0026gt; py -3 -m venv venv ## 激活 \u0026gt; venv\\Scripts\\activate 然后就通过pip安装Flask即可：\n$ pip install Flask Hello World Flask是一个Web框架，更准备的说是一个提供接口的后端服务，只需要5行代码就能实现一个接口：\nfrom flask import Flask app = Flask(__name__) @app.route(\u0026#34;/\u0026#34;) def hello_world(): return \u0026#34;\u0026lt;p\u0026gt;Hello, World!\u0026lt;/p\u0026gt;\u0026#34; 截止到目前，还没见过哪个Web框架是这么玩的，有点颠覆对Web框架的认知，印象中的Web框架都是有点庞大的，Flask却很轻巧的就实现了。\n运行后会在控制台输出：\n\u0026gt; set FLASK_APP=hello \u0026gt; flask run * Running on http://127.0.0.1:5000/ 浏览器访问http://127.0.0.1:5000/就会看到Hello, World!字样。\n代码解析 这几行代码会做这4件事：\nfrom flask import Flask，Flask类的实例就是一个WSGI应用，这样才能跑起来。 app = Flask(__name__)，参数__name__是模块或包名，这样Flask才能去查找Web相关文件。 @app.route(\u0026quot;/\u0026quot;)，定义了接口的URL，也就是路由配置，默认是GET方法。 hello_world()函数是具体的接口处理逻辑代码，返回了一个HTML字符串。 小结 本文简单介绍了Flask是什么，怎么安装Flask，演示了Hello World的代码实现Demo，并对代码进行了解析。为什么一定要学Flask呢？我最近看了一篇Django vs Flask的最新的比较文章，原文是国外某个博客发的，里面说道，如果你想在当前快速实现某个需求，那么可以选择使用Django。但是请你有时间务必回过头来学一下Flask，因为这样才能知道用Python做Web开发的所有细节。\n参考资料：\nhttps://flask.palletsprojects.com/en/2.0.x/\nhttps://flask.palletsprojects.com/en/2.0.x/foreword/\nhttps://flask.palletsprojects.com/en/2.0.x/installation/\nhttps://flask.palletsprojects.com/en/2.0.x/quickstart/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/flask/000002-flaskpython%E5%BE%AE%E5%9E%8Bweb%E6%A1%86%E6%9E%B6flask%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"flask是什么\"\u003eFlask是什么\u003c/h1\u003e\n\u003cp\u003e如果说Django是一个开箱即用的自带完善功能的Web框架，那么Flask就是一个只提供基础结构的微型框架。这一点从它的GitHub源码就能看出端倪：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/08c1545ca5614686adca7d9a7a52dfbb.png\"\u003e\u003c/p\u003e\n\u003cp\u003e它的核心代码就只有这么多。微型意味着Flask本身只提供了简单的核心的功能，但它具备了强大的可扩展性。比如，它不会强制你使用某个ORM框架，而是把选择权交给你，让你自己决定集成哪个扩展。\u003c/p\u003e\n\u003cp\u003e无论如何，都应该学习一下Flask，因为它让你从零开始，了解一个Web框架是如何诞生的：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/7ca10b9c77844f18a73ed4955fa05bb6.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"安装flask\"\u003e安装Flask\u003c/h1\u003e\n\u003cp\u003eFlask需要Python3.6及以上才能使用，最新的\u003ccode\u003easync\u003c/code\u003e异步支持需要Python3.7及以上。\u003c/p\u003e\n\u003cp\u003e安装Flask前可以先创建一个虚拟环境，这样相关的包都放在这个虚拟环境中。\u003c/p\u003e\n\u003cp\u003eMac用户：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e## 创建\n$ mkdir myproject\n$ cd myproject\n$ python3 -m venv venv\n## 激活\n$ . venv/bin/activate\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eWindows用户：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e## 创建\n\u0026gt; mkdir myproject\n\u0026gt; cd myproject\n\u0026gt; py -3 -m venv venv\n## 激活\n\u0026gt; venv\\Scripts\\activate\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后就通过pip安装Flask即可：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e$ pip install Flask\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"hello-world\"\u003eHello World\u003c/h1\u003e\n\u003cp\u003eFlask是一个Web框架，更准备的说是一个提供接口的后端服务，只需要5行代码就能实现一个接口：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003eflask\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e Flask\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapp \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e Flask\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#953800\"\u003e__name__\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.route\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003ehello_world\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u0026lt;p\u0026gt;Hello, World!\u0026lt;/p\u0026gt;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e截止到目前，还没见过哪个Web框架是这么玩的，有点颠覆对Web框架的认知，印象中的Web框架都是有点庞大的，Flask却很轻巧的就实现了。\u003c/p\u003e\n\u003cp\u003e运行后会在控制台输出：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt; set FLASK_APP=hello\n\u0026gt; flask run\n * Running on http://127.0.0.1:5000/\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e浏览器访问\u003ccode\u003ehttp://127.0.0.1:5000/\u003c/code\u003e就会看到\u003ccode\u003eHello, World!\u003c/code\u003e字样。\u003c/p\u003e","title":"Python微型Web框架Flask快速入门"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nunittest和pytest是Python的2个强大的测试框架，经常用来做UI自动化或接口自动化。unittest是PyCharm的默认集成工具，也是我们大多数人入门自动化的首选框架。pytest提供了更丰富的功能，相对的上手难度也要高一点。如果学了pytest后，想快速用pytest写项目，用于工作实践，那么可以试试我写的这款测试工具：tep，try easy pytest。\ntep简介 在Google中输入python tep：\ntep是一款测试工具，不是测试框架，它做的事情只是帮助你更轻松的使用pytest写项目代码，主要功能如下：\n项目脚手架 pytest用例组织 输出HTML测试报告 基于fixture管理环境变量 基于fixture共享公共函数 访问MySQL数据库 集成常用第三方包 登录接口示例代码 工具安装 安装 tep已经上传到了PyPI（Python Package Index）：\n可以使用pip命令安装：\npip install tep 外网速度慢，pandas可能安装失败，推荐用国内镜像：\npip --default-timeout=6000 install -i https://pypi.tuna.tsinghua.edu.cn/simple tep 由于集成了第三方包，安装可能会花几分钟，请耐心等待。\nPython版本 3.6+\n检查 安装完成后，检查是否安装成功：\ntep -V 0.5.3 或者：\ntep --version 0.5.3 升级 使用-U参数：\npip install -U tep 或者指定版本：\npip install tep==0.5.3 快速创建项目 tep提供了脚手架，预置了项目结构和代码，打开cmd，使用startproject命令快速创建项目：\ncd some_directory tep startproject project_name 创建后的项目结果如下：\n当前版本主要用来做接口自动化，建议把不同业务系统建成多个项目。\n1个文件2处修改完成登录请求 tep已经预置了登录相关代码，只需要打开fixtures\\fixture_admin.py，修改2个地方的代码，就可以完成登录请求。\n修改环境变量 编辑fixtures/fixture_admin.py：\n@pytest.fixture(scope=\u0026#34;session\u0026#34;) def env_vars(config): class Clazz: def __init__(self): env = config[\u0026#34;env\u0026#34;] self.mapping = { \u0026#34;qa\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://qa.com\u0026#34;, \u0026#34;mysql_engine\u0026#34;: mysql_engine(\u0026#34;127.0.0.1\u0026#34;, # host \u0026#34;2306\u0026#34;, # port \u0026#34;root\u0026#34;, # username \u0026#34;123456\u0026#34;, # password \u0026#34;test\u0026#34;) # db_name }, \u0026#34;release\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://release.com\u0026#34;, \u0026#34;mysql_engine\u0026#34;: mysql_engine(\u0026#34;127.0.0.1\u0026#34;, \u0026#34;2306\u0026#34;, \u0026#34;root\u0026#34;, \u0026#34;123456\u0026#34;, \u0026#34;release\u0026#34;) } # Add your env and variables } self.domain = self.mapping[env][\u0026#34;domain\u0026#34;] self.mysql_engine = self.mapping[env][\u0026#34;mysql_engine\u0026#34;] # Add properties def add(self, env, key, value): self.mapping[config[\u0026#34;env\u0026#34;]][key] = value return Clazz() mapping是个字典，预置了2个环境：qa和release，每个环境预置了2个变量：domain和mysql_engine。\ndomain\nurl = domain +uri，例如https://qa.com/api/users/login/的domain是``https://qa.com，uri是/api/users/login/`。\n首先把qa环境的domain值改为你的测试域名。\n修改登录代码 继续编辑fixtures/fixture_admin.py：\ndef _jwt_headers(token): return {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;authorization\u0026#34;: f\u0026#34;Bearer {token}\u0026#34;} @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login(): # Code your login logger.info(\u0026#34;Administrator login\u0026#34;) response = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users/login/\u0026#34;), headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 response_token = jmespath.search(\u0026#34;token\u0026#34;, response.json()) class Clazz: token = response_token jwt_headers = _jwt_headers(response_token) return Clazz 在# Code your login处开始修改代码，替换API路径/api/users/login/和请求参数json。如果你的登录接口不会返回token，那么修改jmespath.search(\u0026quot;token\u0026quot;, response.json())为实际响应取值。\ntep预置登录返回了2个值：token和jwt_headers。\n测试登录 打开tests/login_test.py：\nfrom loguru import logger def test_login(login): logger.info(login.token) 执行后就会调用登录接口发起请求。\n遇到问题无法解决请留言或加群。\n编写测试用例 用例组织 测试用例全部放在tests目录下：\n每个.py模块以test_前缀或_test后缀命名，每个test函数以test前缀命名。只要遵循这个规则，目录层次不影响。示例：\n建议每个.py模块只包含1个test函数，也就是1条测试用例。\n接口请求设计 每条测试用例可以包含单个或多个接口请求。tep预置了1个POST请求示例代码，打开tests/post_test.py：\nimport jmespath from loguru import logger from tep.client import request def test_post(faker_ch, url, login): # description logger.info(\u0026#34;test post\u0026#34;) # data fake = faker_ch # request response = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users\u0026#34;), headers=login.jwt_headers, json={ \u0026#34;name\u0026#34;: fake.name() } ) # assert assert response.status_code \u0026lt; 400 # extract user_id = jmespath.search(\u0026#34;id\u0026#34;, response.json()) 每个请求由5部分组成，从上往下分别是描述、数据、请求、断言和提取。\n借鉴了JMeter元件和参数化关联的设计思想。\n描述\n描述这个请求是干嘛的。\n数据\n初始化局部变量，例如使用faker库造测试数据。\n请求\n如果需要打印请求日志，可以from tep.client import request，tep.client.request对requests.request除了记录日志外，没有做任何修改。\n如果请求报错了，那么很可能是代码写错了。\n也可以直接使用原生request，from requests import request。\n不同请求方法的代码如下：\nrequest(\u0026#34;get\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, json={}) request(\u0026#34;post\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, params={}) request(\u0026#34;put\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, json={}) request(\u0026#34;delete\u0026#34;, url=\u0026#34;\u0026#34;, headers={}) # 上传excel request(\u0026#34;post\u0026#34;, url=\u0026#34;\u0026#34;, headers={}, files={ \u0026#34;file\u0026#34;: ( file_name, open(file_path, \u0026#34;rb\u0026#34;), \u0026#34;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\u0026#34; ) }, verify=False ) 更多用法参考requests.request。\n断言\n断言使用Python标准断言assert 表达式，表达式跟if语句表达式一模一样。\n提取\n推荐使用jmespath提取JSON数据，也可以使用Python[]中括号提取。\nHTML测试报告 批量执行用例 单个测试用例，在PyCharm中执行就可以了。多个测试用例批量执行，需要打开左下角的Terminal，输入命令来执行：\n# 先进入tests目录 cd tests # 批量执行 pytest 只收集不执行 如果只想统计用例条数，不执行代码，那么可以使用以下命令：\npytest --co HTML测试报告 tep提供了--tep-reports参数来生成allure测试报告：\npytest --tep-reports 报告文件存放在根目录的reports/中：\n右键index.html选择：\n就会打开浏览器显示测试报告了：\n默认allure报告右上角的TREND是无法显示数据的，--tep-reports修复了这个问题，根据history数据生成TREND走势图。\n自定义环境变量 静态添加 编辑fixtures/fixture_admin.py，在mapping字典中# Add your env and variables处添加环境变量，在# Add properties处添加属性，参考domain和mysql_engine。示例：\n动态添加 使用add()方法动态添加环境变量：\nenv_vars.add(\u0026#34;my_var\u0026#34;, 789) 使用环境变量 env_vars返回了类对象实例，通过.运算符来使用环境变量：\nenv_vars.domain env_vars.mysql_engine env_vars.my_var 不需要import，而是给test函数添加参数，如def test(env_vars):\n切换环境 在conf.yaml文件中配置：\nenv: qa 默认为qa环境。\n团队共享公共函数 通过fixture技术，我们可以把重复代码提取出来，在团队间进行复用。tep重度使用了这个技术，在fixtures目录中，除了预置的fixture_admin.py，可以继续添加团队成员的fixture：\n新添加的fixture_your_name.py需要到根目录的conftest.py中注册才能生效：\n# Import fixtures exec(\u0026#34;from .fixtures.fixture_admin import *\u0026#34;) exec(\u0026#34;from .fixtures.fixture_your_name import *\u0026#34;) conftest.py模块中建议全部定义成fixture，不对外提供function。\n为了避免命名冲突和代码跟踪，团队成员的公共函数命名建议加上_yourname后缀。\n访问MySQL数据库 首先在fixtures\\fixture_admin.py中修改mysql_engine：\n\u0026#34;mysql_engine\u0026#34;: mysql_engine(\u0026#34;127.0.0.1\u0026#34;, # host \u0026#34;2306\u0026#34;, # port \u0026#34;root\u0026#34;, # username \u0026#34;123456\u0026#34;, # password \u0026#34;test\u0026#34;) # db_name 依次填写主机名、端口、用户名、密码、数据库名。\n接着使用pandas来访问数据库，tests\\mysql_test.py预置了示例代码：\nfrom loguru import logger def test_mysql(pd, env_vars): logger.info(pd.read_sql(\u0026#34;select 1 from dual\u0026#34;, env_vars.mysql_engine)) 返回的数据可以通过data[\u0026quot;列名\u0026quot;][\u0026quot;行号\u0026quot;] 的方式读取。\n如果想在console中以表格方式显示表数据，可以使用tep.dao.print_db_table()函数，如：\nfrom loguru import logger from tep.dao import print_db_table def test_mysql(pd, env_vars): data = pd.read_sql(\u0026#34;select 1 from dual\u0026#34;, env_vars.mysql_engine) logger.info(print_db_table(data)) 第三方包集成说明 tep使用了Poetry来管理包和依赖：\n目前已集成：\n# 造测试数据 faker = \u0026#34;^4.1.1\u0026#34; # 提取JSON数据 jmespath = \u0026#34;^0.10.0\u0026#34; # 屏蔽HTTPS警告 urllib3 = \u0026#34;^1.25.9\u0026#34; # HTTP/HTTPS请求 requests = \u0026#34;^2.24.0\u0026#34; # HTML测试报告 allure-pytest = \u0026#34;^2.8.16\u0026#34; allure-python-commons = \u0026#34;^2.8.16\u0026#34; # 打印日志 loguru = \u0026#34;^0.5.1\u0026#34; # 访问数据库 pandas = \u0026#34;^1.1.0\u0026#34; # 配置文件 pyyaml = \u0026#34;^5.3.1\u0026#34; # 为pandas访问数据库提供engine sqlalchemy = \u0026#34;^1.3.22\u0026#34; # 把数据库表打印成表格 texttable = \u0026#34;^1.6.3\u0026#34; # 支持sqlalchemy使用 pymysql = \u0026#34;^0.10.1\u0026#34; 小结 最早写这个工具的想法是以项目工程方式使用pytest，并结合第三方包完成接口自动化落地。如果你也有类似的想法，那么可以参考tep的做法。tep用户手册是针对于tep工具本身的使用说明，不包含pytest框架搭建和基本使用，如果想入门pytest并深度学习，可以阅读我写的pytest系列文章，共8篇。tep的最新版本是0.5.3，还不够稳定，也存在bug，我会坚持维护下去，既是实用工具，也是技术沉淀。对tep有任何建议或问题，欢迎公众号后台回复“加群”讨论哦：\n参考资料：\n源码 https://github.com/dongfanger/tep\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000002-tep%E7%94%A8%E6%88%B7%E6%89%8B%E5%86%8C%E5%B8%AE%E4%BD%A0%E4%BB%8Eunittest%E8%BF%87%E6%B8%A1%E5%88%B0pytest/","summary":"unittest和pytest是Python的2个强大的测试框架，经常用来做UI自动化或接口自动化。unittest是PyCharm的默认集成工具，也是我们大多数人入门自动化的首选框架。pytest提供了更丰富的功能，相对的上手难度也要高一点。如果学了pytest后，想快速用pytest写项目，用于工作实践，那么可以试试我写的这款测试工具：\u003ccode\u003etep\u003c/code\u003e，try easy pytest。","title":"tep用户手册帮你从unittest过渡到pytest"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n一、引言 黑盒测试犹如案发现场，只能根据表象推断事件经过。\n代码评审即深入调查，挖掘蛛丝马迹的线索，揭示背后的真相。\n\u0026ldquo;They think I am hiding in the shadows, but I am the shadows.\u0026rdquo;\n二、黑盒测试与白盒测试的区别 黑盒测试存在一些局限性：\n可能无法发现与系统实现相关的问题\n可能无法覆盖所有的测试场景\n测试效率较低，比如准备物料、模拟场景\n强依赖需求文档，如果文档不全，测试会漏\n对于测试人员来说，可以在代码评审阶段，通过白盒测试改进测试的质量和效率。\n三、代码评审的定义和意义 代码评审，Code Review（CR），是一种通过检查代码来提高代码质量的过程。\n对于测试人员来说，参与代码评审，可以尽量提前发现问题，减少修复代价，提高效能。\n四、代码评审的形式 多人讨论\n组织会议，研发牵头讲解代码，架构和测试参与，讨论交流。这是最普遍的一种形式。\nCode Diff\n查看Code Diff，可以借助Gitlab或IDEA，比较分支差异或版本差异。\n对比时机：\n提测前和测试中，自行走查代码\n发现缺陷，定位代码原因\n修复缺陷后，评估影响范围\n上线前，是否夹带代码\n精准测试\n评估测试用例的代码覆盖率，查漏补缺，Jacoco的on-the-fly模式支持动态收集代码覆盖率数据。\n五、代码评审的方法 面向业务，面向业务，面向业务。重要的事情说三遍。\n刚开始做代码评审，很容易把注意力集中在找代码规范问题上面，比如命名不规范、注释不清楚、代码实现冗长等。这些问题不是测试人员关注的重点，需要由研发团队或代码扫描工具来解决。\n在做Code Diff时，也没必要把每个文件、每行代码的意思搞懂，比如研发对代码结构做了调整，在diff时要梳理清楚的话，ROI会非常低，因为既消耗时间，又发现不了问题。\n那该怎么做代码评审呢？ 关注业务：\n跟需求文档比较，哪些需求是遗漏的，哪些代码是补充的，哪些代码是夹带的\n关注核心业务代码逻辑，使用条件覆盖、路径覆盖等方法设计测试用例\n优化测试用例，针对代码实现考虑异常、边界、幂等、并发等场景\n代码评审要求测试人员具备代码能力，理解编程语言，掌握软件设计，熟悉代码结构和架构，多与开发同学交流，共同优化代码质量。\n六、代码评审的实际案例 1、空指针异常\nimport org.mybatis.spring.SqlSessionTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private final SqlSessionTemplate sqlSessionTemplate; @Autowired public UserService(SqlSessionTemplate sqlSessionTemplate) { this.sqlSessionTemplate = sqlSessionTemplate; } public void getUserById(String userId) { User user = sqlSessionTemplate.selectOne(\u0026#34;com.example.mapper.UserMapper.getUserById\u0026#34;, userId); System.out.println(user.getName()); // 可能导致空指针异常 } } 如果取到的 user 对象为空，就会导致空指针异常。\n2、String类型判空用StringUtils.isBlank()，Collection类判空用CollectionUtils.isEmpty()\nimport org.apache.commons.lang3.StringUtils; public class ExampleStringUtils { public static void main(String[] args) { String str1 = \u0026#34;\u0026#34;; String str2 = null; String str3 = \u0026#34; \u0026#34;; if (StringUtils.isBlank(str1)) { System.out.println(\u0026#34;str1 is blank or null\u0026#34;); } if (StringUtils.isBlank(str2)) { System.out.println(\u0026#34;str2 is blank or null\u0026#34;); } if (StringUtils.isBlank(str3)) { System.out.println(\u0026#34;str3 is blank or null\u0026#34;); } } } import org.apache.commons.collections4.CollectionUtils; import java.util.ArrayList; import java.util.List; public class ExampleCollectionUtils { public static void main(String[] args) { List\u0026lt;String\u0026gt; list1 = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;String\u0026gt; list2 = null; if (CollectionUtils.isEmpty(list1)) { System.out.println(\u0026#34;list1 is empty or null\u0026#34;); } if (CollectionUtils.isEmpty(list2)) { System.out.println(\u0026#34;list2 is empty or null\u0026#34;); } } } 3、写操作的事务一致性\n@Service public class UserService { private final UserMapper userMapper; private final AccountMapper accountMapper; // 省略构造方法 public void addUserAndDeductBalance(User user, double amount) { try { userMapper.insertUser(user); // 插入用户信息 accountMapper.deductBalance(user.getAccountId(), amount); // 扣除账户余额 // 其他写操作... } catch (Exception e) { // 处理异常 } } } 没有使用 @Transactional，不会进行事务管理和回滚，如果执行accountMapper.deductBalance()时异常，那么已经执行的 userMapper.insertUser() 操作无法回滚，用户信息被插入但账户余额未扣除，导致数据的不一致性。\n4、根据判断条件补充用例\npublic class ECommerceSystem { public static void main(String[] args) { String productCategory = \u0026#34;electronics\u0026#34;; float productPrice = 999.99f; int userPoints = 100; if (\u0026#34;electronics\u0026#34;.equals(productCategory)) { if (productPrice \u0026gt; 1000) { if (userPoints \u0026gt; 50) { applyDiscount(0.2f); } else { applyDiscount(0.1f); } } else { if (userPoints \u0026gt; 100) { applyDiscount(0.15f); } else { applyDiscount(0.05f); } } } else if (\u0026#34;clothing\u0026#34;.equals(productCategory)) { if (productPrice \u0026gt; 500) { if (userPoints \u0026gt; 100) { applyDiscount(0.3f); } else { applyDiscount(0.2f); } } else { if (userPoints \u0026gt; 50) { applyDiscount(0.1f); } else { applyDiscount(0.05f); } } } else { if (productPrice \u0026gt; 100) { if (userPoints \u0026gt; 10) { applyDiscount(0.1f); } else { applyDiscount(0.05f); } } } } private static void applyDiscount(float discount) { System.out.println(\u0026#34;Applying discount of \u0026#34; + discount * 100 + \u0026#34;%\u0026#34;); // 执行折扣逻辑 } } 复杂的判断条件，文档很可能描述不全所有场景，需要针对代码实现，补充测试用例。\n5、代码放在不同位置，影响范围变小\npublic class ShoppingCart { private List\u0026lt;Product\u0026gt; products = new ArrayList\u0026lt;\u0026gt;(); public void addToCart(Product product) { products.add(product); updateCartTotal(); } public void removeFromCart(Product product) { products.remove(product); updateCartTotal(); } private void updateCartTotal() { float total = 0; for (Product product : products) { total += product.getPrice(); } System.out.println(\u0026#34;Cart Total: \u0026#34; + total); } } public class Product { private String name; private float price; // constructor, getters and setters public float getPrice() { return price; } } 如果需要更改计算总金额的逻辑，只需修改 updateCartTotal() 方法即可，而不需要修改调用该方法的其他部分代码，测试点更少，影响面更小。\n6、for循环性能优化\npublic class PerformanceOptimization { public static void main(String[] args) { List\u0026lt;Integer\u0026gt; numbers = Arrays.asList(1, 2, 3, 4, 5); // 原始的 for 循环 long startTime = System.nanoTime(); for (int i = 0; i \u0026lt; numbers.size(); i++) { int number = numbers.get(i); System.out.println(number); } long endTime = System.nanoTime(); long elapsedTime = endTime - startTime; System.out.println(\u0026#34;原始 for 循环耗时: \u0026#34; + elapsedTime + \u0026#34; 纳秒\u0026#34;); // 使用增强 for 循环 startTime = System.nanoTime(); for (int number : numbers) { System.out.println(number); } endTime = System.nanoTime(); elapsedTime = endTime - startTime; System.out.println(\u0026#34;增强 for 循环耗时: \u0026#34; + elapsedTime + \u0026#34; 纳秒\u0026#34;); } } 如果for循环里面接口调用或计算量大，可能会导致性能问题。\n7、finally块的return覆盖try-catch块中的return\npublic class ReturnInFinally { public static void main(String[] args) { System.out.println(testMethod()); } public static int testMethod() { try { System.out.println(\u0026#34;Inside try block\u0026#34;); return 1; } catch (Exception ex) { System.out.println(\u0026#34;Inside catch block\u0026#34;); return 2; } finally { System.out.println(\u0026#34;Inside finally block\u0026#34;); return 3; } } } 如果确实需要在 finally 块中执行一些清理或资源释放操作，并希望保留 try-catch 块中的返回结果，可以将返回值存储在一个变量中，在 finally 块之后再进行返回。\n8、多表同时更新，使用分布式事务\ntry { // 开启分布式事务 beginDistributedTransaction(); // 执行事务操作1 updateTable1(); // 执行事务操作2 updateTable2(); // 执行事务操作3 updateTable3(); // 提交分布式事务 commitDistributedTransaction(); } catch (Exception e) { // 回滚分布式事务 rollbackDistributedTransaction(); // 处理异常 handleException(e); } 假设有两个服务，一个是订单服务，负责处理用户下单和创建订单；另一个是库存服务，负责管理商品的库存数量。当用户下单时，订单服务需要创建订单并扣减对应商品的库存。可能会出现数据不一致：在订单服务创建订单之后，库存服务还未扣减库存的情况下发生了故障，导致订单已经创建但库存没有被正确扣减。这会导致订单和库存之间的数据不一致。如果只是简单地依次执行两个操作，无法保证它们的原子性。\n9、幂等\npublic class OrderService { public String createOrder(OrderData orderData) { // 生成订单号 String orderId = generateOrderId(); // 检查订单是否已经存在 if (!isOrderExist(orderId)) { // 创建订单 saveOrder(orderId, orderData); // 扣减库存 decreaseInventory(orderData); return \u0026#34;订单创建成功\u0026#34;; } else { return \u0026#34;订单已存在\u0026#34;; } } private String generateOrderId() { // 省略具体实现 return \u0026#34;123456789\u0026#34;; } private boolean isOrderExist(String orderId) { // 省略具体实现 return false; } private void saveOrder(String orderId, OrderData orderData) { // 省略具体实现 } private void decreaseInventory(OrderData orderData) { // 省略具体实现 } } 如果发起重复请求，上个请求还未处理完，可能会重复创建相同订单。考虑使用分布式锁来保证接口的幂等性。\n10、执行频率高的代码日志，增加级别判断\nimport org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyClass { private static final Logger logger = LoggerFactory.getLogger(MyClass.class); public void myMethod() { // 判断日志级别是否为 INFO if (logger.isInfoEnabled()) { String message = \u0026#34;This is an info message.\u0026#34;; logger.info(message); } } } 11、枚举类\npublic enum InvoiceStatus { PENDING(\u0026#34;待处理\u0026#34;), APPROVED(\u0026#34;已批准\u0026#34;), REJECTED(\u0026#34;已拒绝\u0026#34;), CANCELLED(\u0026#34;已取消\u0026#34;), PAID(\u0026#34;已支付\u0026#34;); // 省略定义 } 如果筛选几个枚举作为状态判断，可能不准确。\n12、更多业务类案例：\n代码未找到需求相关实现，参考需求文档\n上下游接口字段未对齐，参考接口文档\n修改了公共方法，回归范围扩大\n修改了方法A1，未修改方法A2，A1和A2是不同入口，都需要修改\n七、总结 从业务需求角度出发，剖析代码逻辑，运用测试经验，以更高的效率，发现更多的缺陷，这就是代码评审带来的烧脑体验。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E7%99%BD%E7%9B%92%E6%B5%8B%E8%AF%95/000002-%E4%BB%A3%E7%A0%81%E8%AF%84%E5%AE%A1%E6%8F%AD%E7%A4%BA%E9%BB%91%E7%9B%92%E8%83%8C%E5%90%8E%E7%9A%84%E7%9C%9F%E7%9B%B8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"一引言\"\u003e一、引言\u003c/h1\u003e\n\u003cp\u003e黑盒测试犹如案发现场，只能根据表象推断事件经过。\u003c/p\u003e\n\u003cp\u003e代码评审即深入调查，挖掘蛛丝马迹的线索，揭示背后的真相。\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;They think I am hiding in the shadows, but I am the shadows.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4ff9ff50f1044666a546f23760a2e663.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"二黑盒测试与白盒测试的区别\"\u003e二、黑盒测试与白盒测试的区别\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f8d7e7c0dd1d46c2874552514624fed8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e黑盒测试存在一些局限性：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e可能无法发现与系统实现相关的问题\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e可能无法覆盖所有的测试场景\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e测试效率较低，比如准备物料、模拟场景\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e强依赖需求文档，如果文档不全，测试会漏\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e对于测试人员来说，可以在代码评审阶段，通过白盒测试改进测试的质量和效率。\u003c/p\u003e\n\u003ch1 id=\"三代码评审的定义和意义\"\u003e三、代码评审的定义和意义\u003c/h1\u003e\n\u003cp\u003e代码评审，Code Review（CR），是一种通过检查代码来提高代码质量的过程。\u003c/p\u003e\n\u003cp\u003e对于测试人员来说，参与代码评审，可以尽量提前发现问题，减少修复代价，提高效能。\u003c/p\u003e\n\u003ch1 id=\"四代码评审的形式\"\u003e四、代码评审的形式\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e多人讨论\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e组织会议，研发牵头讲解代码，架构和测试参与，讨论交流。这是最普遍的一种形式。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCode Diff\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e查看Code Diff，可以借助Gitlab或IDEA，比较分支差异或版本差异。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b9ae62348bf4455c93d6a0e2f42f8bd8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e对比时机：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e提测前和测试中，自行走查代码\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e发现缺陷，定位代码原因\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e修复缺陷后，评估影响范围\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e上线前，是否夹带代码\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e精准测试\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e评估测试用例的代码覆盖率，查漏补缺，Jacoco的on-the-fly模式支持动态收集代码覆盖率数据。\u003c/p\u003e\n\u003ch1 id=\"五代码评审的方法\"\u003e五、代码评审的方法\u003c/h1\u003e\n\u003cp\u003e面向业务，面向业务，面向业务。重要的事情说三遍。\u003c/p\u003e\n\u003cp\u003e刚开始做代码评审，很容易把注意力集中在找代码规范问题上面，比如命名不规范、注释不清楚、代码实现冗长等。这些问题不是测试人员关注的重点，需要由研发团队或代码扫描工具来解决。\u003c/p\u003e\n\u003cp\u003e在做Code Diff时，也没必要把每个文件、每行代码的意思搞懂，比如研发对代码结构做了调整，在diff时要梳理清楚的话，ROI会非常低，因为既消耗时间，又发现不了问题。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e那该怎么做代码评审呢？\u003c/strong\u003e 关注业务：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e跟需求文档比较，哪些需求是遗漏的，哪些代码是补充的，哪些代码是夹带的\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e关注核心业务代码逻辑，使用条件覆盖、路径覆盖等方法设计测试用例\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e优化测试用例，针对代码实现考虑异常、边界、幂等、并发等场景\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e代码评审要求测试人员具备代码能力，理解编程语言，掌握软件设计，熟悉代码结构和架构，多与开发同学交流，共同优化代码质量。\u003c/p\u003e\n\u003ch1 id=\"六代码评审的实际案例\"\u003e六、代码评审的实际案例\u003c/h1\u003e\n\u003cp\u003e1、空指针异常\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.mybatis.spring.SqlSessionTemplate\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.springframework.beans.factory.annotation.Autowired\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.springframework.stereotype.Service\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@Service\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eUserService\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eSqlSessionTemplate\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esqlSessionTemplate\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Autowired\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eUserService\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eSqlSessionTemplate\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esqlSessionTemplate\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethis\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esqlSessionTemplate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esqlSessionTemplate\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetUserById\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003euserId\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eUser\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003euser\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003esqlSessionTemplate\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eselectOne\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;com.example.mapper.UserMapper.getUserById\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003euserId\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eSystem\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eout\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eprintln\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003euser\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetName\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e());\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 可能导致空指针异常\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e如果取到的 \u003ccode\u003euser\u003c/code\u003e 对象为空，就会导致空指针异常。\u003c/p\u003e","title":"代码评审，揭示黑盒背后的真相"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n多线程，通道，读写锁（单写多读），随机（洗牌），是本文涉及的主要知识点。\n先看一下做出来的效果，因为是实验程序，跟真实的斗地主还是有差距，理解万岁！\n[发牌员]：洗牌咯。 刷刷刷... [发牌员]：牌洗好了。 [发牌员]：开始发牌。 [发牌员]：每个人17张牌。 [发牌员]：抢地主。 [fang]：哈哈，我是地主！ fang的牌是[♣9 ♦9 ♥A ♠9 ♣6 ♣5 ♦3 ♣10 ♥5 ♣8 ♠Q ♠A ♠8 ♦4 ♥4 ♦K ♥7 ♣A ♠K ♥3]，共20张。 dong的牌是[大王 ♦8 ♠5 小王 ♠6 ♣Q ♠10 ♣7 ♠3 ♦A ♦Q ♥J ♣K ♥6 ♥9 ♥Q ♣2]，共17张。 er的牌是[♣A ♠K ♥3 ♥2 ♠4 ♦2 ♦5 ♥K ♦10 ♠2 ♥8 ♦6 ♣4 ♦J ♣3 ♣J ♠7]，共17张。 [fang]：我开始出牌了。 [er]：我开始出牌了。 [dong]：我开始出牌了。 赢家是er。 基本流程是洗牌-\u0026gt;发牌-\u0026gt;抢地主-\u0026gt;打牌-\u0026gt;gg。\n哈哈这个程序的精髓是，由于时(lan)间(de)有(xie)限(le)，打牌是哪个线程抢到了就出牌，直到牌出完了，就赢了。（多线程写斗地主，是我大学操作系统课程的实验项目，当时是完整实现了斗地主算法的，用的是C++和MFC，可以在界面上交互打牌）\n边看代码变讲。\n主函数\nfunc main() { // 洗牌 cards := shuffle() // 发牌 dealCards := deal(cards) // 抢地主 fmt.Println(\u0026#34;[发牌员]：抢地主。\u0026#34;) go player(order[0], dealCards[0]) go player(order[1], dealCards[1]) go player(order[2], dealCards[2]) // Winner winner := \u0026lt;-winner fmt.Printf(\u0026#34;赢家是%s。\\n\u0026#34;, winner) } 解析：\n1.main里面是打牌的步骤，洗牌，发牌，抢地主，打牌，gg。 2.用go player()，开了3个线程，也就是3个玩家。 3.发牌的时候，是留了3张底牌的，存在通道“bottom”里面，抢地主的时候，3个线程就去取，谁先取到谁就是地主。 4.打牌打到最后，会往另外一个通道“winner”里面写值，谁先打完，就把自己的name存进去。 5.3个玩家在打牌的时候，main是阻塞的，等待从通道“winner”读取值，有玩家打完了，通道“winner”有值了，就激活。 洗牌函数\nfunc shuffle() []string { fmt.Println(\u0026#34;[发牌员]：洗牌咯。\u0026#34;) fmt.Println(\u0026#34;刷刷刷...\u0026#34;) cards := cards() rand.Seed(time.Now().UnixNano()) rand.Shuffle(len(cards), func(i, j int) { cards[i], cards[j] = cards[j], cards[i] }) fmt.Println(\u0026#34;[发牌员]：牌洗好了。\u0026#34;) return cards } 解析：\n1.rand默认是假的随机，因为不管运行多少次都是一样的，需要设置种子，time.Now().UnixNano()，让每次随机结果都不同。 2.rand.Shuffle()洗牌，随机交换2个牌的位置。 发牌函数\nfunc deal(cards []string) [][]string { fmt.Println(\u0026#34;[发牌员]：开始发牌。\u0026#34;) var dealCards [][]string dealCards = append(dealCards, cards[0:17]) dealCards = append(dealCards, cards[17:34]) dealCards = append(dealCards, cards[34:51]) fmt.Println(\u0026#34;[发牌员]：每个人17张牌。\u0026#34;) go leaveBottom(cards[51:54]) return dealCards } 解析：\n1.因为已经洗了牌了，直接先切3份牌出来，每份17张。 2.留了3张底牌，放到通道“bottom”中。 3.如果这里不再开线程，会发生死锁！因为main本身也是个线程，直接存通道的话，会把main阻塞，直到有线程把通道的值读出去；而main阻塞后，是无法继续执行后面的代码的，也就无法再起3个玩家线程来读值了，就会发生死锁。 4.所以leaveBottom()起了一个单独的线程。 Desk牌桌\ntype Desk struct { mutex sync.RWMutex playCards []string } func (d *Desk) write(card string) { d.mutex.Lock() defer d.mutex.Unlock() d.playCards = append(d.playCards, card) } func (d *Desk) read() []string { d.mutex.RLock() defer d.mutex.RUnlock() return d.playCards } 解析：\n1.定义了结构Desk，包括读写锁和牌桌上打的牌。 2.定义了write()和read()2个函数，3个线程可以同时读，但只能一次写，也就是单写多读锁。 player函数\nfunc player(name string, hands []string) { landlord := \u0026lt;-bottom if len(landlord) \u0026gt; 0 { fmt.Printf(\u0026#34;[%s]：哈哈，我是地主！\\n\u0026#34;, name) hands = append(hands, landlord...) desk.write(name) } fmt.Printf(\u0026#34;%s的牌是%s，共%d张。\\n\u0026#34;, name, hands, len(hands)) time.Sleep(time.Second) i := 0 for true { playCards := desk.read() if playCards[len(playCards)-1] == name { if i == 1 { fmt.Printf(\u0026#34;[%s]：我开始出牌了。\\n\u0026#34;, name) } desk.write(hands[i]) desk.write(order[(getOrderID(name)+1)%3]) i += 1 if i == len(hands) { winner \u0026lt;- name break } } } } 解析：\n1.玩家函数，第一个参数是名字，第二个参数是手上拿的牌。 2.3个线程都有这样一段代码。 3.首先从通道“bottom”读取值，也就是抢地主。 4.抢到地主的玩家，会把底牌放到自己的手牌中，并且把自己的名字写到牌桌上（根据名字来看该谁出牌），地主先出牌。 5.for true {}循环不停的出牌，从第一张到最后一张，先从牌桌上看是不是自己的名字，是自己的名字才轮到出牌。 6.牌出完了，就把自己的名字写到通道“winner”，游戏结束。 本文的程序只是为了实验go的多线程特性，不具备可玩性，期待更多的同学请见谅。\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/golang/000002-golang%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%AE%80%E5%8D%95%E6%96%97%E5%9C%B0%E4%B8%BB/","summary":"多线程，通道，读写锁（单写多读），随机（洗牌），是本文涉及的主要知识点。","title":"多线程简单斗地主"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPairwise算法是什么 对于以下测试场景：\n浏览器：M，O，P 操作平台：W（windows），L（linux），i（ios） 语言：C（chinese），E（english） 该如何设计功能测试用例呢？\n根据数学统计分析，73%的缺陷（单因子是35%，双因子是38%）是由单因子或2个因子相互作用产生的。19%的缺陷是由3个因子相互作用产生的。也就是说，大多数的bug都是条件的两两组合造成的。\nPairwise算法是L. L. Thurstone在1927年首先提出来的，他是美国的一位心理统计学家。Pairwise算法基于两两组合，过滤出性价比高的用例集。它的思路是：如果某一组用例的两两组合结果，在其他组合中均出现，就删除该组用例，从而精简用例。\n对于上述测试场景，可以通过笛卡尔积设计18条两两组合的测试用例：\n1，M W C 2，M W E 3，M L C 4，M L E 5，M I C 6，M I E 7，O W C 8，O W E 9，O L C 10，O L E 11，O I C 12，O I E 13，P W C 14，P W E 15，P L C 16，P L E 17，P I C 18，P I E 对于第18条用例P I E来说，两两组合是PI ，PE ，IE，PI在17号，PE在16号，IE在12号出现过，所以第18条用例可以过滤掉。按照这个算法继续过滤，最终剩下9条用例：\n1，M W C 4，M L E 6，M I E 7，O W E 9，O L C 11，O I C 14，P W E 15，P L C 17，P I C **用例减少了50%！**而且维度越多越明显，当有10个维度的时候4*4*4*4*3*3*3*2*2*2=55296个测试case，pairwise为24个，是原始测试用例规模的0.04%。\nPython实现 源码已上传：https://github.com/dongfanger/python-tools/blob/main/pairwise.py\n#!/usr/bin/python ### encoding=utf-8 \u0026#34;\u0026#34;\u0026#34; @Author : Don @Date : 2021/11/03 20:34 @Desc : \u0026#34;\u0026#34;\u0026#34; import copy import itertools from sys import stdout from loguru import logger def parewise(option): \u0026#34;\u0026#34;\u0026#34;pairwise算法\u0026#34;\u0026#34;\u0026#34; cp = [] # 笛卡尔积 s = [] # 两两拆分 for x in eval(\u0026#39;itertools.product\u0026#39; + str(tuple(option))): cp.append(x) s.append([i for i in itertools.combinations(x, 2)]) logger.info(\u0026#39;笛卡尔积:%s\u0026#39; % len(cp)) del_row = [] bar(0) s2 = copy.deepcopy(s) for i in range(len(s)): # 对每行用例进行匹配 if (i % 100) == 0 or i == len(s) - 1: bar(int(100 * i / (len(s) - 1))) t = 0 for j in range(len(s[i])): # 对每行用例的两两拆分进行判断，是否出现在其他行 flag = False for i2 in [x for x in range(len(s2)) if s2[x] != s[i]]: # 找同一列 if s[i][j] == s2[i2][j]: t = t + 1 flag = True break if not flag: # 同一列没找到，不用找剩余列了 break if t == len(s[i]): del_row.append(i) s2.remove(s[i]) res = [cp[i] for i in range(len(cp)) if i not in del_row] logger.info(\u0026#39;过滤后:%s\u0026#39; % len(res)) return res def bar(i): \u0026#34;\u0026#34;\u0026#34;进度条\u0026#34;\u0026#34;\u0026#34; c = int(i / 10) jd = \u0026#39;\\r %2d%% [%s%s]\u0026#39; a = \u0026#39;■\u0026#39; * c b = \u0026#39;□\u0026#39; * (10 - c) msg = jd % (i, a, b) stdout.write(msg) stdout.flush() if __name__ == \u0026#39;__main__\u0026#39;: pl = [[\u0026#39;M\u0026#39;, \u0026#39;O\u0026#39;, \u0026#39;P\u0026#39;], [\u0026#39;W\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;I\u0026#39;], [\u0026#39;C\u0026#39;, \u0026#39;E\u0026#39;]] a = parewise(pl) print() for i in a: print(i) 输出结果：\n100% [■■■■■■■■■■] (\u0026#39;M\u0026#39;, \u0026#39;W\u0026#39;, \u0026#39;E\u0026#39;) (\u0026#39;M\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;E\u0026#39;) (\u0026#39;M\u0026#39;, \u0026#39;I\u0026#39;, \u0026#39;C\u0026#39;) (\u0026#39;O\u0026#39;, \u0026#39;W\u0026#39;, \u0026#39;E\u0026#39;) (\u0026#39;O\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;E\u0026#39;) (\u0026#39;O\u0026#39;, \u0026#39;I\u0026#39;, \u0026#39;C\u0026#39;) (\u0026#39;P\u0026#39;, \u0026#39;W\u0026#39;, \u0026#39;C\u0026#39;) (\u0026#39;P\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;C\u0026#39;) (\u0026#39;P\u0026#39;, \u0026#39;I\u0026#39;, \u0026#39;E\u0026#39;) 2021-11-07 11:38:56.850 | INFO | __main__:parewise:24 - 笛卡尔积:18 2021-11-07 11:38:56.850 | INFO | __main__:parewise:45 - 过滤后:9 参考资料：\nhttps://blog.csdn.net/ztf312/article/details/78792906\nhttps://www.cnblogs.com/df888/p/11747616.html\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/%E5%B0%8F%E5%B7%A5%E5%85%B7/000002-%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7%E5%8A%9F%E8%83%BD%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90%E7%AE%97%E6%B3%95pairwise%E5%B7%A5%E5%85%B7/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"pairwise算法是什么\"\u003ePairwise算法是什么\u003c/h2\u003e\n\u003cp\u003e对于以下测试场景：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e浏览器：M，O，P\u003c/li\u003e\n\u003cli\u003e操作平台：W（windows），L（linux），i（ios）\u003c/li\u003e\n\u003cli\u003e语言：C（chinese），E（english）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e该如何设计功能测试用例呢？\u003c/p\u003e\n\u003cp\u003e根据数学统计分析，73%的缺陷（单因子是35%，双因子是38%）是由单因子或2个因子相互作用产生的。19%的缺陷是由3个因子相互作用产生的。也就是说，\u003cstrong\u003e大多数的bug都是条件的两两组合造成的\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003ePairwise算法是L. L. Thurstone在1927年首先提出来的，他是美国的一位心理统计学家。Pairwise算法基于两两组合，过滤出性价比高的用例集。它的思路是：\u003cstrong\u003e如果某一组用例的两两组合结果，在其他组合中均出现，就删除该组用例，从而精简用例\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e对于上述测试场景，可以通过笛卡尔积设计18条两两组合的测试用例：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e1，M W C\n2，M W E\n3，M L C\n4，M L E\n5，M I C\n6，M I E\n7，O W C\n8，O W E\n9，O L C\n10，O L E\n11，O I C\n12，O I E\n13，P W C\n14，P W E\n15，P L C\n16，P L E\n17，P I C\n18，P I E\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e对于第18条用例\u003ccode\u003eP I E\u003c/code\u003e来说，两两组合是\u003ccode\u003ePI ，PE ，IE\u003c/code\u003e，\u003ccode\u003ePI\u003c/code\u003e在17号，\u003ccode\u003ePE\u003c/code\u003e在16号，\u003ccode\u003eIE\u003c/code\u003e在12号出现过，所以第18条用例可以过滤掉。按照这个算法继续过滤，最终剩下9条用例：\u003c/p\u003e","title":"功能测试用例自动生成算法Pairwise工具"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n不管测试平台有没有实际用处，具备测试平台开发能力，是测试开发工程师的必备技能之一。 测试平台的实际收益有多少，仁者见仁智者见智，这是一个辩证问题。测试平台开发能力是否具备，一清二楚，这是一个现实问题。我们能做的只有提高自己，解决现实问题。\n测试平台要解决的三大痛点是，一、管理能力：将成千上万的自动化用例管理起来；二、共享能力：将自己的用例轻松的共享给别人使用；三、降低上手难度：让不会代码的人也能编写自动化。按照用途来分，又分为两类：用例管理平台、低代码平台。用例管理平台是将本地代码编写的自动化用例，通过测试平台来进行管理，编排，执行。低代码平台是用例编写完全在平台上进行，不需要写代码，录入关键信息就可以做自动化。\n坚持开发原创测试平台，是一件长期受益的事。 我使用Vue+Django开发过一个测试平台，用途是用例管理，叫做teprunner。在我的电子书有完整的文章教程，在我的B站有完整的视频教程，在我的GitHub有详细的带注释的项目源码。开发平台对我来说，最大的收获就是，很多人从这个平台认识了我，关注了我。也让我在换工作时，从测试工程师，转变为了测试开发工程师。而在工作中，更获得了参与部门工具平台建设的机会。\n我正在开发4个测试平台：\n测试平台基础结构，登录、首页、菜单，使用Vue+SpringBoot实现；\nhttps://gitee.com/dongfanger/platform\n测试平台基础结构，登录、首页、菜单，使用Vue+FastAPI实现；\nhttps://gitee.com/dongfanger/platform-fastapi\n基于Pytest的接口测试平台，teprunner翻新版，EasyPytest，使用Vue+FastAPI实现；\nhttps://gitee.com/dongfanger/easy-pytest\n基于JMeter的接口测试平台，秘密研发中；\n假如你还没入门，那么可以从teprunner入手，文章+视频+源码，适合自主学习和动手能力强的同学。也可以看看测试平台基础结构，前端Vue结构相对简单，后端既有SpringBoot也有FastAPI。\n假如你具备代码能力，但仅限于用Python写小工具，那么可以尝试复刻一个teprunner，从0到1开发测试平台。如何开始？遇到问题怎么解决？需要指导？我开设了一个辅导班，内容包括定制化的个人开发计划，至少每月一次的技术分享，一对一的代码指导等等。报名开发者66元/人，报名围观学习者15元/人。\n假如你有平台化经验，想找个项目练练手，那么可以参与EasyPytest众创开发，一起完成一个更强大的基于Pytest的接口测试平台。我开设了一个开发班，内容包括各阶段的探讨会议，一对一的代码优化，开发过程中的答疑等等。报名开发者35元/人，报名围观学习者15元/人。跟teprunner辅导班的区别在于，开发者要求能够独立编写代码。\n关于测试平台的选择问题，一是如何选择编程语言，我的建议是选Python，只有你具备较强的代码能力以后，再考虑Java。二是如何选择后端框架，我的建议是选Django、Flask、FastAPI任意一个，只要你会用1个框架完成测试平台开发，那么即使再学SpringBoot也能很快上手。\n添加微信cekaigang了解更多，开发原创测试平台，坚持，坚持，再坚持，努力不一定成功，但是一定会进步。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/japi/000002-%E5%9D%9A%E6%8C%81%E5%BC%80%E5%8F%91%E5%8E%9F%E5%88%9B%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0/","summary":"\u003cstrong\u003e不管测试平台有没有实际用处，具备测试平台开发能力，是测试开发工程师的必备技能之一。\u003c/strong\u003e 测试平台的实际收益有多少，仁者见仁智者见智，这是一个辩证问题。测试平台开发能力是否具备，一清二楚，这是一个现实问题。我们能做的只有提高自己，解决现实问题。","title":"坚持开发原创测试平台"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n错误码不分层的接口，你只能靠 msg 断言，改个文案用例就跑失败。返回快照的字段返回了实时，数据就无法核对上。\n接口设计得好不好，一个重要的判断标准是：验证它麻不麻烦，也就是可测试性。\n接口设计的可测试性 决策1：URL 是名词还是动词？ RESTful 到底在解决什么？我们知道GET/POST/PUT/DELETE对应查/增/改/删，它有用的地方在于，URL 描述资源、方法描述动作，你能猜出接口长什么样。\n# 不好：每个接口一个动词，URL 无规律，猜不出来 \u0026#34;/createOrder\u0026#34;, \u0026#34;/orderCancel\u0026#34;, \u0026#34;/getOrderInfo\u0026#34; # 好：资源 + 动作，规律可预测 \u0026#34;POST /orders\u0026#34; # 创建 \u0026#34;POST /orders/{id}/cancel\u0026#34; # 业务动作，允许不严格 REST \u0026#34;GET /orders/{id}\u0026#34; # 查询 什么时候违反？业务动作不是CRUD：取消订单、申请退款，写成POST /orders/{id}/cancel比硬套PUT /orders/{id}更友好。\n测试视角：URL 有规律，你的自动化脚本就能参数化、能批量生成用例、新人接手也能快速明白。\n决策2：错误码 三类错误必须分开：\n最坑的是什么都返回200：\n# HTTP 状态码是 200，但 body 说失败了 —— 到底算成功还是失败？ return 200, {\u0026#34;code\u0026#34;: 500, \u0026#34;msg\u0026#34;: \u0026#34;系统繁忙\u0026#34;} 后果：网关认为成功不重试、监控认为健康不告警、前端不知道该不该重试。你的用例也会误判，明明失败了，断言 HTTP 200 却过了。\n三种错误的返回对比：\n# 参数错：调用方的锅 return 400, {\u0026#34;code\u0026#34;: \u0026#34;INVALID_PARAM\u0026#34;, \u0026#34;msg\u0026#34;: \u0026#34;count 必须大于 0\u0026#34;} # 业务拒绝：传对了，但业务不允许 —— HTTP 仍是 200 return 200, {\u0026#34;code\u0026#34;: \u0026#34;STOCK_NOT_ENOUGH\u0026#34;, \u0026#34;msg\u0026#34;: \u0026#34;库存不足\u0026#34;} # 系统错：我的锅，你得重试 return 500, {\u0026#34;code\u0026#34;: \u0026#34;INTERNAL_ERROR\u0026#34;, \u0026#34;msg\u0026#34;: \u0026#34;服务异常\u0026#34;} 测试视角：断言 code，永远别断言 msg。更重要的是：这些失败场景我怎么造？\n库存不足怎么造？（把库存改成 0？有没有隐藏库存？） 券过期怎么造？（能改有效期吗？还是要等它自然过期？） 支付失败怎么造？（有没有挡板？要不要真实小额支付？） 决策3：快照 vs 实时 判断依据：这个值变了算不算问题？变了算问题→快照（价格、商品名、地址）；变了才正常→实时（库存、状态）。\n有个常见的坑：应该快照的返回了实时。比如运营改价 99→89，历史订单详情页重新查商品价格→显示 89，但用户实付 99→账对不上。\n{ \u0026#34;order_id\u0026#34;: \u0026#34;20260914001\u0026#34;, \u0026#34;sku_name\u0026#34;: \u0026#34;xxx 商品\u0026#34;, # 快照：下单时的名字，之后改名不影响 \u0026#34;price\u0026#34;: 99.00, # 快照：下单时的价格，之后改价不影响 \u0026#34;stock_left\u0026#34;: 5, # 实时：当前剩余库存 \u0026#34;status\u0026#34;: \u0026#34;WAIT_PAY\u0026#34; # 实时：当前状态 } 测试视角：这个字段源头改了之后，该跟着变还是不变，最好一开始就跟研发确认清楚。不只是价格，收货地址（用户改了地址，历史订单不能跟着变）、商品名（下架改名后订单还得能看）、优惠券信息（券删了订单还得显示用了什么券）都要快照。\n决策4：版本兼容 结论：加字段尽量兼容；加枚举值要按破坏性变更处理；删/改必须走版本。\nRPC接口还要注意序列化问题，新增字段放在最后追加，如果放中间可能序列化报错。\n例子：加个枚举值后影响历史功能：\n# v1 订单状态：WAIT_PAY / PAID / DONE # v2 加了：\u0026#34;PARTIAL_REFUNDED\u0026#34; 部分退款 # 老 App 的写法：不是 DONE 就显示\u0026#34;待处理\u0026#34; if status != \u0026#34;DONE\u0026#34;: show(\u0026#34;待处理\u0026#34;) # 结果：部分退款的订单，老 App 一直显示\u0026#34;待处理\u0026#34; → 用户投诉 测试视角：兼容性用例要按版本号驱动——你的自动化里必须有\u0026quot;老版本 App + 新接口\u0026quot;这一组。\n决策5：幂等 服务端不该猜\u0026quot;这两个请求是不是同一个\u0026quot;（靠参数比对、靠时间窗都不靠谱），得客户端明确传一个幂等标识。\n# 客户端生成，全局唯一，同一次操作重试用同一个 {\u0026#34;request_id\u0026#34;: \u0026#34;a1b2c3-xxxx\u0026#34;, \u0026#34;sku_id\u0026#34;: 1001, \u0026#34;count\u0026#34;: 2} 服务端拿 request_id 做去重键，第一次处理，之后直接返回首次结果。第二次返回必须跟第一次一模一样，具体做法在第3篇介绍。\n测试视角：有了幂等标识，你才能可靠地造重复请求——把同一个 request_id 发两次就是重试，换一个就是新请求。\n二、汇总表 三、结尾 以后接手一个接口，再搞懂3个问题：\n它的错误码，能让我一眼分清\u0026quot;我传错了 / 业务不允许 / 服务挂了\u0026quot;吗？ 返回里哪些是快照、哪些是实时？ 我能不能造出它的失败场景？ 下一篇介绍幂等。\u0026ldquo;重复\u0026quot;和\u0026quot;并发\u0026quot;是两回事，大多数所谓的幂等，只防得住重复，防不住并发。\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%90%8E%E7%AB%AF%E6%B5%8B%E8%AF%95/000002-%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%8F%AF%E6%B5%8B%E8%AF%95%E6%80%A7/","summary":"错误码不分层的接口，你只能靠 msg 断言，改个文案用例就跑失败。返回快照的字段返回了实时，数据就无法核对上。","title":"接口设计的可测试性"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n选择技术栈。Python、Java、Go三选一。能选Python就选Python。只会Java或只会Go才选Java Go。Python语法简单，上手容易，有一整套成熟的接口自动化测试方案，社区完善且持续迭代更新。\n选择编写方式。Python代码、Robot、YAML、Excel、JSON五选一。尽量选择Python代码或Robot，可选YAML，不要选择Excel、JSON。不管选择哪种都需要学习语法，学习Python语法是最通用的，Python学起来很快，代码写起来也简单。Robot是RobotFramework的源文件格式，自带解释器，关键字驱动写法。YAML是HttpRuner的源文件格式，优点是不需要懂代码，缺点也很明显，功能受限和难以调试。Excel在自动化早期流传甚广，也许只有培训机构还在教。JSON冗余内容太多，不适合直接拿来写用例。\n选择框架。pytest、RobotFramework、HttpRunner三选一。优先pytest，其次RobotFramework，最后HttpRunner。网上能搜到很多基于pytest搭建的接口自动化测试框架，学习资料丰富，无需多言，使用pytest是绝对正确的。RobotFramework是关键字驱动框架，很成熟，在特定领域适用比如硬件，设计框架多看看。如果完全不想接触代码，倒也可以试试HttpRuner，源码易懂，可以二次开发。\n还有2个隐藏因素，一是测试人员能力，作为发起者，要有觉悟，这个框架80%都需要你来搭建和维护，其他测试人员能力可以考虑，但不是重点，最重要的还是你用起来顺手。选好框架后立刻动手，做出一定成果后找机会汇报，至于能不能推广，心态随缘，效果好加上领导支持，星星之火可以燎原。二是个人成长，把“独立搭建接口自动化测试框架”或“基于开源框架二次开发”作为目标，以终为始，熟悉成熟框架，学习他人框架，设计实用框架。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000002-%E6%8E%A5%E5%8F%A3%E8%87%AA%E5%8A%A8%E5%8C%96%E6%A1%86%E6%9E%B6%E9%80%89%E5%9E%8B/","summary":"选择技术栈。Python、Java、Go三选一。能选Python就选Python。只会Java或只会Go才选Java Go。Python语法简单，上手容易，有一整套成熟的接口自动化测试方案，社区完善且持续迭代更新。","title":"接口自动化框架选型"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n创建SpringBoot项目最便捷的方式一是在https://start.spring.io/根据步骤选择后，下载项目工程压缩包：\n方式二是在IDEA旗舰版中根据步骤选择后，直接创建项目：\n本文则会根据官方文档，介绍一种最原始的方式，一个一个文件来开发一个SpringBoot应用。\n在开始介绍之前，先执行以下命令检查Java和Maven版本正确：\n$ java -version java version \u0026#34;1.8.0_102\u0026#34; Java(TM) SE Runtime Environment (build 1.8.0_102-b14) Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode) $ mvn -v Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00) Maven home: /usr/local/Cellar/maven/3.3.9/libexec Java version: 1.8.0_102, vendor: Oracle Corporation 创建POM 新建一个Maven的pom.xml文件：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;myproject\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.5.6\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;!-- Additional lines to be added here... --\u0026gt; \u0026lt;/project\u0026gt; 然后执行命令mvn package，测试一下，出现BUILD SUCCESS就说明环境已经OK了，SpringBoot也已经下载到了本地仓库中。\n添加Classpath依赖 SpringBoot提供了很多starter，用来把jar包添加到classpath中。前面使用的spring-boot-starter-parent是一个特殊的starter，必须先添加这个starter才能通过Maven把其他starter的依赖管理起来。\n可以使用以下命令查看依赖树：\n$ mvn dependency:tree [INFO] com.example:myproject:jar:0.0.1-SNAPSHOT 可以看出来spring-boot-starter-parent没有提供任何依赖，所以继续添加另外一个starter：\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 再查看依赖树，就会发现多了很多：\n既包括了SpringBoot依赖，也包括了Tomcat等附加依赖。\n编写代码 新建目录和文件src/main/java/MyApplication.java，添加代码：\nimport org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @EnableAutoConfiguration public class MyApplication { @RequestMapping(\u0026#34;/\u0026#34;) String home() { return \u0026#34;Hello World!\u0026#34;; } public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } 注解是SpringBoot相比于其他框架来说，我个人认为最牛逼的设计之一。\n@RestController注解表示RESTful风格，直接返回结果。 @RequestMapping注解路由HTTP请求，这里将/路径映射到home方法。 @EnableAutoConfiguration根据添加的依赖自动配置SpringBoot。 启动服务 确保已经配置好了Java环境变量，然后执行命令mvn spring-boot:run，就能看到服务启起来了：\n$ mvn spring-boot:run . ____ _ __ _ _ /\\\\ / ___\u0026#39;_ __ _ _(_)_ __ __ _ \\ \\ \\ \\ ( ( )\\___ | \u0026#39;_ | \u0026#39;_| | \u0026#39;_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) \u0026#39; |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.6) ....... . . . ....... . . . (log output here) ....... . . . ........ Started MyApplication in 2.222 seconds (JVM running for 6.514) 访问localhost:8080，页面返回：\nHello World! 打个可执行Jar包 要上生产环境，需要把代码打成一个可执行的jar包，里面包含了编译好的类，以及所有的依赖。\n先在pom.xml中添加maven插件：\n\u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 然后执行命令mvn package：\n$ mvn package [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building myproject 0.0.1-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] .... .. [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject --- [INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar [INFO] [INFO] --- spring-boot-maven-plugin:2.5.6:repackage (default) @ myproject --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ 在target目录下jar包就打好了：\n执行命令java -jar myproject-0.0.1-SNAPSHOT.jar，也能启动服务了。\n参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#getting-started.first-application\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000002-spring%E5%BC%80%E5%8F%91%E4%BD%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AAspringboot%E5%BA%94%E7%94%A8/","summary":"创建SpringBoot项目最便捷的\u003cstrong\u003e方式一\u003c/strong\u003e是在https://start.spring.io/根据步骤选择后，下载项目工程压缩包：","title":"开发你的第一个SpringBoot应用"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n如何评价接口自动化项目 我理想中的接口自动化项目，是有一套脚本，能够100%覆盖核心业务场景，能够回归线上频繁出现的问题，能够支持手工无法验证的场景，能够抛出偶现问题，能够包含容易遗漏的边界场景、异常场景，每天都能定时运行，出具报告，给予反馈。\n用实际的指标量化下：\n功能测试自动化率 = 自动化测试用例数/总测试用例数\n自动化发现bug占比 = 自动化发现的bug/总bug数\n自动化发现bug有效性 = 自动化发现的有效bug/自动化提交的bug总数\n自动化执行成功率 = 自动化执行成功的次数/自动化执行总次数\n周活跃自动化用例占比 = (∑每周执行过的自动化测试用例)/(∑周维度自动化测试用例)\n应用维度自动化代码覆盖率 = 自动化用例覆盖代码行数/应用维度代码总行数\n现状是什么(Situation) 我负责的业务是发票中台，数据流简化后如下图所示：\n发票中台对调用方提供RPC服务，底层有读写MySQL、同步ES、消息队列、异步任务等服务，上游是用户和订单，需要账号、增票资质、订单等物料；下游是财务，推送申请单给财务开具发票等。\n发票的接口自动化在多年前是写了的，用Java+TestNG+RPC本地化了一套框架来实现，因为人员离职和业务变更，这套自动化到我手上的时候，已经不能用了，本地化的接口自动化项目也无法做数据度量，只有接口测试平台写的自动化用例才会被统计到。\n任务是什么(Task) 我给自己定的绩效是在Q2完成95条自动化用例，按照一个标准化的自动化项目来实现发票的接口自动化，结合测试环境、测试物料、流水线、精准分析，打造一个个人的稳定的沉淀物，无论是晋升还是求职，都能拿来讲点故事。\n行动是什么(Action) 编写自动化用例的方式 1、最理想的方式，是能够录制线上流量直接转换为自动化用例，修修改改即可。虽然我们有流量录制回放平台，但是它跟接口自动化平台并没有打通，很可惜不能采取这种方式。\n2、参考线上流量日志，抓取接口、入参，人工转化为自动化用例。\n3、参考接口的代码设计，比如controller/provider/sdk都定义了应用接口，能知道哪些覆盖到了，哪些还需要补充。\n4、根据入参模板自行构造参数。参数个数太多的话造起来很麻烦。\n我主要采用第2种和第3种方式结合来编写自动化用例。\n典型自动化用例 前置步骤：初始化数据，将订单发票类型更新为3。\n预设变量：提供参数化数据。\n执行步骤：调用修改订单发票类型接口，将发票类型3改为2，入参引用预设变量。\n财务接口：依赖测试物料无法满足，通过Mock来返回模板数据，让接口继续往下走。\n基础断言：接口响应状态码200。\n业务断言：调用订单列表查询接口，用JSONPath提取响应中的发票类型，断言已经改成了2。\n场景用例 数据准备：通过物料工具造订单数据，写入MySQL数据库中。\n更新MySQL数据：更新订单的部分字段数据，触发binlake，将数据同步至ES，因为提交开票申请是读取的ES数据；从预设变量获取orderId。\n提交开票申请：从ES读取订单数据，提交开票申请；从预设变量获取orderId和userId。\n查询申请单号：根据订单号查询申请单号；从预设变量获取orderId和userId。用JSONPath提取申请单号，存入动态变量中。\n推送财务：把申请单号推送给财务；从动态变量获取applyId。\n覆盖更多代码 ①看代码：\n在Java工程里面的Controller/Provider找到所有接口，分析可以用来做自动化的接口，设计自动化用例进行覆盖。\n②代码覆盖率：\n借助于jacoco，在执行自动化用例后，把代码覆盖率跑出来，分析没覆盖的代码，补充更多正常/异常/边界的自动化用例。也就是精准测试。\n自动化巡检 每天早上8点，定时任务执行用例集；发送邮件，抄送测试组、开发组、领导，让测试、开发和领导都知道自动化建设情况；如果出现问题，一定要在当天解决，并邮件回复问题原因；确保当天的自动化用例结果都是正确的；第二天接着继续巡检。\n流水线 在持续集成中，引入自动化用例环节，把自动化用例跑通作为提测卡点，不通过可以驳回。\n产出是什么(Result) 定性来看：\n结合测试环境、测试物料、自动化、流水线、精准测试，完成了一个能对环境和业务进行稳定性巡检，能对研发提测进行质量门禁，能对应用进行代码覆盖率分析的接口自动化项目。\n定量来看：\n功能测试自动化率 = 自动化测试用例数/总测试用例数 95/100=95% 自动化发现bug占比 = 自动化发现的bug/总bug数 2/53=3.7% 自动化发现bug有效性 = 自动化发现的有效bug/自动化提交的bug总数 2/2=100% 自动化执行成功率 = 自动化执行成功的次数/自动化执行总次数 5396/5621=96% 周活跃自动化用例占比 = (∑每周执行过的自动化测试用例)/(∑周维度自动化测试用例) 77% 应用维度自动化代码覆盖率 = 自动化用例覆盖代码行数/应用维度代码总行数 39% 这个Q主要完成了核心流程和重要接口的自动化用例，还需要持续补充场景用例和异常/边界的自动化用例。\n你理想中的接口自动化项目是什么样的呢？\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E8%87%AA%E5%8A%A8%E5%8C%96/000002-%E7%90%86%E6%83%B3%E4%B8%AD%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%87%AA%E5%8A%A8%E5%8C%96%E9%A1%B9%E7%9B%AE/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"如何评价接口自动化项目\"\u003e如何评价接口自动化项目\u003c/h1\u003e\n\u003cp\u003e我理想中的接口自动化项目，是有一套脚本，能够100%覆盖核心业务场景，能够回归线上频繁出现的问题，能够支持手工无法验证的场景，能够抛出偶现问题，能够包含容易遗漏的边界场景、异常场景，每天都能定时运行，出具报告，给予反馈。\u003c/p\u003e\n\u003cp\u003e用实际的\u003cstrong\u003e指标量化\u003c/strong\u003e下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e功能测试自动化率 = 自动化测试用例数/总测试用例数\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e自动化发现bug占比 = 自动化发现的bug/总bug数\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e自动化发现bug有效性 = 自动化发现的有效bug/自动化提交的bug总数\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e自动化执行成功率 = 自动化执行成功的次数/自动化执行总次数\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e周活跃自动化用例占比 = (∑每周执行过的自动化测试用例)/(∑周维度自动化测试用例)\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e应用维度自动化代码覆盖率 = 自动化用例覆盖代码行数/应用维度代码总行数\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"现状是什么situation\"\u003e现状是什么(Situation)\u003c/h1\u003e\n\u003cp\u003e我负责的业务是发票中台，数据流简化后如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b337c73fa45a4a5b80cc91b13305c987.png\"\u003e\u003c/p\u003e\n\u003cp\u003e发票中台对调用方提供RPC服务，底层有读写MySQL、同步ES、消息队列、异步任务等服务，上游是用户和订单，需要账号、增票资质、订单等物料；下游是财务，推送申请单给财务开具发票等。\u003c/p\u003e\n\u003cp\u003e发票的接口自动化在多年前是写了的，用Java+TestNG+RPC本地化了一套框架来实现，因为人员离职和业务变更，这套自动化到我手上的时候，已经不能用了，本地化的接口自动化项目也无法做数据度量，只有接口测试平台写的自动化用例才会被统计到。\u003c/p\u003e\n\u003ch1 id=\"任务是什么task\"\u003e任务是什么(Task)\u003c/h1\u003e\n\u003cp\u003e我给自己定的绩效是在Q2完成95条自动化用例，按照一个标准化的自动化项目来实现发票的接口自动化，结合测试环境、测试物料、流水线、精准分析，打造一个个人的稳定的沉淀物，无论是晋升还是求职，都能拿来讲点故事。\u003c/p\u003e\n\u003ch1 id=\"行动是什么action\"\u003e行动是什么(Action)\u003c/h1\u003e\n\u003ch2 id=\"编写自动化用例的方式\"\u003e\u003cstrong\u003e编写自动化用例的方式\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e1、最理想的方式，是能够录制线上流量直接转换为自动化用例，修修改改即可。虽然我们有流量录制回放平台，但是它跟接口自动化平台并没有打通，很可惜不能采取这种方式。\u003c/p\u003e\n\u003cp\u003e2、参考线上流量日志，抓取接口、入参，人工转化为自动化用例。\u003c/p\u003e\n\u003cp\u003e3、参考接口的代码设计，比如controller/provider/sdk都定义了应用接口，能知道哪些覆盖到了，哪些还需要补充。\u003c/p\u003e\n\u003cp\u003e4、根据入参模板自行构造参数。参数个数太多的话造起来很麻烦。\u003c/p\u003e\n\u003cp\u003e我主要采用第2种和第3种方式结合来编写自动化用例。\u003c/p\u003e\n\u003ch2 id=\"典型自动化用例\"\u003e\u003cstrong\u003e典型自动化用例\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5f98cecbf9624b92b0947b266bf882e3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e前置步骤：初始化数据，将订单发票类型更新为3。\u003c/p\u003e\n\u003cp\u003e预设变量：提供参数化数据。\u003c/p\u003e\n\u003cp\u003e执行步骤：调用修改订单发票类型接口，将发票类型3改为2，入参引用预设变量。\u003c/p\u003e\n\u003cp\u003e财务接口：依赖测试物料无法满足，通过Mock来返回模板数据，让接口继续往下走。\u003c/p\u003e\n\u003cp\u003e基础断言：接口响应状态码200。\u003c/p\u003e\n\u003cp\u003e业务断言：调用订单列表查询接口，用JSONPath提取响应中的发票类型，断言已经改成了2。\u003c/p\u003e\n\u003ch2 id=\"场景用例\"\u003e场景用例\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/97835629bcf84376b1b076ecc2efe71f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e数据准备：通过物料工具造订单数据，写入MySQL数据库中。\u003c/p\u003e\n\u003cp\u003e更新MySQL数据：更新订单的部分字段数据，触发binlake，将数据同步至ES，因为提交开票申请是读取的ES数据；从预设变量获取orderId。\u003c/p\u003e\n\u003cp\u003e提交开票申请：从ES读取订单数据，提交开票申请；从预设变量获取orderId和userId。\u003c/p\u003e\n\u003cp\u003e查询申请单号：根据订单号查询申请单号；从预设变量获取orderId和userId。用JSONPath提取申请单号，存入动态变量中。\u003c/p\u003e\n\u003cp\u003e推送财务：把申请单号推送给财务；从动态变量获取applyId。\u003c/p\u003e\n\u003ch2 id=\"覆盖更多代码\"\u003e覆盖更多代码\u003c/h2\u003e\n\u003cp\u003e①看代码：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9044fdece03b49cabe47430cce991cc4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在Java工程里面的Controller/Provider找到所有接口，分析可以用来做自动化的接口，设计自动化用例进行覆盖。\u003c/p\u003e\n\u003cp\u003e②代码覆盖率：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/7e8be8a6cb654d5cb6b90ba93382873f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e借助于jacoco，在执行自动化用例后，把代码覆盖率跑出来，分析没覆盖的代码，补充更多\u003cstrong\u003e正常/异常/边界\u003c/strong\u003e的自动化用例。也就是精准测试。\u003c/p\u003e\n\u003ch2 id=\"自动化巡检\"\u003e自动化巡检\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bb44fe887eb3478685555b893a1b483c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e每天早上8点，定时任务执行用例集；发送邮件，抄送测试组、开发组、领导，让测试、开发和领导都知道自动化建设情况；如果出现问题，一定要在当天解决，并邮件回复问题原因；确保当天的自动化用例结果都是正确的；第二天接着继续巡检。\u003c/p\u003e\n\u003ch2 id=\"流水线\"\u003e流水线\u003c/h2\u003e\n\u003cp\u003e在持续集成中，引入自动化用例环节，把自动化用例跑通作为提测卡点，不通过可以驳回。\u003c/p\u003e\n\u003ch1 id=\"产出是什么result\"\u003e产出是什么(Result)\u003c/h1\u003e\n\u003cp\u003e定性来看：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e结合测试环境、测试物料、自动化、流水线、精准测试，完成了一个能对环境和业务进行稳定性巡检，能对研发提测进行质量门禁，能对应用进行代码覆盖率分析的接口自动化项目。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e定量来看：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e功能测试自动化率 = 自动化测试用例数/总测试用例数 \u003cstrong\u003e95/100=95%\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e自动化发现bug占比 = 自动化发现的bug/总bug数 \u003cstrong\u003e2/53=3.7%\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e自动化发现bug有效性 = 自动化发现的有效bug/自动化提交的bug总数 \u003cstrong\u003e2/2=100%\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e自动化执行成功率 = 自动化执行成功的次数/自动化执行总次数 \u003cstrong\u003e5396/5621=96%\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e周活跃自动化用例占比 = (∑每周执行过的自动化测试用例)/(∑周维度自动化测试用例) \u003cstrong\u003e77%\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e应用维度自动化代码覆盖率 = 自动化用例覆盖代码行数/应用维度代码总行数 \u003cstrong\u003e39%\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这个Q主要完成了核心流程和重要接口的自动化用例，还需要持续补充场景用例和异常/边界的自动化用例。\u003c/p\u003e","title":"理想中的接口自动化项目"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n电子书网站由AI实现，Claude Code+DeepSeek-V4-Pro。技术栈Python+MkDocs，主题Read the Docs，静态托管GitHub Pages。具有中文搜索功能。\n源码 https://github.com/dongfanger/ebook-mkdocs\n配置 ├── docs/ # 内容目录(mkdocs 默认,按分类/子分类组织) │ ├── 简介.md # 首页 │ └── _static/ # 静态资源(extra.css / lunr.zh.js / favicon.ico) ├── scripts/ │ ├── gen_nav.py # 扫描 docs 生成 mkdocs.yml │ └── post_build.py # 构建后 hook(中文搜索 + favicon) ├── serve.py # 本地预览入口 ├── deploy.py # 部署入口 └── requirements.txt 网站配置文件是scripts/gen_nav.py。 MkDocs原生配置文件其实是mkdocs.yml，但为了生成导航，mkdocs.yml由gen_nav.py动态创建，所以在gen_nav.py配置。\n完整代码：https://github.com/dongfanger/ebook-mkdocs/blob/main/scripts/gen_nav.py\n代码片段说明：\nROOT = Path(__file__).resolve().parent.parent # 项目根目录 SRC = ROOT / \u0026#34;docs\u0026#34; # 不参与导航的内容目录 SKIP_DIRS = {\u0026#34;_static\u0026#34;, \u0026#34;_images\u0026#34;, \u0026#34;assets\u0026#34;, \u0026#34;.git\u0026#34;, \u0026#34;__pycache__\u0026#34;} # 一级目录顺序(留空 = 按目录名自动扫描排序;想固定顺序就填,如 [\u0026#34;分类A\u0026#34;, \u0026#34;分类B\u0026#34;]) TOP_ORDER = [] # 各一级目录下的二级目录顺序(留空 = 自动扫描;如 {\u0026#34;分类A\u0026#34;: [\u0026#34;子分类1\u0026#34;, \u0026#34;子分类2\u0026#34;]}) SUB_ORDER = {} ... def build_nav(): lines = [\u0026#34;nav:\u0026#34;] # 首页: 优先 简介.md,回退 index.md(兼容旧命名) home = \u0026#34;简介.md\u0026#34; if (SRC / \u0026#34;简介.md\u0026#34;).exists() else \u0026#34;index.md\u0026#34; lines.append(f\u0026#34; - 简介: {home}\u0026#34;) # 根目录下其余 .md(如 关于本书.md)作为顶层导航项,按文件名排序 for md in sorted(SRC.glob(\u0026#34;*.md\u0026#34;)): if md.name == home: continue lines.append(f\u0026#34; - {md.stem}: {md.name}\u0026#34;) # 一级目录按硬编码顺序,兜底补上未列到的目录 for top in list_subdirs(SRC, TOP_ORDER): top_path = SRC / top lines.append(f\u0026#34; - {top}:\u0026#34;) for sub in list_subdirs(top_path, SUB_ORDER.get(top, [])): lines.append(f\u0026#34; - {sub}:\u0026#34;) emit_md_lines(lines, top_path / sub, indent=\u0026#34; \u0026#34;) return \u0026#34;\\n\u0026#34;.join(lines) HEADER = \u0026#34;\u0026#34;\u0026#34;site_name: My Docs site_description: 基于 mkdocs 的技术电子书 theme: name: readthedocs plugins: - search: lang: zh markdown_extensions: - admonition - codehilite - attr_list # 扁平 URL(/xxx.html 而非 /xxx/) use_directory_urls: false # 自定义样式:blockquote 引用块(主题默认无样式) extra_css: - _static/css/extra.css # 中文搜索:构建后用纯 JS 分词器覆盖默认 lunr.zh.js(修复 nodejieba 依赖导致的搜索失效) hooks: - scripts/post_build.py \u0026#34;\u0026#34;\u0026#34; ... Markdown读取目录，默认为docs：\nSRC = ROOT / \u0026#34;docs\u0026#34; 一级目录，可为空，配置后指定顺序：\n# 一级目录顺序(留空 = 按目录名自动扫描排序;想固定顺序就填,如 [\u0026#34;分类A\u0026#34;, \u0026#34;分类B\u0026#34;]) TOP_ORDER = [] 二级目录，可为空，配置后指定顺序：\n# 各一级目录下的二级目录顺序(留空 = 自动扫描;如 {\u0026#34;分类A\u0026#34;: [\u0026#34;子分类1\u0026#34;, \u0026#34;子分类2\u0026#34;]}) SUB_ORDER = {} 首页默认简介.md，可自定义：\n# 首页: 优先 简介.md,回退 index.md(兼容旧命名) home = \u0026#34;简介.md\u0026#34; if (SRC / \u0026#34;简介.md\u0026#34;).exists() else \u0026#34;index.md\u0026#34; lines.append(f\u0026#34; - 简介: {home}\u0026#34;) 网站信息和主题：\nHEADER = \u0026#34;\u0026#34;\u0026#34;site_name: My Docs site_description: 基于 mkdocs 的技术电子书 theme: name: readthedocs 更多主题\nMkDocs预置主题：https://www.mkdocs.org/user-guide/choosing-your-theme/\n自定义主题：https://www.mkdocs.org/user-guide/customizing-your-theme/\n还有其他更多配置可在gen_nav.py的HEADER里添加：\nlocale: zh_CN（中文本地化） navigation_depth（导航深度） sticky_navigation（侧边栏跟随滚动） 文章 文章用Markdown编写，保存到docs目录，文件名建议以六位数字加短横线如000001-开头，保证顺序。\n推荐使用PyCharm配合Markdown Image Kit插件管理图片资源，复制图片粘贴到md中，自动转存为assets/相对路径：\n预览 执行serve.py：\nINFO - Building documentation... INFO - Cleaning site directory INFO - Building documentation to directory: /var/folders/xs/g_0461kx28n2xk7whfkrsnw00000gp/T/mkdocs_38bvc8cg INFO - Documentation built in 0.10 seconds INFO - [13:10:59] Watching paths for changes: \u0026#39;docs\u0026#39;, \u0026#39;mkdocs.yml\u0026#39; INFO - [13:10:59] Serving on http://127.0.0.1:8000/ 访问http://127.0.0.1:8000/效果如下：\n发布 GitHub新建仓库，以github.io作为后缀，并设置GitHub Pages：\n克隆yourname.github.io到本地，建议选择SSH。\n和ebook-mkdocs平级：\n├── ebook-mkdocs ├── yourname.github.io 修改deploy.py的目录：\n# ⚠️ 改成你自己的 GitHub Pages 仓库名(与本项目平级,已 clone 且配好推送) TARGET = ROOT.parent / \u0026#34;yourname.github.io\u0026#34; 执行deploy.py，它会生成site静态文件夹，复制到yourname.github.io推送GitHub仓库。\n访问https://yourname.github.io，即可在线查看电子书。搭建电子书网站全程免费。\n搜索 MkDocs默认使用Lunr.js做搜索，但Lunr不支持中文分词。AI解决了这个问题：在docs/_static/目录下放入lunr.zh.js（中文分词插件），然后在构建钩子post_build.py中将其注入到搜索配置中，这样搜索就能支持中文了。\n结语 到这里，从零搭建一个免费电子书网站的全流程就介绍完了。回顾下核心步骤：\n克隆源码：拿到项目模版，安装依赖 编写文章：用Markdown写内容，按规范命名保证顺序 本地预览：serve.py一键启动预览 发布上线：deploy.py一键推送到Github Pages 核心思路是：用脚本自动化配置文件生成，让你专注内容创作。MkDocs负责静态站点生成，Github Pages负责免费托管。整套源码由AI辅助生成，也解决了中文搜索等问题。\n如果你在实践过程中遇到问题，或者想交流搭建心得，欢迎加入技术群或私信联系我。也欢迎给GitHub仓库点个Star，你的支持是我持续创作的动力。\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/000002-ai%E5%BC%80%E5%8F%91%E5%85%8D%E8%B4%B9%E6%90%AD%E5%BB%BA%E4%BD%A0%E7%9A%84%E7%94%B5%E5%AD%90%E4%B9%A6%E7%BD%91%E7%AB%99%E9%99%84%E6%BA%90%E7%A0%81/","summary":"电子书网站由AI实现，Claude Code+DeepSeek-V4-Pro。技术栈Python+MkDocs，主题Read the Docs，静态托管GitHub Pages。具有中文搜索功能。","title":"免费搭建你的电子书网站（附源码）"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nApache Kafka是一个开源的分布式事件流处理平台，主要用于高性能数据管道、流式分析、数据集成和关键任务应用。\n对于数据流来说，速度是极其重要的。无论是跟踪网站用户活动、处理金融交易还是监控物联网设备，都需要一个能处理连续数据流且延迟最小的系统。\nKafka正是为此类需求而设计的，它为什么可以这么快？\n1、分布式架构 Kafka不是单机服务器，而是多节点集群。分布式架构允许Kafka进行水平扩展，也就是说，可以动态添加机器，来处理不断增长的数据。分布式架构的每个节点，都能独立的处理数据，这给Kafka带来了并发能力。节点越多，并发能力越强，从而显著提高吞吐量和性能。\nKafka的处理过程如下图所示：\n多节点并发，实时处理海量数据，这是Kafka高速度的关键原因之一。\n2、分区 分区是Kafka的核心概念之一，将数据分割成多个分区，每个分区被分配给特定的集群节点。\nKafka确保每个分区只能被一个消费者独立处理，从而允许多个消费者同时从同一主题的不同分区读取数据，实现了并行处理，提高吞吐量。\n补充说明：单个分区内是顺序处理的，能保证顺序性。多个分区之间是并行的，由不同的消费者并行处理。\n3、读写优化 在写方面，Kafka采用了追加写入存储技术，新消息被追加到分区末尾。也就是说，写入是顺序的，而不是随机插入的，顺序写入比随机插入速度更快。\n在读方面，Kafka结合了内存存储和磁盘存储，将频繁访问的数据保存在内存中，减少了对磁盘的读取，提高了读取性能。内存访问速度远快于磁盘访问。\n4、零拷贝技术 在传统系统中，数据在用户空间和内核空间之间，通常需要多次拷贝，例如，当一个应用程序从磁盘读取数据并发送到网络时，数据可能会经历以下步骤：\n从磁盘读取到内核空间的缓冲区。 从内核空间拷贝到用户空间的缓冲区。 从用户空间拷贝回内核空间的网络缓冲区。 最后通过网络发送出去。 这种多次拷贝会带来显著的CPU开销和延迟。\n在Kafka中，零拷贝技术允许数据直接从文件系统缓存传输到网络缓冲区，而无需在用户空间和内核空间之间进行多次拷贝。\n5、批量发送和压缩 在Kafka中，生产者可以将多个消息组合成一个批次，在一次网络请求中发送多个消息，减少网络往返次数，减少每条消息单独发送所带来的开销。\n同时，Kafka引入了压缩算法，生产者在发送消息之前对其进行压缩，消费者在接收消息时再进行解压缩。压缩算法减少了数据传输和存储的大小，从而优化性能。\n参考资料：\nhttps://dev.to/somadevtoo/how-does-apache-kafka-work-why-is-kafka-so-fast-463i\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E4%B8%AD%E9%97%B4%E4%BB%B6/000002-%E4%B8%BA%E4%BB%80%E4%B9%88kafka%E8%BF%99%E4%B9%88%E5%BF%AB/","summary":"Apache Kafka是一个开源的分布式事件流处理平台，主要用于高性能数据管道、流式分析、数据集成和关键任务应用。","title":"为什么Kafka这么快"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n前言 2021年，测试平台如雨后春笋般冒了出来，我就是其中一员，写了一款pytest内核测试平台，在公司落地。分享出来后，有同学觉得挺不错，希望能开源，本着“公司代码不要传到网上去，以免引起不必要麻烦”的原则，只能在家从头写一个，边重新梳理代码边温习巩固知识点，以学习交流为目的，定义为“学习版”。\n功能展示 登录 接口自动化 接口自动化\u0026ndash;Dashboard：\n接口自动化\u0026ndash;环境变量：\n接口自动化\u0026ndash;fixtures：\n接口自动化\u0026ndash;用例管理：\n接口自动化\u0026ndash;用例管理\u0026ndash;编辑用例：\n接口自动化\u0026ndash;测试计划：\n接口自动化\u0026ndash;语法说明：\n小工具 小工具\u0026ndash;共享脚本：\n小工具\u0026ndash;HTTP状态查询：\n后台管理 后台管理\u0026ndash;用户管理\n后台管理\u0026ndash;项目管理\n本文开发内容 登录，登出 首页，修改密码，个人信息 后台管理，用户管理 JWT认证 本文先打个基础，既是测试平台基本结构，也可以作为CMS基础框架，定制开发各种小型项目。\n技术栈 Node.js 12.16.3 Vue 4.5.11 Python 3.8 Django 3.1.3 Django REST framework 3.12.2 SQLite 3 IDE编辑器推荐PyCharm旗舰版，既能写Django也能写Vue项目。数据库使用Django自带SQLite ，省去安装MySQL和Navicat/Workbench麻烦，轻量级开发。SQLiteStudio为SQLite数据库可视化工具，只需要下载即可，无需安装，解压就用：：\nhttps://sqlitestudio.pl/\n由于会用到models.JSONField，SQLite默认不兼容，所以需要下载sqlite3.dll文件替换下：\nhttps://www.sqlite.org/download.html\n根据Python版本选择，比如我的windows安装的Python38-32，下载了sqlite-dll-win32-x86-3340100.zip这个软件包，解压后将D:\\Program Files (x86)\\Python38-32\\DLLs\\sqlite3.dll替换。\n创建Vue项目 设置npm淘宝镜像：\nnpm config set registry https://registry.npm.taobao.org 安装Vue CLI：\nnpm install -g @vue/cli 创建teprunner-frontend项目：\nvue create teprunner-frontend 项目名字请随意。\n默认选项点击回车进行创建：\n编写Vue代码 添加静态资源：\n包括css样式、字体样式、图标、logo。\n推荐一个图标下载网站：https://www.easyicon.net/。\n编辑package.json，安装项目所需依赖：\naxios用于异步请求，发送http给后端。element-ui为饿了么开源前端框架，简化了从头写html麻烦，高度复用，统一风格。vue-router提供了路由跳转，在上个时代，路由是在后端来控制的，把页面渲染后返回给前端直接展示，前后端分离后，后端只负责返回数据，把控制权交给前端。\ndevDependencies是写代码用到的依赖，这里把eslint和prettier标出来了，它们是用来做代码静态检查的，配置后能给与代码规范提示，帮你写出更漂亮的代码，同样是在package.json文件编辑：\n接着执行npm install进行安装。有可能会出现下图提示：\n执行npm audit fix就修复好了：\n新建vue.config.js文件，添加Vue项目配置：\nargs[0].title给网页设置了浏览器title。proxy指定了后端接口根路径为/api，后端服务器访问地址为http://127.0.0.1:8000/，这是Django启动后默认本地域名和端口。element-ui默认页面是会出现滚动条的，在登录页会显得很丑，需要在public/index.html加上样式：\nVue程序执行入口是main.js，把需要初始化加载的代码写在这里：\napp会挂载到index.html文件中div：\n这是整个Vue项目唯一的html文件，其他组件都是挂载到这个div下面的。其中有个App.vue：\n它叫做根组件，router-view是一块区域，用来展示路由匹配到的组件，也就是说所有路由匹配到的组件都会通过App.vue根组件来展示。路由配置在router/index.js文件中编辑：\n第一层路由是/login登录和/首页，首页只有菜单，没有具体内容，显示没有意义，所以重定向到了后台管理的用户管理。第二层路由是具体的功能模块，作为子路由放在首页路由下，比如后台管理。后台管理的子模块用户管理也放到了后台管理的子路由，根据url访问路径定义父子路由关系。\n为了在未登录的情况下，不允许访问首页，需要再加上访问拦截：\n同时添加了meta.requireAuth，用来设置哪些路由需要拦截，这里把首页设置为True：\n登录就不需要了。路由配置完成了，接着编写页面代码，Vue项目的页面只有index.html一个html文件，其他页面都是放在views文件夹下，新建一个views/login/index.vue文件：\n使用el-form标签添加用户名、密码、忘记密码和登录按钮。:model给表单绑定了数据对象，分别填充到form.username、form.password、form.rememberMe：\n:rules定义了表单规则，比如是否必填：\n登录没有做用户名和密码校验，新增用户时才会做校验。\n在创建登录界面时，从localStorage中移除userInfo和token，登录信息保留7天：\n点击登录按钮会调用login方法，发起登录请求：\n新建views/home/index.vue，编写首页代码：\n\u0026lt;router-link\u0026gt;提供了链接跳转，左上角logo跳转到首页，顶部导航栏根据后端返回的authList权限菜单进行显示，因为后台管理只有管理员才能访问。接着编写右上角区域代码：\n包括修改密码、个人信息和退出登录，为了简单一点，没有弄头像了。修改密码使用el-dialog做了个弹出框：\n包括当前密码、新密码、确认新密码。并添加了校验规则：\n修改密码会调用/users/passwords/set接口：\n同时初始化菜单权限，从后端获取authList，并判断是否有权限，没有权限的话跳转到登录页面：\n首页除了左上角logo，顶部导航栏，右上角个人信息，还有一个重要的版块就是左侧菜单。由于有了顶部导航栏，左侧菜单如果也放到首页来写，由于层级关系会让代码显得很臃肿，所以菜单是放到每个子模块来做的。每个子模块有左侧菜单，也会存在很多重复容易的代码，为了复用，就抽成组件，放到components文件夹下：\n用到了el-menu标签：\nslot是个插槽，相当于挖个坑在这，用的时候填一下坑，类似于模板。然后用el-breadcrumb做了个面包屑，点击可跳转到相应路由。接着就把左侧菜单应用到后台管理模块上，新建views/console/index.vue：\n左侧菜单搞定了，右侧内容也是类似的，查询、表格、分页、增删改查，也需要抽成组件：\n再新建views/console/userManagement.vue，编写用户管理代码：\n用到了el-form和el-table标签。表格数据通过:data绑定到了tableData对象，调用后端接口后，从响应中拿数据填充：\n新增用户弹窗的入口也是放在这个文件中的：\ndialogFormVisible默认为False不可见，点击新增按钮后，会设置为True。新建views/console/addUser.vue文件编写用户弹窗的代码：\n用户管理userManagement.vue和新增用户addUser.vue这两个组件叫做父子组件，父组件如果想传值给子组件，需要通过props来实现：\nwatch能监视传值的状态，及时渲染。\nwatch不是必须的，等到做编辑用例和用例运行结果的时候，会更加体会到它的作用。\n新增用户时，会对用户名和密码做校验：\nnameValidator和pwdValidator是公共方法，定义在utils/const.js文件中：\nutils文件夹下还有个commonMethods.js文件，定义了一些公共js方法：\n本次前端代码基本编写完成了：\n最后还有个axios.js，它定义了异步请求实例：\n添加了一个请求拦截器：\n校验header需要包括jwt请求头：Authorization: Bearer 。还添加了一个响应拦截器：\n对错误信息进行捕获并弹框提示。\n创建Django项目 安装Django：\npip install --default-timeout=6000 -i https://pypi.tuna.tsinghua.edu.cn/simple django 创建teprunner-backend项目：\ndjango-admin startproject teprunnerbackend 项目名字请随意。\n注意这条命令的项目名字不能带短横线-，如果想用短横线，可以先去掉短横线执行命令，再手动改回来，外层这个名字对项目没有任何影响：\n编写Django代码 安装依赖包：\npip install --default-timeout=6000 -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt 创建user应用:\ndjango-admin startapp user 配置teprunnerbackend/settings.py:\ndjango-cors-headers为Django提供了跨域访问的解决方案，需要配置ALLOWED_HOSTS为*，允许所有域访问，并注册INSTALLED_APPS和MIDDLEWARE。user应用也需要在INSTALLED_APPS注册后才会生效。继续改配置，把时区改为Asia/Shanghai：\n继续：\nDjango自带了一个权限管理系统，为了简单一点，直接复用。不过需要对user表进行自定义改造，所以通过配置里面的AUTH_USER_MODEL指定为刚刚创建的user应用的User。REST_FRAMEWORK是Django RESTful framework的配置项，同样要进行自定义改造，所以这里通过配置DEFAULT_AUTHENTICATION_CLASSES指定认证鉴权类为CustomJSONWebTokenAuthentication，通过EXCEPTION_HANDLER指定异常处理函数为custom_exception_handler，通过DEFAULT_PAGINATION_CLASS指定分页类为CustomPagination。JWT_AUTH是jwt的配置项，定义了过期时间为30天，允许刷新，刷新间隔，响应处理，header前缀。最后补充了django-cors-headers的3个配置。\n接着配置teprunnerbackend/urls.py：\n把user的url都添加到api/users/下面。新建user/urls.py文件：\n分别添加登录、用户增删改查、重置密码、角色列表、修改密码几个路径。Django的视图有两个类型：类视图和函数视图。path()只接受可调用对象，所以类视图需要使用as_view()进行转化，比如views.UserLogin.as_view()。函数视图直接写上函数名就可以了，比如views.update_password。\n打开user/models.py文件，添加数据模型：\nmodel建立了代码和数据库的映射，这称为orm，对象关系映射。基础表定义了共有的created_at和updated_at字段。auto_now_add表示记录新增时间，auto_now表示记录更新时间，都是自动进行，无需手动写代码来处理。用户表继承了Django自带的AbstractUser，REQUIRED_FIELDS规定了哪些字段必填，username和password是隐式规定了必填的，不需要设置，默认email也是必填，这里把它去掉。\nDjango默认表名会加上django_前缀，使用Meta.db_table来定义没有前缀的表名。\nmodel写完了，执行以下命令同步到数据库中，创建表结构：\npython manage.py makemigrations python manage.py migrate 打开SQLiteStudio，选择根目录的db.sqlite3文件：\n看到表结构已经创建好了：\nRole有个models.JSONField字段，为菜单权限JSON，使用Django的fixtures给项目添加初始化数据：\nfixtures名字是固定的，就像pytest的conftest.py一样，只认这个名字。user.json存放数据：\n包括管理员用户、角色权限、管理员角色对照关系。其中角色权限数据共3条：\nauth里面定义了菜单，对应首页的顶部导航栏的栏目，比如本文只添加了后台管理。access表示角色是否有权限访问，只有管理员的这条数据，access为true。通过以下命令把这些数据写入数据库中：\npython manage.py loaddata user Django会在user.fixtures目录下自动找名字为user的.json、.xml或.yaml文件进行加载。\n接着新建一个user/serializers.py文件写序列化的代码。Django序列化是指，把数据库的数据转化为json返回给前端，反序列化是指把前端传过来的json写入数据库。先写登录的序列化器：\n继承自serializers.ModelSerializer，一般需要在Meta定义两个属性，model指定相应的模型，fields指定所需要的的字段，这些字段就是json的key。图中的roleName不属于User表的字段，所以通过定义为serializers.SerializerMethodField()，再定义get_roleName()方法来取。serializer写好后，写视图，编辑user/views.py：\n由于是jwt认证，所以这里继承了JSONWebTokenAPIView，提取请求参数，check_password()简单校验了下请求的密码和数据库的密码hash值是否相等，后面的代码是JSONWebTokenAPIView.post方法现成的，重写了一遍。ErrInvalidPassword等是在user/errors.py定义的错误响应：\n这样可读性会更高。响应状态码也建议这么写status=status.HTTP_500_INTERNAL_SERVER_ERROR，from rest_framework import status已经定义好了所有状态码的常量。\n新建user/utils.py文件，编写jwt_response_payload_handler来定义登录接口的响应结构：\n返回token、user、auth三个字段。custom_exception_handler规范了一下异常响应信息。这2个方法都是在settings.py中的REST_FRAMEWORK配置过的，还有一项配置是分页，新建user/pagination.py文件：\n继承了PageNumberPagination，指定了查询参数名page、perPage，自定义了响应字段名currentPage、items、totalNum、totalPage，并添加了2个字段hasNext和hasPrev。\n最后还有一个配置项，自定义认证鉴权，新增user/authentications.py：\n继承了BaseJSONWebTokenAuthentication。通过get_authorization_header提取请求头中的Authorization字段，没有就提示“缺失JWT请求头”。后面的代码是父类现成的。\n至此，整个后端基本代码都写完了，jwt认证也做好了：\n后面的代码就集中在serializers.py和views.py两个文件，序列化器提供数据库表字段和响应json的序列化和反序列化，视图使用序列化器，编写业务处理代码。按照这个思路，编写用户的增删改查功能，先在serializers.py文件中写2个序列化器UserCreateSerializer和UserPagingSerializer：\n由于新增用户和用户列表展示的字段不一样，所以给同一个User模型创建了2个序列化器。图中标红了代码是把int的id值转化为了str类型，方便前端处理。is_staff表示是否为管理员，这个名字是Django定的。再写views.py：\nGenericViewSet是Django REST framework提供了超级封装的类视图，一般不需要重写，给queryset和serializer_class赋值就可以了。不过因为有些自定义规则，所以本项目进行了复写。permission_classes指定了接口访问权限，IsAdminUser表示必须管理员才能访问，也是Django定义好的，和前面的is_staff相对应：\n类似的，我在user/permissions.py新建了个IsTester，用来控制某些功能只能测试使用：\n本文还用不到这个。\n重写查询用户列表list方法：\n增加username和nickname的模糊查询。\n重写新增用户create方法：\n首先写user表，根据角色名是否包含管理员，判断是否写is_staff字段，接着用入库后产生的user_id写user_role表。注意最后一行的status，新增的话，状态码返回201。\n重写修改用户的put方法：\n和新增用户的区别在于，更新user_role表数据时，需要根据老角色和新角色，比较差异后，添加新增的，删除废旧的。\n重写删除用户的delete方法：\n同时删除user表和user_role表。注意最后一行的status，删除的话，状态码返回204。\n另外还自定义了user_detail方法，返回单个用户信息：\nGenericViewSet的这些请求方法在user/urls.py文件中配置映射关系：\n\u0026lt;int:pk\u0026gt;定义了url中的整形参数，pk为变量名，通过kwargs[\u0026quot;pk\u0026quot;]来取。\n在新增用户的时候，需要从角色列表中选择角色，需要后端提供这样的接口，使用ListAPIView：\n4行代码搞定一个接口，这就是Django的好处，除了ListAPIView，还有CreateAPIView、RetrieveAPIView、ListCreateAPIView等，按需取用。\n密码重置接口用APIView来实现：\n定义了put方法，从请求url中获取参数值user_id，查询user对象后，调用预置的set_password方法，把密码重置为qa123456。记得调用user.save()把数据更新到数据库。\n除了类视图，Django也提供了函数视图，并且Django REST framework提供了函数视图的方法装饰器，可以像flask框架一样，感受写纯后端接口的体验，按这个方法来写修改密码接口：\n@api_view(['PUT'])是Django REST framework提供的方法装饰器。修改密码时，会对jwt进行解码，获取到user_id，然后检查老密码是否和数据库中的密码hash值一致。\n前后端联调 根据以上思路把前后端的代码写完以后，就可以把项目跑起来看看效果了。先启动Django项目：\npython manage.py runserver 接着启动Vue项目：\nnpm run serve 访问：\nhttp://localhost:8080/\n就能看到登录页面了，默认超管用户名为admin，密码为qa123456，登录成功后可以尝试走一遍功能检查下：\n点击左上角logo，不会出现跳转到空白页。 点击右上角信息，弹出下拉菜单，分别有修改密码、个人信息、退出登录。 点击退出，返回登录页，重新登录。 查询右上角个人信息，包括用户名、昵称、角色。 通过右上角下拉菜单修改密码，和老密码不匹配会提示修改失败，填写正确信息会修改成功，自动跳转到登录页面重新登录。输入老密码登录失败，输入新密码登录成功。 新增用户，保持默认密码，新增成功后，用qa123456登录成功。 新增用户，选择自定义密码，新增成功后，用qa123456登录失败，用自定义密码登录成功。 新增用户，分别创建管理员、开发、测试3个角色用户。 使用新用户登录，管理员用户能登录成功，开发和测试由于没有后台管理权限，点击登录接口后会提示“无菜单权限”。 修改用户，修改用户名、密码，修改测试角色用户为管理员角色，重新登录，能看到用户名、密码已更新为修改后的用户名、密码，并且管理员角色生效，能登进去看到后台管理功能。 输入用户名或昵称，点击搜索按钮，测试模糊查询功能正常，重置后清空搜索框，自动查询一次列表。 点击删除按钮，提示是否确认删除，确认后删除成功，检查数据库user_role表数据也被清理干净。 切换分页，刷新列表，选择不同分页条数，正常计算显示相应的分页总数。 找到自定义密码的用户，点击重置密码，重置成功后，重新登录，使用自定义密码登录失败，使用默认密码qa123456登录成功。 点击左侧菜单旁边的面包屑按钮，能收起和展开左侧菜单。 由于时间关系，目前还没有做角色管理功能，角色通过后端Django的fixtures/user.json进行数据初始化。\nPostman搭建Mock Server 在写前端代码过程中，后端还没有写好，可以找一个服务模拟后端，提供响应数据进行前端调试，这项技术叫做Mock，这个服务称为Mock Server。Mock Server可以使用JavaScript的mock.js来搭建，也可以使用Python的FastAPI ，不嫌麻烦用Spring Boot或者Nginx做一个也可以。不过为了方便起见，选择上手就能用的可以省不少心。一些网站会提供在线Mock服务，在网站上填写url和response body，有个缺点是我找了一圈都没有发现能设置响应状态码的，比如在调试axios.js的响应拦截器时，就需要根据404、500来进行调试，看效果。寻寻觅觅，发现平时都在用的Postman，直接可以做Mock Server。首先登陆Postman，只有登陆后才能使用这个功能：\n可以选择用Google账号登陆，也可以注册一个。点击左上角+ New：\n选择Mock Server。依次填写请求方法、请求路径、响应状态码、响应体：\n点击表格右上角的三个点还能添加请求体和接口描述：\n接着点击下一步：\n填写Mock Server的名字为hello，其他选项默认，点击Create Mock Server进行创建：\n关闭后，Mock Server就建好了：\n接着从左侧Collections中找到这个接口，点击打开：\n此时还不能发送请求，需要在右上角选择环境hello：\n发送请求成功：\n其中url是隐藏了的，点击右上角环境旁边的眼睛图标查看：\n修改已创建接口mock数据的入口在Examples：\n点击Default：\n提供了新增时更直观的操作界面，比如我把响应状态码改成了404，响应体改成了{\u0026quot;msg\u0026quot;: \u0026quot;hello not found\u0026quot;}，点击右上角Save Example保存后，再次请求：\n实际mock的状态码和响应体也更新了。\n小结 本文是学习版pytest内核测试平台开发的入门篇，内容比较充实，全文字数上万，一共截了103张图，借助FastStone Capture这个小工具，还算轻松，希望能让读者更直观的看到平台功能和代码逻辑。“编写Vue代码”和“编写Django代码”2个小节的内容是一气呵成写完的，没有做修改，怕改了之后反而影响行文思路，不够畅快。前端项目参考了一些开源项目如Tcloud、FasterRunner等，把代码看懂后，自己重新组织了代码和规范，在调试过程中，也学会了写Vue，做学习版teprunner时就从头写了一遍。后端代码完全是我自己写的，先学了一遍Django和Django REST framework官方教程，其中《Django认证系统并不鸡肋反而很重要》这篇文章在腾讯云+社区2020年度征文活动中，被评选为了最受喜爱作者奖，如果对Django认证系统不是很清楚的话，可以看看。最后，个人水平有限，有错误或不足之处，还请见谅。虽然测试平台不一定能完全落地，但是做一遍开发对能力的提升是极大的。teprunner并不算优秀平台，和真正企业级项目比起来，就是个小玩具。如果能博君一乐，也算是体现价值了。\n参考资料：\n源码前端 https://github.com/dongfanger/teprunner-frontend\n源码后端 https://github.com/dongfanger/teprunner-backend\n个人博客 https://dongfanger.gitee.io/blog/\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000002-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0%E5%AD%A6%E4%B9%A0%E7%89%88pytest%E5%86%85%E6%A0%B8%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E5%BC%80%E5%8F%91%E4%B8%87%E5%AD%97%E9%95%BF%E6%96%87%E5%85%A5%E9%97%A8%E7%AF%87/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"前言\"\u003e前言\u003c/h1\u003e\n\u003cp\u003e2021年，测试平台如雨后春笋般冒了出来，我就是其中一员，写了一款pytest内核测试平台，在公司落地。分享出来后，有同学觉得挺不错，希望能开源，本着“公司代码不要传到网上去，以免引起不必要麻烦”的原则，只能在家从头写一个，边重新梳理代码边温习巩固知识点，以学习交流为目的，定义为“学习版”。\u003c/p\u003e\n\u003ch1 id=\"功能展示\"\u003e功能展示\u003c/h1\u003e\n\u003ch2 id=\"登录\"\u003e登录\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a3424569c09d42aca658cf3b885278e3.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"接口自动化\"\u003e接口自动化\u003c/h2\u003e\n\u003cp\u003e接口自动化\u0026ndash;Dashboard：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/41db611e68aa42019522249b82b90997.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接口自动化\u0026ndash;环境变量：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/896567b6022149c896fe3ee25a085775.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接口自动化\u0026ndash;fixtures：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e78af47ce1b246888d8efec56167a2d2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接口自动化\u0026ndash;用例管理：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/562b086acf334c8c9d716b09f40ee67e.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接口自动化\u0026ndash;用例管理\u0026ndash;编辑用例：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d9ff4d3f91374aadb002a9feac0acab9.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接口自动化\u0026ndash;测试计划：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bac839ba20b642c484b6c61c8487711b.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接口自动化\u0026ndash;语法说明：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e6f5f342fe4c4ca7a94aa1425116e772.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"小工具\"\u003e小工具\u003c/h2\u003e\n\u003cp\u003e小工具\u0026ndash;共享脚本：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8d2d8773432847dabd769a1e23358c85.png\"\u003e\u003c/p\u003e\n\u003cp\u003e小工具\u0026ndash;HTTP状态查询：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6abb077760834ea3b36d856bcc0dd4f8.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"后台管理\"\u003e后台管理\u003c/h2\u003e\n\u003cp\u003e后台管理\u0026ndash;用户管理\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/82bdce6ce2b14a3694103b41159881df.png\"\u003e\u003c/p\u003e\n\u003cp\u003e后台管理\u0026ndash;项目管理\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/dcf99d0331e34d3ab08a233d42735b7d.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"本文开发内容\"\u003e本文开发内容\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e登录，登出\u003c/li\u003e\n\u003cli\u003e首页，修改密码，个人信息\u003c/li\u003e\n\u003cli\u003e后台管理，用户管理\u003c/li\u003e\n\u003cli\u003eJWT认证\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e本文先打个基础，既是测试平台基本结构，也可以作为CMS基础框架，定制开发各种小型项目。\u003c/p\u003e\n\u003ch1 id=\"技术栈\"\u003e技术栈\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eNode.js 12.16.3\u003c/li\u003e\n\u003cli\u003eVue 4.5.11\u003c/li\u003e\n\u003cli\u003ePython 3.8\u003c/li\u003e\n\u003cli\u003eDjango 3.1.3\u003c/li\u003e\n\u003cli\u003eDjango REST framework 3.12.2\u003c/li\u003e\n\u003cli\u003eSQLite 3\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIDE编辑器推荐PyCharm旗舰版，既能写Django也能写Vue项目。数据库使用Django自带SQLite ，省去安装MySQL和Navicat/Workbench麻烦，轻量级开发。SQLiteStudio为SQLite数据库可视化工具，只需要下载即可，无需安装，解压就用：：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://sqlitestudio.pl/\"\u003ehttps://sqlitestudio.pl/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/cf7f04cd903646ae90dadc0ab66233b0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e由于会用到\u003ccode\u003emodels.JSONField\u003c/code\u003e，SQLite默认不兼容，所以需要下载\u003ccode\u003esqlite3.dll\u003c/code\u003e文件替换下：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.sqlite.org/download.html\"\u003ehttps://www.sqlite.org/download.html\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e根据Python版本选择，比如我的windows安装的Python38-32，下载了\u003ccode\u003esqlite-dll-win32-x86-3340100.zip\u003c/code\u003e这个软件包，解压后将\u003ccode\u003eD:\\Program Files (x86)\\Python38-32\\DLLs\\sqlite3.dll\u003c/code\u003e替换。\u003c/p\u003e\n\u003ch1 id=\"创建vue项目\"\u003e创建Vue项目\u003c/h1\u003e\n\u003cp\u003e设置\u003ccode\u003enpm\u003c/code\u003e淘宝镜像：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003enpm config \u003cspan style=\"color:#6639ba\"\u003eset\u003c/span\u003e registry https://registry.npm.taobao.org\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e安装\u003ccode\u003eVue CLI\u003c/code\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003enpm install -g @vue/cli\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e创建\u003ccode\u003eteprunner-frontend\u003c/code\u003e项目：\u003c/p\u003e","title":"学习版pytest内核测试平台开发万字长文入门篇"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nHTTP协议是无状态的，无状态意味着，服务器无法给不同的客户端响应不同的信息。这样一些交互业务就无法支撑了。Cookie应运而生。\nCookie 通过F12开发者工具，先瞅瞅Cookie的颜值\n从图中可以看到Cookie包括这些内容：Name，Value，Domain，Path，Expires / Max-Age，Size，HttpOnly，Secure，SameSite，Priority。\nCookie的传递会经历这4步\nClient发送HTTP请求给Server Server响应，并附带Set-Cookie的头部信息 Client保存Cookie，之后请求Server会附带Cookie的头部信息 Server从Cookie知道Client是谁了，返回相应的响应 Cookie的英文翻译是甜品，使用Cookie可以自动填写用户名、记住密码等，是给用户的一点甜头。\nServer拿到Cookie后，通过什么信息才能判断是哪个Client呢？服务器的SessionID。\nSession 如果把用户名、密码等重要隐私都存到客户端的Cookie中，还是有泄密风险。为了更安全，把机密信息保存到服务器上，这就是Session。Session是服务器上维护的客户档案，可以理解为服务器端数据库中有一张user表，里面存放了客户端的用户信息。SessionID就是这张表的主键ID。\nCookie中保存SessionID\nSession信息存到服务器，必然占用内存。用户多了以后，开销必然增大。为了提高效率，需要做分布式，做负载均衡。因为认证的信息保存在内存中，用户访问哪台服务器，下次还得访问相同这台服务器才能拿到授权信息，这就限制了负载均衡的能力。而且SeesionID存在Cookie，还是有暴露的风险，比如CSRF（Cross-Site Request Forgery，跨站请求伪造）。\n如何解决这些问题呢？基于Token令牌鉴权。\nToken 首先，Token不需要再存储用户信息，节约了内存。其次，由于不存储信息，客户端访问不同的服务器也能进行鉴权，增强了扩展能力。然后，Token可以采用不同的加密方式进行签名，提高了安全性。\nToken就是一段字符串\nToken传递的过程跟Cookie类似，只是传递对象变成了Token。用户使用用户名、密码请求服务器后，服务器就生成Token，在响应中返给客户端，客户端再次请求时附带上Token，服务器就用这个Token进行认证鉴权。\nToken虽然很好的解决了Session的问题，但仍然不够完美。服务器在认证Token的时候，仍然需要去数据库查询认证信息做校验。为了不查库，直接认证，JWT出现了。\nJWT JWT的英文全称是JSON Web Token。JWT把所有信息都存在自己身上了，包括用户名密码、加密信息等，且以JSON对象存储的。\nJWT长相是xxxxx.yyyyy.zzzzz，极具艺术感。包括三部分内容\nHeader\n包括token类型和加密算法（HMAC SHA256 RSA）。\n{ \u0026#34;alg\u0026#34;: \u0026#34;HS256\u0026#34;, \u0026#34;typ\u0026#34;: \u0026#34;JWT\u0026#34; } Payload\n传输内容。\n{ \u0026#34;sub\u0026#34;: \u0026#34;1234567890\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;admin\u0026#34;: true } Signature\n签名，把header和payload用base64编码后\u0026quot;.\u0026ldquo;拼接，加盐secret（服务器私钥）。\nHMACSHA256( base64UrlEncode(header) + \u0026#34;.\u0026#34; + base64UrlEncode(payload), secret) 再画个妆，漂亮\n可以到https://jwt.io/#debugger-io这个网址卸妆哦\n给Token穿个外套\nAuthorization: Bearer \u0026lt;token\u0026gt; 这就是我们在请求Header里面看到的内容格式了。\n简要回顾 本文简单介绍了Cookie、Session、Token、JWT的概念，以及为什么需要这些技术。至于更深入的原理和代码使用，就请读者自行研究了哦。至少这篇文章能让你搞懂，看到不会觉得陌生了。哈哈哈。\n参考资料\nhttps://jwt.io/introduction/\njwt-handbook-v0_14_1\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%9F%A5%E8%AF%86/000002-%E4%B8%80%E6%96%87%E6%90%9E%E6%87%82cookiesessiontokenjwt/","summary":"HTTP协议是无状态的，无状态意味着，服务器无法给不同的客户端响应不同的信息。这样一些交互业务就无法支撑了。Cookie应运而生。","title":"一文搞懂Cookie,Session,Token,JWT"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n这篇文章很难写！我最开始学 Python，和大多数人一样，是看的菜鸟教程：\n在写完这篇文章的第一遍后，我发现并没有写出新意，很可能读者看到后，会和我当初一样，很快就忘了。我现在已经不是读者而是作者了，我想我可以做出一些改变，降低阅读门槛，加强理解记忆。\n版本选择 Python 有 2 个不兼容的版本，Python2 和 Python3。Python2 已经逐渐被淘汰，现在只需要学 Python3 即可。\n语法是什么 先直观感受一下，用不同编程语言在界面上打印出 HelloWorld 字样：\nC 语言\n#include \u0026lt;stdio.h\u0026gt; int main(void) { printf(\u0026#34;Hello World!\u0026#34;); return 0; } Python\nprint(\u0026#34;HelloWorld!\u0026#34;) 看到了么，结果一样，但是编程语言的写法不一样，就像中文和英文，同一个意思，却是用不同字符表达出来的。语法就是用来告诉你该怎么表达的一套规则。学 Python 只有懂 Python 语法，才能知道怎么写代码。\n从一个例子讲起 假设我们要用 Python 实现这样一个功能，键盘输入\u0026quot;大河向东流\u0026quot;后，屏幕上会显示“天上的星星参北斗”。用 Python 实现的代码为：\n# 获取键盘输入 pre_sentence = input() # 判断如果输入的句子是大河向东流 if pre_sentence == \u0026#34;大河向东流\u0026#34;: # 在屏幕上打印天上的星星参北斗 print(\u0026#34;天上的星星参北斗\u0026#34;) 这个例子涉及到这些名词术语：编码、标识符、语句、缩进、代码块、引号、注释、空行、函数。\n编码 编码，是指把人类语言编成计算机能懂的二进制（010101 这种）。不同国家的字符是不一样的，用来编码的字符集也就不一样。美国的字符集包括英文字母、标点符号字符、阿拉伯数字，叫做 ASCII。欧洲的字符集，加入了带重音的字符、希腊字母等叫做，Latin-1。中国！加入了汉字！叫做 GB2312。后来加入了生僻字、繁体字及日韩汉字等，叫做 GBK。天下大乱，乱码无法交流，ISO 国际标准化组织决定一统江湖，Unicode 诞生。再优化优化，UTF-8。\n默认情况下，Python3 源码文件都是以 UTF-8 编码，字符串都是 unicode 字符串，让全球不同国家无障碍编程。代码中既可以写英文也可以写中文，但是中文只能用来显示和注释，不决定程序运行。\n标识符 标识符是编程时使用的名字，比如例子中的 pre_sentence。在 Python 中，名字由字母、数字、下划线组成，不能以数字开头，并且区分大小写。比如以下是合法的：\na_12_b_ _a1 _A1 以下是非法的：\n1abc $ddd 我们在写代码的时候，经常需要取名字，取名最好具有业务意义。有些名字包含多个英文单词，写在一起不好阅读，名字又不能包含空格，要么首字母大写，要么用下划线来分隔。现有的 Python 代码库中，这两种方式都存在，因为 Python 是由开源社区发展来的，五花八门，百花齐放。为了越来越规范，Google 制定了 PEP8 规范（Python Enhancement Proposal 8），建议大家用下划线来分隔。\n下划线除了用来分隔多个单词，在 Python 中还有更多的作用。单个下划线开头 _foo 代表私有的，只能“自己”用。单个下划线结尾 class_ 可以用来解决和 Python 已有名字冲突。双下划线 __main__ 在 Python 中有极其特殊的作用。\nPython 已经把有些名字用了，你不能再用，否则会冲突报错。可以在 cmd 中输入 python 进入 Python 后，输入以下代码查看哪些名字已经被用了：\n\u0026gt;\u0026gt;\u0026gt; import keyword \u0026gt;\u0026gt;\u0026gt; keyword.kwlist [\u0026#39;False\u0026#39;, \u0026#39;None\u0026#39;, \u0026#39;True\u0026#39;, \u0026#39;and\u0026#39;, \u0026#39;as\u0026#39;, \u0026#39;assert\u0026#39;, \u0026#39;break\u0026#39;, \u0026#39;class\u0026#39;, \u0026#39;continue\u0026#39;, \u0026#39;def\u0026#39;, \u0026#39;del\u0026#39;, \u0026#39;elif\u0026#39;, \u0026#39;else\u0026#39;, \u0026#39;except\u0026#39;, \u0026#39;finally\u0026#39;, \u0026#39;for\u0026#39;, \u0026#39;from\u0026#39;, \u0026#39;global\u0026#39;, \u0026#39;if\u0026#39;, \u0026#39;import\u0026#39;, \u0026#39;in\u0026#39;, \u0026#39;is\u0026#39;, \u0026#39;lambda\u0026#39;, \u0026#39;nonlocal\u0026#39;, \u0026#39;not\u0026#39;, \u0026#39;or\u0026#39;, \u0026#39;pass\u0026#39;, \u0026#39;raise\u0026#39;, \u0026#39;return\u0026#39;, \u0026#39;try\u0026#39;, \u0026#39;while\u0026#39;, \u0026#39;with\u0026#39;, \u0026#39;yield\u0026#39;] 语句 交流不可能是按单词来交流，而是按语句。Python 中一行代码就是一条语句，这是一般规定。也有少数例外，比如可以使用反斜杠(\\)，把一条语句写成多行：\ntotal = item_one + \\ item_two + \\ item_three 如果在 [], {}, 或 () 中，不用反斜杠(\\)也可以写成多行：\ntotal = [\u0026#39;item_one\u0026#39;, \u0026#39;item_two\u0026#39;, \u0026#39;item_three\u0026#39;, \u0026#39;item_four\u0026#39;, \u0026#39;item_five\u0026#39;] 反之亦然，Python 中多条语句也是能写成一行的（不推荐）：\nimport sys; x = \u0026#39;runoob\u0026#39;; sys.stdout.write(x + \u0026#39;\\n\u0026#39;) 缩进 有些话不是一句就能说完的，比如我现在说的这句，哈哈。为了代码有层次，阅读更方便，编程语言会使用缩进来断句。Python 的缩进有点异类，不是用花括号 {}，而是用 4 个空格，而且必须要缩进一致，否则就会报错。比如：\nif True: print (\u0026#34;Answer\u0026#34;) print (\u0026#34;True\u0026#34;) else: print (\u0026#34;Answer\u0026#34;) # 没有严格缩进，在执行时会报错 print (\u0026#34;False\u0026#34;) 报错：\nIndentationError: unindent does not match any outer indentation level 如果用 PyCharm，那么就完全不用担心这个问题了，因为它有智能提示，在编写的时候就能纠正过来。我已经完全适应了敲空格，为什么空格键设计的那么大？就是用来敲的！\n代码块 “多条缩进相同的语句就叫做代码块”，这是教程给的定义，我们平时说的代码块，更多指的是相关联的多行代码。比如，如果 xxx，那么 xxx，否则 xxx：\nif expression : suite else : suite 引号 引号常用来框住字符串，Python 同时支持单引号、双引号、三引号，只要保证引号的开始和结束一致就行：\nword = \u0026#39;word\u0026#39; sentence = \u0026#34;这是一个句子。\u0026#34; paragraph = \u0026#34;\u0026#34;\u0026#34;这是一个段落。 包含了多个语句\u0026#34;\u0026#34;\u0026#34; 单行用单引号或双引号，多行用三引号。我一般会优先用双引号，看着规范（毫无依据，哈哈哈）。\n注释 Python 的单行注释用 #：\n# 第一个注释 print (\u0026#34;Hello, Python!\u0026#34;) # 第二个注释 Python 的多行注释用三个单引号或三个双引号：\n\u0026#39;\u0026#39;\u0026#39; 这是多行注释，使用单引号。 这是多行注释，使用单引号。 这是多行注释，使用单引号。 \u0026#39;\u0026#39;\u0026#39; \u0026#34;\u0026#34;\u0026#34; 这是多行注释，使用双引号。 这是多行注释，使用双引号。 这是多行注释，使用双引号。 \u0026#34;\u0026#34;\u0026#34; 空行 空行也是 Python 代码的一部分，既能分隔不同业务代码，也能提高阅读体验。不过不需要刻意记哪些地方需要多少空行，在 PyCharm 中 CTRL + ALT + SHIFT + L 组合键可以快速格式化为符合 PEP8 规范的代码，标准化的空行一目了然：\n函数 例子中 input()、print() 这种 名字 + () 形式的叫做函数，写 Python 经常会用到函数，也需要我们自己写函数。函数是一块代码片段，有输入，有输出，可以重复使用，有特定功能，比如 input() 用来键盘输入，print() 用来打印到屏幕显示。我们写 Python 代码都是在 .py 文件中来写的，每个 .py 文件在 Python 中叫做模块。模块多了以后，我们需要把它们按照业务意义分别放到不同的目录进行管理，有些目录中包含 __init__.py 文件，这种目录叫做包。\nimport 与 from\u0026hellip;import 模块与模块之间是分离的，除了 Python 已经为我们提供的基础语法，如果想用其他模块的代码，就需要使用 import 或 from..import 来导入。视使用需要不同，导入格式也不同：\n将整个模块（ somemodule ）导入，格式为： import somemodule\n从某个模块中导入某个函数，格式为： from somemodule import somefunction\n从某个模块中导入多个函数，格式为： from somemodule import firstfunc, secondfunc, thirdfunc\n将某个模块中的全部函数导入，格式为： from somemodule import *\n动态语言 Python 是动态语言，不需要指定数据类型。比如：\na = 1 b = \u0026#34;string\u0026#34; 强类型语言 Python 是强类型语言，经常会有人搞错。比如：\na = 1 b = \u0026#34;string\u0026#34; c = a + b 是会报错的：\nTypeError: unsupported operand type(s) for +: \u0026#39;int\u0026#39; and \u0026#39;str\u0026#39; 不是相同的类型不能直接相加。\nJavaScript 才是弱类型语言，不同数据类型可以直接相加。\n小结 本文虽然参考了菜鸟教程，但是重新组织了写作手法。先介绍了版本选择和语法的概念，接着从一个例子讲起，把相关的概念挨个讲解了一遍，然后简单提到了模块和包，引出了 import 的原因，最后说明了 Python 是动态、强类型语言。\n参考资料：\nhttps://www.runoob.com/python3/python3-basic-syntax.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000002-python%E7%9C%9F%E9%A6%99%E7%90%86%E8%A7%A3%E8%AE%B0%E5%BF%86%E6%B3%95%E5%AD%A6%E4%B9%A0python%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95/","summary":"这篇文章很难写！我最开始学 Python，和大多数人一样，是看的菜鸟教程：","title":"真香，理解记忆法学习Python基础语法"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n正则表达式是什么？ 正则表达式，是一个表达式，用少量的简单字符，来表示复杂的长字符串。\n实际工作用到很多，比如日志搜索、查找替换、JMeter 响应数据提取等。\n先看一个例子，用户名包含字符、数字、下划线和连字符，并限制字符的个数：\n这个表达式可以接受 john_doe、jo-hn_doe、john12_as，但是无法匹配到 Jo，因为它包含了大写的字母而且太短了。\n字符匹配 正则表达式之所以强大，是因为通过一些特殊字符，能让它匹配到更多字符串。\n表达式. 匹配任意单个字符除了换行符。\n例：.ar =\u0026gt; The car parked in the garage.\n表达式[] 字符集。匹配方括号内的任意字符。并且字符是无先后顺序的。\n例：[Tt]he =\u0026gt; The car parked in the garage.\n方括号的句号就表示句号。\n例：ar[.] =\u0026gt; A garage is a good place to park a car.\n表达式[^] 否定的字符集。匹配除了方括号里的任意字符。\n例：[^c]ar =\u0026gt; The car parked in the garage.\n表达式* 匹配 \u0026gt;= 0 个重复的在 * 号之前的字符。\n例：a* =\u0026gt; babaa123.\n例：[a-z]* =\u0026gt; The car parked in the garage #21.\n* 字符和 . 字符搭配可以匹配所有的字符 .*。\n表达式+ 匹配 \u0026gt;= 1个重复的 + 号前的字符。\n例：c.+t =\u0026gt; The fat cat sat on the mat.\n表达式? 标记 ? 之前的字符为可选，即出现 0 或 1 次。\n例：[T]?he =\u0026gt; The car is parked in the garage.\n表达式{n,m} 匹配 num 个大括号之前的字符或字符集 (n \u0026lt;= num \u0026lt;= m)。\n例：[0-9]{2,3} =\u0026gt; The number was 9.9997 but we rounded it off to 10.0.\n可以省略第二个参数，表示至少。\n例：[0-9]{2,} =\u0026gt; The number was 9.9997 but we rounded it off to 10.0.\n逗号也省略掉则表示重复固定的次数。\n例：[0-9]{3} =\u0026gt; The number was 9.9997 but we rounded it off to 10.0.\n表达式(xyz) 字符集，匹配与 xyz 完全相等的字符串，括号内的被视为一个整体。\nJMeter 中，通过小括号来提取字符串中的子字符串。\n例：\u0026quot;token\u0026quot;:\u0026quot;(.*?)\u0026quot;,\u0026quot;expired_at\u0026quot; =\u0026gt; \u0026ldquo;token\u0026rdquo;:\u0026quot;123\u0026quot;,\u0026ldquo;expired_at\u0026rdquo;\n表达式| 或运算符，匹配符号前或后的字符。\n例：\u0026quot;(T|t)he|car\u0026quot; =\u0026gt; The car is parked in the garage.\n表达式^ 字符串开头。\n例：\u0026quot;^(T|t)he\u0026quot; =\u0026gt; The car is parked in the garage.\n表达式$ 字符串结尾。\n例：\u0026quot;(at\\.)$\u0026quot; =\u0026gt; The fat cat. sat. on the mat.\n转义 对于正则表达式的这些特殊字符：\n{ } [ ] ( ) / \\ + * . $ ^ | ? 如果需要匹配，那么需要使用反斜杠 \\ 进行转义。\n例：\u0026quot;(f|c|m)at\\.?\u0026quot; =\u0026gt; The fat cat sat on the mat.\n简写字符集 贪婪匹配与非贪婪匹配 正则表达式默认是贪婪匹配，也就是会尽可能多的匹配字符串。可以使用 ? 将贪婪匹配模式转化为非贪婪匹配模式。\n例：\n默认贪婪：\n使用 ? 转为非贪婪：\n忽略大小写 不同编程语言写法不同，比如 Python 可以通过 re.compile('pattern', re.I) 来忽略大小写。\n在线测试 https://tool.oschina.net/regex/\n小结 本文首先通过一个例子介绍了正则表达式是什么样子的，接着对字符匹配的特殊字符进行了讲解和示例说明，然后给出了简写字符集。正则表达式默认是贪婪模式，可以使用 ? 来转为非贪婪。也可以添加参数，让正则表达式忽略大小写等。最后给出了在线测试正则表达式地址，方便调试看结果。本文内容已经足够在工作中使用，更多高级用法可以通过参考资料进一步阅读。\n参考资料：\nhttps://en.wikipedia.org/wiki/Regular_expression\nhttps://docs.python.org/3/library/re.html\nhttps://github.com/ziishaned/learn-regex\nhttps://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%9F%A5%E8%AF%86/000001-5%E5%88%86%E9%92%9F%E8%BD%BB%E6%9D%BE%E5%AD%A6%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"正则表达式是什么\"\u003e正则表达式是什么？\u003c/h1\u003e\n\u003cp\u003e正则表达式，是一个表达式，用少量的简单字符，来表示复杂的长字符串。\u003c/p\u003e\n\u003cp\u003e实际工作用到很多，比如日志搜索、查找替换、JMeter 响应数据提取等。\u003c/p\u003e\n\u003cp\u003e先看一个例子，用户名包含字符、数字、下划线和连字符，并限制字符的个数：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0b7069281bf04135b6d4b9d6ec11ed20.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这个表达式可以接受 \u003ccode\u003ejohn_doe\u003c/code\u003e、\u003ccode\u003ejo-hn_doe\u003c/code\u003e、\u003ccode\u003ejohn12_as\u003c/code\u003e，但是无法匹配到 \u003ccode\u003eJo\u003c/code\u003e，因为它包含了大写的字母而且太短了。\u003c/p\u003e\n\u003ch1 id=\"字符匹配\"\u003e字符匹配\u003c/h1\u003e\n\u003cp\u003e正则表达式之所以强大，是因为通过一些特殊字符，能让它匹配到更多字符串。\u003c/p\u003e\n\u003ch2 id=\"表达式\"\u003e表达式\u003ccode\u003e.\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003e匹配任意单个字符除了换行符。\u003c/p\u003e\n\u003cp\u003e例：\u003ccode\u003e.ar \u003c/code\u003e=\u0026gt; The \u003cfont color=red\u003ecar\u003c/font\u003e \u003cfont color=red\u003epar\u003c/font\u003eked in the \u003cfont color=red\u003egar\u003c/font\u003eage.\u003c/p\u003e\n\u003ch2 id=\"表达式-1\"\u003e表达式\u003ccode\u003e[]\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003e字符集。匹配方括号内的任意字符。并且字符是无先后顺序的。\u003c/p\u003e\n\u003cp\u003e例：\u003ccode\u003e[Tt]he\u003c/code\u003e =\u0026gt; \u003cfont color=red\u003eThe\u003c/font\u003e car parked in \u003cfont color=red\u003ethe\u003c/font\u003e garage.\u003c/p\u003e\n\u003cp\u003e方括号的句号就表示句号。\u003c/p\u003e\n\u003cp\u003e例：\u003ccode\u003ear[.]\u003c/code\u003e =\u0026gt; A garage is a good place to park a c\u003cfont color=red\u003ear.\u003c/font\u003e\u003c/p\u003e\n\u003ch2 id=\"表达式-2\"\u003e表达式\u003ccode\u003e[^]\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003e否定的字符集。匹配除了方括号里的任意字符。\u003c/p\u003e\n\u003cp\u003e例：\u003ccode\u003e[^c]ar\u003c/code\u003e =\u0026gt; The \u003cfont color=gray\u003ecar\u003c/font\u003e \u003cfont color=red\u003epar\u003c/font\u003eked in the \u003cfont color=red\u003egar\u003c/font\u003eage.\u003c/p\u003e\n\u003ch2 id=\"表达式-3\"\u003e表达式\u003ccode\u003e*\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003e匹配 \u0026gt;= 0 个重复的在 * 号之前的字符。\u003c/p\u003e\n\u003cp\u003e例：\u003ccode\u003ea*\u003c/code\u003e =\u0026gt; b\u003cfont color=red\u003ea\u003c/font\u003eb\u003cfont color=red\u003eaa\u003c/font\u003e123.\u003c/p\u003e\n\u003cp\u003e例：\u003ccode\u003e[a-z]*\u003c/code\u003e =\u0026gt; T\u003cfont color=red\u003ehe car parked in the garage\u003c/font\u003e #21.\u003c/p\u003e","title":"5分钟轻松学正则表达式"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n简介 MCP是什么？通俗易懂地讲，在互联网时代，应用程序是通过HTTP来交互的，而来到AI时代，大模型可以通过MCP来进行交互。MCP是Model Context Protocol缩写，专为大模型（LLM，Large Language Model）设计的大模型上下文协议。\nMCP Server是什么？MCP Server就像网关之于应用程序，数据库连接池之于数据库，位于中间位置，接收请求，调用大模型。可以调用Server进行业务开发，也可以自建Server实现特定功能。\nMCP Python SDK是什么？MCP的Python实现，它包含以下功能（官方原文）：\nBuild MCP clients that can connect to any MCP server Create MCP servers that expose resources, prompts and tools Use standard transports like stdio, SSE, and Streamable HTTP Handle all MCP protocol messages and lifecycle events 安装 推荐使用uv工具来管理Python项目。\n安装3.10以上版本Python：\nuv python install 3.10 创建示例项目：\nuv init mcp-server-demo cd mcp-server-demo 创建虚拟环境：\nuv venv ### 如果当前不是默认Python3.10，需指定版本 uv venv --python 3.10 执行后会提示：\nCreating virtual environment at: .venv Activate with: source .venv/bin/activate 激活：\nsource .venv/bin/activate 安装MCP Python SDK：\nuv add \u0026#34;mcp[cli]\u0026#34; 快速入门 ### server.py from mcp.server.fastmcp import FastMCP ### Create an MCP server mcp = FastMCP(\u0026#34;server\u0026#34;) ### Add a dynamic greting resource @mcp.resource(\u0026#34;greeting://{name}\u0026#34;) def get_greeting(name: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Get a personalized greeting\u0026#34;\u0026#34;\u0026#34; return f\u0026#34;Hello, {name}!\u0026#34; ### Add an addition tool @mcp.tool() def add(a: int, b: int) -\u0026gt; int: \u0026#34;\u0026#34;\u0026#34;Add two numbers\u0026#34;\u0026#34;\u0026#34; return a + b @mcp.prompt() def review_code(code: str) -\u0026gt; str: return f\u0026#34;Please review this code:\\n\\n{code}\u0026#34; if __name__ == \u0026#34;__main__\u0026#34;: # Initialize and run the server mcp.run(transport=\u0026#39;stdio\u0026#39;) 这里创建了1个简单的Mock Server，它定义了3个函数，在进行一些配置后，使用自然语言向大模型提问，大模型就会调用相应的函数获取结果：\n@mcp.resource，相当于GET，加载信息到大模型上下文\n@mcp.tool，相当于POST，执行某些操作\n@mcp.prompt，提示词，大模型交互的模版\n如果想要调试，执行命令mcp dev server.py，打开自带Inspector在Web浏览器进行调试。\n如果想要大模型响应，需要在大模型的配置文件中配置，配置文件通常为JSON：\n{ \u0026#34;mcpServers\u0026#34;: { \u0026#34;server\u0026#34;: { \u0026#34;command\u0026#34;: \u0026#34;uv\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;--directory\u0026#34;, \u0026#34;/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;server.py\u0026#34; ] } } } 配置好以后，给大模型提问，比如：\u0026ldquo;使用server打个招呼吧，我的名字是Gang\u0026rdquo;。大模型的回答是：\n用户要求使用MCP服务器\u0026#34;server\u0026#34;来打招呼，名字是\u0026#34;Gang\u0026#34; 查看已连接的MCP服务器，发现\u0026#34;server\u0026#34;服务器提供了一个\u0026#34;greeting\u0026#34;资源模板 需要获取这个资源并传入用户名参数 使用get_mcp_resource工具，指定server_name为\u0026#34;server\u0026#34;，uri为\u0026#34;greeting://Gang\u0026#34; 想要在 server MCP 服务器上访问一个资源： greeting://Gang get_greeting: Get a personalized greeting 返回值 未知类型 响应 Hello, Gang! 已使用server MCP服务器完成打招呼任务，返回的问候语是：\u0026#34;Hello, Gang!\u0026#34; 这个回答有点啰嗦，继续对话：\u0026ldquo;不要中间这些描述，直接返回结果\u0026rdquo;\nHello, Gang! 在实际应用时，也是需要优化对话来让大模型按照我们预期输出结果。\n示例 基于MCP Server实现天气服务器：\n### weather.py from typing import Any import httpx from mcp.server.fastmcp import FastMCP ### Initialize FastMCP server mcp = FastMCP(\u0026#34;weather\u0026#34;) ### Constants NWS_API_BASE = \u0026#34;https://api.weather.gov\u0026#34; USER_AGENT = \u0026#34;weather-app/1.0\u0026#34; 从国家气象局获取天气数据并格式化：\nasync def make_nws_request(url: str) -\u0026gt; dict[str, Any] | None: \u0026#34;\u0026#34;\u0026#34;Make a request to the NWS API with proper error handling.\u0026#34;\u0026#34;\u0026#34; headers = { \u0026#34;User-Agent\u0026#34;: USER_AGENT, \u0026#34;Accept\u0026#34;: \u0026#34;application/geo+json\u0026#34; } async with httpx.AsyncClient() as client: try: response = await client.get(url, headers=headers, timeout=30.0) response.raise_for_status() return response.json() except Exception: return None def format_alert(feature: dict) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Format an alert feature into a readable string.\u0026#34;\u0026#34;\u0026#34; props = feature[\u0026#34;properties\u0026#34;] return f\u0026#34;\u0026#34;\u0026#34; Event: {props.get(\u0026#39;event\u0026#39;, \u0026#39;Unknown\u0026#39;)} Area: {props.get(\u0026#39;areaDesc\u0026#39;, \u0026#39;Unknown\u0026#39;)} Severity: {props.get(\u0026#39;severity\u0026#39;, \u0026#39;Unknown\u0026#39;)} Description: {props.get(\u0026#39;description\u0026#39;, \u0026#39;No description available\u0026#39;)} Instructions: {props.get(\u0026#39;instruction\u0026#39;, \u0026#39;No specific instructions provided\u0026#39;)} \u0026#34;\u0026#34;\u0026#34; 实现工具：\n@mcp.tool() async def get_alerts(state: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Get weather alerts for a US state. Args: state: Two-letter US state code (e.g. CA, NY) \u0026#34;\u0026#34;\u0026#34; url = f\u0026#34;{NWS_API_BASE}/alerts/active/area/{state}\u0026#34; data = await make_nws_request(url) if not data or \u0026#34;features\u0026#34; not in data: return \u0026#34;Unable to fetch alerts or no alerts found.\u0026#34; if not data[\u0026#34;features\u0026#34;]: return \u0026#34;No active alerts for this state.\u0026#34; alerts = [format_alert(feature) for feature in data[\u0026#34;features\u0026#34;]] return \u0026#34;\\n---\\n\u0026#34;.join(alerts) @mcp.tool() async def get_forecast(latitude: float, longitude: float) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Get weather forecast for a location. Args: latitude: Latitude of the location longitude: Longitude of the location \u0026#34;\u0026#34;\u0026#34; # First get the forecast grid endpoint points_url = f\u0026#34;{NWS_API_BASE}/points/{latitude},{longitude}\u0026#34; points_data = await make_nws_request(points_url) if not points_data: return \u0026#34;Unable to fetch forecast data for this location.\u0026#34; # Get the forecast URL from the points response forecast_url = points_data[\u0026#34;properties\u0026#34;][\u0026#34;forecast\u0026#34;] forecast_data = await make_nws_request(forecast_url) if not forecast_data: return \u0026#34;Unable to fetch detailed forecast.\u0026#34; # Format the periods into a readable forecast periods = forecast_data[\u0026#34;properties\u0026#34;][\u0026#34;periods\u0026#34;] forecasts = [] for period in periods[:5]: # Only show next 5 periods forecast = f\u0026#34;\u0026#34;\u0026#34; {period[\u0026#39;name\u0026#39;]}: Temperature: {period[\u0026#39;temperature\u0026#39;]}°{period[\u0026#39;temperatureUnit\u0026#39;]} Wind: {period[\u0026#39;windSpeed\u0026#39;]} {period[\u0026#39;windDirection\u0026#39;]} Forecast: {period[\u0026#39;detailedForecast\u0026#39;]} \u0026#34;\u0026#34;\u0026#34; forecasts.append(forecast) return \u0026#34;\\n---\\n\u0026#34;.join(forecasts) 编写启动服务代码：\nif __name__ == \u0026#34;__main__\u0026#34;: # Initialize and run the server mcp.run(transport=\u0026#39;stdio\u0026#39;) 完整代码链接：https://github.com/modelcontextprotocol/quickstart-resources/blob/main/weather-server-python/weather.py\n使用uv run weather.py运行，此时看不到任何效果，没有报错即可。\n接下来需要找个大模型，比如Claude桌面版，在配置文件中添加：\n~/Library/Application Support/Claude/claude_desktop_config.json\n{ \u0026#34;mcpServers\u0026#34;: { \u0026#34;weather\u0026#34;: { \u0026#34;command\u0026#34;: \u0026#34;uv\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;--directory\u0026#34;, \u0026#34;/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;weather.py\u0026#34; ] } } } FastMCP能通过Python类型注解（Type Hints）和文档字符串（Docstrings）自动生成工具定义，所以向大模型提问：What\u0026rsquo;s the weather in New York tomorrow?（因为这个示例对接的是美国国家气象局服务，只能问美国天气），它就能知道你想问天气，然后根据函数的文档字符串，找到相应的工具：\n@mcp.tool() async def get_alerts(state: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Get weather alerts for a US state. Args: state: Two-letter US state code (e.g. CA, NY) \u0026#34;\u0026#34;\u0026#34; @mcp.tool() async def get_forecast(latitude: float, longitude: float) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Get weather forecast for a location. Args: latitude: Latitude of the location longitude: Longitude of the location \u0026#34;\u0026#34;\u0026#34; 具体流程如下：\n客户端把问题发给大模型\n大模型分析可用工具并决定调用哪些工具\n客户端通过MCP服务器执行工具\n执行结果回传大模型\n大模型根据结果组织自然语言\n将响应返给客户端进行展示\n总结，MCP是应用与大模型之间交互的桥梁，想要自动生成测试用例，可以创建MCP Server，提供需求和代码给大模型进行分析，并要求它按照固定格式输出测试用例。当然前提是要有1个懂业务懂代码的大模型支持。随着AI技术发展，大模型迭代创新，想象中的大模型终有一天会照进现实。彼时我们编写测试工具，可能就是通过MCP来实现了，就像现在通过HTTP接口编写自动化一样。\n参考资料：\nhttps://pypi.org/project/mcp/\nhttps://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E6%B5%8B%E8%AF%95/000001-ai%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7%E5%BC%80%E5%8F%91%E6%96%B0%E5%A7%BF%E5%8A%BFmcp-python-sdk/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"简介\"\u003e简介\u003c/h1\u003e\n\u003cp\u003eMCP是什么？通俗易懂地讲，在互联网时代，应用程序是通过HTTP来交互的，而来到AI时代，大模型可以通过MCP来进行交互。MCP是Model Context Protocol缩写，专为大模型（LLM，Large Language Model）设计的大模型上下文协议。\u003c/p\u003e\n\u003cp\u003eMCP Server是什么？MCP Server就像网关之于应用程序，数据库连接池之于数据库，位于中间位置，接收请求，调用大模型。可以调用Server进行业务开发，也可以自建Server实现特定功能。\u003c/p\u003e\n\u003cp\u003eMCP Python SDK是什么？MCP的Python实现，它包含以下功能（官方原文）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBuild MCP clients that can connect to any MCP server\u003c/li\u003e\n\u003cli\u003eCreate MCP servers that expose resources, prompts and tools\u003c/li\u003e\n\u003cli\u003eUse standard transports like stdio, SSE, and Streamable HTTP\u003c/li\u003e\n\u003cli\u003eHandle all MCP protocol messages and lifecycle events\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"安装\"\u003e安装\u003c/h1\u003e\n\u003cp\u003e推荐使用uv工具来管理Python项目。\u003c/p\u003e\n\u003cp\u003e安装3.10以上版本Python：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003euv python install 3.10\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e创建示例项目：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003euv init mcp-server-demo\ncd mcp-server-demo\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e创建虚拟环境：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003euv venv\n### 如果当前不是默认Python3.10，需指定版本\nuv venv --python 3.10\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e执行后会提示：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e激活：\u003c/p\u003e","title":"AI测试工具开发新姿势MCP Python SDK"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nWhat-什么是CodeReview？ CodeReview，即代码评审，是白盒测试中，静态测试的一种方法，通过阅读代码，提出代码层面的缺陷。\nWhy-为什么做CodeReview？ 测试左移：在测试前就发现代码缺陷。\n明确测试范围：根据需求文档和代码，对齐测试范围，防止研发夹带本次需求范围外的代码上线。\n提升代码能力：学习研发优秀的编程技术。\n深入原理：了解系统内部结构及实现。\n发现隐藏缺陷：能发现某些极端场景下的问题或概率性问题。\n减少测试研发周期占比：节省用例执行个数，尤其多个同类异常梳理，不用执行类似用例，CodeReview一行代码即可。\nWho-谁适合做CodeReview？ CodeReview需要三大门槛：\n①熟悉编程语言，Java/Python/Go等。\n②熟悉编程规范，《阿里巴巴XXX编码规范》、《华为XXX编码规范》、《京东XXX编码规范》。\n③了解需求、架构和设计，需求文档/架构图/设计文档。\n达到门槛才适合做CodeReview。\nWhen-什么时候做CodeReview？ 提测前，通过Code Diff Review，自己走读代码或者参与研发代码评审会议均可，\n发现功能缺陷时，通过走查代码定位代码原因，并分析是否有关联缺陷。\n修复缺陷时，阅读研发commit记录，检查是否引入新缺陷。\n上线前，通过merge范围检查是否有夹带代码，为了测试改动的临时代码是否恢复。\nWhere-在哪里做CodeReview？ 线上仓库（如Gitlab），适合于修改点单一，不需要查看上下文信息就能找到缺陷。直接在网页端评审代码且发表comment。\n本地仓库（如IDEA），适合于修改点较多，需要查看上下文信息（变量或方法跳转到定义或使用之处）。在本地找到缺陷后在线上发表comment。\nHow-CodeReview如何上手？ 准备：了解需求、架构和设计；学习编程语言；学习代码规范；\n实践：坚持CodeReview，记录缺陷，定期总结。如果看代码找缺陷比较困难，可以先从发现功能缺陷后，定位代码原因入手。\n常见CR缺陷：\n除数为0、整数溢出、精度损失；\n可能死循环；\n在finally程序块中关闭或者释放资源；\n异常未处理或提示不明确（没有catch异常，集合等没有判空和长度为0）；\n公式计算错误；\n字符串对比不能用==，使用equals；\n数组可能越界；\n传递引用错误；\n类型转换错误；\n条件范围选择错误；\n重点检查项：\n一致性检查：是否需求相关；是否和方案设计一致；代码风格、日志规范、异常处理等是否和统一规范一致；\n完整性检查：服务层是否包含了所有业务功能；数据层是否包含所有需要的数据和操作；\n正确性检查：计算逻辑等业务逻辑是否正确；变量是否被正确定义和使用；代码是否符合制定的标准；\n可修改性检查：代码涉及到常量是否易于修改，比如使用配置，定义为常量类等；建议业务方法只有一个出口和一个入口（异常处理除外）；重要公共方法是否有交叉注释说明；重要的对外方法修改后影响多个下游接口；\n可预测性检查：变量初始化；方法稳定性；代码是否存在死循环；代码无穷递归检查；\n健壮性检查：代码是否采取措施避免运行时错误（如数组边界溢出、被零除、值越界、堆栈溢出等）\n高可用性检查：是否有预案（降级开发、限流配置、兜底策略）；补偿方案是否合理；\n结构性检查：程序的每个功能是否都作为一个可辨识的代码块存在；\n可理解性检查：是否使用到不明确或不必要的复杂代码；代码中的算法是否符合开发文档中描述的数学模型；每个变量都定义了合法的取值范围；\n可验证性检查：代码功能是否便于测试；单元测试覆盖度是否足够；\n可追溯性检查：代码是否包括一个修订历史记录，记录中对代码的修改和原因都有记录；\n编码规范：\n《阿里巴巴Java开发手册》\n最新版本：黄山版（2022.2.3发布）\nhttps://github.com/alibaba/p3c\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E7%99%BD%E7%9B%92%E6%B5%8B%E8%AF%95/000001-codereview%E6%8A%80%E5%B7%A7%E5%92%8C%E8%A7%84%E8%8C%83/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"what-什么是codereview\"\u003eWhat-什么是CodeReview？\u003c/h1\u003e\n\u003cp\u003eCodeReview，即代码评审，是白盒测试中，静态测试的一种方法，通过阅读代码，提出代码层面的缺陷。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/58e1d84948084828a44a609553cbbe3f.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"why-为什么做codereview\"\u003eWhy-为什么做CodeReview？\u003c/h1\u003e\n\u003cp\u003e测试左移：在测试前就发现代码缺陷。\u003c/p\u003e\n\u003cp\u003e明确测试范围：根据需求文档和代码，对齐测试范围，防止研发夹带本次需求范围外的代码上线。\u003c/p\u003e\n\u003cp\u003e提升代码能力：学习研发优秀的编程技术。\u003c/p\u003e\n\u003cp\u003e深入原理：了解系统内部结构及实现。\u003c/p\u003e\n\u003cp\u003e发现隐藏缺陷：能发现某些极端场景下的问题或概率性问题。\u003c/p\u003e\n\u003cp\u003e减少测试研发周期占比：节省用例执行个数，尤其多个同类异常梳理，不用执行类似用例，CodeReview一行代码即可。\u003c/p\u003e\n\u003ch1 id=\"who-谁适合做codereview\"\u003eWho-谁适合做CodeReview？\u003c/h1\u003e\n\u003cp\u003eCodeReview需要三大门槛：\u003c/p\u003e\n\u003cp\u003e①熟悉编程语言，Java/Python/Go等。\u003c/p\u003e\n\u003cp\u003e②熟悉编程规范，《阿里巴巴XXX编码规范》、《华为XXX编码规范》、《京东XXX编码规范》。\u003c/p\u003e\n\u003cp\u003e③了解需求、架构和设计，需求文档/架构图/设计文档。\u003c/p\u003e\n\u003cp\u003e达到门槛才适合做CodeReview。\u003c/p\u003e\n\u003ch1 id=\"when-什么时候做codereview\"\u003eWhen-什么时候做CodeReview？\u003c/h1\u003e\n\u003cp\u003e提测前，通过Code Diff Review，自己走读代码或者参与研发代码评审会议均可，\u003c/p\u003e\n\u003cp\u003e发现功能缺陷时，通过走查代码定位代码原因，并分析是否有关联缺陷。\u003c/p\u003e\n\u003cp\u003e修复缺陷时，阅读研发commit记录，检查是否引入新缺陷。\u003c/p\u003e\n\u003cp\u003e上线前，通过merge范围检查是否有夹带代码，为了测试改动的临时代码是否恢复。\u003c/p\u003e\n\u003ch1 id=\"where-在哪里做codereview\"\u003eWhere-在哪里做CodeReview？\u003c/h1\u003e\n\u003cp\u003e线上仓库（如Gitlab），适合于修改点单一，不需要查看上下文信息就能找到缺陷。直接在网页端评审代码且发表comment。\u003c/p\u003e\n\u003cp\u003e本地仓库（如IDEA），适合于修改点较多，需要查看上下文信息（变量或方法跳转到定义或使用之处）。在本地找到缺陷后在线上发表comment。\u003c/p\u003e\n\u003ch1 id=\"how-codereview如何上手\"\u003eHow-CodeReview如何上手？\u003c/h1\u003e\n\u003cp\u003e准备：了解需求、架构和设计；学习编程语言；学习代码规范；\u003c/p\u003e\n\u003cp\u003e实践：坚持CodeReview，记录缺陷，定期总结。如果看代码找缺陷比较困难，可以先从发现功能缺陷后，定位代码原因入手。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e常见CR缺陷：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e除数为0、整数溢出、精度损失；\u003c/p\u003e\n\u003cp\u003e可能死循环；\u003c/p\u003e\n\u003cp\u003e在finally程序块中关闭或者释放资源；\u003c/p\u003e\n\u003cp\u003e异常未处理或提示不明确（没有catch异常，集合等没有判空和长度为0）；\u003c/p\u003e\n\u003cp\u003e公式计算错误；\u003c/p\u003e\n\u003cp\u003e字符串对比不能用==，使用equals；\u003c/p\u003e\n\u003cp\u003e数组可能越界；\u003c/p\u003e\n\u003cp\u003e传递引用错误；\u003c/p\u003e\n\u003cp\u003e类型转换错误；\u003c/p\u003e\n\u003cp\u003e条件范围选择错误；\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e重点检查项：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e一致性检查：是否需求相关；是否和方案设计一致；代码风格、日志规范、异常处理等是否和统一规范一致；\u003c/p\u003e\n\u003cp\u003e完整性检查：服务层是否包含了所有业务功能；数据层是否包含所有需要的数据和操作；\u003c/p\u003e\n\u003cp\u003e正确性检查：计算逻辑等业务逻辑是否正确；变量是否被正确定义和使用；代码是否符合制定的标准；\u003c/p\u003e\n\u003cp\u003e可修改性检查：代码涉及到常量是否易于修改，比如使用配置，定义为常量类等；建议业务方法只有一个出口和一个入口（异常处理除外）；重要公共方法是否有交叉注释说明；重要的对外方法修改后影响多个下游接口；\u003c/p\u003e\n\u003cp\u003e可预测性检查：变量初始化；方法稳定性；代码是否存在死循环；代码无穷递归检查；\u003c/p\u003e\n\u003cp\u003e健壮性检查：代码是否采取措施避免运行时错误（如数组边界溢出、被零除、值越界、堆栈溢出等）\u003c/p\u003e\n\u003cp\u003e高可用性检查：是否有预案（降级开发、限流配置、兜底策略）；补偿方案是否合理；\u003c/p\u003e\n\u003cp\u003e结构性检查：程序的每个功能是否都作为一个可辨识的代码块存在；\u003c/p\u003e\n\u003cp\u003e可理解性检查：是否使用到不明确或不必要的复杂代码；代码中的算法是否符合开发文档中描述的数学模型；每个变量都定义了合法的取值范围；\u003c/p\u003e\n\u003cp\u003e可验证性检查：代码功能是否便于测试；单元测试覆盖度是否足够；\u003c/p\u003e\n\u003cp\u003e可追溯性检查：代码是否包括一个修订历史记录，记录中对代码的修改和原因都有记录；\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e编码规范：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e《阿里巴巴Java开发手册》\u003c/p\u003e\n\u003cp\u003e最新版本：黄山版（2022.2.3发布）\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/alibaba/p3c\"\u003ehttps://github.com/alibaba/p3c\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3d60395eb6974806aabce4480055cbd8.png\"\u003e\u003c/p\u003e","title":"CodeReview技巧和规范"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nDjango 是 Python 的 一款 Web 开发框架，另外还有 Tornado，Flask，Twisted。为什么我要选择学 Django？原因很简单，上家公司来了个网易的测开，就是用 Django 开发的测试平台。\n这位测开没多久就离职去腾讯了，我啥也没学到，看了他的代码，很多是写的 Vue 代码，哭啊。\nDjango 诞生于 2003 年的秋天，由 Lawrence Journal-World 报纸的程序员 Adrian Holovaty 和 Simon Willison 编写而成。新闻编辑室的开发节奏是非常快的，正因如此，Django 相比于其他框架的特点就是短、平、快。这也符合 Python 的风格。时至今日，Django 已经发展到了 3.1.3 版本。本系列将基于这一版本的官方文档，边学习边实践，整理成文，分享给大家。\nDjango 系列不是教程，而是学习笔记、心得体会、踩坑记录，内容编排上可能会有点乱。需要看教程请阅读官方文档，水平有限，实在抱歉。\nDjango 遵循 MVC 架构模式，所以接下来就看看如何使用 Django 完成 Web 开发。特别注意，本文的内容不具有实操性，看看即可。\n定义 model model 是数据模型，定义了数据库的表和字段。\n例如：\nfrom django.db import models class Reporter(models.Model): full_name = models.CharField(max_length=70) def __str__(self): return self.full_name class Article(models.Model): pub_date = models.DateField() headline = models.CharField(max_length=200) content = models.TextField() reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE) def __str__(self): return self.headline 通过类和属性，分别定义了 2 张表 Reporter 和 Article，以及它们的字段（Reporter 1 个字段，Article 4 个字段）。\n这其实就是 ORM，即 Object Relational Mapping，对象关系映射，把程序代码中的对象映射到关系型数据库中，不用写 SQL，就可以直接操作数据了。ORM 实现了数据持久化。我们都知道程序是运行在内存中的，跑完就没了。为了把数据保存下来，就需要使用 ORM 技术把内存中的数据（程序对象）存到关系型数据库中，进而转移到磁盘上。Django 自带了一个 ORM，开箱即用。\n数据迁移 使用 2 条命令，就可以把 model 迁移到数据库中：\n$ python manage.py makemigrations $ python manage.py migrate 执行完成后，就会在数据库中按照 model 定义的表名、字段名、约束条件等，创建表结构。\n数据操作 接着就可以在程序中写代码操作数据了。为了直观看到结果，这里以命令行形式进行演示：\n# 导入已创建的 models \u0026gt;\u0026gt;\u0026gt; from news.models import Article, Reporter # 查询表 Reporter 为空 \u0026gt;\u0026gt;\u0026gt; Reporter.objects.all() \u0026lt;QuerySet []\u0026gt; # 实例化对象，创建 1 条数据，表 Reporter 的字段是 full_name \u0026gt;\u0026gt;\u0026gt; r = Reporter(full_name=\u0026#39;John Smith\u0026#39;) # 必须显式调用 save() 函数，才会真正写数据到数据库 \u0026gt;\u0026gt;\u0026gt; r.save() # 保存后就有 id 了 \u0026gt;\u0026gt;\u0026gt; r.id 1 # 查询表 Reporter 有数据了 \u0026gt;\u0026gt;\u0026gt; Reporter.objects.all() \u0026lt;QuerySet [\u0026lt;Reporter: John Smith\u0026gt;]\u0026gt; # 访问对象属性 \u0026gt;\u0026gt;\u0026gt; r.full_name \u0026#39;John Smith\u0026#39; # Django 提供了 get() 函数来支持更多查询方式 \u0026gt;\u0026gt;\u0026gt; Reporter.objects.get(id=1) \u0026lt;Reporter: John Smith\u0026gt; \u0026gt;\u0026gt;\u0026gt; Reporter.objects.get(full_name__startswith=\u0026#39;John\u0026#39;) \u0026lt;Reporter: John Smith\u0026gt; \u0026gt;\u0026gt;\u0026gt; Reporter.objects.get(full_name__contains=\u0026#39;mith\u0026#39;) \u0026lt;Reporter: John Smith\u0026gt; \u0026gt;\u0026gt;\u0026gt; Reporter.objects.get(id=2) Traceback (most recent call last): ... DoesNotExist: Reporter matching query does not exist. # 给表 Article 添加 1 条数据 # 有 4 个字段 pub_date, headline, content, reporter # reporter=r，用 Reporter 对象赋值 \u0026gt;\u0026gt;\u0026gt; from datetime import date \u0026gt;\u0026gt;\u0026gt; a = Article(pub_date=date.today(), headline=\u0026#39;Django is cool\u0026#39;, ... content=\u0026#39;Yeah.\u0026#39;, reporter=r) \u0026gt;\u0026gt;\u0026gt; a.save() # 表 Article 也有数据了 \u0026gt;\u0026gt;\u0026gt; Article.objects.all() \u0026lt;QuerySet [\u0026lt;Article: Django is cool\u0026gt;]\u0026gt; # a.reporter 可以赋值给 r \u0026gt;\u0026gt;\u0026gt; r = a.reporter \u0026gt;\u0026gt;\u0026gt; r.full_name \u0026#39;John Smith\u0026#39; # r 也可以访问 Article \u0026gt;\u0026gt;\u0026gt; r.article_set.all() \u0026lt;QuerySet [\u0026lt;Article: Django is cool\u0026gt;]\u0026gt; # 可以借助 filter() 函数按条件过滤数据 \u0026gt;\u0026gt;\u0026gt; Article.objects.filter(reporter__full_name__startswith=\u0026#39;John\u0026#39;) \u0026lt;QuerySet [\u0026lt;Article: Django is cool\u0026gt;]\u0026gt; # 赋值后调用 save() 函数更新数据 \u0026gt;\u0026gt;\u0026gt; r.full_name = \u0026#39;Billy Goat\u0026#39; \u0026gt;\u0026gt;\u0026gt; r.save() # 使用 delete() 函数删除对象，数据库这条数据也会被删除 \u0026gt;\u0026gt;\u0026gt; r.delete() 自带 Admin 后台 一般不会用它。\n设计 URLs 我们是通过 URL 发送请求的，服务端程序做处理，处理的函数叫做回调函数。Django 在 urls.py 文件中编写 URL 和回调函数的映射关系。例如：\nfrom django.urls import path from . import views urlpatterns = [ path(\u0026#39;articles/\u0026lt;int:year\u0026gt;/\u0026#39;, views.year_archive), path(\u0026#39;articles/\u0026lt;int:year\u0026gt;/\u0026lt;int:month\u0026gt;/\u0026#39;, views.month_archive), path(\u0026#39;articles/\u0026lt;int:year\u0026gt;/\u0026lt;int:month\u0026gt;/\u0026lt;int:pk\u0026gt;/\u0026#39;, views.article_detail), ] path() 的第一参数是 URL，这里使用了 \u0026lt;\u0026gt; 参数标签来获取 URL 中的数据，然后传入到回调函数中。第二个参数是回调函数，位于 views 中。\n如果请求 URL “/articles/2005/05/39323/”，Django 就会获取参数值后调用回调函数：\nnews.views.article_detail(request, year=2005, month=5, pk=39323) Django 在启动加载时就会把这些 path 编译为正则表达式，查找速度飞快。匹配到第一个后就会停止查找，调用回调函数。如果找完了都没有，就会调用 404 这个特殊 view，表示没找到。\n编写 views 在 views 中编写回调函数。每个回调函数只做 1 件事，要么返回包含响应的 HttpResponse 对象，要么抛出异常，如 Http404 。例如：\nfrom django.shortcuts import render from .models import Article def year_archive(request, year): a_list = Article.objects.filter(pub_date__year=year) context = {\u0026#39;year\u0026#39;: year, \u0026#39;article_list\u0026#39;: a_list} return render(request, \u0026#39;news/year_archive.html\u0026#39;, context) return render() 函数会返回一个 HttpResponse 对象。\n注意，这个例子用到的是 Django 自带的模板引擎。所谓模板引擎，就是前端的 HTML 模板，里面的数据可以写成变量，从后端动态获取。除了内置的这个，还有其他模板引擎如 Thymeleaf、FreeMarker 等。不过这些使用都很少了。现在流行前后端分离，后端不需要写 HTML，只提供 RESTful 接口就可以了。说到 RESTful，就不得不提另外一个 Django 的衍生框架 DRF（Django REST Framework）。一步一步来，先学好了 Django，才能更好理解 DRF。\n内置模板引擎 暂时不做介绍。\n小结 本文以 Web 后台为例，讲解了从 model，到 ORM，到数据操作，到 URL 映射，到 views 回调函数的编写链路。实际操作会复杂得多。以前学其他框架有点懵，写这篇文章，倒是让我明白了 MVC 这一套是这么一回事。\n参考资料：\nhttps://docs.djangoproject.com/en/3.1/intro/overview/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/django/000001-djangodjango%E5%8C%86%E5%8C%86%E4%B8%80%E7%9C%BC%E5%8D%B4%E8%A7%A3%E7%AD%94%E4%BA%86%E5%A4%9A%E5%B9%B4%E7%96%91%E6%83%91/","summary":"Django 是 Python 的 一款 Web 开发框架，另外还有 Tornado，Flask，Twisted。为什么我要选择学 Django？原因很简单，上家公司来了个网易的测开，就是用 Django 开发的测试平台。","title":"Django匆匆一眼却解答了多年疑惑"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nEasyPytest测试平台，帮你高效管理Pytest测试用例。\nEP平台的定位是Pytest测试用例管理平台，80%的自动化用例开发在PyCharm等专业的IDE中进行，20%的用例调度和结果查看就交给EP平台来管理。\nEP平台是对teprunner测试平台的一次全新升级，迭代一的开发目标是将后端Django切换为FastAPI，代码以更合理更精简更规范的方式重构，并做部分功能优化。\n相对于teprunner平台，重新设计的EasyPytest平台有2个较大变化：\n1、用例支持串行和并行（pytest-xdist）；\n2、测试计划的结果直接链接到Allure HTML报告（FastAPI mount）；\n业务架构图 系统架构图 数据库设计 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `username` varchar(32) NOT NULL COMMENT \u0026#39;用户名\u0026#39;, `nickname` varchar(32) NOT NULL COMMENT \u0026#39;昵称\u0026#39;, `password` varchar(128) NOT NULL COMMENT \u0026#39;密码\u0026#39;, `is_superuser` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否管理员：0-否，1-是\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;用户表\u0026#39;; DROP TABLE IF EXISTS `project`; CREATE TABLE `project` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `name` varchar(64) NOT NULL COMMENT \u0026#39;项目名称\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;项目表\u0026#39;; DROP TABLE IF EXISTS `environment`; CREATE TABLE `environment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `name` varchar(32) NOT NULL COMMENT \u0026#39;环境名称\u0026#39;, `project_id` bigint(20) NOT NULL COMMENT \u0026#39;项目id\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;环境表\u0026#39;; DROP TABLE IF EXISTS `variable`; CREATE TABLE `variable` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `name` varchar(64) NOT NULL COMMENT \u0026#39;变量名\u0026#39;, `value` varchar(512) NOT NULL COMMENT \u0026#39;变量值\u0026#39;, `desc` varchar(256) NOT NULL COMMENT \u0026#39;变量描述\u0026#39;, `project_id` bigint(20) NOT NULL COMMENT \u0026#39;项目id\u0026#39;, `env_id` bigint(20) NOT NULL COMMENT \u0026#39;环境id\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;变量表\u0026#39;; DROP TABLE IF EXISTS `fixture`; CREATE TABLE `fixture` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `name` varchar(64) NOT NULL COMMENT \u0026#39;fixture名称\u0026#39;, `code` text NOT NULL COMMENT \u0026#39;fixture代码\u0026#39;, `desc` varchar(256) NOT NULL COMMENT \u0026#39;fixture描述\u0026#39;, `project_id` bigint(20) NOT NULL COMMENT \u0026#39;项目id\u0026#39;, `creator_id` bigint(20) NOT NULL COMMENT \u0026#39;创建人id\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;用例依赖表\u0026#39;; DROP TABLE IF EXISTS `case`; CREATE TABLE `case` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `name` varchar(64) NOT NULL COMMENT \u0026#39;用例名称\u0026#39;, `code` text NOT NULL COMMENT \u0026#39;用例代码\u0026#39;, `desc` varchar(256) NOT NULL COMMENT \u0026#39;用例描述\u0026#39;, `project_id` bigint(20) NOT NULL COMMENT \u0026#39;项目id\u0026#39;, `creator_id` bigint(20) NOT NULL COMMENT \u0026#39;创建人id\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;用例表\u0026#39;; DROP TABLE IF EXISTS `result`; CREATE TABLE `result` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `case_id` bigint(20) NOT NULL COMMENT \u0026#39;用例id\u0026#39;, `status` varchar(32) NOT NULL COMMENT \u0026#39;结果状态\u0026#39;, `log` text NOT NULL COMMENT \u0026#39;运行日志\u0026#39;, `elapsed` varchar(32) NOT NULL COMMENT \u0026#39;运行耗时\u0026#39;, `run_env_id` bigint(20) NOT NULL COMMENT \u0026#39;运行环境id\u0026#39;, `run_user_id` bigint(20) NOT NULL COMMENT \u0026#39;运行用户id\u0026#39;, `run_time` timestamp NOT NULL COMMENT \u0026#39;运行时间\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;用例结果表\u0026#39;; DROP TABLE IF EXISTS `plan`; CREATE TABLE `plan` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `name` varchar(64) NOT NULL COMMENT \u0026#39;计划名称\u0026#39;, `onoff` tinyint(2) NOT NULL COMMENT \u0026#39;启动开关：0-关闭，1-启动\u0026#39;, `crontab` varchar(128) NOT NULL COMMENT \u0026#39;crontab执行周期表达式\u0026#39;, `run_env_id` bigint(20) NOT NULL COMMENT \u0026#39;运行环境id\u0026#39;, `creator_id` bigint(20) NOT NULL COMMENT \u0026#39;创建人id\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;测试计划表\u0026#39;; DROP TABLE IF EXISTS `plan_case`; CREATE TABLE `plan_case` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `plan_id` bigint(20) NOT NULL COMMENT \u0026#39;计划id\u0026#39;, `case_id` bigint(20) NOT NULL COMMENT \u0026#39;用例id\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;计划用例关系表\u0026#39;; DROP TABLE IF EXISTS `report`; CREATE TABLE `report` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;自增id\u0026#39;, `plan_id` bigint(20) NOT NULL COMMENT \u0026#39;计划id\u0026#39;, `location` varchar(256) NOT NULL COMMENT \u0026#39;报告地址\u0026#39;, `run_env_id` bigint(20) NOT NULL COMMENT \u0026#39;运行环境id\u0026#39;, `run_user_id` bigint(20) NOT NULL COMMENT \u0026#39;运行用户id\u0026#39;, `run_time` timestamp NOT NULL COMMENT \u0026#39;运行时间\u0026#39;, `is_deleted` tinyint(2) DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;是否逻辑删除：0-否，1-是\u0026#39;, `created_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;创建时间\u0026#39;, `updated_at` timestamp NULL DEFAULT NULL COMMENT \u0026#39;最近修改时间\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;测试报告表\u0026#39;; 接口设计 接口文档使用FastAPI生成，源码：\nhttps://gitee.com/dongfanger/ep-api\n项目目录结构设计 参考FastAPI官方项目生成器：\nhttps://github.com/tiangolo/full-stack-fastapi-postgresql\n生成的示例项目：\n设计EP平台的项目目录结构：\n各模块尽量独立，以尽量减少多人协作开发代码冲突，提高编码效率。\nEP平台在开发过程中会不定期发布开发日志，开发完成后也会有完善的帮助手册，后续迭代应该还会采用报名+多人协作开发的模式，欢迎持续关注~\n参考资料：\n开发阶段源码： https://gitee.com/dongfanger/easy-pytest\n发布阶段源码： https://github.com/dongfanger/EasyPytest\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/easypytest/000001-easypytest%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97%E4%B9%8B%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/","summary":"\u003cstrong\u003eEasyPytest测试平台，帮你高效管理Pytest测试用例。\u003c/strong\u003e","title":"EasyPytest测试平台开发日志之系统设计"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nPython第三流行的Web框架 在2020年的Python开发者调查结果中，有这样一段话：“FastAPI在此次调查迭代中首次被引为选项，表现为Python第三流行的Web框架。”\nFastAPI创立于2018年12月，不到2年就成为仅次于Flask和Django的第三流行的Web框架。而又经过了一年发展来到2022年，虽然2021年Python开发者调查结果还没有出来，但是从GitHub的star来看，Flask 58.7k，Django 63.6k，FastAPI 44.2k，这个差距缩得越来越小。\nFastAPI特性 这里就不做机器翻译了，大家看下原文：\n我说下我选择FastAPI的理由：首先就是HttpRunner集成了FastAPI，有大佬背书，相信这个框架足以优秀。其次是注解，用多了SpringBoot以后，越来越喜欢注解，层次清晰。对于前后端分离项目来说，Flask虽然非常精简却又自带了Jinja模板引擎，Django虽然是百宝箱却又显得太重，而FastAPI介于两者之间，就是一个纯粹的后端应用。并且FastAPI是基于Starlette框架的，集成了实用功能比如类型检查、OpenAPI（Swagger）等等，这跟我基于pytest框架做tep测试工具的理念很相似。\n安装 对Python版本要求是3.6+。\n先安装FastAPI：\npip install fastapi 再安装ASGI服务器，比如Uvicorn：\npip install \u0026#34;uvicorn[standard]\u0026#34; 也可以同时安装fastapi和uvicorn：\npip install \u0026#34;fastapi[all]\u0026#34; 运行 写个main.py文件：\nfrom typing import Optional from fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/\u0026#34;) def read_root(): return {\u0026#34;Hello\u0026#34;: \u0026#34;World\u0026#34;} @app.get(\u0026#34;/items/{item_id}\u0026#34;) def read_item(item_id: int, q: Optional[str] = None): return {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;q\u0026#34;: q} 在命令行输入启动应用：\nuvicorn main:app --reload main是Python模块名。\napp是app = FastAPI()。\n--reload在代码变化时自动重启服务器。\n打开浏览器访问：\nhttp://127.0.0.1:8000/items/5?q=somequery\n就能看到JSON响应：\n{\u0026#34;item_id\u0026#34;: 5, \u0026#34;q\u0026#34;: \u0026#34;somequery\u0026#34;} 访问：\nhttp://127.0.0.1:8000/docs\n就能看到Swagger接口文档：\npydantic pydantic是一个数据验证的库，FastAPI使用它来做模型校验。比如：\nfrom typing import Optional from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float is_offer: Optional[bool] = None @app.get(\u0026#34;/\u0026#34;) def read_root(): return {\u0026#34;Hello\u0026#34;: \u0026#34;World\u0026#34;} @app.get(\u0026#34;/items/{item_id}\u0026#34;) def read_item(item_id: int, q: Optional[str] = None): return {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;q\u0026#34;: q} @app.put(\u0026#34;/items/{item_id}\u0026#34;) def update_item(item_id: int, item: Item): return {\u0026#34;item_name\u0026#34;: item.name, \u0026#34;item_id\u0026#34;: item_id} Item是个入参模型，它的name必须str类型，price必须float类型，is_offer是可选的，可以为bool类型或不传。\nPUT http://127.0.0.1:8000/items/6 { \u0026#34;name\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;price\u0026#34;: 2.3, \u0026#34;is_offer\u0026#34;: true } { \u0026#34;item_name\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;item_id\u0026#34;: 6 } 路径参数 把路径参数传递给函数：\nfrom fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/items/{item_id}\u0026#34;) async def read_item(item_id): return {\u0026#34;item_id\u0026#34;: item_id} 也可以指定Python类型：\nfrom fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/items/{item_id}\u0026#34;) async def read_item(item_id: int): return {\u0026#34;item_id\u0026#34;: item_id} 效果是访问 http://127.0.0.1:8000/items/foo 会返回{\u0026quot;item_id\u0026quot;:\u0026quot;foo\u0026quot;}。\n指定了Python类型后，FastAPI会强制检查，比如传str会报错：\nhttp://127.0.0.1:8000/items/foo\n{ \u0026#34;detail\u0026#34;: [ { \u0026#34;loc\u0026#34;: [ \u0026#34;path\u0026#34;, \u0026#34;item_id\u0026#34; ], \u0026#34;msg\u0026#34;: \u0026#34;value is not a valid integer\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;type_error.integer\u0026#34; } ] } 传float也会报错：\nhttp://127.0.0.1:8000/items/4.2\n匹配先后顺序\n代码定义的先后顺序会决定匹配结果，比如正常来说，下面的/users/me会返回{\u0026quot;user_id\u0026quot;: \u0026quot;the current user\u0026quot;}：\nfrom fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/users/me\u0026#34;) async def read_user_me(): return {\u0026#34;user_id\u0026#34;: \u0026#34;the current user\u0026#34;} @app.get(\u0026#34;/users/{user_id}\u0026#34;) async def read_user(user_id: str): return {\u0026#34;user_id\u0026#34;: user_id} 假如这2个path定义顺序反过来，那么/users/me就会匹配到/users/{user_id}而返回{\u0026quot;user_id\u0026quot;: me}了。\n枚举路径\n借助于Enun类，可以实现枚举路径：\nfrom enum import Enum from fastapi import FastAPI class ModelName(str, Enum): alexnet = \u0026#34;alexnet\u0026#34; resnet = \u0026#34;resnet\u0026#34; lenet = \u0026#34;lenet\u0026#34; app = FastAPI() @app.get(\u0026#34;/models/{model_name}\u0026#34;) async def get_model(model_name: ModelName): if model_name == ModelName.alexnet: return {\u0026#34;model_name\u0026#34;: model_name, \u0026#34;message\u0026#34;: \u0026#34;Deep Learning FTW!\u0026#34;} if model_name.value == \u0026#34;lenet\u0026#34;: return {\u0026#34;model_name\u0026#34;: model_name, \u0026#34;message\u0026#34;: \u0026#34;LeCNN all the images\u0026#34;} return {\u0026#34;model_name\u0026#34;: model_name, \u0026#34;message\u0026#34;: \u0026#34;Have some residuals\u0026#34;} 效果：\npath匹配\nFastAPI提供了一个path类型，可以用来对文件路径进行格式匹配：\nfrom fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/files/{file_path:path}\u0026#34;) async def read_file(file_path: str): return {\u0026#34;file_path\u0026#34;: file_path} 查询参数 查询参数是跟在路径参数后面，用?分隔用\u0026amp;连接的参数，比如http://127.0.0.1:8000/items/?skip=0\u0026amp;limit=10。\n实现：\nfrom fastapi import FastAPI app = FastAPI() fake_items_db = [{\u0026#34;item_name\u0026#34;: \u0026#34;Foo\u0026#34;}, {\u0026#34;item_name\u0026#34;: \u0026#34;Bar\u0026#34;}, {\u0026#34;item_name\u0026#34;: \u0026#34;Baz\u0026#34;}] @app.get(\u0026#34;/items/\u0026#34;) async def read_item(skip: int = 0, limit: int = 10): return fake_items_db[skip : skip + limit] 参数是可选的并且设置了默认值：limit: int = 10\n参数是可选的，无默认值：limit: Optional[int] = None\n注意：是否可选是由None来决定的，而Optional只是为编译器提供支持，跟FastAPI没有关系。\n参数是必填的：limit: int\n请求体 FastAPI的请求体借助于pydantic来实现：\nfrom typing import Optional from fastapi import FastAPI from pydantic import BaseModel class Item(BaseModel): name: str description: Optional[str] = None price: float tax: Optional[float] = None app = FastAPI() @app.post(\u0026#34;/items/\u0026#34;) async def create_item(item: Item): return item 继承于BaseModel来自定义Model，FastAPI会自动转换为JSON。\nPydantic PyCharm Plugin插件提高编码体验：\nauto-completion type checks refactoring searching inspections 路径参数+查询参数+请求体\n总结一下，在函数参数中，url path中定义的叫做路径参数，没有定义的叫做查询参数，类型是pydantic model的叫做请求体，FastAPI会根据这套规则来自动识别：\nfrom typing import Optional from fastapi import FastAPI from pydantic import BaseModel class Item(BaseModel): name: str description: Optional[str] = None price: float tax: Optional[float] = None app = FastAPI() @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def create_item(item_id: int, item: Item, q: Optional[str] = None): result = {\u0026#34;item_id\u0026#34;: item_id, **item.dict()} if q: result.update({\u0026#34;q\u0026#34;: q}) return result 查询参数字符串校验 FastAPI提供了Query来支持对入参的字符串校验，比如最小长度和最大长度：\nfrom typing import Optional from fastapi import FastAPI, Query app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items( q: Optional[str] = Query(None, min_length=3, max_length=50, regex=\u0026#34;^fixedquery$\u0026#34;) ): results = {\u0026#34;items\u0026#34;: [{\u0026#34;item_id\u0026#34;: \u0026#34;Foo\u0026#34;}, {\u0026#34;item_id\u0026#34;: \u0026#34;Bar\u0026#34;}]} if q: results.update({\u0026#34;q\u0026#34;: q}) return results 甚至其中也能包含正则表达式：regex=\u0026quot;^fixedquery$\u0026quot;。\n用Query时指定默认值：\nfrom fastapi import FastAPI, Query app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(q: str = Query(\u0026#34;fixedquery\u0026#34;, min_length=3)): results = {\u0026#34;items\u0026#34;: [{\u0026#34;item_id\u0026#34;: \u0026#34;Foo\u0026#34;}, {\u0026#34;item_id\u0026#34;: \u0026#34;Bar\u0026#34;}]} if q: results.update({\u0026#34;q\u0026#34;: q}) return results 用Query时必填：\nfrom fastapi import FastAPI, Query app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(q: str = Query(..., min_length=3)): results = {\u0026#34;items\u0026#34;: [{\u0026#34;item_id\u0026#34;: \u0026#34;Foo\u0026#34;}, {\u0026#34;item_id\u0026#34;: \u0026#34;Bar\u0026#34;}]} if q: results.update({\u0026#34;q\u0026#34;: q}) return results 查询参数传list\nfrom typing import List, Optional from fastapi import FastAPI, Query app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(q: Optional[List[str]] = Query(None)): query_items = {\u0026#34;q\u0026#34;: q} return query_items 指定默认值：\nfrom typing import List from fastapi import FastAPI, Query app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(q: List[str] = Query([\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;])): query_items = {\u0026#34;q\u0026#34;: q} return query_items url就像这样：http://localhost:8000/items/?q=foo\u0026amp;q=bar\n指定别名，比如http://127.0.0.1:8000/items/?item-query=foobaritems中的item-query不是Python变量命名，那么可以设置别名：\nfrom typing import Optional from fastapi import FastAPI, Query app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(q: Optional[str] = Query(None, alias=\u0026#34;item-query\u0026#34;)): results = {\u0026#34;items\u0026#34;: [{\u0026#34;item_id\u0026#34;: \u0026#34;Foo\u0026#34;}, {\u0026#34;item_id\u0026#34;: \u0026#34;Bar\u0026#34;}]} if q: results.update({\u0026#34;q\u0026#34;: q}) return results 路径参数数字校验 查询参数用Query做字符串（String）校验，路径参数用Path做数字（Numeric）校验：\nfrom fastapi import FastAPI, Path app = FastAPI() @app.get(\u0026#34;/items/{item_id}\u0026#34;) async def read_items( *, item_id: int = Path(..., title=\u0026#34;The ID of the item to get\u0026#34;, gt=0, le=1000), q: str, ): results = {\u0026#34;item_id\u0026#34;: item_id} if q: results.update({\u0026#34;q\u0026#34;: q}) return results 路径参数永远都是必填的，因为它是url一部分。...表示必填，就算设置为None也没有用，仍然是必填。\nge表示大于等于，greater equal。\nle表示小于等于，less equal。\ngt表示大于，greater than。\nlt表示小于，less than。\n请求体-多参数 一、如果请求体嵌套了多个JSON：\n{ \u0026#34;item\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The pretender\u0026#34;, \u0026#34;price\u0026#34;: 42.0, \u0026#34;tax\u0026#34;: 3.2 }, \u0026#34;user\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;dave\u0026#34;, \u0026#34;full_name\u0026#34;: \u0026#34;Dave Grohl\u0026#34; } } 那么就需要在FastAPI中定义多参数：\nfrom typing import Optional from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Optional[str] = None price: float tax: Optional[float] = None class User(BaseModel): username: str full_name: Optional[str] = None @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item, user: User): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item, \u0026#34;user\u0026#34;: user} return results 这里定义了2个Model：Item和User。\n二、而如果多个参数中有个参数只是单个值，比如这里的importance：\n{ \u0026#34;item\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The pretender\u0026#34;, \u0026#34;price\u0026#34;: 42.0, \u0026#34;tax\u0026#34;: 3.2 }, \u0026#34;user\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;dave\u0026#34;, \u0026#34;full_name\u0026#34;: \u0026#34;Dave Grohl\u0026#34; }, \u0026#34;importance\u0026#34;: 5 } 那么定义成变量并赋值= Body()即可：\n@app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item, user: User, importance: int = Body()): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item, \u0026#34;user\u0026#34;: user, \u0026#34;importance\u0026#34;: importance} return results 三、在只有一个Item的时候，FastAPI默认会接收这样的body：\n{ \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The pretender\u0026#34;, \u0026#34;price\u0026#34;: 42.0, \u0026#34;tax\u0026#34;: 3.2 } 假如想把item也放到JSON中：\n{ \u0026#34;item\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The pretender\u0026#34;, \u0026#34;price\u0026#34;: 42.0, \u0026#34;tax\u0026#34;: 3.2 } } 那么可以使用Body(embed=True))：\nfrom typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item = Body(embed=True)): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 请求体-字段 Pydantic提供了Field来给body中的字段添加额外校验：\nfrom typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel, Field app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = Field( default=None, title=\u0026#34;The description of the item\u0026#34;, max_length=300 ) price: float = Field(gt=0, description=\u0026#34;The price must be greater than zero\u0026#34;) tax: Union[float, None] = None @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item = Body(embed=True)): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 跟FastAPI提供的Query、Path、Body作用类似。\n请求体-嵌套模型 传List：\nfrom typing import List, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: List[str] = [] @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 传Set，自动去重：\nfrom typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 传Model：\nfrom typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Image(BaseModel): url: str name: str class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() image: Union[Image, None] = None @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 入参会像这样：\n{ \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The pretender\u0026#34;, \u0026#34;price\u0026#34;: 42.0, \u0026#34;tax\u0026#34;: 3.2, \u0026#34;tags\u0026#34;: [\u0026#34;rock\u0026#34;, \u0026#34;metal\u0026#34;, \u0026#34;bar\u0026#34;], \u0026#34;image\u0026#34;: { \u0026#34;url\u0026#34;: \u0026#34;http://example.com/baz.jpg\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;The Foo live\u0026#34; } } 对于url，pydantic提供了HttpUrl来做校验：\nclass Image(BaseModel): url: HttpUrl name: str 传Model的List：\nfrom typing import List, Set, Union from fastapi import FastAPI from pydantic import BaseModel, HttpUrl app = FastAPI() class Image(BaseModel): url: HttpUrl name: str class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: Set[str] = set() images: Union[List[Image], None] = None @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 入参像这样：\n{ \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The pretender\u0026#34;, \u0026#34;price\u0026#34;: 42.0, \u0026#34;tax\u0026#34;: 3.2, \u0026#34;tags\u0026#34;: [ \u0026#34;rock\u0026#34;, \u0026#34;metal\u0026#34;, \u0026#34;bar\u0026#34; ], \u0026#34;images\u0026#34;: [ { \u0026#34;url\u0026#34;: \u0026#34;http://example.com/baz.jpg\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;The Foo live\u0026#34; }, { \u0026#34;url\u0026#34;: \u0026#34;http://example.com/dave.jpg\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;The Baz\u0026#34; } ] } 添加示例请求 通过Config和schema_extra添加示例请求：\nfrom typing import Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None class Config: schema_extra = { \u0026#34;example\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;A very nice Item\u0026#34;, \u0026#34;price\u0026#34;: 35.4, \u0026#34;tax\u0026#34;: 3.2, } } @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 在使用以下任一时，都可以添加example：\nPath() Query() Header() Cookie() Body() Form() File() 比如：\nfrom typing import Union from fastapi import FastAPI from pydantic import BaseModel, Field app = FastAPI() class Item(BaseModel): name: str = Field(example=\u0026#34;Foo\u0026#34;) description: Union[str, None] = Field(default=None, example=\u0026#34;A very nice Item\u0026#34;) price: float = Field(example=35.4) tax: Union[float, None] = Field(default=None, example=3.2) @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item(item_id: int, item: Item): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item( item_id: int, item: Item = Body( example={ \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;A very nice Item\u0026#34;, \u0026#34;price\u0026#34;: 35.4, \u0026#34;tax\u0026#34;: 3.2, }, ), ): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def update_item( *, item_id: int, item: Item = Body( examples={ \u0026#34;normal\u0026#34;: { \u0026#34;summary\u0026#34;: \u0026#34;A normal example\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;A **normal** item works correctly.\u0026#34;, \u0026#34;value\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;A very nice Item\u0026#34;, \u0026#34;price\u0026#34;: 35.4, \u0026#34;tax\u0026#34;: 3.2, }, }, \u0026#34;converted\u0026#34;: { \u0026#34;summary\u0026#34;: \u0026#34;An example with converted data\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;FastAPI can convert price `strings` to actual `numbers` automatically\u0026#34;, \u0026#34;value\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Bar\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;35.4\u0026#34;, }, }, \u0026#34;invalid\u0026#34;: { \u0026#34;summary\u0026#34;: \u0026#34;Invalid data is rejected with an error\u0026#34;, \u0026#34;value\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Baz\u0026#34;, \u0026#34;price\u0026#34;: \u0026#34;thirty five point four\u0026#34;, }, }, }, ), ): results = {\u0026#34;item_id\u0026#34;: item_id, \u0026#34;item\u0026#34;: item} return results 额外数据类型 FastAPI除了支持常见的数据类型：\nint float str bool 还支持额外的数据类型：\nUUID datetime.datetime datetime.date datetime.time datetime.timedelta frozenset bytes Decimal 示例：\nfrom datetime import datetime, time, timedelta from typing import Union from uuid import UUID from fastapi import Body, FastAPI app = FastAPI() @app.put(\u0026#34;/items/{item_id}\u0026#34;) async def read_items( item_id: UUID, start_datetime: Union[datetime, None] = Body(default=None), end_datetime: Union[datetime, None] = Body(default=None), repeat_at: Union[time, None] = Body(default=None), process_after: Union[timedelta, None] = Body(default=None), ): start_process = start_datetime + process_after duration = end_datetime - start_process return { \u0026#34;item_id\u0026#34;: item_id, \u0026#34;start_datetime\u0026#34;: start_datetime, \u0026#34;end_datetime\u0026#34;: end_datetime, \u0026#34;repeat_at\u0026#34;: repeat_at, \u0026#34;process_after\u0026#34;: process_after, \u0026#34;start_process\u0026#34;: start_process, \u0026#34;duration\u0026#34;: duration, } Cookie from typing import Union from fastapi import Cookie, FastAPI app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(ads_id: Union[str, None] = Cookie(default=None)): return {\u0026#34;ads_id\u0026#34;: ads_id} 跟Query和 Path用法类似。\nHeader from typing import Union from fastapi import FastAPI, Header app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(user_agent: Union[str, None] = Header(default=None)): return {\u0026#34;User-Agent\u0026#34;: user_agent} 多重header用List，比如：\nfrom typing import Union from fastapi import FastAPI, Header app = FastAPI() @app.get(\u0026#34;/items/\u0026#34;) async def read_items(user_agent: Union[str, None] = Header(default=None)): return {\u0026#34;User-Agent\u0026#34;: user_agent} X-Token: foo X-Token: bar { \u0026#34;X-Token values\u0026#34;: [ \u0026#34;bar\u0026#34;, \u0026#34;foo\u0026#34; ] } 响应模型 通过response_model定义返回模型：\nfrom typing import List, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None tags: List[str] = [] @app.post(\u0026#34;/items/\u0026#34;, response_model=Item) async def create_item(item: Item): return item response_model的作用是对函数返回值进行过滤，比如：\nfrom typing import Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr app = FastAPI() class UserIn(BaseModel): username: str password: str email: EmailStr full_name: Union[str, None] = None class UserOut(BaseModel): username: str email: EmailStr full_name: Union[str, None] = None @app.post(\u0026#34;/user/\u0026#34;, response_model=UserOut) async def create_user(user: UserIn): return user 函数返回值是UserIn模型的对象user，而response_model的值为UserOut（UserOut相比于UserIn来说，没有password），那么FastAPI的响应，就是用UserOut对UserIn进行了过滤，返回的是没有password的UserOut。\n响应模型可以返回默认值：\nfrom typing import List, Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: float = 10.5 tags: List[str] = [] items = { \u0026#34;foo\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;price\u0026#34;: 50.2}, \u0026#34;bar\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Bar\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The bartenders\u0026#34;, \u0026#34;price\u0026#34;: 62, \u0026#34;tax\u0026#34;: 20.2}, \u0026#34;baz\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Baz\u0026#34;, \u0026#34;description\u0026#34;: None, \u0026#34;price\u0026#34;: 50.2, \u0026#34;tax\u0026#34;: 10.5, \u0026#34;tags\u0026#34;: []}, } @app.get(\u0026#34;/items/{item_id}\u0026#34;, response_model=Item, response_model_exclude_unset=True) async def read_item(item_id: str): return items[item_id] response_model_exclude_unset=True不返回未显式设置的字段，response_model_exclude_defaults不返回带默认值的字段，response_model_exclude_none不返回None的字段。\n附加模型 在上面的示例中，UserIn是入参，UserOut是出参，不包含password，但是在实际情况中，还需要第三个模型UserInDB，在存入数据库时，把password进行加密。\n代码实现如下：\nfrom typing import Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr app = FastAPI() class UserIn(BaseModel): username: str password: str email: EmailStr full_name: Union[str, None] = None class UserOut(BaseModel): username: str email: EmailStr full_name: Union[str, None] = None class UserInDB(BaseModel): username: str hashed_password: str email: EmailStr full_name: Union[str, None] = None def fake_password_hasher(raw_password: str): return \u0026#34;supersecret\u0026#34; + raw_password def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) print(\u0026#34;User saved! ..not really\u0026#34;) return user_in_db @app.post(\u0026#34;/user/\u0026#34;, response_model=UserOut) async def create_user(user_in: UserIn): user_saved = fake_save_user(user_in) return user_saved 重点是user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)里面的**user_in.dict()。\nuser_in是UserIn类的Pydantic模型，它有个dict()方法能返回字典。**是拆包，把字典拆成key value的形式，上面这行代码等价于：\nUserInDB( username=\u0026#34;john\u0026#34;, password=\u0026#34;secret\u0026#34;, email=\u0026#34;john.doe@example.com\u0026#34;, full_name=None, hashed_password=hashed_password ) 也相当于：\nUserInDB( username = user_dict[\u0026#34;username\u0026#34;], password = user_dict[\u0026#34;password\u0026#34;], email = user_dict[\u0026#34;email\u0026#34;], full_name = user_dict[\u0026#34;full_name\u0026#34;], hashed_password = hashed_password, ) FastAPI的一大设计原则是尽量减少重复代码，所以对于UserIn、UserOut、UserInDB可以把里面的相同字段抽象为一个UserBase，其他Model继承即可：\nfrom typing import Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr app = FastAPI() class UserBase(BaseModel): username: str email: EmailStr full_name: Union[str, None] = None class UserIn(UserBase): password: str class UserOut(UserBase): pass class UserInDB(UserBase): hashed_password: str def fake_password_hasher(raw_password: str): return \u0026#34;supersecret\u0026#34; + raw_password def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) print(\u0026#34;User saved! ..not really\u0026#34;) return user_in_db @app.post(\u0026#34;/user/\u0026#34;, response_model=UserOut) async def create_user(user_in: UserIn): user_saved = fake_save_user(user_in) return user_saved response_model除了定义一个Model以外，也能定义多个附加模型。\n比如Union：\nfrom typing import Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class BaseItem(BaseModel): description: str type: str class CarItem(BaseItem): type = \u0026#34;car\u0026#34; class PlaneItem(BaseItem): type = \u0026#34;plane\u0026#34; size: int items = { \u0026#34;item1\u0026#34;: {\u0026#34;description\u0026#34;: \u0026#34;All my friends drive a low rider\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;car\u0026#34;}, \u0026#34;item2\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Music is my aeroplane, it\u0026#39;s my aeroplane\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;plane\u0026#34;, \u0026#34;size\u0026#34;: 5, }, } @app.get(\u0026#34;/items/{item_id}\u0026#34;, response_model=Union[PlaneItem, CarItem]) async def read_item(item_id: str): return items[item_id] 比如List：\nfrom typing import List from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str items = [ {\u0026#34;name\u0026#34;: \u0026#34;Foo\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;There comes my hero\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;Red\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;It\u0026#39;s my aeroplane\u0026#34;}, ] @app.get(\u0026#34;/items/\u0026#34;, response_model=List[Item]) async def read_items(): return items 比如Dict：\nfrom typing import Dict from fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/keyword-weights/\u0026#34;, response_model=Dict[str, float]) async def read_keyword_weights(): return {\u0026#34;foo\u0026#34;: 2.3, \u0026#34;bar\u0026#34;: 3.4} 参考资料：\nhttps://fastapi.tiangolo.com/\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/fastapi/000001-fastapifastapi%E5%AE%98%E6%96%B9%E6%95%99%E7%A8%8B%E5%A4%AA%E6%A3%92%E4%BA%86%E4%B8%8A/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"python第三流行的web框架\"\u003ePython第三流行的Web框架\u003c/h1\u003e\n\u003cp\u003e在2020年的Python开发者调查结果中，有这样一段话：“FastAPI在此次调查迭代中首次被引为选项，表现为Python第三流行的Web框架。”\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f425fc4cd3bf42b5b787415b79fbb641.png\"\u003e\u003c/p\u003e\n\u003cp\u003eFastAPI创立于2018年12月，不到2年就成为仅次于Flask和Django的第三流行的Web框架。而又经过了一年发展来到2022年，虽然2021年Python开发者调查结果还没有出来，但是从GitHub的star来看，Flask 58.7k，Django 63.6k，FastAPI 44.2k，这个差距缩得越来越小。\u003c/p\u003e\n\u003ch1 id=\"fastapi特性\"\u003eFastAPI特性\u003c/h1\u003e\n\u003cp\u003e这里就不做机器翻译了，大家看下原文：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8ceced28c6334744888c392ec1168f46.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我说下我选择FastAPI的理由：首先就是HttpRunner集成了FastAPI，有大佬背书，相信这个框架足以优秀。其次是注解，用多了SpringBoot以后，越来越喜欢注解，层次清晰。对于前后端分离项目来说，Flask虽然非常精简却又自带了Jinja模板引擎，Django虽然是百宝箱却又显得太重，而FastAPI介于两者之间，就是一个纯粹的后端应用。并且FastAPI是基于Starlette框架的，集成了实用功能比如类型检查、OpenAPI（Swagger）等等，这跟我基于pytest框架做tep测试工具的理念很相似。\u003c/p\u003e\n\u003ch1 id=\"安装\"\u003e安装\u003c/h1\u003e\n\u003cp\u003e对Python版本要求是3.6+。\u003c/p\u003e\n\u003cp\u003e先安装FastAPI：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epip install fastapi\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a884b79f6e784853b1ccf28068b0ee06.png\"\u003e\u003c/p\u003e\n\u003cp\u003e再安装ASGI服务器，比如Uvicorn：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epip install \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;uvicorn[standard]\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c64d7cd9792a49d098af8efedd74b559.png\"\u003e\u003c/p\u003e\n\u003cp\u003e也可以同时安装fastapi和uvicorn：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epip install \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;fastapi[all]\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"运行\"\u003e运行\u003c/h1\u003e\n\u003cp\u003e写个\u003ccode\u003emain.py\u003c/code\u003e文件：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003etyping\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e Optional\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efrom\u003c/span\u003e \u003cspan style=\"color:#24292e\"\u003efastapi\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e FastAPI\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapp \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e FastAPI\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.get\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eread_root\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;Hello\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;World\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e@app.get\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;/items/\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e{item_id}\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eread_item\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eitem_id\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e q\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e Optional\u003cspan style=\"color:#1f2328\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003estr\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#cf222e\"\u003eNone\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;item_id\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e item_id\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;q\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e q\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e在命令行输入启动应用：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003euvicorn main:app --reload\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003emain\u003c/code\u003e是Python模块名。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eapp\u003c/code\u003e是\u003ccode\u003eapp = FastAPI()\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e--reload\u003c/code\u003e在代码变化时自动重启服务器。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/09a86f834dbb454ebb0660b4026dae56.png\"\u003e\u003c/p\u003e\n\u003cp\u003e打开浏览器访问：\u003c/p\u003e\n\u003cp\u003ehttp://127.0.0.1:8000/items/5?q=somequery\u003c/p\u003e\n\u003cp\u003e就能看到JSON响应：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026#34;item_id\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e5\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e\u0026#34;q\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;somequery\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e访问：\u003c/p\u003e\n\u003cp\u003ehttp://127.0.0.1:8000/docs\u003c/p\u003e\n\u003cp\u003e就能看到Swagger接口文档：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6e16e75da06942838f0b2c3aa80b4aee.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"pydantic\"\u003epydantic\u003c/h1\u003e\n\u003cp\u003epydantic是一个数据验证的库，FastAPI使用它来做模型校验。比如：\u003c/p\u003e","title":"FastAPI官方教程太棒了（上）"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n如果安装了Python，并且安装了Flask：\npip install flask 那么就可以在短短一分钟内Mock出来一个API，而且只需要用到一个文件。\n彻底告别在线Mock网站无法指定请求方法，Postman配置繁琐的问题。\n建一个文件 随便在哪创建一个py文件，比如app.py。\n写一段代码 from flask import Flask app = Flask(__name__) @app.route(\u0026#34;/\u0026#34;) def hello_world(): return \u0026#34;Hello, World!\u0026#34; if __name__ == \u0026#34;__main__\u0026#34;: app.run() 跑一条命令 在cmd或shell执行python app.py，服务就起来了：\nD:\\\u0026gt;python app.py * Serving Flask app \u0026#34;app\u0026#34; (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: off * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 测试一下：\nD:\\\u0026gt;curl http://127.0.0.1:5000/ Hello, World! GET请求 不带参数 代码：\n@app.route(\u0026#34;/testGet\u0026#34;) def my_get(): return \u0026#34;This is GET\u0026#34; 测试：\nD:\\\u0026gt;curl http://127.0.0.1:5000/testGet This is GET 带参数 代码：\n@app.route(\u0026#34;/testGetParams\u0026#34;) def my_get_params(): return request.args 测试：\nD:\\\u0026gt;curl \u0026#34;http://127.0.0.1:5000/testGetParams?a=1\u0026amp;b=2\u0026#34; {\u0026#34;a\u0026#34;:\u0026#34;1\u0026#34;,\u0026#34;b\u0026#34;:\u0026#34;2\u0026#34;} POST请求 不带参数 代码：\n@app.route(\u0026#34;/testPost\u0026#34;, methods=[\u0026#34;POST\u0026#34;]) def my_post(): return \u0026#34;This is POST\u0026#34; 测试：\nD:\\\u0026gt;curl -X POST \u0026#34;http://127.0.0.1:5000/testPost\u0026#34; This is POST 带Json参数 代码：\n@app.route(\u0026#34;/testPostJson\u0026#34;, methods=[\u0026#34;POST\u0026#34;]) def my_post_json(): return request.json test.json：\n{ \u0026#34;name\u0026#34;: \u0026#34;dongfanger\u0026#34;, \u0026#34;alias\u0026#34;: \u0026#34;redsun\u0026#34; } 测试：\nD:\\\u0026gt;curl -H \u0026#34;Content-Type: application/json\u0026#34; -d \u0026#34;@test.json\u0026#34; \u0026#34;http://127.0.0.1:5000/testPostJson\u0026#34; {\u0026#39;name\u0026#39;: \u0026#39;dongfanger\u0026#39;, \u0026#39;alias\u0026#39;: \u0026#39;redsun\u0026#39;} 同时GET和POST 代码：\n@app.route(\u0026#34;/testGetPost\u0026#34;, methods=[\u0026#34;GET\u0026#34;, \u0026#34;POST\u0026#34;]) def my_get_post(): if request.method == \u0026#34;GET\u0026#34;: return \u0026#34;This is GET\u0026#34; if request.method == \u0026#34;POST\u0026#34;: return \u0026#34;This is POST\u0026#34; 测试：\nD:\\\u0026gt;curl http://127.0.0.1:5000/testGetPost This is GET D:\\\u0026gt;curl http://127.0.0.1:5000/testGetPost -X POST This is POST 请求头 代码：\n@app.route(\u0026#34;/testHeaders\u0026#34;) def my_headers(): return str(request.headers) 测试：\nD:\\\u0026gt;curl http://127.0.0.1:5000/testHeaders Host: 127.0.0.1:5000 User-Agent: curl/7.55.1 Accept: */* 完整代码解析 from flask import Flask, request ## Flask实例 app = Flask(__name__) ## @app.route添加路由 @app.route(\u0026#34;/testGet\u0026#34;) def my_get(): return \u0026#34;This is GET\u0026#34; @app.route(\u0026#34;/testGetParams\u0026#34;) def my_get_params(): # flask.request里面封装了请求数据，可以看需要获取 return request.args ## methods指定请求方法 @app.route(\u0026#34;/testPost\u0026#34;, methods=[\u0026#34;POST\u0026#34;]) def my_post(): return \u0026#34;This is POST\u0026#34; @app.route(\u0026#34;/testPostJson\u0026#34;, methods=[\u0026#34;POST\u0026#34;]) def my_post_json(): return request.json ## 可以同时指定GET和POST @app.route(\u0026#34;/testGetPost\u0026#34;, methods=[\u0026#34;GET\u0026#34;, \u0026#34;POST\u0026#34;]) def my_get_post(): # 判断请求方法是GET或POST if request.method == \u0026#34;GET\u0026#34;: return \u0026#34;This is GET\u0026#34; if request.method == \u0026#34;POST\u0026#34;: return \u0026#34;This is POST\u0026#34; @app.route(\u0026#34;/testHeaders\u0026#34;) def my_headers(): return str(request.headers) if __name__ == \u0026#34;__main__\u0026#34;: app.run() 小结 本文介绍了如何使用Flask在一分钟内Mock一个API，只需要一个文件，一段代码，一条命令，即可完成。然后分别介绍了常用的GET请求和POST请求，以及带不带参数，获取请求头的用法。在测试时用到了curl命令，它的名字是Client URL的意思，在Mac和Windows都可以安装使用。\n参考资料：\nhttps://flask.palletsprojects.com/en/2.0.x/quickstart/\nhttp://www.ruanyifeng.com/blog/2019/09/curl-reference.html\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/flask/000001-flaskflask%E4%B8%80%E5%88%86%E9%92%9Fmock%E4%B8%80%E4%B8%AAapi/","summary":"如果安装了Python，并且安装了Flask：","title":"Flask一分钟Mock一个API"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n大家好，我是刚哥。\n从这篇文章开始，将对HttpRunner3的源码进行剖析。\n为什么是HttpRunner3？其实pytest是一个更强大的测试框架，无奈它的源码实在是太复杂了，连学习pytest的使用都要花上很多时间，哪还有那么多精力去看它的源码。而其他很多所谓的测试框架实在是太粗糙了，学不到东西不说，还可能学错。HttpRunner正好介于两者之间，既不复杂，也不简单，刚刚好。\n对源码进行怎样的剖析？HttpRunner的源码包括这几个目录：docs（文档）、examples（示例）、httprunner（源文件）、tests（测试文件），我只会选择httprunner（源文件）来进行剖析，它的源码全在这个目录下。看源码是特别费脑的，但是就算费脑也可能还是看的云里雾里，这时候方法就显得格外重要了。我觉得需要带着问题去看，比如**__init__文件干了些啥？项目初始化结构是怎么来的？请求是怎么发出去的？用例是怎么运行的？报告是怎么生成的？**等等等。\n本文先从httprunner/__init__.py这个初始化文件开始，由浅入深，由易入难。它的内容很简单：\n__version__ = \u0026#34;3.1.6\u0026#34; __description__ = \u0026#34;One-stop solution for HTTP(S) testing.\u0026#34; ## import firstly for monkey patch if needed from httprunner.ext.locust import main_locusts from httprunner.parser import parse_parameters as Parameters from httprunner.runner import HttpRunner from httprunner.testcase import Config, Step, RunRequest, RunTestCase __all__ = [ \u0026#34;__version__\u0026#34;, \u0026#34;__description__\u0026#34;, \u0026#34;HttpRunner\u0026#34;, \u0026#34;Config\u0026#34;, \u0026#34;Step\u0026#34;, \u0026#34;RunRequest\u0026#34;, \u0026#34;RunTestCase\u0026#34;, \u0026#34;Parameters\u0026#34;, ] __version__是版本号，会在很多地方用到：\n__description__是描述，也会在很多地方用到：\n这一行代码：\n## import firstly for monkey patch if needed from httprunner.ext.locust import main_locusts main_locusts是在httprunner/ext/locust/__init__.py文件中定义的。注释是说，先把这个模块导入进来，如果需要的话，可以在这里添加猴子补丁。有点费解，只有这一处代码：\n先看下__all__的作用，再来猜测。\n__all__是为了方便其他地方直接通过from httprunner import来导入包，比如里面的HttpRunner是from httprunner.runner import HttpRunner这样导入的，加入到__all__以后，就能直接from httprunner import HttpRunner来导入了：\n省去了from httprunner.runner这种代码。\n那么那个猴子补丁，我对它用法的猜测是这样的：\n## import firstly for monkey patch if needed from httprunner.ext.locust import main_locusts def main_locusts_monkey_patch(): # ... main_locusts = main_locusts_monkey_patch __all__ = [ \u0026#34;main_locusts\u0026#34; ] 有任何不同想法，欢迎加群讨论哟。\n参考资料：\nhttps://github.com/httprunner/httprunner\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/httprunner/000001-httprunnerhttprunner3%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90__init__%E6%96%87%E4%BB%B6%E5%B9%B2%E4%BA%86%E4%BA%9B%E5%95%A5/","summary":"大家好，我是刚哥。","title":"HttpRunner3源码剖析__init__文件干了些啥"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n项目简介 japi项目，是一个接口自动化平台开发项目，由【交流群】招人共建，基于JMeter，参考MeterSphere，以学习为目的，完成一个SpringBoot+Vue技术栈的纯粹的接口自动化平台开发。\n项目成员 经过\u0026quot;填写人员分工表\u0026quot;以后，最终有38人参与了本次项目：\n刚哥、布鲁、嘉宥、守正、大刚、风雨、奉孝、大姚、畅月、渔夫、小禹、钰琪、天湖、迷龙、肥涵、胖子、局长、呼呼、明亮、安宁、木棉、番茄、玥玥、哈瑞、虚竹、十五、阿新、卡罗、大白、库昊、左右、小涌、古风、玉洁、小韩、小卷、千凝、昆明。\n随着项目进行，后续“提交学习总结”等环节层层递进，真正留下来的项目成员，会在GitHub的README中署名。\n在填表时收集了大家的历史经验，这里汇总整理了一下，供参考：\n“Vue+Django写过测试平台；SpringBoot写过后端应用；Python小工具开发；Python+Pytest+Requests搭建接口自动化框架；Python+Unittest或Pytest自动化测试框架搭建及脚本开发；各类开源自动化测试平台搭建，如：HttpRunnerManager、流马、Yapi、Sonic等；js+Django2实现UI自动化测试平台；使Testng框架进行接口测试；Vue+fastapi写过测试平台；python+selenium UI自动化；python+httprunner，接口文档解析自动生成字段校验用例；python+RobotFramework做过UI；Java做过单元测试，熟悉基础语法；学过JavaSE，JavaEE，实战经验不多；Python Pyqt GUI工具开发；Python 接口自动化、APP自动化框架设计及编码；独立负责主导Python+Locust性能测试；三方库和标准库：unittest、pytest、appium、selenium、request、httprunner；画过简单的html、css；App UI自动化框架搭建：pytest+appium+allure+jenkins接口自动化框架搭建：pytest+request+allure+jenkins二次开发及维护过flask+vue接口测试平台；二次开发及维护过springboot+angularUI测试平台；Pytest+Playwright UI自动化脚本编写；”\n我总结了几个关键字：Vue、Django、SpringBoot、Flask、FastAPI、ElementUI、RobotFramework、脚本开发、工具开发、平台开发、接口自动化、UI自动化、APP自动化。\n项目计划 招募，人员分工，需求分析，设计，编码，测试，上线。\n由于是以学习为目的，加上都是业余时间投入，也有其他事情要忙，所以该项目不会像真正项目那样，设置里程碑和交付节点，会给足够的学习时间，战线拉得比较长，过程大于结果。\n需求分析 MeterSphere官方给出的功能列表，接口自动化相关的模块有接口定义和接口自动化。\n接口定义包括以下功能：\n在线添加并编辑接口定义; 以树状形式展示项目的模块及对应接口; 支持HTTP、Dubbo、SQL、TCP类型接口请求; 支持通过MeterSphere Json、Postman、Swagger 等多种格式文件快速导入接口，Swagger可通过URL定时同步; 支持接口快捷调试; 根据接口定义快速配置HTTP、TCP协议Mock服务; API列表、CASE列表、文档模式多种模式灵活切换; 接口页面API、TEST、CASE、MOCK多种操作灵活切换; 测试环境信息管理功能; 单接口用例支持引用环境; 支持正则匹配、jsonpath、xpath等多种类型的断言规则; 支持正则匹配、jsonpath、xpath等多种类型的参数提取; 支持BeanShell、Python、Groovy、JavaScript等多种语言的前后置脚本; 支持前置SQL、后置SQL; 支持上传并引用自定义Jar包; 支持查看接口及用例变更历史; 手动执行、定时任务、Jenkins插件触发等多种执行方式; API、CASE一键复制; CASE一键创建性能测试; 接口定义依赖关系设置;\n接口自动化包括以下功能：\n创建多接口串联的场景化测试; 从接口列表中快速导入接口或接口用例; 跨项目的场景引用功能; 客户端SSL证书认证配置; 场景用例支持引用环境; 支持条件、循环、事务、等待等多种类型的控制器; 支持正则匹配、jsonpath、xpath等多种类型的断言规则; 支持正则匹配、jsonpath、xpath等多种类型的参数提取; 支持BeanShell、Python、Groovy、JavaScript等多种语言的前后置脚本; 场景变量提供多种类型; 提供Mock数据和JMeter变量表达式; 环境配置增加区分模块、路径两种方式添加域名; 增加全局前置脚本、全局后置脚本; 基于插件机制实现其他协议; 支持上传并引用自定义Jar包; 支持查看场景用例变更历史; 手动执行、定时任务、Jenkins插件触发等多种执行方式; 串行、并行两种批量执行方式自由选择; 支持指定测试资源池远程执行; 可多场景串行、并行执行; 场景一键创建性能测试; 场景一键复制; 场景批量执行可自由选择独立报告或集合报告; 场景一键添加到测试计划；\n除此之外还有：\n支持消息通知(微信、钉钉、飞书、邮件)；\n本项目要做的，基本就是这些功能。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/japi/000001-japi%E9%A1%B9%E7%9B%AE%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90%E9%98%B6%E6%AE%B5/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"项目简介\"\u003e项目简介\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003ejapi\u003c/strong\u003e项目，是一个接口自动化平台开发项目，由【交流群】招人共建，基于JMeter，参考MeterSphere，以学习为目的，完成一个SpringBoot+Vue技术栈的纯粹的接口自动化平台开发。\u003c/p\u003e\n\u003ch1 id=\"项目成员\"\u003e项目成员\u003c/h1\u003e\n\u003cp\u003e经过\u0026quot;填写人员分工表\u0026quot;以后，最终有38人参与了本次项目：\u003c/p\u003e\n\u003cp\u003e刚哥、布鲁、嘉宥、守正、大刚、风雨、奉孝、大姚、畅月、渔夫、小禹、钰琪、天湖、迷龙、肥涵、胖子、局长、呼呼、明亮、安宁、木棉、番茄、玥玥、哈瑞、虚竹、十五、阿新、卡罗、大白、库昊、左右、小涌、古风、玉洁、小韩、小卷、千凝、昆明。\u003c/p\u003e\n\u003cp\u003e随着项目进行，后续“提交学习总结”等环节层层递进，真正留下来的项目成员，会在GitHub的README中署名。\u003c/p\u003e\n\u003cp\u003e在填表时收集了大家的历史经验，这里汇总整理了一下，供参考：\u003c/p\u003e\n\u003cp\u003e“Vue+Django写过测试平台；SpringBoot写过后端应用；Python小工具开发；Python+Pytest+Requests搭建接口自动化框架；Python+Unittest或Pytest自动化测试框架搭建及脚本开发；各类开源自动化测试平台搭建，如：HttpRunnerManager、流马、Yapi、Sonic等；js+Django2实现UI自动化测试平台；使Testng框架进行接口测试；Vue+fastapi写过测试平台；python+selenium UI自动化；python+httprunner，接口文档解析自动生成字段校验用例；python+RobotFramework做过UI；Java做过单元测试，熟悉基础语法；学过JavaSE，JavaEE，实战经验不多；Python Pyqt GUI工具开发；Python 接口自动化、APP自动化框架设计及编码；独立负责主导Python+Locust性能测试；三方库和标准库：unittest、pytest、appium、selenium、request、httprunner；画过简单的html、css；App UI自动化框架搭建：pytest+appium+allure+jenkins接口自动化框架搭建：pytest+request+allure+jenkins二次开发及维护过flask+vue接口测试平台；二次开发及维护过springboot+angularUI测试平台；Pytest+Playwright UI自动化脚本编写；”\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e我总结了几个关键字：Vue、Django、SpringBoot、Flask、FastAPI、ElementUI、RobotFramework、脚本开发、工具开发、平台开发、接口自动化、UI自动化、APP自动化。\u003c/strong\u003e\u003c/p\u003e\n\u003ch1 id=\"项目计划\"\u003e项目计划\u003c/h1\u003e\n\u003cp\u003e招募，人员分工，需求分析，设计，编码，测试，上线。\u003c/p\u003e\n\u003cp\u003e由于是以学习为目的，加上都是业余时间投入，也有其他事情要忙，所以该项目不会像真正项目那样，设置里程碑和交付节点，会给足够的学习时间，战线拉得比较长，过程大于结果。\u003c/p\u003e\n\u003ch1 id=\"需求分析\"\u003e需求分析\u003c/h1\u003e\n\u003cp\u003eMeterSphere官方给出的功能列表，接口自动化相关的模块有接口定义和接口自动化。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e接口定义包括以下功能：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e在线添加并编辑接口定义;\n以树状形式展示项目的模块及对应接口;\n支持HTTP、Dubbo、SQL、TCP类型接口请求;\n支持通过MeterSphere Json、Postman、Swagger 等多种格式文件快速导入接口，Swagger可通过URL定时同步;\n支持接口快捷调试;\n根据接口定义快速配置HTTP、TCP协议Mock服务;\nAPI列表、CASE列表、文档模式多种模式灵活切换;\n接口页面API、TEST、CASE、MOCK多种操作灵活切换;\n测试环境信息管理功能;\n单接口用例支持引用环境;\n支持正则匹配、jsonpath、xpath等多种类型的断言规则;\n支持正则匹配、jsonpath、xpath等多种类型的参数提取;\n支持BeanShell、Python、Groovy、JavaScript等多种语言的前后置脚本;\n支持前置SQL、后置SQL;\n支持上传并引用自定义Jar包;\n支持查看接口及用例变更历史;\n手动执行、定时任务、Jenkins插件触发等多种执行方式;\nAPI、CASE一键复制;\nCASE一键创建性能测试;\n接口定义依赖关系设置;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e接口自动化包括以下功能：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e创建多接口串联的场景化测试;\n从接口列表中快速导入接口或接口用例;\n跨项目的场景引用功能;\n客户端SSL证书认证配置;\n场景用例支持引用环境;\n支持条件、循环、事务、等待等多种类型的控制器;\n支持正则匹配、jsonpath、xpath等多种类型的断言规则;\n支持正则匹配、jsonpath、xpath等多种类型的参数提取;\n支持BeanShell、Python、Groovy、JavaScript等多种语言的前后置脚本;\n场景变量提供多种类型;\n提供Mock数据和JMeter变量表达式;\n环境配置增加区分模块、路径两种方式添加域名;\n增加全局前置脚本、全局后置脚本;\n基于插件机制实现其他协议;\n支持上传并引用自定义Jar包;\n支持查看场景用例变更历史;\n手动执行、定时任务、Jenkins插件触发等多种执行方式;\n串行、并行两种批量执行方式自由选择;\n支持指定测试资源池远程执行;\n可多场景串行、并行执行;\n场景一键创建性能测试;\n场景一键复制;\n场景批量执行可自由选择独立报告或集合报告;\n场景一键添加到测试计划；\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e除此之外还有：\u003c/strong\u003e\u003c/p\u003e","title":"japi项目需求分析阶段"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n环境搭建 据说，很多人都被繁琐的环境搭建给吓到了。\n是的，确实，繁琐。\nnode.js 下载地址：\nhttps://nodejs.org/en/download/\ncmd 输入 node -v，出现下图说明成功：\nJDK 下载地址：\nhttps://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html\n百度一下“配置Java”，配置环境变量。\ncmd 输入 java -version，出现下图说明成功：\nAndroid SDK 下载地址：\nhttps://www.androiddevtools.cn/\n百度一下\u0026quot;配置Android SDK环境变量\u0026quot;。\ncmd 输入 adb，出现下图说明成功：\nAppium Desktop 下载地址：\nhttps://github.com/appium/appium-desktop/releases\n或\nhttps://testerhome.com/topics/680\nMaven 下载地址：\nhttp://maven.apache.org/download.cgi\n百度一下“ maven环境配置”。\ncmd 输入 mvn -version，出现下图说明成功：\nIntelliJ IDEA 下载地址：\nhttp://www.jetbrains.com/idea/\n安卓模拟器 下载地址：\nhttps://www.yeshen.com/\n把 Android SDK 目录下 \\platform-tool 的 adb.exe 拷贝。\n打开 Nox\\bin 目录，粘贴替换 adb.exe。\n打开模拟器。\ncmd 输入 adb connect 127.0.0.1:62001，如下：\n再输入 adb devices，如下\n有道计算器 下载地址：\nhttp://math.youdao.com/\n下载后，apk 文件存放在某目录下即可。\n将 apk 文件直接拖入安卓模拟器中：\n至此，环境搭建完毕，共安装了8个程序。\n实现步骤 打开 IntelliJ。\n创建 maven 项目：\n点击 ok 后，复制下面代码到 pom.xm 中：\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.appium\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;java-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.1.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.testng\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;testng\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;6.14.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 复制粘贴下面代码：\nimport org.testng.annotations.Test; import org.testng.annotations.BeforeClass; import org.testng.annotations.AfterClass; import io.appium.java_client.AppiumDriver; import io.appium.java_client.android.AndroidDriver; import java.net.URL; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DesiredCapabilities; public class AppDemo { private AppiumDriver driver; @BeforeClass public void setup() throws Exception { DesiredCapabilities cap = new DesiredCapabilities(); cap.setCapability(CapabilityType.BROWSER_NAME, \u0026#34;\u0026#34;); cap.setCapability(\u0026#34;platformName\u0026#34;, \u0026#34;Android\u0026#34;); //指定测试平台 cap.setCapability(\u0026#34;deviceName\u0026#34;, \u0026#34;127.0.0.1:62001\u0026#34;); //指定测试机的ID,通过adb命令`adb devices`获取 cap.setCapability(\u0026#34;platformVersion\u0026#34;, \u0026#34;5.1.1\u0026#34;); //将上面获取到的包名和Activity名设置为值 cap.setCapability(\u0026#34;appPackage\u0026#34;, \u0026#34;com.youdao.calculator\u0026#34;); cap.setCapability(\u0026#34;appActivity\u0026#34;, \u0026#34;com.youdao.calculator.activities.MainActivity\u0026#34;); // //A new session could not be created的解决方法 // cap.setCapability(\u0026#34;appWaitActivity\u0026#34;, \u0026#34;com.meizu.flyme.calculator.Calculator\u0026#34;); // //每次启动时覆盖session，否则第二次后运行会报错不能新建session // cap.setCapability(\u0026#34;sessionOverride\u0026#34;, true); driver = new AndroidDriver(new URL(\u0026#34;http://127.0.0.1:4723/wd/hub\u0026#34;), cap); } @Test public void plus() throws Exception { Thread.sleep(3000); int width = driver.manage().window().getSize().width; int height = driver.manage().window().getSize().height; int x0 = (int)(width * 0.8); // 起始x坐标 int x1 = (int)(height * 0.2); // 终止x坐标 int y = (int)(height * 0.5); // y坐标 for (int i=0; i\u0026lt;5; i++) { driver.swipe(x0, y, x1, y, 500); Thread.sleep(1000); } driver.findElementById(\u0026#34;com.youdao.calculator:id/guide_button\u0026#34;).click(); for (int i=0; i\u0026lt;6; i++) { driver.findElementByXPath(\u0026#34;//android.webkit.WebView[@text=\u0026#39;Mathbot Editor\u0026#39;]\u0026#34;).click(); Thread.sleep(1000); } String btn_xpath = \u0026#34;//*[@resource-id=\u0026#39;com.youdao.calculator:id/view_pager_keyboard\u0026#39;]/android.widget.GridView/android.widget.FrameLayout[%d]/android.widget.FrameLayout\u0026#34;; driver.findElementByXPath(String.format(btn_xpath, 7)).click(); driver.findElementByXPath(String.format(btn_xpath, 10)).click(); driver.findElementByXPath(String.format(btn_xpath, 8)).click(); Thread.sleep(3000); } @AfterClass public void tearDown() throws Exception { driver.quit(); } } 文件右键 run：\n然后可以看到脚本在跑了，\n打开安卓模拟器，\n一会就看到，\n计算器 app 在自己动了！\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E7%A7%BB%E5%8A%A8%E7%AB%AF/000001-java+appium+%E5%AE%89%E5%8D%93%E6%A8%A1%E6%8B%9F%E5%99%A8%E5%AE%9E%E7%8E%B0app%E8%87%AA%E5%8A%A8%E5%8C%96demo/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"环境搭建\"\u003e环境搭建\u003c/h1\u003e\n\u003cp\u003e据说，很多人都被繁琐的环境搭建给吓到了。\u003c/p\u003e\n\u003cp\u003e是的，确实，繁琐。\u003c/p\u003e\n\u003ch2 id=\"nodejs\"\u003enode.js\u003c/h2\u003e\n\u003cp\u003e下载地址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://nodejs.org/en/download/\"\u003ehttps://nodejs.org/en/download/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003ecmd 输入 node -v，出现下图说明成功：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/9a8a97cbb71242eeba95dfda59f7d141.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"jdk\"\u003eJDK\u003c/h2\u003e\n\u003cp\u003e下载地址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html\"\u003ehttps://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e百度一下“配置Java”，配置环境变量。\u003c/p\u003e\n\u003cp\u003ecmd 输入 java -version，出现下图说明成功：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/147d699f24154e4cbdb6c3da1c03b5fa.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"android-sdk\"\u003eAndroid SDK\u003c/h2\u003e\n\u003cp\u003e下载地址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.androiddevtools.cn/\"\u003ehttps://www.androiddevtools.cn/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/7611fee79e654c34842a7d9877ba3f51.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0e592229d5de460caf92ccde1004760c.png\"\u003e\u003c/p\u003e\n\u003cp\u003e百度一下\u0026quot;配置Android SDK环境变量\u0026quot;。\u003c/p\u003e\n\u003cp\u003ecmd 输入 adb，出现下图说明成功：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/aa74daf57c50425a98dbd916d1a1cea0.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"appium-desktop\"\u003eAppium Desktop\u003c/h2\u003e\n\u003cp\u003e下载地址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/appium/appium-desktop/releases\"\u003ehttps://github.com/appium/appium-desktop/releases\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e或\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://testerhome.com/topics/680\"\u003ehttps://testerhome.com/topics/680\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bc6783ed94584136bbb7bec00ecb67e1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d335ce8b69474881ba5a09e3c9aa880a.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"maven\"\u003eMaven\u003c/h2\u003e\n\u003cp\u003e下载地址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://maven.apache.org/download.cgi\"\u003ehttp://maven.apache.org/download.cgi\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e百度一下“ maven环境配置”。\u003c/p\u003e\n\u003cp\u003ecmd 输入 mvn -version，出现下图说明成功：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/2b4136dfdb6b45d3a33061601eacd9d4.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"intellij-idea\"\u003eIntelliJ IDEA\u003c/h2\u003e\n\u003cp\u003e下载地址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://www.jetbrains.com/idea/\"\u003ehttp://www.jetbrains.com/idea/\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"安卓模拟器\"\u003e安卓模拟器\u003c/h2\u003e\n\u003cp\u003e下载地址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.yeshen.com/\"\u003ehttps://www.yeshen.com/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e把 Android SDK 目录下 \u003ccode\u003e\\platform-tool\u003c/code\u003e 的 adb.exe 拷贝。\u003c/p\u003e\n\u003cp\u003e打开 \u003ccode\u003eNox\\bin\u003c/code\u003e 目录，粘贴替换 adb.exe。\u003c/p\u003e\n\u003cp\u003e打开模拟器。\u003c/p\u003e","title":"java+appium+安卓模拟器实现app自动化Demo"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n7个Web术语 Website: static vs dynamic\nHTTP\nHTTP Requests\nGET vs POST\nServlet Container\nServer: Web vs Application\nContent Type\nWebsite: static vs dynamic 网站内容包括文本、图片、音频、视频，通过URL来访问。网站分为静态网站和动态网站。\n静态网站\n静态网站是用HTML编写的，所有内容都包含在网页内，就像是打印出来的一样。\n动态网站\n动态网站分为客户端和服务端，内容是可以动态变化的，可以根据用户请求动态更新网页内容。服务端语言有Servlet、ASP.NET等。\nHTTP HTTP是Hypertext Transfer Protocol缩写，超文本传输协议，位于OSI网络分层模型的应用层（第七层）。\n超文本（Hypertext）是一种文本形式，它不仅包含普通的文本内容，还包含了可以链接到其他文本或资源的元素。这些链接通常称为“超链接”（Hyperlinks）。超文本的概念由Ted Nelson在1960年代提出，并在万维网（World Wide Web）的发展中得到了广泛应用。\nHTTP具有以下特点：\nmedia independent：支持任何类型的媒体内容\nconnectionless：无连接，客户端发送请求后，就断开了连接\nstateless：无状态，服务端不会保留客户端请求的状态，不知道谁是谁\nHTTP是基于TCP/IP协议的，通过TCP建立可靠连接。为什么说HTTP是无连接的，但又是基于TCP可靠连接的？如果想要可靠地传输数据，那么就必须要有稳定可靠的连接，保证数据不丢失，这个连接就是TCP连接。HTTP正是通过TCP连接来发送和接收数据的。\n在HTTP/1.0中，每次请求都会建立一个新的TCP连接，完成请求后立即关闭连接。在HTTP/1.1中，引入了持久连接，允许多个请求/响应在同一个TCP连接上进行，但每个请求/响应对话仍然是独立的。HTTP/1.1是持久连接，那么还能叫做无连接吗？是的，仍然是无连接。持久连接是通过 Connection: keep-alive 头部实现的，允许连接在一段时间内保持打开状态，不需要重新建立和关闭连接。无连接的本质是每个请求/响应对话都是独立的，强调的是独立。\nHTTP Requests HTTP请求是客户端发送给服务端的请求，包含以下内容：\nThe Request-line（请求行） The analysis of source IP address, proxy and port（源信息） The analysis of destination IP address, protocol, port and host（目标信息） The Requested URI (Uniform Resource Identifier)（链接） The Request method and Content（方法和内容） The User-Agent header（请求头） The Connection control header（请求头） The Cache control header（请求头） 可以在浏览器中通过F12开发者工具，查看HTTP请求信息。\nGET vs POST GET和POST是HTTP请求的2个主要方法，它们的区别如下：\n数据传输方式 GET：将请求参数附加在URL的查询字符串中，数据通过URL传输。例如：http://example.com/page?param1=value1\u0026amp;param2=value2 POST：将请求参数包含在请求体中，数据通过请求体传输，不会显示在URL中 安全性 GET：由于参数显示在URL中，敏感信息容易被暴露在浏览器历史记录、日志文件等地方，因此不适合传输敏感数据 POST：参数在请求体中传输，相对来说更安全，但并不意味着完全安全，仍需使用 HTTPS 来加密数据 数据长度限制 GET：由于URL长度限制（通常为2048字符），GET请求传输的数据量有限 POST：没有明显的数据长度限制，可以传输大量数据 幂等性 GET：幂等的，即多次相同的GET请求应该产生相同的结果，不会对服务器资源产生副作用 POST：非幂等的，即多次相同的POST请求可能会产生不同的结果，通常用于提交数据或触发服务器上的操作 缓存 GET：可以被浏览器缓存，适合用于获取不变的数据 POST：默认情况下不会被缓存，适合用于提交数据或执行操作 用途 GET：主要用于请求数据，例如获取网页内容、查询信息等 POST：主要用于提交数据，例如提交表单、上传文件等 Servlet Container Servlet容器（Servlet Container）是一个运行环境，用于管理和执行Java Servlet。它属于Java EE，负责处理客户端请求、管理Servlet生命周期以及提供各种服务（如安全性、并发性、资源管理等）。\nServlet容器是Web服务器（Web Server）的一部分，我们常说的Apache Tomcat，既是一个Web服务器（内置HTTP Server），也是一个Servlet容器。它能够处理静态内容和动态内容，提供了一个完整的运行环境来开发和部署Java Web应用。\nServer: Web vs Application Web服务器主要处理静态内容和HTTP请求，而应用服务器主要处理动态内容和业务逻辑。\n传统的Web服务器：\nApache HTTP Server：一个开源的Web服务器，广泛用于托管静态网站和动态Web应用 Nginx：一个高性能的Web服务器，特别擅长处理大量并发连接，常用于反向代理和负载均衡 现代的Web服务器：\nNode.js：一个基于Chrome V8引擎的JavaScript运行时环境，使用事件驱动、非阻塞I/O模型，使其非常适合构建高性能、可扩展的网络应用 Caddy：一个现代的Web服务器，默认支持HTTPS，配置简单，支持自动TLS证书管理 应用服务器：\nApache Tomcat：虽然主要是一个Servlet容器，但也可以作为一个轻量级的应用服务器 GlassFish：一个完整的Java EE应用服务器，支持所有Java EE规范 JBoss/WildFly：一个开源的Java EE应用服务器，提供全面的企业级功能 WebLogic：Oracle提供的企业级应用服务器，支持广泛的企业功能 WebSphere：IBM提供的企业级应用服务器，广泛用于大型企业应用 在实际应用中，Web服务器和应用服务器通常会一起使用，比如：\nNginx作为前端Web服务器，处理静态内容，将动态请求转发给后端的Tomcat应用服务器处理\nNginx作为反向代理，接收所有请求，并根据请求类型进行处理或转发，给后端的应用服务器Tomcat，同时可以实现负载均衡\nServlet容器既可以是Web服务器的一部分，例如，Apache Tomcat就是一个既包含Web服务器功能又包含Servlet容器功能的服务器。Tomcat能够处理静态内容（如HTML、CSS、JavaScript文件）和动态内容（如通过Servlet生成的内容）。\nServlet容器也可以是应用服务器的一部分，例如，在企业级应用服务器（如GlassFish、JBoss/WildFly）中，应用服务器不仅包含Servlet容器，还提供了许多其他企业级功能，如事务管理、消息队列、EJB（Enterprise JavaBeans）支持等。\n+----------------------+ +----------------------+ | Web Server | | Application Server | | | | | | +------------------+ | | +------------------+ | | | Servlet Container| | | | Servlet Container| | | | (Tomcat) | | | | (GlassFish, | | | +------------------+ | | | JBoss/WildFly) | | | | | +------------------+ | +----------------------+ | | | +------------------+ | | | Other Enterprise | | | | Features (EJB, | | | | Transactions, | | | | Messaging, etc.) | | | +------------------+ | +----------------------+ Content Type Content-Type是HTTP协议中的一个Header字段，用于指示发送给接收者的数据的媒体类型（MIME类型）。它告诉客户端或服务器如何解释和处理请求或响应中的数据。Content-Type在HTTP请求和响应中都可以使用。\n常见Content-Type如下：\n文本类型：\ntext/html：HTML文档 text/plain：纯文本 text/css：CSS样式表 text/javascript：JavaScript代码 应用类型：\napplication/json：JSON格式的数据 application/xml：XML格式的数据 application/x-www-form-urlencoded：表单数据，通常用于POST请求 application/octet-stream：二进制数据流，通常用于文件下载 多部分类型：\nmultipart/form-data：用于表单文件上传，表单数据可以包含文件 multipart/byteranges：表示响应包含多个部分，每个部分是一个字节范围 图像类型：\nimage/jpeg：JPEG图像 image/png：PNG图像 image/gif：GIF图像 音频和视频类型：\naudio/mpeg：MPEG音频文件 video/mp4：MP4视频文件 参考资料：\nhttps://www.javatpoint.com/web-terminology\nChatGPT\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/javaweb%E5%BC%80%E5%8F%91/000001-javaweb%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%807%E4%B8%AAweb%E6%9C%AF%E8%AF%AD%E8%A7%A3%E6%9E%90/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"7个web术语\"\u003e7个Web术语\u003c/h1\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eWebsite: static vs dynamic\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eHTTP\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eHTTP Requests\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eGET vs POST\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServlet Container\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eServer: Web vs Application\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eContent Type\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"website-static-vs-dynamic\"\u003eWebsite: static vs dynamic\u003c/h1\u003e\n\u003cp\u003e网站内容包括文本、图片、音频、视频，通过URL来访问。网站分为静态网站和动态网站。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e静态网站\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4049c3055e7c4b94b82e2f3045900890.png\"\u003e\u003c/p\u003e\n\u003cp\u003e静态网站是用HTML编写的，所有内容都包含在网页内，就像是打印出来的一样。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e动态网站\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/80e2b03f34184141a0339afca10a6a11.png\"\u003e\u003c/p\u003e\n\u003cp\u003e动态网站分为客户端和服务端，内容是可以动态变化的，可以根据用户请求动态更新网页内容。服务端语言有Servlet、ASP.NET等。\u003c/p\u003e\n\u003ch1 id=\"http\"\u003eHTTP\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4ba063afa32541c18a4e3a5faf113c7e.png\"\u003e\u003c/p\u003e\n\u003cp\u003eHTTP是Hypertext Transfer Protocol缩写，超文本传输协议，位于OSI网络分层模型的应用层（第七层）。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e超文本（Hypertext）是一种文本形式，它不仅包含普通的文本内容，还包含了可以链接到其他文本或资源的元素。这些链接通常称为“超链接”（Hyperlinks）。超文本的概念由Ted Nelson在1960年代提出，并在万维网（World Wide Web）的发展中得到了广泛应用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eHTTP具有以下特点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003emedia independent：支持任何类型的媒体内容\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003econnectionless：无连接，客户端发送请求后，就断开了连接\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003estateless：无状态，服务端不会保留客户端请求的状态，不知道谁是谁\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHTTP是基于TCP/IP协议的，通过TCP建立可靠连接。为什么说HTTP是无连接的，但又是基于TCP可靠连接的？如果想要可靠地传输数据，那么就必须要有稳定可靠的连接，保证数据不丢失，这个连接就是TCP连接。HTTP正是通过TCP连接来发送和接收数据的。\u003c/p\u003e\n\u003cp\u003e在HTTP/1.0中，每次请求都会建立一个新的TCP连接，完成请求后立即关闭连接。在HTTP/1.1中，引入了持久连接，允许多个请求/响应在同一个TCP连接上进行，但每个请求/响应对话仍然是独立的。HTTP/1.1是持久连接，那么还能叫做无连接吗？是的，仍然是无连接。持久连接是通过 \u003ccode\u003eConnection: keep-alive\u003c/code\u003e 头部实现的，允许连接在一段时间内保持打开状态，不需要重新建立和关闭连接。无连接的本质是每个请求/响应对话都是独立的，强调的是独立。\u003c/p\u003e\n\u003ch1 id=\"http-requests\"\u003eHTTP Requests\u003c/h1\u003e\n\u003cp\u003eHTTP请求是客户端发送给服务端的请求，包含以下内容：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/95b4f7c92d7f4276964b9da3397867d0.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe Request-line（请求行）\u003c/li\u003e\n\u003cli\u003eThe analysis of source IP address, proxy and port（源信息）\u003c/li\u003e\n\u003cli\u003eThe analysis of destination IP address, protocol, port and host（目标信息）\u003c/li\u003e\n\u003cli\u003eThe Requested URI (Uniform Resource Identifier)（链接）\u003c/li\u003e\n\u003cli\u003eThe Request method and Content（方法和内容）\u003c/li\u003e\n\u003cli\u003eThe User-Agent header（请求头）\u003c/li\u003e\n\u003cli\u003eThe Connection control header（请求头）\u003c/li\u003e\n\u003cli\u003eThe Cache control header（请求头）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e可以在浏览器中通过F12开发者工具，查看HTTP请求信息。\u003c/p\u003e","title":"JavaWeb开发基础7个Web术语解析"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n为什么学JUnit5 Java技术栈的单元测试框架有两个：JUnit和TestNG，有种说法是TestNG比JUnit更强大，学TestNG就够了，但是当我打开GitHub看到star的时候，犹豫了：\nJUnit\nTestNG\n相差了足足有3K之多。带着这个困惑，我在网上查阅了一番资料，原来JUnit5相较于JUnit4有了重大升级，已经包含了TestNG的所有功能。为了坚定我学JUnit的想法，我咨询了身边做Java开发的朋友，开发写UT都是用的JUnit。这两个理由足以让我开始对JUnit5的学习。\nJUnit5是什么 跟之前的JUnit有所不同，JUnit5由三个子项目的模块组成：\nJUnit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage\nJUnit Platform 在JVM上启动测试框架的基础，不仅支持Junit自制的测试引擎，其他测试引擎也都可以接入。提供了从命令行启动平台的Console Launcher，和支持JUnit4环境的JUnit4 based Runner。IDE一般都集成了JUnit Platform，比如IntelliJ IDEA。 JUnit Jupiter JUnit5新特性，结合了新的programming model和extension model，以便于在JUnit5中编写测试和扩展。提供了在平台上运行基于Jupiter测试的TestEngine。 JUnit Vintage 提供了在平台上运行基于JUnit3和JUnit4测试的TestEngine，照顾老的项目。 JUnit5新特性 使用JUnit Jupiter写测试 示例代码：\nimport static org.junit.jupiter.api.Assertions.assertEquals; import example.util.Calculator; import org.junit.jupiter.api.Test; class MyFirstJUnitJupiterTests { private final Calculator calculator = new Calculator(); @Test void addition() { assertEquals(2, calculator.add(1, 1)); } } 注意这个导包的路径org.junit.jupiter.api，Jupiter就是JUnit5的新特性。\n兼容JUnit4和JUnit3 JUnit Vintage提供了在平台上运行基于JUnit3和JUnit4测试的TestEngine， 并且新的包org.junit.jupiter避免了和JUnit4的org.junit在classpath上有任何冲突。\n多IDE运行 JUnit能在IntelliJ IDEA、Eclipse、NetBeans、Visual Studio Code等IDE中运行。\n从JUnit Jupiter扩展Model 示例：\n@ExtendWith(RandomParametersExtension.class) @Test void test(@Random int i) { // ... } 给test方法注册了一个新的RandomParametersExtension。\n更多高级用法 JUnit5提供了更多高级用法，比如JUnit Platform Launcher API和JUnit Platform Test Kit等，这些会在之后的文章逐一进行介绍。\n小结 本文首先解释了为什么要学JUnit5，一是因为全新一代JUnit功能更加强大，二是Java开发写单元测试都是用的JUnit，然后介绍了JUnit5是什么以及它的新特性。学习JUnit，在单元测试这条路上，又往前迈了一步。\n参考资料：\nhttps://junit.org/junit5/docs/current/user-guide/\nhttps://blog.csdn.net/weixin_45876795/article/details/111059031\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/000001-junitjava%E6%96%B0%E4%B8%80%E4%BB%A3%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E6%A1%86%E6%9E%B6junit5%E9%80%9F%E8%A7%88/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"为什么学junit5\"\u003e为什么学JUnit5\u003c/h1\u003e\n\u003cp\u003eJava技术栈的单元测试框架有两个：JUnit和TestNG，有种说法是TestNG比JUnit更强大，学TestNG就够了，但是当我打开GitHub看到star的时候，犹豫了：\u003c/p\u003e\n\u003cp\u003eJUnit\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ed3f70478ba3485b8f7640107f425fe7.png\"\u003e\u003c/p\u003e\n\u003cp\u003eTestNG\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d46a649ad5cb4a6b999f0cf96ea846bb.png\"\u003e\u003c/p\u003e\n\u003cp\u003e相差了足足有\u003cstrong\u003e3K\u003c/strong\u003e之多。带着这个困惑，我在网上查阅了一番资料，原来\u003cstrong\u003eJUnit5相较于JUnit4有了重大升级，已经包含了TestNG的所有功能\u003c/strong\u003e。为了坚定我学JUnit的想法，我咨询了身边做Java开发的朋友，\u003cstrong\u003e开发写UT都是用的JUnit\u003c/strong\u003e。这两个理由足以让我开始对JUnit5的学习。\u003c/p\u003e\n\u003ch1 id=\"junit5是什么\"\u003eJUnit5是什么\u003c/h1\u003e\n\u003cp\u003e跟之前的JUnit有所不同，JUnit5由三个子项目的模块组成：\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eJUnit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage\u003c/code\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eJUnit Platform\u003c/strong\u003e 在JVM上启动测试框架的基础，不仅支持Junit自制的测试引擎，其他测试引擎也都可以接入。提供了从命令行启动平台的Console Launcher，和支持JUnit4环境的JUnit4 based Runner。IDE一般都集成了JUnit Platform，比如IntelliJ IDEA。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJUnit Jupiter\u003c/strong\u003e JUnit5新特性，结合了新的programming model和extension model，以便于在JUnit5中编写测试和扩展。提供了在平台上运行基于Jupiter测试的TestEngine。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJUnit Vintage\u003c/strong\u003e 提供了在平台上运行基于JUnit3和JUnit4测试的TestEngine，照顾老的项目。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"junit5新特性\"\u003eJUnit5新特性\u003c/h1\u003e\n\u003ch2 id=\"使用junit-jupiter写测试\"\u003e使用JUnit Jupiter写测试\u003c/h2\u003e\n\u003cp\u003e示例代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport static\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Assertions.assertEquals\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eexample.util.Calculator\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003eorg.junit.jupiter.api.Test\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eMyFirstJUnitJupiterTests\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCalculator\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecalculator\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eCalculator\u003cspan style=\"color:#1f2328\"\u003e();\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Test\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003eaddition\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003eassertEquals\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e2\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003ecalculator\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eadd\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e1\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e注意这个导包的路径\u003ccode\u003eorg.junit.jupiter.api\u003c/code\u003e，\u003cstrong\u003eJupiter就是JUnit5的新特性\u003c/strong\u003e。\u003c/p\u003e","title":"Java新一代单元测试框架JUnit5速览"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nJMeter是性能测试工具，在使用JMeter变多以后，越来越觉得之所以它能成为当今时代最主流测试工具，是因为它太成熟了，举个小例子，编写GET请求的时候，需要把查询参数，也就是?号后面的请求参数添加进去，JMeter竟然有个Add from Clipboard功能，在浏览器复制以后，点这个按钮就填充好了:\n有优点也有缺点，它的UI界面确实丑了点，英文版稍微弥补了一下，相较于中文，只由24个字母组成的英文感官上更简洁一点点。JMeter系列文章都将基于英文版进行截图，重要信息会翻译为中文进行解释。\nJMeter最核心的元素是组件，它们构成了JMeter结构图：\n这是一个三维空间，先看Z轴，只有监听器，用于对测试结果数据进行收集和统计。X轴分为5个阶段，从上往下是运行流程。Y轴中的Y1元件很多，用来模拟用户请求，Y2是断言，对请求响应结果进行验证。接下来一个一个元件来看。\nThreads 第一步就得添加这个，没有线程组，JMeter也没法使用。一个线程模拟一个用户，线程组用来模拟多个用户。\nSampler Sampler是一个无界面浏览器，用于发送请求，接收响应。支持现有各种协议，比如HTTP、WebSocket、MQTT等。默认没有的话，添加个第三方jar包就可以了，还支持自定义jar包，扩展性极强。\nAssertions 响应对不对，用断言来验证，从图中可以看到，断言类型有很多种。\nListener 用的最多的是第一个：View Results True，实时查看请求响应数据。压测时还会添加Aggregate Report，实时查看线程组统计数据，包括平均响应时间、吞吐量、TPS等性能测试指标。此外，还有其他类型的图表报告，可以导出成本地文件进行分析。\nPre Processors 请求前的预处理，比如连接数据库等。\nConfig Element JMeter四种参数化方式这里面就有两个：CSV Data Set Config和User Defined Variables，顾名思义它们就是为请求提供配置数据的，还有HTTP Header Manager给请求添加Header，HTTP Cookie Manager自动记录服务器返回的Cookie。\nPost Processors 对响应进行数据提取使用后置处理器，常见的是使用Regular Expression Extractor正则表达式提取，我现在用的比较多的是JSON JMESPath Extractor，语法更直观，匹配更精确。\nLogic Controller JMeter也具备逻辑控制能力，预置了多种控制器，比如Once Only Controller，可以用来只读取一次csv文件，或者只登陆一次。\nTimer 它提供了各种功能的定时器，比如Constant Timer就像脚本里面的sleep，等待一段时间。比如Synchronizing Timer设置集合点，让多个线程同一时刻发起请求。\nTest Fragment 可以通过 测试计划|Test Fragment 添加。线程组也可以添加。\n可用于临时备份元件，Test Fragment下的元件不会被运行。封装后可被模块控制器调用，就像编程一样。\n小结 本文是JMeter系列开篇，介绍了全部组件的概览情况，从结构图可以知道，取样器、断言、监听器组合在一起可以完成发送请求、验证结果和记录结果。前置处理器、配置元件、后置处理器为取样器提供了数据支持。逻辑控制器具备流程控制能力，实现业务需求。定时器设置等待时间和集合点等。Test Fragment用来备份元件，底下的元件不会被运行。这些组件的底层是由线程组驱动的。\n参考资料：\n《全栈性能测试修炼宝典JMeter实战》\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/000001-jmeterjmeter%E5%85%A8%E9%83%A8%E7%BB%84%E4%BB%B6%E6%A6%82%E8%A7%88/","summary":"JMeter是性能测试工具，在使用JMeter变多以后，越来越觉得之所以它能成为当今时代最主流测试工具，是因为它太成熟了，举个小例子，编写GET请求的时候，需要把查询参数，也就是?号后面的请求参数添加进去，JMeter竟然有个Add from Clipboard功能，在浏览器复制以后，点这个按钮就填充好了:","title":"JMeter全部组件概览"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n将key-value的JSON数据，转换为insert的SQL语句，进而实现网页数据存储到数据库。\n解析MySQL建表语句，找出“字符串”字段\n遍历JSON结构，拼接为SQL语句\n1、解析建表语句\n之所以要解析建表语句，是因为在拼接SQL时，“字符串”字段没有双引号，需要手动加上。第一步就是通过JDBC来解析表字段，根据字段类型，筛选出“字符串”字段。\npublic static Map\u0026lt;String, String[]\u0026gt; parseTable(String tableName) { List\u0026lt;String\u0026gt; stringType = new ArrayList\u0026lt;\u0026gt;(Arrays.asList(\u0026#34;CHAR\u0026#34;, \u0026#34;VARCHAR\u0026#34;, \u0026#34;TEXT\u0026#34;, \u0026#34;DATE\u0026#34;, \u0026#34;TIME\u0026#34;, \u0026#34;DATETIME\u0026#34;, \u0026#34;TIMESTAMP\u0026#34;)); List\u0026lt;String\u0026gt; stringFieldList = new ArrayList\u0026lt;\u0026gt;(); try { DatabaseMetaData databaseMetaData = conn.getMetaData(); ResultSet specificResultSet = databaseMetaData.getColumns(null, \u0026#34;%\u0026#34;, tableName, \u0026#34;%\u0026#34;); String columnName; String columnType; while (specificResultSet.next()) { columnName = specificResultSet.getString(\u0026#34;COLUMN_NAME\u0026#34;); columnType = specificResultSet.getString(\u0026#34;TYPE_NAME\u0026#34;); if (stringType.contains(columnType) \u0026amp;\u0026amp; !stringFieldList.contains(columnName)) { stringFieldList.add(columnName); } } } catch (Exception e) { log.error(\u0026#34;parse error \u0026#34;, e); } Map\u0026lt;String, String[]\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(tableName, stringFieldList.toArray(new String[0])); return map; } “字符串”字段识别了类型为\u0026quot;CHAR\u0026quot;, \u0026ldquo;VARCHAR\u0026rdquo;, \u0026ldquo;TEXT\u0026rdquo;, \u0026ldquo;DATE\u0026rdquo;, \u0026ldquo;TIME\u0026rdquo;, \u0026ldquo;DATETIME\u0026rdquo;, \u0026ldquo;TIMESTAMP\u0026rdquo;。conn是通过mysql-connector-java创建的数据库连接对象。\n2、拼接insert语句\npublic static String json2Sql(JSONObject json, String tableName) { Map\u0026lt;String, String[]\u0026gt; map = parseTable(tableName); ArrayList\u0026lt;String\u0026gt; keys = new ArrayList\u0026lt;\u0026gt;(); ArrayList\u0026lt;String\u0026gt; values = new ArrayList\u0026lt;\u0026gt;(); for (Map.Entry\u0026lt;String, Object\u0026gt; entry : json.entrySet()) { if (entry.getKey().equals(\u0026#34;id\u0026#34;)) { continue; } keys.add(entry.getKey()); String value = entry.getValue() == null ? null : entry.getValue().toString(); if (Arrays.asList(map.get(tableName)).contains(entry.getKey())) { if (value != null) { if (value.indexOf(\u0026#39;\u0026#34;\u0026#39;) != -1) { // 处理扩展字段包含json情况 value = String.format(\u0026#34;\\\u0026#34;%s\\\u0026#34;\u0026#34;, value.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\\\\\\\u0026#34;\u0026#34;)); } else { value = \u0026#39;\u0026#34;\u0026#39; + value + \u0026#39;\u0026#34;\u0026#39;; // 字符串字段给sql语句手动加上双引号 } } } values.add(value); } return String.format(\u0026#34;insert into %s(%s) values (%s);\u0026#34;, tableName, String.join(\u0026#34;,\u0026#34;, keys), String.join(\u0026#34;,\u0026#34;, values)); } 拼接时先过滤id字段，然后判空，最后根据“字符串”字段判断是否需要手动添加双引号，注意扩展字段有可能json，也就是会出现双重双引号，需要加上转移字符。\n3、测试\n@Test void json2sql() { String json = \u0026#34;{\\n\u0026#34; + \u0026#34; \\\u0026#34;id\\\u0026#34;: \\\u0026#34;1\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;name\\\u0026#34;: \\\u0026#34;gang\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;no\\\u0026#34;: null,\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: 1,\\n\u0026#34; + \u0026#34; \\\u0026#34;created\\\u0026#34;: \\\u0026#34;2020-05-09 11:35:40\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;ext\\\u0026#34;: \\\u0026#34;{\\\\\\\u0026#34;email\\\\\\\u0026#34;:\\\\\\\u0026#34;abc@qq.com\\\\\\\u0026#34;}\\\u0026#34;\\n\u0026#34; + \u0026#34;}\u0026#34;; JSONObject jsonObject = JSON.parseObject(json); String sql = DBUtil.json2Sql(jsonObject, \u0026#34;my_test_table\u0026#34;); log.info(sql); } 输出：\ninsert into my_test_table(ext,no,created,name,type) values (\u0026#34;{\\\u0026#34;email\\\u0026#34;:\\\u0026#34;abc@qq.com\\\u0026#34;}\u0026#34;,null,\u0026#34;2020-05-09 11:35:40\u0026#34;,\u0026#34;gang\u0026#34;,1); JSON转SQL，需要根据字段类型做特殊处理转换。\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/%E5%B0%8F%E5%B7%A5%E5%85%B7/000001-json%E8%BD%ACsql%E5%B0%8F%E5%B7%A5%E5%85%B7%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/","summary":"将key-value的JSON数据，转换为insert的SQL语句，进而实现网页数据存储到数据库。","title":"JSON转SQL小工具源码分享"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n安装Python和编辑器PyCharm Python安装包下载地址是： https://www.python.org/downloads/ PyCharm安装包下载地址是： https://www.jetbrains.com/pycharm/download/ 下载后按常规软件安装。\npytest pytest是Python内置包，无需单独安装，但是需要设置。\n创建1个Project和1个test.py来试试，左上角点击 File | New Project | create，创建项目。在项目中右键选择 New | File 创建test.py\n接着设置PyCharm，默认PyCharm自带的测试框架是Unittests，在 File | Settings | Tools | Python Integrated Tools | Testing | Default test runner，修改为pytest\n在test.py文件中添加代码\ndef test(): pass 这个时候def的左边会出现一个绿色的箭头，点击之后就可以使用pytest运行，注意一定要是 pytest for\u0026hellip; 才可以，如果只是 Run test.py，是不走pytest执行的（右上角Edit Configurations删掉来解决）\n执行之后控制台输出信息就可以了\nTesting started at 11:10 AM ... \u0026#34;D:\\Program Files (x86)\\Python38-32\\python.exe\u0026#34; \u0026#34;D:\\Program Files\\JetBrains\\PyCharm Community Edition 2020.1.2\\plugins\\python-ce\\helpers\\pycharm\\_jb_pytest_runner.py\u0026#34; --target test.py::test Launching pytest with arguments test.py::test in D:\\PycharmProjects\\untitled ============================= test session starts ============================= platform win32 -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 -- D:\\Program Files (x86)\\Python38-32\\python.exe cachedir: .pytest_cache rootdir: D:\\PycharmProjects\\untitled collecting ... collected 1 item test.py::test PASSED [100%] ============================== 1 passed in 0.14s ============================== Process finished with exit code 0 至此，pytest的环境就准备好了。\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/pytest/000001-pytestpytest%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"安装python和编辑器pycharm\"\u003e安装Python和编辑器PyCharm\u003c/h2\u003e\n\u003cp\u003ePython安装包下载地址是：\n\u003ca href=\"https://www.python.org/downloads/\"\u003ehttps://www.python.org/downloads/\u003c/a\u003e\nPyCharm安装包下载地址是：\n\u003ca href=\"https://www.jetbrains.com/pycharm/download/\"\u003ehttps://www.jetbrains.com/pycharm/download/\u003c/a\u003e\n下载后按常规软件安装。\u003c/p\u003e\n\u003ch2 id=\"pytest\"\u003epytest\u003c/h2\u003e\n\u003cp\u003epytest是Python内置包，无需单独安装，但是\u003cstrong\u003e需要设置\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e创建1个Project和1个test.py来试试，左上角点击 File | New Project | create，创建项目。在项目中右键选择 New | File 创建test.py\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/04e917d2accb49c898675998ef16712f.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/7abfb30af4024c75bd0b31291206334d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/741e459b613d48e290763371ba172eef.png\"\u003e\u003c/p\u003e\n\u003cp\u003e接着设置PyCharm，默认PyCharm自带的测试框架是Unittests，在 File | Settings | Tools | Python Integrated Tools | Testing | Default test runner，修改为pytest\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/183120bd3788416d8e9e89c26b5e9f36.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在test.py文件中添加代码\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#cf222e\"\u003epass\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这个时候def的左边会出现一个绿色的箭头，点击之后就可以使用pytest运行，注意一定要是 \u003cstrong\u003epytest for\u0026hellip;\u003c/strong\u003e 才可以，如果只是 Run test.py，是不走pytest执行的（右上角Edit Configurations删掉来解决）\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3ee3e41db80b4ef9b4d3937d147888d8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e执行之后控制台输出信息就可以了\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eTesting started at 11:10 AM ...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;D:\\Program Files (x86)\\Python38-32\\python.exe\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;D:\\Program Files\\JetBrains\\PyCharm Community Edition 2020.1.2\\plugins\\python-ce\\helpers\\pycharm\\_jb_pytest_runner.py\u0026#34;\u003c/span\u003e --target test.py::test\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eLaunching pytest with arguments test.py::test in D:\u003cspan style=\"color:#0a3069\"\u003e\\P\u003c/span\u003eycharmProjects\u003cspan style=\"color:#0a3069\"\u003e\\u\u003c/span\u003entitled\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e=============================\u003c/span\u003e \u003cspan style=\"color:#6639ba\"\u003etest\u003c/span\u003e session \u003cspan style=\"color:#953800\"\u003estarts\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e=============================\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eplatform win32 -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 -- D:\u003cspan style=\"color:#0a3069\"\u003e\\P\u003c/span\u003erogram Files \u003cspan style=\"color:#0550ae\"\u003e(\u003c/span\u003ex86\u003cspan style=\"color:#0550ae\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\\P\u003c/span\u003eython38-32\u003cspan style=\"color:#0a3069\"\u003e\\p\u003c/span\u003eython.exe\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecachedir: .pytest_cache\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003erootdir: D:\u003cspan style=\"color:#0a3069\"\u003e\\P\u003c/span\u003eycharmProjects\u003cspan style=\"color:#0a3069\"\u003e\\u\u003c/span\u003entitled\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecollecting ... collected \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e item\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etest.py::test PASSED                                                     \u003cspan style=\"color:#0550ae\"\u003e[\u003c/span\u003e100%\u003cspan style=\"color:#0550ae\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e==============================\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e1\u003c/span\u003e passed in 0.14s \u003cspan style=\"color:#0550ae\"\u003e==============================\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eProcess finished with \u003cspan style=\"color:#6639ba\"\u003eexit\u003c/span\u003e code \u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e至此，pytest的环境就准备好了。\u003c/p\u003e","title":"pytest快速入门"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n测试平台，有人说它鸡肋，有人说它有用，有人说它轮子，众说纷纭，不如从自身出发，考虑是否要做测试平台：\n第1阶段，用Python+requests写接口自动化。 第2阶段，选择unitttest或pytest，更熟悉pytest选了pytest。 第3阶段，快速搭建pytest项目脚手架，封装tep测试工具。 第4阶段，通过Git管理测试脚本，多分支合并代码。 第5阶段，去除本地环境同步麻烦，方便团队共享脚本。 需要有个测试平台。\n使用篇 环境变量 环境变量是字符串键值对，全局作用域。比如不同环境不同域名：\n使用：env_vars.name。\nfixtures fixtures即pytest的fixture，可以添加自定义函数，供测试用例使用。比如封装登录接口返回token：\ntep.fixture提供了url fixture，自动拼接环境变量env_vars.domain + uri。\n测试用例 在前端网页写代码，1条用例对应1个pytest的test_name.py文件。比如调用login fixture登录：\n本地编写 PyCharm写代码体验更好，正确姿势是从平台下载包含环境变量和fixtures等项目结构代码，本地编写用例，调试，跑通后，粘贴到平台上共享和维护：\n本地和平台环境一致，省去前期搭建，关注tests用例。\n扩展能力 用例是Python代码，理论上所有Python能写出来的，平台都能支持，比如HTTP、WebSocket、Protobuf等协议。\n原理篇 pytest内核 vue2-ace-editor作为前端代码编辑组件。 前端把代码通过HTTP请求传给后端。 后端把代码存入MySQL数据库。 运行用例，从数据库取出代码，生成pytest文件。 Shell命令调用pytest -s test_name.py，执行测试。 后端把运行结果日志返给前端展示。 之所以要折腾数据库，是因为每次部署后docker容器里面的文件会被清掉，只能动态生成。\ntep脚手架 测试平台功能是从tep项目脚手架中抽取出来的：\n把fixture_env_vars.py做成了环境变量功能。 把fixture_login.py等做成了fixtures功能。 把tests做成了测试用例功能。 运行用例 整体流程如下：\ntep startproject project_name\n运行用例时，判断项目目录是否存在，如果不存在就调用tep startproject project_name创建项目脚手架。\n更新conf.yaml中env\n把前端传的当前运行环境更新到conf.yaml文件中：\nenv: qa 动态生成或更新fixture_env_vars.py文件\n根据环境变量功能模块的数据，动态生成fixture_env_vars.py文件：\n#!/usr/bin/python ## encoding=utf-8 from tep.dao import mysql_engine from tep.fixture import * @pytest.fixture(scope=\u0026#34;session\u0026#34;) def env_vars(config): class Clazz(TepVars): env = config[\u0026#34;env\u0026#34;] \u0026#34;\u0026#34;\u0026#34;Variables define start\u0026#34;\u0026#34;\u0026#34; # Environment and variables mapping = { \u0026#34;qa\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://qa.com\u0026#34;, }, \u0026#34;release\u0026#34;: { \u0026#34;domain\u0026#34;: \u0026#34;https://release.com\u0026#34;, } # Add your environment and variables } # Define properties for auto display domain = mapping[env][\u0026#34;domain\u0026#34;] \u0026#34;\u0026#34;\u0026#34;Variables define end\u0026#34;\u0026#34;\u0026#34; return Clazz() 动态生成或更新fixtures目录下所有文件\n根据fixtures功能模块的数据，动态生成fixture_login.py等所有文件：\nfrom tep.client import request from tep.fixture import * def _jwt_headers(token): return {\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;authorization\u0026#34;: f\u0026#34;Bearer {token}\u0026#34;} @pytest.fixture(scope=\u0026#34;session\u0026#34;) def login(url): # Code your login logger.info(\u0026#34;Administrator login\u0026#34;) response = request( \u0026#34;post\u0026#34;, url=url(\u0026#34;/api/users/login\u0026#34;), headers={\u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, json={ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34;, } ) assert response.status_code \u0026lt; 400 response_token = jmespath.search(\u0026#34;token\u0026#34;, response.json()) class Clazz: token = response_token jwt_headers = _jwt_headers(response_token) return Clazz conftest.py会自动查找后import，tests用例直接使用。\n动态生成或更新tests某个test_文件\n从数据库拿到用例代码，动态生成test_文件。\nShell执行pytest命令\n从上一步拿到case_path，调用pytest -s case_path执行测试。\n计划后续添加suite和marker两种批量执行用例方式。\n小结 本文介绍了我第一次做的测试平台的使用和原理，技术栈为Vue+Django+Django REST Framework+JWT+MySQL+pytest+Git+BitBucket+Drone+Nginx+Docker+K8S，已在公司落地，还未大规模产出，由于服务端有较多磁盘IO读写，大量使用后不知道性能如何，目前来看问题不大，需要持续观察和优化。测试平台底层是pytest，用到了tep，那就叫teprunner。\n参考资料：\nhttps://github.com/dongfanger/tep\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/teprunner/000001-%E5%BC%80%E6%BA%90%E5%B9%B3%E5%8F%B0pytest%E5%86%85%E6%A0%B8%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0%E8%90%BD%E5%9C%B0%E5%88%9D%E4%BD%93%E9%AA%8C/","summary":"测试平台，有人说它鸡肋，有人说它有用，有人说它轮子，众说纷纭，不如从自身出发，考虑是否要做测试平台：","title":"pytest内核测试平台落地初体验"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n消息队列 RabbitMQ是一个消息队列，它能够接收和转发消息。这个过程就像寄快递一样，把物件打包给快递小哥，快递小哥会负责把物件派送到正确的地址。\n生产者和消费者 生产者就是用来生产消息（发送消息）的：\n消费者就是用来消费消息（接收消息）的：\n在生产者和消费者之间的就是消息队列：\n它相当于消息缓冲区，最多能存储多少数据只受限于机器的内存和磁盘。多个生产者可以发送消息给同一个队列，多个消费者也可以从同一个队列接收消息。\nWindows安装RabbitMQ 参考mall商城学习教程的RabbitMQ部分内容：\nhttp://www.macrozheng.com/#/architect/mall_arch_09?id=rabbitmq\n原文中rabbitmq-server-3.7.14.exe下载地址失效了，改从这里下载：\nhttps://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.14\n安装完成后，确认服务已开启：\n进入RabbitMQ安装目录下的sbin目录：\n在地址栏输入cmd并回车启动命令行，然后输入以下命令启动管理功能：\nrabbitmq-plugins enable rabbitmq_management RabbitMQ运行在本地机器上：\nhttp://localhost:15672/\n默认用户名密码为guest / guest：\nJava客户端 amqp-client-5.7.1.jar是RabbitMQ官方提供的Java客户端：\nhttps://www.rabbitmq.com/tutorials/tutorial-one-java.html\n既可以直接下载jar包，也可以在Maven中添加依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.rabbitmq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;amqp-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.14.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 生产消息 导包：\nimport com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Connection; import com.rabbitmq.client.Channel; 创建类Send，定义队列名为hello：\npublic class Send { private final static String QUEUE_NAME = \u0026#34;hello\u0026#34;; public static void main(String[] argv) throws Exception { ... } } 建立连接：\nConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { } 代码中创建了一个Connection实例和一个Channel实例，它们都用try语句包裹了起来，这是因为Connection和Channel类都实现了java.io.Closeable，try语句会自动关闭连接。\n声明消息队列，并发送Hello World!消息到队列中：\nchannel.queueDeclare(QUEUE_NAME, false, false, false, null); String message = \u0026#34;Hello World!\u0026#34;; channel.basicPublish(\u0026#34;\u0026#34;, QUEUE_NAME, null, message.getBytes()); System.out.println(\u0026#34; [x] Sent \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); 声明消息队列是个幂等操作，重复声明不会重复创建队列。\n消息体是字节数组（byte array）。\nSend.java完整代码：\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/Send.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.nio.charset.StandardCharsets; public class Send { private final static String QUEUE_NAME = \u0026#34;hello\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.queueDeclare(QUEUE_NAME, false, false, false, null); String message = \u0026#34;Hello World!\u0026#34;; channel.basicPublish(\u0026#34;\u0026#34;, QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8)); System.out.println(\u0026#34; [x] Sent \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); } } } 消费消息 消费消息的代码跟生产消息的代码类似，也需要导包，建立连接：\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; public class Recv { private final static String QUEUE_NAME = \u0026#34;hello\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); System.out.println(\u0026#34; [*] Waiting for messages. To exit press CTRL+C\u0026#34;); } } 消费者也声明了一个消息队列，因为有可能消费者比生产者先启动。这样能确保消费消息时，有队列存在。\n消费者没有用try语句，因为消费者一直在异步监听消息，如果把连接关闭了，它就没法消费了。\n导包中有个DeliverCallback，通过它就能消费消息：\nDeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { String message = new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;); System.out.println(\u0026#34; [x] Received \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); }; channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -\u0026gt; { }); 因为发送消息和接收消息都是异步的，所以它叫做，callback，回调。\nRecv.java完整代码：\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/Recv.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; public class Recv { private final static String QUEUE_NAME = \u0026#34;hello\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); System.out.println(\u0026#34; [*] Waiting for messages. To exit press CTRL+C\u0026#34;); DeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { String message = new String(delivery.getBody(), StandardCharsets.UTF_8); System.out.println(\u0026#34; [x] Received \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); }; channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -\u0026gt; { }); } } 运行效果 运行Send.java生产消息后，能看到RabbitMQ后台已经有1条消息：\n和1个消息队列：\n并且发送完成后就断开了连接。\n运行Recv.java消费消息后，能看到队列中已经没有消息了：\n而消费者仍然保持着连接，持续监控新消息。如果把消费者停掉，连接就会断开。\n从消息队列中能看到整个过程如下图所示：\n任务分发 任务分发是把多个任务扔进队列，然后分发给多个worker来执行。之所以要用队列来实现，是因为任务处理需要一定时长，如果一直等待会导致阻塞，而异步把任务排到队列里，就能加快分发，在取出任务时，也能根据各个worker负载情况，均衡分配。尤其是在Web应用中，HTTP连接短暂就会断开，异步处理就特别适用。排队比蜂拥而至办事效率更高。\n实现任务分发，新建NewTask.java发送消息：\nString message = String.join(\u0026#34; \u0026#34;, argv); channel.basicPublish(\u0026#34;\u0026#34;, \u0026#34;hello\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34; [x] Sent \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); 用message来模拟任务。\n新建Worker.java接收消息：\nDeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { String message = new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;); System.out.println(\u0026#34; [x] Received \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); try { doWork(message); } finally { System.out.println(\u0026#34; [x] Done\u0026#34;); } }; boolean autoAck = true; // acknowledgment is covered below channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -\u0026gt; { }); private static void doWork(String task) throws InterruptedException { for (char ch: task.toCharArray()) { if (ch == \u0026#39;.\u0026#39;) Thread.sleep(1000); } } doWrok()模拟任务处理，用.来表示时长，hello...就代表要处理3秒。\n然后在2个shell启动2个worker：\n## shell 1 java -cp $CP Worker ## =\u0026gt; [*] Waiting for messages. To exit press CTRL+C ## shell 2 java -cp $CP Worker ## =\u0026gt; [*] Waiting for messages. To exit press CTRL+C 发送5条任务：\n## shell 3 java -cp $CP NewTask First message. ## =\u0026gt; [x] Sent \u0026#39;First message.\u0026#39; java -cp $CP NewTask Second message.. ## =\u0026gt; [x] Sent \u0026#39;Second message..\u0026#39; java -cp $CP NewTask Third message... ## =\u0026gt; [x] Sent \u0026#39;Third message...\u0026#39; java -cp $CP NewTask Fourth message.... ## =\u0026gt; [x] Sent \u0026#39;Fourth message....\u0026#39; java -cp $CP NewTask Fifth message..... ## =\u0026gt; [x] Sent \u0026#39;Fifth message.....\u0026#39; worker的处理情况如下：\njava -cp $CP Worker ## =\u0026gt; [*] Waiting for messages. To exit press CTRL+C ## =\u0026gt; [x] Received \u0026#39;First message.\u0026#39; ## =\u0026gt; [x] Received \u0026#39;Third message...\u0026#39; ## =\u0026gt; [x] Received \u0026#39;Fifth message.....\u0026#39; java -cp $CP Worker ## =\u0026gt; [*] Waiting for messages. To exit press CTRL+C ## =\u0026gt; [x] Received \u0026#39;Second message..\u0026#39; ## =\u0026gt; [x] Received \u0026#39;Fourth message....\u0026#39; 任务是循环调度的，worker1总是处理奇数序列的任务，worker2总是处理偶数序列的任务。\n消息确认 RabbitMQ支持消息确认，consumer在接收到消息并处理后，会回传一个ack给producer，告诉RabbitMQ这条消息已经接收成功了。它的好处是能防止worker挂掉而丢失消息，因为假如producer没有收到消息确认，它会保留这条消息，重新发送给其他worker。消息确认过程默认有30秒的超时时间，超过30秒没有收到消息确认，就会重试。\n在代码中有个默认设置：\nboolean autoAck = true; channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -\u0026gt; { }); autoAck表示自动确认，在消息发送出去以后，就自动确认了。这就起不到防止消息丢失的效果，所以通常会设置为：\nboolean autoAck = false; 消息持久化 RabbitMQ重启以后，所有的队列和消息都会丢失，消息持久化能保留这些数据，在重启后恢复所有的队列和消息。\n队列持久到用到了durable参数：\nboolean durable = true; channel.queueDeclare(\u0026#34;task_queue\u0026#34;, durable, false, false, null); 需要注意的是，修改队列的参数必须重新命名新的队列，因为RabbitMQ不支持对现有队列的参数进行修改。\n消息持久化用到了MessageProperties：\nimport com.rabbitmq.client.MessageProperties; channel.basicPublish(\u0026#34;\u0026#34;, \u0026#34;task_queue\u0026#34;, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes()); 均衡调度 均衡调度是根据worker负载情况来合理分配任务。前面实现的任务分发是循环调度的，worker1总是处理奇数序列的任务，worker2总是处理偶数序列的任务。假如奇数序列的任务始终比偶数序列的任务繁忙，处理起来耗时长，那么就会导致worker1一直繁忙而worker2处于空闲。这显然不是很合理。\n这是因为RabbitMQ默认只是盲目的将第n个消息发给第n个consumer，而不会去管有多少个未确认的消息数量。\nRabbitMQ提供了prefetchCount参数来实现均衡调度：\nint prefetchCount = 1; channel.basicQos(prefetchCount); 通过设置prefetchCount为1，RabbitMQ一次只会给一个worker分发一条消息，假如某个worker比较繁忙，那么只会等它处理完成回传消息确认（Message acknowledgment）后，才会分发新消息给它。\n完整代码 NewTask.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MessageProperties; public class NewTask { private static final String TASK_QUEUE_NAME = \u0026#34;task_queue\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); String message = String.join(\u0026#34; \u0026#34;, argv); channel.basicPublish(\u0026#34;\u0026#34;, TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(\u0026#34;UTF-8\u0026#34;)); System.out.println(\u0026#34; [x] Sent \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); } } } Worker.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; public class Worker { private static final String TASK_QUEUE_NAME = \u0026#34;task_queue\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); final Connection connection = factory.newConnection(); final Channel channel = connection.createChannel(); channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); System.out.println(\u0026#34; [*] Waiting for messages. To exit press CTRL+C\u0026#34;); channel.basicQos(1); DeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { String message = new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;); System.out.println(\u0026#34; [x] Received \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); try { doWork(message); } finally { System.out.println(\u0026#34; [x] Done\u0026#34;); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); } }; channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -\u0026gt; { }); } private static void doWork(String task) { for (char ch : task.toCharArray()) { if (ch == \u0026#39;.\u0026#39;) { try { Thread.sleep(1000); } catch (InterruptedException _ignored) { Thread.currentThread().interrupt(); } } } } } 发布订阅 发布订阅是发送一条消息给多个consumer的一对多模式。不同于任务分发的一个消息只发送给一个worker的一对一模式。\n接下来将实现一个日志系统来说明发布订阅的一对多模式，它由2个程序组成，第一个程序负责提交日志消息，第二个程序负责接收消息并打印出来。第二个程序有2个实例，第1个实例接收消息存储到磁盘，同时第2个实例把日志打印到屏幕。\nExchange 回顾几个概念：\nproducer：生产者，发送消息。\nqueue：队列，消息缓存。\nconsumer：消费者，接收消息。\n实际上producer并不会直接给queue发送消息，它并不知道消息会发给哪个queue。在producer和queue中间，有一个叫做Exchange的东西：\nproducer只会把消息发送给Exchage，而Exchange的作用就是定义消息转发规则：\n消息发给一个队列？\n消息发给多个队列？\n消息应该被忽略掉？\n等等等\nExchange有几种类型：direct, topic, headers 和fanout，这里讨论最后一个fanout，它会把所有消息广播给所有队列。\n创建命名为logs的Exchange：\nchannel.exchangeDeclare(\u0026#34;logs\u0026#34;, \u0026#34;fanout\u0026#34;); 发送消息到Exchange：\nchannel.basicPublish( \u0026#34;logs\u0026#34;, \u0026#34;\u0026#34;, null, message.getBytes()); 还记得之前的代码么：\nchannel.basicPublish(\u0026#34;\u0026#34;, \u0026#34;hello\u0026#34;, null, message.getBytes()); 第一个参数为空字符串，这并不是说没有Exchange，而是使用了RabbitMQ默认的Exchange。\n有Exchange的消息队列，才是真正的消息队列。\n临时队列 临时队列主要用来临时传输消息。在将要实现的日志系统中，因为是广播所有消息，所以并不关心转发给哪个具体的队列，只要有个队列就行。并且每次连接到队列时，都希望队列里面是空的，没有老数据。在consumer断开连接后，队列也能自动删除。\n可以通过无参数的queueDeclare()来实现临时队列：\nString queueName = channel.queueDeclare().getQueue(); 这样能创建一个非持久化的、独占的、自动删除的队列。它的队列名是个随机名字，比如amq.gen-JzTY20BRgKO-HjmUJj0wLg。\n绑定 Exchange需要跟队列绑定：\nchannel.queueBind(queueName, \u0026#34;logs\u0026#34;, \u0026#34;\u0026#34;); 绑定后Exchange才知道把消息发给哪些队列。\n可以使用命令查看RabbitMQ存在哪些绑定：\nrabbitmqctl list_bindings 完整代码 EmitLog.java\npublic class EmitLog { private static final String EXCHANGE_NAME = \u0026#34;logs\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.exchangeDeclare(EXCHANGE_NAME, \u0026#34;fanout\u0026#34;); String message = argv.length \u0026lt; 1 ? \u0026#34;info: Hello World!\u0026#34; : String.join(\u0026#34; \u0026#34;, argv); channel.basicPublish(EXCHANGE_NAME, \u0026#34;\u0026#34;, null, message.getBytes(\u0026#34;UTF-8\u0026#34;)); System.out.println(\u0026#34; [x] Sent \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); } } } producer并不需要定义队列，它只把消息发给Exchange即可。\nReceiveLogs.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; public class ReceiveLogs { private static final String EXCHANGE_NAME = \u0026#34;logs\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(EXCHANGE_NAME, \u0026#34;fanout\u0026#34;); String queueName = channel.queueDeclare().getQueue(); channel.queueBind(queueName, EXCHANGE_NAME, \u0026#34;\u0026#34;); System.out.println(\u0026#34; [*] Waiting for messages. To exit press CTRL+C\u0026#34;); DeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { String message = new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;); System.out.println(\u0026#34; [x] Received \u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); }; channel.basicConsume(queueName, true, deliverCallback, consumerTag -\u0026gt; { }); } } 运行效果 配置环境变量：\nmac：\nexport CP=.:amqp-client-5.7.1.jar:slf4j-api-1.7.26.jar:slf4j-simple-1.7.26.jar windows：\nset CP=.;amqp-client-5.7.1.jar;slf4j-api-1.7.26.jar;slf4j-simple-1.7.26.jar mac用$CP，windows用%CP。\n编译：\njavac -cp $CP EmitLog.java ReceiveLogs.java 保存日志到本地文件：\njava -cp $CP ReceiveLogs \u0026gt; logs_from_rabbit.log 打印日志到屏幕：\njava -cp $CP ReceiveLogs 此时就建立了2个临时队列，通过命令可以查看Exchange和队列的绑定：\nsudo rabbitmqctl list_bindings ## =\u0026gt; Listing bindings ... ## =\u0026gt; logs exchange amq.gen-JzTY20BRgKO-HjmUJj0wLg queue [] ## =\u0026gt; logs exchange amq.gen-vso0PVvyiRIL2WoV3i48Yg queue [] ## =\u0026gt; ...done. 发送消息：\njava -cp $CP EmitLog 就能看到2个consumer在同时消费消息了，一个会保存日志到本地文件，一个会打印日志到屏幕。\n消息路由 消息路由是指Exchange把某些消息转发到指定队列。已经实现的日志系统是把所有消息都转发给了所有队列，接下来将实现只把error日志保存到本地文件，把info、warning和error都打印到屏幕。\nbinding key 在给Exchange和Queue绑定的时候，可以指定第三个参数：\nchannel.queueBind(queueName, EXCHANGE_NAME, \u0026#34;black\u0026#34;); 这个参数叫做binding key。binding key的意义是根据Exchange来决定的，比如fanout类型的Exchange会忽略binding key而把所有消息转发给所有队列，而direct类型的Exchange会查找binding key匹配的routing key，然后把消息转发到匹配的队列中去。以下图示能说明这个匹配过程：\nExchange X的type是direct类型，它绑定了2个队列Q1和Q2。队列Q1有1个binding key orange，队列Q2有2个bingding key black和green。Exchange会把routing key为orange的消息转发给Q1，而把routing key为black或green的消息转发给Q2。其他消息则会被Exchange忽略。\n1个Exchange能够使用相同的binding key跟多个队列进行绑定：\n如图所示，Exchange会把带有routing key为black的消息同时转发给Q1和Q2。\n代码实现 创建Exchange：\nchannel.exchangeDeclare(EXCHANGE_NAME, \u0026#34;direct\u0026#34;); 发送消息：\nchannel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes()); severity是info、warning、error三者其中之一。\n绑定Exchange和队列：\nString queueName = channel.queueDeclare().getQueue(); for(String severity : argv){ channel.queueBind(queueName, EXCHANGE_NAME, severity); } EmitLogDirect.java完整代码如下：\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/EmitLogDirect.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; public class EmitLogDirect { private static final String EXCHANGE_NAME = \u0026#34;direct_logs\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.exchangeDeclare(EXCHANGE_NAME, \u0026#34;direct\u0026#34;); String severity = getSeverity(argv); String message = getMessage(argv); channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes(\u0026#34;UTF-8\u0026#34;)); System.out.println(\u0026#34; [x] Sent \u0026#39;\u0026#34; + severity + \u0026#34;\u0026#39;:\u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); } } //.. } ReceiveLogsDirect.java完整代码如下：\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/ReceiveLogsDirect.java\nimport com.rabbitmq.client.*; public class ReceiveLogsDirect { private static final String EXCHANGE_NAME = \u0026#34;direct_logs\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(EXCHANGE_NAME, \u0026#34;direct\u0026#34;); String queueName = channel.queueDeclare().getQueue(); if (argv.length \u0026lt; 1) { System.err.println(\u0026#34;Usage: ReceiveLogsDirect [info] [warning] [error]\u0026#34;); System.exit(1); } for (String severity : argv) { channel.queueBind(queueName, EXCHANGE_NAME, severity); } System.out.println(\u0026#34; [*] Waiting for messages. To exit press CTRL+C\u0026#34;); DeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { String message = new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;); System.out.println(\u0026#34; [x] Received \u0026#39;\u0026#34; + delivery.getEnvelope().getRoutingKey() + \u0026#34;\u0026#39;:\u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); }; channel.basicConsume(queueName, true, deliverCallback, consumerTag -\u0026gt; { }); } } 运行效果 编译：\njavac -cp $CP ReceiveLogsDirect.java EmitLogDirect.java 只把error日志保存到本地文件：\njava -cp $CP ReceiveLogsDirect error \u0026gt; logs_from_rabbit.log 把info、warning、error日志都输出到屏幕：\njava -cp $CP ReceiveLogsDirect info warning error ## =\u0026gt; [*] Waiting for logs. To exit press CTRL+C 发送error消息：\njava -cp $CP EmitLogDirect error \u0026#34;Run. Run. Or it will explode.\u0026#34; ## =\u0026gt; [x] Sent \u0026#39;error\u0026#39;:\u0026#39;Run. Run. Or it will explode.\u0026#39; Topic topic是一种带有特殊含义的routing key。topic不是随意命名的，它由一个或多个单词构成，以.点号分隔，比如stock.usd.nyse、nyse.vmw、quick.orange.rabbit，且长度限制了255字节。在前面的日志系统中，已经实现了按照日志级别（info/warning/error）进行消息路由，但是比较单一。接下来将通过topic来实现既能按照日志级别，也能按照日志来源（auth/cron/kern）进行路由。\nTopic Exchange topic类型的Exchange就是用来支持Topic的。它跟direct类型的Exchange比较类似，只是对于routing key的命名有要求，并且支持两种特殊字符：\n*代表1个单词。\n#代表0个或多个单词。\n通过以下图示可以直观看到topic类型的Exchange是如何路由的：\n所有消息都是\u0026lt;speed\u0026gt;.\u0026lt;colour\u0026gt;.\u0026lt;species\u0026gt;格式的，分别代表速度、颜色、物种。队列Q1的binding key是*.orange.*，队列Q2的binding key是*.*.rabbit和lazy.#。\n带有quick.orange.rabbit和lazy.orange.elephant的routing key的消息会转发到Q1和Q2。 带有quick.orange.fox routing key的消息只会转发到Q1。 lazy.brown.fox 只会转发到Q2。 lazy.pink.rabbit只会转发到Q2一次，虽然它同时命中了2个binding key，但还是只会转发1次。 quick.brown.fox会被Exchange忽视，不转发给任何队列。 只传一个单词orange或者四个单词quick.orange.male.rabbit，会被Exchange忽视。 四个单词lazy.orange.male.rabbit，只转发给Q2。 topic类型的Exchange是很灵活的，如果binding key设置为#，那么它就相当于fanout类型，转发所有消息。如果binding key里面不包含*或者#，那么它就相当于direct类型，转发指定消息。\n完整代码 EmitLogTopic.java\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/EmitLogTopic.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; public class EmitLogTopic { private static final String EXCHANGE_NAME = \u0026#34;topic_logs\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.exchangeDeclare(EXCHANGE_NAME, \u0026#34;topic\u0026#34;); String routingKey = getRouting(argv); String message = getMessage(argv); channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes(\u0026#34;UTF-8\u0026#34;)); System.out.println(\u0026#34; [x] Sent \u0026#39;\u0026#34; + routingKey + \u0026#34;\u0026#39;:\u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); } } //.. } ReceiveLogsTopic.java\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/ReceiveLogsTopic.java\nimport com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; public class ReceiveLogsTopic { private static final String EXCHANGE_NAME = \u0026#34;topic_logs\u0026#34;; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(EXCHANGE_NAME, \u0026#34;topic\u0026#34;); String queueName = channel.queueDeclare().getQueue(); if (argv.length \u0026lt; 1) { System.err.println(\u0026#34;Usage: ReceiveLogsTopic [binding_key]...\u0026#34;); System.exit(1); } for (String bindingKey : argv) { channel.queueBind(queueName, EXCHANGE_NAME, bindingKey); } System.out.println(\u0026#34; [*] Waiting for messages. To exit press CTRL+C\u0026#34;); DeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { String message = new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;); System.out.println(\u0026#34; [x] Received \u0026#39;\u0026#34; + delivery.getEnvelope().getRoutingKey() + \u0026#34;\u0026#39;:\u0026#39;\u0026#34; + message + \u0026#34;\u0026#39;\u0026#34;); }; channel.basicConsume(queueName, true, deliverCallback, consumerTag -\u0026gt; { }); } } topic的格式是\u0026lt;facility\u0026gt;.\u0026lt;severity\u0026gt;。\n运行效果 编译：\njavac -cp $CP ReceiveLogsTopic.java EmitLogTopic.java 接收所有日志：\njava -cp $CP ReceiveLogsTopic \u0026#34;#\u0026#34; 只接收来源于kern的日志：\njava -cp $CP ReceiveLogsTopic \u0026#34;kern.*\u0026#34; 只接收critical严重级别的日志：\njava -cp $CP ReceiveLogsTopic \u0026#34;*.critical\u0026#34; 也可以多重绑定：\njava -cp $CP ReceiveLogsTopic \u0026#34;kern.*\u0026#34; \u0026#34;*.critical\u0026#34; 发送日志消息：\njava -cp $CP EmitLogTopic \u0026#34;kern.critical\u0026#34; \u0026#34;A critical kernel error\u0026#34; RPC RPC是Remote Procedure Call的缩写，远程过程调用，比如在远程机器中执行函数，并拿到返回结果。RabbitMQ能用来实现RPC服务。接下来就简单实现一个调用生成斐波那契数列的RPC服务。\n整体设计如图所示：\nClient发送RPC请求到rpc_queue里面，然后阻塞，等待返回。在请求中会指定一个回调队列的地址，通过reply_to来指定。Server从rpc_queue读取消息进行处理，根据reply_to把响应放到回调队列中。请求中还设置了一个correlation_id，Client在收到响应时，根据这个关联id来匹配，匹配上的消息才进行接收。\ncorrelation_id匹配不上的消息会忽略，为什么是忽略而不是抛出异常呢？因为RPC Server有可能发返回ack确认前就宕机，当它重启以后，会重新处理请求消息，从而导致消息重复处理。对于RPC来说最好是能够让重复场景是幂等的。\n代码实现 RPCServer.java\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/RPCServer.java\nimport com.rabbitmq.client.*; public class RPCServer { private static final String RPC_QUEUE_NAME = \u0026#34;rpc_queue\u0026#34;; private static int fib(int n) { if (n == 0) return 0; if (n == 1) return 1; return fib(n - 1) + fib(n - 2); } public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null); channel.queuePurge(RPC_QUEUE_NAME); //设置n个Server进程 channel.basicQos(1); System.out.println(\u0026#34; [x] Awaiting RPC requests\u0026#34;); Object monitor = new Object(); DeliverCallback deliverCallback = (consumerTag, delivery) -\u0026gt; { AMQP.BasicProperties replyProps = new AMQP.BasicProperties .Builder() .correlationId(delivery.getProperties().getCorrelationId()) .build(); String response = \u0026#34;\u0026#34;; try { String message = new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;); int n = Integer.parseInt(message); System.out.println(\u0026#34; [.] fib(\u0026#34; + message + \u0026#34;)\u0026#34;); response += fib(n); } catch (RuntimeException e) { System.out.println(\u0026#34; [.] \u0026#34; + e.toString()); } finally { channel.basicPublish(\u0026#34;\u0026#34;, delivery.getProperties().getReplyTo(), replyProps, response.getBytes(\u0026#34;UTF-8\u0026#34;)); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // RabbitMq consumer worker thread notifies the RPC server owner thread synchronized (monitor) { monitor.notify(); } } }; channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -\u0026gt; { })); // Wait and be prepared to consume the message from RPC client. while (true) { synchronized (monitor) { try { monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } RPCClient.java\nhttps://github.com/rabbitmq/rabbitmq-tutorials/blob/master/java/RPCClient.java\nimport com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeoutException; public class RPCClient implements AutoCloseable { private Connection connection; private Channel channel; private String requestQueueName = \u0026#34;rpc_queue\u0026#34;; public RPCClient() throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(\u0026#34;localhost\u0026#34;); connection = factory.newConnection(); channel = connection.createChannel(); } public static void main(String[] argv) { try (RPCClient fibonacciRpc = new RPCClient()) { for (int i = 0; i \u0026lt; 32; i++) { String i_str = Integer.toString(i); System.out.println(\u0026#34; [x] Requesting fib(\u0026#34; + i_str + \u0026#34;)\u0026#34;); String response = fibonacciRpc.call(i_str); System.out.println(\u0026#34; [.] Got \u0026#39;\u0026#34; + response + \u0026#34;\u0026#39;\u0026#34;); } } catch (IOException | TimeoutException | InterruptedException e) { e.printStackTrace(); } } public String call(String message) throws IOException, InterruptedException { final String corrId = UUID.randomUUID().toString(); String replyQueueName = channel.queueDeclare().getQueue(); AMQP.BasicProperties props = new AMQP.BasicProperties .Builder() .correlationId(corrId) .replyTo(replyQueueName) .build(); channel.basicPublish(\u0026#34;\u0026#34;, requestQueueName, props, message.getBytes(\u0026#34;UTF-8\u0026#34;)); //在响应到达前挂起main阻塞等待，1表示只等待1个响应 final BlockingQueue\u0026lt;String\u0026gt; response = new ArrayBlockingQueue\u0026lt;\u0026gt;(1); String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -\u0026gt; { if (delivery.getProperties().getCorrelationId().equals(corrId)) { response.offer(new String(delivery.getBody(), \u0026#34;UTF-8\u0026#34;)); } }, consumerTag -\u0026gt; { }); String result = response.take(); channel.basicCancel(ctag); return result; } public void close() throws IOException { connection.close(); } } 运行效果 编译：\njavac -cp $CP RPCClient.java RPCServer.java 启动Server：\njava -cp $CP RPCServer ## =\u0026gt; [x] Awaiting RPC requests 运行Client：\njava -cp $CP RPCClient ## =\u0026gt; [x] Requesting fib(30) 如果Server处理慢，那么可以再在控制台启动新的Server来消费。\n注意，本文的示例都只是为了演示而编写的，不是真正意义上的实现。\n对于可靠的消息确认，RabbitMQ提供了一个扩展，感兴趣的话可以阅读：\nhttps://www.rabbitmq.com/tutorials/tutorial-seven-java.html\n参考资料：\nhttps://www.rabbitmq.com/getstarted.html\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E4%B8%AD%E9%97%B4%E4%BB%B6/000001-rabbitmq%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%AE%98%E6%96%B9%E6%95%99%E7%A8%8Bjava%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"消息队列\"\u003e消息队列\u003c/h1\u003e\n\u003cp\u003eRabbitMQ是一个消息队列，它能够接收和转发消息。这个过程就像寄快递一样，把物件打包给快递小哥，快递小哥会负责把物件派送到正确的地址。\u003c/p\u003e\n\u003ch2 id=\"生产者和消费者\"\u003e生产者和消费者\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e生产者\u003c/strong\u003e就是用来生产消息（发送消息）的：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/aee9bb0edda94241b7a1a6e145e18cbc.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e消费者\u003c/strong\u003e就是用来消费消息（接收消息）的：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/44ab4de0d0864a7f9e6596ce8a131320.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在生产者和消费者之间的就是\u003cstrong\u003e消息队列\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/0c06f7ee7ce5415da9c5182d3104b5bf.png\"\u003e\u003c/p\u003e\n\u003cp\u003e它相当于消息缓冲区，最多能存储多少数据只受限于机器的内存和磁盘。多个生产者可以发送消息给同一个队列，多个消费者也可以从同一个队列接收消息。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/93b9bb047745491aa52d23f3442d86bb.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"windows安装rabbitmq\"\u003eWindows安装RabbitMQ\u003c/h2\u003e\n\u003cp\u003e参考mall商城学习教程的RabbitMQ部分内容：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://www.macrozheng.com/#/architect/mall_arch_09?id=rabbitmq\"\u003ehttp://www.macrozheng.com/#/architect/mall_arch_09?id=rabbitmq\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e原文中\u003ccode\u003erabbitmq-server-3.7.14.exe\u003c/code\u003e下载地址失效了，改从这里下载：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.14\"\u003ehttps://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.14\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e安装完成后，确认服务已开启：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/084def7f50a4442892623081654a08a1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e进入RabbitMQ安装目录下的sbin目录：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6d7095ae8c2e421e8a541f7536f4ffc3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在地址栏输入cmd并回车启动命令行，然后输入以下命令启动管理功能：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003erabbitmq-plugins \u003cspan style=\"color:#6639ba\"\u003eenable\u003c/span\u003e rabbitmq_management\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRabbitMQ运行在本地机器上：\u003c/p\u003e\n\u003cp\u003ehttp://localhost:15672/\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b1ab7f41e1c04ed7a180b8ec7039ce25.png\"\u003e\u003c/p\u003e\n\u003cp\u003e默认用户名密码为guest / guest：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/29c2ea4899894062b13ec51a64d4d6d1.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"java客户端\"\u003eJava客户端\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eamqp-client-5.7.1.jar\u003c/code\u003e是RabbitMQ官方提供的Java客户端：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.rabbitmq.com/tutorials/tutorial-one-java.html\"\u003ehttps://www.rabbitmq.com/tutorials/tutorial-one-java.html\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b3cea6467f40488480d09025c1b292bd.png\"\u003e\u003c/p\u003e\n\u003cp\u003e既可以直接下载jar包，也可以在Maven中添加依赖：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003edependency\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003egroupId\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003ecom\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003erabbitmq\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/\u003c/span\u003egroupId\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eartifactId\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003eamqp\u003cspan style=\"color:#0550ae\"\u003e-\u003c/span\u003eclient\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/\u003c/span\u003eartifactId\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eversion\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e5\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e14\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/\u003c/span\u003eversion\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/\u003c/span\u003edependency\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"生产消息\"\u003e生产消息\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/238e8e863e984af5a8c4ce21a93648f3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e导包：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ecom.rabbitmq.client.ConnectionFactory\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ecom.rabbitmq.client.Connection\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eimport\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#24292e\"\u003ecom.rabbitmq.client.Channel\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e创建类Send，定义队列名为hello：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003eSend\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eprivate\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efinal\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eQUEUE_NAME\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;hello\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estatic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003evoid\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003emain\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#0550ae\"\u003e[]\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eargv\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ethrows\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eException\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e      \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e...\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e建立连接：\u003c/p\u003e","title":"RabbitMQ消息队列官方教程Java学习笔记"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nSpringBoot简介 SpringBoot是Java技术栈的一个Web开发框架，在整个Web开发框架领域中，说SpringBoot是最成熟、最稳定、生态最强大的，一点也不为过。它最伟大的变革是让Java的Web框架也变得便捷起来，开箱即用，只需要简单配置就能快速搭建起一个后端应用，这也是它出现后迅速抢占市场，实现技术更替的重要原因。\n除了Web应用开发外，SpringBoot还提供了内嵌服务器（Tomcat等）、安全、监控、健康检查等能力。\nSpringBoot安装 本文主要介绍Maven这种方式：\n第一步，安装Java，SpringBoot 2.5.6版本需要Java8及以上版本，最新的Java17也兼容。\n第二步，安装Maven，不需要单独安装，下载一个IntelliJ就可以了，详细步骤可以参考我博客中这篇文章：\n001-2021年搭建Java环境最快办法\n第三步，在pom.xml中配置SpringBoot：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;myproject\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.5.6\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;!-- Additional lines to be added here... --\u0026gt; \u0026lt;/project\u0026gt; 下篇文章《开发你的第一个SpringBoot应用》将继续探究SpringBoot安装后的使用。\n参考资料：\nhttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#getting-started\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/spring%E6%8A%80%E6%9C%AF%E6%A0%88/000001-springspringboot%E7%AE%80%E4%BB%8B%E4%B8%8E%E5%AE%89%E8%A3%85/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"springboot简介\"\u003eSpringBoot简介\u003c/h1\u003e\n\u003cp\u003eSpringBoot是Java技术栈的一个Web开发框架，在整个Web开发框架领域中，说SpringBoot是最成熟、最稳定、生态最强大的，一点也不为过。它最伟大的变革是让Java的Web框架也变得便捷起来，\u003cstrong\u003e开箱即用\u003c/strong\u003e，只需要简单配置就能快速搭建起一个后端应用，这也是它出现后迅速抢占市场，实现技术更替的重要原因。\u003c/p\u003e\n\u003cp\u003e除了Web应用开发外，SpringBoot还提供了内嵌服务器（Tomcat等）、安全、监控、健康检查等能力。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4afbb6d7d8dc4d1fa79895459f929322.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"springboot安装\"\u003eSpringBoot安装\u003c/h1\u003e\n\u003cp\u003e本文主要介绍Maven这种方式：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e第一步，安装Java，SpringBoot 2.5.6版本需要Java8及以上版本，最新的Java17也兼容。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e第二步，安装Maven，不需要单独安装，下载一个IntelliJ就可以了，详细步骤可以参考我博客中这篇文章：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://dongfanger.gitee.io/blog/Java%20Basic/001-2021%E5%B9%B4%E6%90%AD%E5%BB%BAJava%E7%8E%AF%E5%A2%83%E6%9C%80%E5%BF%AB%E5%8A%9E%E6%B3%95.html\"\u003e001-2021年搭建Java环境最快办法\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/21ad823acd5747309d3880b3ce59c3e3.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e第三步，在pom.xml中配置SpringBoot：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;project\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003exmlns=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003exmlns:xsi=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#1f2328\"\u003exsi:schemaLocation=\u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;modelVersion\u0026gt;\u003c/span\u003e4.0.0\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/modelVersion\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ecom.example\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003emyproject\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e0.0.1-SNAPSHOT\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;parent\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-starter-parent\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2.5.6\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/parent\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#57606a\"\u003e\u0026lt;!-- Additional lines to be added here... --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/project\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e下篇文章《开发你的第一个SpringBoot应用》将继续探究SpringBoot安装后的使用。\u003c/p\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#getting-started\"\u003ehttps://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#getting-started\u003c/a\u003e\u003c/p\u003e","title":"SpringBoot简介与安装"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n『 tep is a testing tool to help you write pytest more easily. Try Easy Pytest! 』\ntep前身 tep的前身是接口自动化测试框架pyface，一款面向对象设计的测试框架，我写过一篇博客介绍。\n测试框架 / 测试工具 tep的定位是 a testing tool，不是 a testing framework。\n框架/工具，是有区别的。最大的区别，就是我自认为是没有足够的能力去自主开发一套“框架”！工具的能力，还是妥妥的！\n自研的框架意味着不稳定，要花很多精力来踩坑填坑，别人不敢随便用的。工具只是站在巨人的肩膀上，出了问题，这个锅我不背！\ntep是 try easy pytest 的首字母缩写，tep的目的是帮助你更简单地写pytest，比如用pytest+requests写接口自动化。\npytest是python的测试框架，很成熟。tep是pytest的测试工具，很简单。\npytest和tep都是开源项目。\n设计理念 很大程度上借鉴了HttpRunner（优秀的框架）。不同的是，tep更着重写python，而不是写YAML文件。\n简单是更好的 每个人都能用python写自动化 这就是tep的设计理念。\n项目结构 tests __init__.py .gitignore conftest.py tep提供了快速创建项目的能力，也就是脚手架。执行 tep startproject project_name，就可以创建项目结构，如，这里创建一个demo，\n$ tep startproject demo 2020-07-28 14:34:57.649 | INFO | tep.scaffold:create_scaffold:40 - Create new project: demo Project root dir: \\PycharmProjects\\demo Created folder: demo Created folder: demo\\tests Created file: demo\\tests\\__init__.py Created file: demo\\conftest.py Created file: demo\\.gitignore tests是一个package，用于存放测试脚本，脚本文件以test_开头或_test结尾，pytest才能识别到。个人喜欢以_test结尾。\nconftest.py是一个全局文件，定义全局变量，也可以定义fixture、hook、plugin等，\nimport os import pytest @pytest.fixture(scope=\u0026#34;session\u0026#34;, autouse=True) def project_cache(request): request.config.cache.set(\u0026#34;project_dir\u0026#34;, os.path.dirname(os.path.abspath(__file__))) class Dev: test_url = \u0026#39;https://dev.com\u0026#39; class Qa: test_url = \u0026#39;https://qa.com\u0026#39; class Release: test_url = \u0026#39;https://release.com\u0026#39; # choose environment env = Qa # you can define your variables and functions and so on 1 定义了一个fixture，把项目路径保存到pytest缓存中。\n2 定义了环境的class，多环境切换，不需要修改测试脚本。\n3 自定义内容，比如用户登录token等。\n专注于写脚本 项目结构很清晰。在conftest.py进行一些初始化/参数化/清理工作，在tests/写测试脚本。\n不像pyface那样面向对象的封装，tep更注重平铺写脚本的方式，这样就离“每个人都能用python写自动化”更近一步。毕竟封装之后看着容易晕，我也晕。\n去除掉框架的约束，给每个人写python的自由，在测试脚本里你可以尽情发挥你的代码风格，代码能力，千人千面。代价呢，就是代码质量参差不齐。\n这又怎么样呢，用过各种开源/自主研发的测试平台，还不是每个人都在写着自己风格的自动化case！\n大胆写，能写，写出来，跑通，就已经是在写自动化，就已经是在创造价值了！\ntep默认是不会创建 reports 文件夹的， 原因有二。\n其一，如果你是本地执行的话，可以使用 --tep-reports 自定义命令行参数，来生成测试报告。\n$ pytest --tep-reports 测试结束后会在 project_dir/reports 生成 report-2020-07-28的allure测试报告。\n其二，如果你是持续集成的话，如Jenkins，已经提供了allure report的插件，配置一下就可以自动生成测试报告，百度“jenkins allure”。\n附上allure常用命令，\npytest --alluredir=result # 报告目录，会生成一堆数据文件 allure generate result -o html # 生成html报告 allure serve html # 启动服务 allure open html # 打开报告（直接执行自动启动服务） PyCharm可以右键index.html选择Open in Browser allure下载地址，下载解压后，把bin绝对路径添加到系统环境变量Path中。allure需要安装jdk。\n轻封装 tep尊重原生用法。\nrequests的封装只通过装饰器做了2个封装，一是记录接口请求响应耗时，二是打印日志。只需要 from tep.client import request ，就可以和 requests.request 一样使用了，没有做任何其他的冗余修改。\n#!/usr/bin/python # encoding=utf-8 \u0026#34;\u0026#34;\u0026#34; @Author : Don @Date : 7/25/2020 2:02 PM @Desc : \u0026#34;\u0026#34;\u0026#34; import decimal import json import time import requests import urllib3 from loguru import logger from requests import sessions from tep.funcs import NpEncoder urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def request_encapsulate(req): def send(*args, **kwargs): # elapsed start = time.process_time() response = req(*args, **kwargs) end = time.process_time() elapsed = str(decimal.Decimal(\u0026#34;%.3f\u0026#34; % float(end - start))) + \u0026#34;s\u0026#34; # log try: log4a = {\u0026#34;method\u0026#34;: args[0]} for k, v in kwargs.items(): # if not json, str() try: json.dumps(v) except TypeError: v = str(v) log4a.setdefault(k, v) log4a.setdefault(\u0026#34;status\u0026#34;, response.status_code) log4a.setdefault(\u0026#34;response\u0026#34;, response.text) log4a.setdefault(\u0026#34;elapsed\u0026#34;, elapsed) logger.info(json.dumps(log4a, ensure_ascii=False, cls=NpEncoder)) except AttributeError: logger.error(\u0026#34;request failed\u0026#34;) except TypeError: logger.warning(log4a) return response return send @request_encapsulate def request(method, url, **kwargs): \u0026#34;\u0026#34;\u0026#34;此处省略1万行代码，没做任何修改，从源码copy过来，只加了个装饰器\u0026#34;\u0026#34;\u0026#34; 1 使用 time.process_time() ，记录了耗时。\n2 打印日志，把请求响应的method、url、headers、参数、响应状态码、响应体、耗时等数据保存到json中，输出控制台。\n日志选择用loguru取代logging，from loguru import logger 直接用，不用再管handler了。\nfaker，造数据工具 jmespath，json解析工具 deepdiff，json比较工具 pandas、numpy，数据处理工具 安装tep，自动就把这些开源利器安装上了，无需单独安装。未来会集成更多实用工具到tep中。\ntep本身是很轻的。\ntep可持续发展 我是2014年参加工作的，2018年才开始接触接口测试（汗！），现在有2年多的接口测试经验，其中包括一整年的纯后端接口测试经验。\n接口自动化第一次写了接口自动化框架AIM（基于unittest），后来又有pyface，以及中间改造过的各种临时版本。也用过一些开源框架如RobotFramwork、HttpRunner，使用过自研工具，如基于JMeter封装的平台。还有一些网上开源的“web接口自动化平台”，这个我是打个大大的问号的。实用性很差，功能很鸡肋，报错还多。接口自动化测试框架的轮子，造也造不完。\ntep“测试工具”的定位完美的避开了所有这些框架的弊端。工具不会定义你如何写自动化脚本，工具只会帮你更好地写自动化脚本。\n有理由相信，tep会成长为一款实用的测试工具。\n小结 这里安利一波pytest官网教程，阅读英文文档，才能真正理解作者的意思。学python，写pytest，用tep。测试更专业！\n参考：\n源码 https://github.com/dongfanger/tep\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/tep/000001-tep%E9%A6%96%E6%AC%A1%E5%BC%80%E6%BA%90v0.2.3%E4%BB%8B%E7%BB%8D/","summary":"『  tep is a testing tool to help you write pytest more easily. Try Easy Pytest!  』","title":"tep首次开源V0.2.3介绍"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n今天就来介绍一款开源、免费、颜值高的“瑞士军刀”—— VS Code (Visual Studio Code)\nVS Code 到底是个啥？ 官方网站：https://code.visualstudio.com/\nVisual Studio Code是微软开发的一款开源代码编辑器，它使用Electron框架构建，这使得VSCode能够跨平台运行在Windows、macOS和Linux上。\nElectron是一个使用Web技术（HTML、CSS、JavaScript）构建跨平台桌面应用的框架，它结合了Chromium渲染引擎和Node.js运行时。VSCode利用Electron的这些特性，提供了丰富的编辑功能、扩展系统和现代化的用户界面。\nvscode最吸引我的特点是“轻量”，轻的就像一个普通的浏览器，家里电脑风扇不再呼呼转。同时它也是开源免费的，有很多插件可以安装，支持多种语言，比如Python和前端，只需要安装这1个软件就行了。\nVS Code 安装，傻瓜式操作零门槛 打开官网https://code.visualstudio.com/，页面会自动识别你的系统，直接点下载按钮就行，Windows 选 64 位安装包，Mac 选对应芯片的版本（Intel/Apple Silicon）。\n下载完成后双击安装包，一路下一步就好，这里提个小建议：Windows 用户记得勾选 “添加到 PATH” 和 “右键打开方式”，后续用起来会方便很多。\nPython 插件安装 VS Code 原生不自带 Python 的专属支持，装个插件就搞定。\n打开 VS Code，左边侧边栏有个扩展图标，也可以按快捷键Ctrl+Shift+X（Mac 是Cmd+Shift+X）打开扩展市场。\n在搜索框里输入Python，第一个就是微软官方出的 Python 插件，图标是个小蛇，直接点安装，等待几秒安装完成就好，不用重启，即时生效。\n环境配置 别被环境配置吓到，其实很简单的。\n我们用惯了PyCharm，切换到vscode会有些不适应，因为pc默认进行了很多配置，这些配置都隐藏在.idea里面。\n这里着重介绍虚拟环境配置，其他问题可以等遇到了再解决。\n打开一个 Python 项目文件夹（重点：一定要先打开文件夹，VS Code 是基于文件夹管理项目的）；\n按Ctrl+Shift+P（Mac 是Cmd+Shift+P）打开命令面板，输入inter\n下拉列表里会显示电脑里所有的 Python 解释器，包括系统自带的和各个虚拟环境的，选你想用于当前项目的那个就行，选完后 VS Code 会自动在项目里生成.vscode文件夹，保存解释器配置，下次打开直接用。\n如果没有虚拟环境，也可以直接创建。\n另外官网也有Python、Django、FastAPI、Flask等环境配置介绍：\n如果文档看着麻烦，可以把链接扔给AI，让它总结重点的方式进行学习。\n界面样式 刚开始用vscode，有3个样式我调整了下。\n1、主题，Dark+，没有那么黑，对眼睛友好点\n2、放大，默认界面的文字有点小，可以设置Zoom Level把UI放大\n3、终端样式，默认的有点太简陋\n在.vscode/settings.json配置，这个配置是AI给我的，大家也可以自行调整\n{ \u0026#34;terminal.integrated.fontFamily\u0026#34;: \u0026#34;Fira Code, \u0026#39;Courier New\u0026#39;, monospace\u0026#34;, \u0026#34;terminal.integrated.fontSize\u0026#34;: 14, \u0026#34;terminal.integrated.fontWeight\u0026#34;: \u0026#34;normal\u0026#34;, \u0026#34;terminal.integrated.lineHeight\u0026#34;: 1.3, \u0026#34;terminal.integrated.letterSpacing\u0026#34;: 0.5, \u0026#34;terminal.integrated.cursorStyle\u0026#34;: \u0026#34;line\u0026#34;, \u0026#34;terminal.integrated.cursorBlinking\u0026#34;: true, \u0026#34;terminal.integrated.cursorWidth\u0026#34;: 2, \u0026#34;terminal.integrated.scrollback\u0026#34;: 10000, \u0026#34;workbench.colorCustomizations\u0026#34;: { \u0026#34;terminal.background\u0026#34;: \u0026#34;#1e1e1e\u0026#34;, \u0026#34;terminal.foreground\u0026#34;: \u0026#34;#d4d4d4\u0026#34;, \u0026#34;terminal.ansiBlack\u0026#34;: \u0026#34;#000000\u0026#34;, \u0026#34;terminal.ansiRed\u0026#34;: \u0026#34;#cd3131\u0026#34;, \u0026#34;terminal.ansiGreen\u0026#34;: \u0026#34;#0dbc79\u0026#34;, \u0026#34;terminal.ansiYellow\u0026#34;: \u0026#34;#e5e510\u0026#34;, \u0026#34;terminal.ansiBlue\u0026#34;: \u0026#34;#2472c8\u0026#34;, \u0026#34;terminal.ansiMagenta\u0026#34;: \u0026#34;#bc3fbc\u0026#34;, \u0026#34;terminal.ansiCyan\u0026#34;: \u0026#34;#11a8cd\u0026#34;, \u0026#34;terminal.ansiWhite\u0026#34;: \u0026#34;#e5e5e5\u0026#34;, \u0026#34;terminal.ansiBrightBlack\u0026#34;: \u0026#34;#666666\u0026#34;, \u0026#34;terminal.ansiBrightRed\u0026#34;: \u0026#34;#f14c4c\u0026#34;, \u0026#34;terminal.ansiBrightGreen\u0026#34;: \u0026#34;#23d18b\u0026#34;, \u0026#34;terminal.ansiBrightYellow\u0026#34;: \u0026#34;#f5f543\u0026#34;, \u0026#34;terminal.ansiBrightBlue\u0026#34;: \u0026#34;#3b8eea\u0026#34;, \u0026#34;terminal.ansiBrightMagenta\u0026#34;: \u0026#34;#d670d6\u0026#34;, \u0026#34;terminal.ansiBrightCyan\u0026#34;: \u0026#34;#29b8db\u0026#34;, \u0026#34;terminal.ansiBrightWhite\u0026#34;: \u0026#34;#ffffff\u0026#34;, \u0026#34;terminalCursor.background\u0026#34;: \u0026#34;#ffffff\u0026#34;, \u0026#34;terminalCursor.foreground\u0026#34;: \u0026#34;#ffffff\u0026#34;, \u0026#34;terminal.selectionBackground\u0026#34;: \u0026#34;#264f78\u0026#34; } } ","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/ide/000001-%E4%BF%9D%E5%A7%86%E7%BA%A7vscode%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97python%E5%85%9A%E7%9B%B4%E6%8E%A5%E6%8A%84%E4%BD%9C%E4%B8%9A/","summary":"今天就来介绍一款开源、免费、颜值高的“瑞士军刀”—— VS Code (Visual Studio Code)","title":"保姆级VSCode入门指南，Python党直接抄作业"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n目标读者 本指南特别适合有Python基础的同学学习Java入门，对于没有任何编程经验的同学可能会存在困难。\n为什么测试人员要学Java 选择Java，应该不错。TesterHome测试论坛知名大佬基本都是以Java技术栈为主，少部分是Golang和Python。我公司2021年校招的两个同济大学研究生，测试开发，也是以Java为主。有一个测开同事跳槽去了美团，拿了高薪，只会Java。有一个测开同事跳槽去了创业公司做了测试架构师，只会Java。招聘网站搜索测试开发20K以上职位，基本都要求会Java。我接触到的开发，Java程序员占80%以上。\n测试人员用Java能干嘛 如果你的公司是以Java技术栈为主，那么你可以用Java做所有事情，而且有些事情只能用Java来做。这个问题挺关键的，学习一门语言肯定是要能够学以致用。在自动化测试，Java会显得稍微有点笨重，而Python会适合一点。你可以用Python来写接口自动化脚本，在小范围内使用，但是当团队变大以后，脚本如何规范如何维护，就成了一个很大的问题，而Java由于静态语言特性和面向对象编程，正好可以用来解决这个问题。在测试平台和性能测试，Java有很成熟的Web开发框架和配套的生态。在测试工具开发，公司封装好的Java库可以拿来即用，碰到问题也可以找开发帮忙。白盒测试，基本上都是要靠Java来做的，看代码，用JaCoCo统计代码覆盖率。其他专项比如流量录制回放、全链路压测、Dubbo接口测试，都只能使用Java来做。\n测试人员怎么学Java 我推荐刷完一遍基础语法后，在LeetCode上面刷算法题来加强练习。Java还是有一定的学习门槛的，初学者可能学起来会有些吃力，可以先从Python入手，了解编程语言的底层逻辑，等对编程有些许感知以后，再学习Java。基础语法可以快速浏览一遍，一遍并不会就能记住了，需要在编码中持续加强记忆。光学语法也不行，学习Java和其他编程语言一样，需要多敲多练，刷算法题，既能熟悉Java语法，也能熟悉算法，一举两得，题做不来就先把答案背一遍再做。另外，可以在工作中主动找研发开通下代码权限，尝试Review开发代码，看多了，有些技巧也会知道了。有机会，可以用Java写点工具或者平台，实践出真知。\n搭建环境 安装Java\nJava的安装包是个.exe文件，特殊地方在于文件名不是java-xxx.exe而是jdk-xxx.exe，比如jdk-8u281-windows-x64.exe，jdk是Java Development Kit 的缩写，指Java开发工具包，包括以下内容：\nJDK的最新版本为15，但是国内大多数公司仍然使用的是JDK8，这是因为JDK8是各方面都很成熟稳定的版本，并且基于JDK8开发的业务系统已经具有一定规模，新版本JDK并不能完全无感知的迁移，需要做代码修改和测试，会是一笔巨大开销，为了降低成本和规避兼容问题风险，JDK8沿用到了至今。JDK8的下载地址如下：\nhttps://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html\n选择相应的版本下载：\n比如我的电脑是Windows 64位的系统，就选择了Windows x64这个版本，会下载到一个jdk-8u281-windows-x64.exe文件。Java已经被Oracle收购了，需要登录Oracle账号后才能下载，没有账号注册一个也很方便：\n下载后可以开始安装了：\n安装到默认位置即可，直接下一步。中途会让选择JRE（Java Runtime Environment，Java运行环境）的目标文件夹：\n依然保持默认即可。安装结束后就能看到两个新文件夹了：\n以前，还需要配置系统环境变量，设置JAVA_HOME和CLASSPATH才能使用Java（具体方法百度有很多）。现在，安装完成即可用，省去了不少麻烦，速度加快。打开cmd输入java -version，命令能执行成功就表示Java已经安装好了：\n安装IntelliJ IDEA\nJetBrains全家桶中也有专门用来写Java的，叫做IntelliJ IDEA，一般只说后面一个单词“哎迪儿”就知道是这个东西了。下载地址为：\nhttps://www.jetbrains.com/idea/download/#section=windows\n个人建议选择旗舰版，看着齐全。下载后双击ideaIU-2020.3.2.exe安装，先别忙着打开。\nMaven仓库\nMaven是用来管理Java第三方包的主流仓库工具，通过pom.xml文件来配置安装即可，在mvnrepository网站上能检索到所需配置信息：\nhttps://mvnrepository.com/\n官方仓库有时候下载速度很慢，本文用国内镜像替代，提高首次打开时同步仓库速度。我们先把Maven中央仓库从Apache替换成阿里云。打开本地目录D:\\Program Files\\JetBrains\\IntelliJ IDEA 2020.3.2\\plugins\\maven\\lib\\maven3\\conf：\n编辑settings.xml文件，找到\u0026lt;mirrors\u0026gt;标签，添加代码：\n\u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;aliyunmaven\u0026lt;/id\u0026gt; \u0026lt;mirrorOf\u0026gt;*\u0026lt;/mirrorOf\u0026gt; \u0026lt;name\u0026gt;阿里云公共仓库\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://maven.aliyun.com/repository/public\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; 这是新版写法，官网https://maven.aliyun.com/mvn/guide有说明。\n接着可以打开IDEA了，欢迎界面映入眼帘：\n新建项目\n选择新建Maven项目：\n填写项目名和存放位置：\n在pom.xml文件中添加仓库配置：\n\u0026lt;repositories\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;public\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://maven.aliyun.com/repository/public\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; \u0026lt;pluginRepositories\u0026gt; \u0026lt;pluginRepository\u0026gt; \u0026lt;id\u0026gt;public\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;https://maven.aliyun.com/repository/public\u0026lt;/url\u0026gt; \u0026lt;/pluginRepository\u0026gt; \u0026lt;/pluginRepositories\u0026gt; 记得点击右上角的Load Maven Changes按钮，此时前面切换仓库的作用就体现出来了，右下角同步速度比默认明显增快。至此，以阿里云为Maven中央仓库的项目就创建好了。\n其他设置\nUTF-8\n网络自动代理\n基础语法 Java虽然学习门槛有点高，但是如果熟悉Python以后，再看Java基础语法，其实是非常简单的。\n4个概念\n对象：Java是纯面向对象编程语言，对象的概念有点抽象，具体来说可以是一条狗，也可以是某个人。 类：类是对象的模板，有点像克隆的母体。 属性：属性就是状态，相当于变量。 方法：方法就是行为，跟函数类似，完成某个特定功能。 Hello World\nJava的Hello World经常拿来和其他语言比较，嫌它啰嗦，然而啰嗦的背后是严谨：\npublic class HelloWorld { public static void main(String[] args) { System.out.println(\u0026#34;Hello World\u0026#34;); } } Java把每个语法都显式的表达了出来，阅读代码就能知道是什么意思，不会产生额外的意想不到的结果。\n基本语法\npublic static void main(String[] args)，是所有Java程序的执行入口。 源文件后缀是.java，源文件名称必须和类名一致。 源文件编译后的文件后缀是.class。 Java是大小写敏感的，类名首字母应该大写，方法名首字母小写，也就是驼峰命名法。 标识符\n类名、属性名和方法名都被称为标识符，Java标识符规则如下：\n标识符是大小写敏感的 所有的标识符都应该以字母（A-Z或者a-z）、美元符（$）、或者下划线（_）开始 首字符之后可以是字母（A-Z或者a-z）,美元符（$）、下划线（_）或数字的任何字符组合 关键字不能用作标识符 比如合法的标识符：\nage、$salary、_value、__1_value 非法的标识符：\n123abc、-salary 修饰符\n访问控制修饰符 : default, public, protected, private 非访问控制修饰符 : final, abstract, static, synchronized Java的修饰符极大的提高了代码可阅读性。\n变量\n因为Java的代码都必须写到类里面，所以就只有以下三种变量：\n类变量（静态变量），相当于全局变量 成员变量（非静态变量） 局部变量 注释\npublic class HelloWorld { /* 这是第一个Java程序 * 它将输出 Hello World * 这是一个多行注释的示例 */ public static void main(String[] args){ // 这是单行注释的示例 /* 这个也是单行注释的示例 */ System.out.println(\u0026#34;Hello World\u0026#34;); } } 八种基本类型\nboolean 布尔型 1个字节 8bit（8位）\nbyte 字节类型 1个字节\nchar 字符类型 2个字节\nshort 短整型 2个字节\nint 整型 4个字节\nlong 长整型 8个字节\ndouble 双精度类型 8个字节\nfloat 浮点型（单精度）4个字节\n默认的整数类型是int，如果要定义为long ，则要在数值后加上L或者l。\n默认的浮点型是double，如果要定义float，则要在数值后面加上F或者f。\n对象和类 我们都知道Java是纯面向对象编程语言，这个对象是什么，类是什么，它们的关系是怎样呢？\n类和对象的关系\n先看一张图：\n类是girl和boy，对象是每个具体的女孩（Lucy、Rose）和男孩（David、Jack）。\n这完美解释了类和对象的关系，即：类是对象的模板。\n状态和行为\n对象有两个特征，一个是状态（又叫做属性），一个是行为（又叫做方法），我们拿狗来举例，狗的状态有品种、大小、颜色、年龄；行为有吃、跑、睡觉。如图所示：\n图的左边是3个狗对象，右边是1个狗类。类定义了对象应该有哪些属性和方法，对象根据类定义好的模板，创建了个性化的实例。\n代码实现如下：\npublic class Dog { // 属性 String breed; int size; String colour; int age; // 方法 void eat() { } void run() { } void sleep(){ } } 构造方法\n在通过类创建对象时，构造方法提供了如何创建对象的细节。Java会给类一个默认的构造方法，你也可以自定义一个或多个构造方法，构造方法命名必须和类名相同，比如：\npublic class Dog{ public Dog(String breed){ } } 代码中定义了2个构造方法，根据参数不同，在构造时会调用相应的构造方法。\n创建对象\n**如果没有对象，那么就new一个。**Java是通过new关键字来创建对象的。比如使用默认构造方法创建对象：\nDog myDog = new Dog(); 或者使用自定义构造方法创建对象：\nDog myDog = new Dog(\u0026#34;Bulldog\u0026#34;); 访问对象属性和方法\nJava和大多数语言一样，采用.运算符访问对象属性和方法，比如：\npublic class Dog { int age; public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } public static void main(String[] args) { // 创建对象 Dog myDog = new Dog(); // 通过方法来设定age myDog.setAge(2); // 调用另一个方法获取age System.out.println(myDog.getAge()); // 也可以像下面这样访问成员变量 System.out.println(myDog.age); } } 类存放规则\nJava源文件是用类来组织的，存放在源文件中的类遵循下面几项规则：\n一个源文件只能有一个public类，可以有多个非public类。 源文件名必须与public类名一致。 package语句在首行，其次是import语句，最后是类。 八大基本数据类型 Java内置了8个基本数据类型，它们分别是byte、short、int、long、float、double、boolean、char。\n每种基本数据类型都有相应的包装类：Byte、Short、Integer、Long、Float、Double、Character。包装类提供了面向对象的实现，比如二进制位数（Integer.SIZE）、最小值（Integer.MIN_VALUE）、最大值（Integer.MAX_VALUE）等。\nbyte\n8位整数，默认值是0，byte类型主要用在大型数组中节约空间，因为它占用的空间只有int类型的四分之一。\nshort\n16位整数，默认值是0。跟byte类型用法类似，占用空间是int类型的二分之一。\nint\n32位整数，默认值是0。最常用的整数类型。\nlong\n64位整数，默认值是0L。超出int范围的整数需要使用long类型。\nfloat\n32位浮点数，默认值是 0.0f。float类型用来表示小数。\ndouble\n64位浮点数，默认值是0.0d。浮点数的默认类似是double类型。\nboolean\n1位真假值（true/false），默认值是false。\nchar\n16位Unicode字符，默认值是u0000。用来存储任何单一字符。\n第九种基本数据类型void\n实际上，Java还有第九种基本数据类型void，我们经常在方法返回值那里见到它，它的包装类是java.lang.Void。\n三大变量类型 Java中的一个类可以包含3种类型的变量：局部变量、成员变量、类变量。它们的定义位置如下图所示：\n局部变量\n局部变量是在方法、构造方法或者语句块中定义的变量。变量声明和初始化都是在方法中，方法结束后，变量就会自动销毁。\n成员变量\n成员变量是在类中，方法体之外定义的变量。变量在创建对象的时候实例化，可以被类中方法、构造方法和特定的语句块访问。\n类变量\n类变量跟成员变量定义的位置相同，只不过必须声明为static类型。\n成员变量和类变量在使用上有个明显的区别是：类变量可以直接通过类名.变量形式进行访问，成员变量则不能。比如：\npublic class B { static int classVar = 1; // 类变量 int memberVar = 2; // 成员变量 } 成员变量在写代码时IDEA就已经报错了。\n修饰符 Java修饰符是让Java变得啰嗦的罪魁祸首之一。其他很多语言并没有这么多修饰符，取而代之的是语法规则，比如Python下划线开头的变量是私有变量，Golang小写字母开头的变量是私有变量。但是，正因为Java有着明确的修饰符，所以Java代码看起来是最直白的表述。\n修饰符概览\nJava修饰符有两大类，它们分别如下：\n访问控制修饰符\ndefault（什么都不写） private public protected 非访问控制修饰符\nstatic final abstract synchronized transient volatile 修饰符通常是放在一行代码最前面的，起到修饰作用，比如：\npublic class ClassName { // ... } private boolean myFlag; static final double weeks = 9.5; protected static final int BOXWIDTH = 42; public static void main(String[] arguments) { // 方法体 } 访问控制修饰符\n**访问控制，指的是对Java类、接口、变量、方法的访问权限的控制。**我们知道Java每个文件是一个类（Class），每个文件夹是一个包（Package），它们彼此之间能不能相互访问，就是通过修饰符决定的。\n访问控制修饰符一览表，从上往下，访问权限越来越小：\ndefault\n什么都不写，不使用任何修饰符，默认为包访问权限，即同一个包内都是可以访问的。\nJava有个很棒的设计：同一个包里面的类不需要import就能直接使用。\n示例：\nString version = \u0026#34;1.5.1\u0026#34;; boolean processOrder() { return true; } private\n意思就像它的名字一样，私有的，只有当前类的内部才可以访问。private用来保护类的隐私，如果外部类想访问private的变量，那么只能通过public的getter方法暴露出去。需要注意的是，private可以用到变量、方法上，但是不能用到类和接口上（这么做没有意义，类总是要被外部调用的，接口总是要被外部实现的）。\n示例：\npublic class Logger { private String format; public String getFormat() { return this.format; } public void setFormat(String format) { this.format = format; } } public\n公开的，所有都能访问，没有任何限制。\n示例：\npublic static void main(String[] arguments) { // ... } protected\n当前类可以访问，子类有可能可以访问。子类有两种情况：\n子类与父类在同一个包中：子类可以访问父类的protected。 子类与父类在不同的包中：子类可以访问，非子类内部的子类实例不能访问。 示例：\npackage test1; public class Base { int defaultInt; protected int protectedInt; } // 不同的包 package test2; // 子类 public class Test extends Base { public void test(){ Test t = new Test(); //t.defaultInt = 2; 不可见 t.protectedInt = 3; } } // 非子类 class TestNotSub{ public void test(){ Test t = new Test(); //t.defaultInt = 2; 不可见 //t.protectedInt = 3; 不可见 } } 需要注意的是，protected可以用到变量、方法上，但是不能用到类（内部类除外）和接口上。\n非访问控制修饰符\nstatic\nstatic用来修饰变量和方法。修饰的变量叫做静态变量/类变量，修饰的方法叫做静态方法或/类方法。\n静态的含义是，无论实例化多少个对象，静态变量或静态方法只有一份。作为类变量或类方法，static的用法是可以直接通过ClassName.varName访问类变量，或直接通过ClassName.methodName()访问类方法，无需实例化对象。\n需要注意的是，静态方法里面不能使用类的非静态变量。\n我以Python举例可以更好说明这一点，Python的类方法第一个入参是cls，如果想访问非类变量，那么入参必须是self。\nfinal\nfinal修饰变量：通常和static一起用来声明常量，比如static final String TITLE = \u0026quot;Manager\u0026quot;; final修饰方法：父类的final方法可以被子类继承，但是不能被重写，防止该方法的内容被篡改。 final修饰类：final类不能被继承。 abstract\nabstract修饰类：抽象类。 abstract修饰方法：抽象方法。 抽象意味着它们只定义了一个形式，没有具体内容，一定会由其他的类或方法进行具体实现。如果类中有抽象方法，那么这个类必须要定义为抽象类，否则会编译报错。\nsynchronized\n用于修饰方法，这个方法同一时间只能被一个线程访问。\ntransient\n修饰变量，用的很少，我也看不懂。\nvolatile\n修饰变量，变量每次被线程访问时，都强制从共享内存中重新读取值，当变量发生变化时，会强制线程将变化值写入共享内存。这样两个线程在任何时候都能看到变量的同一个值。\n运算符 Java一共有以下几类运算符：\n算术运算符 关系运算符 位运算符 逻辑运算符 赋值运算符 其他运算符 总的来说，Java运算符跟其他编程语言的运算符大同小异，可以快速浏览一下。\n算术运算符\n假设整数变量A的值为10，整数变量B的值为20：\n特别的是，前缀自增自减法（++a --a）和后缀自增自减法（a++ a--）：\n前缀自增自减法（++a --a）：先进行自增自减，再进行表达式运算。\n后缀自增自减法（a++ a--）：先进行表达式运算，再进行自增自减。\n示例：\npublic class Test { public static void main(String[] args) { int d = 25; // 查看 d++ 与 ++d 的不同 System.out.println(\u0026#34;d++ = \u0026#34; + (d++) ); System.out.println(\u0026#34;++d = \u0026#34; + (++d) ); } } 结果为：\nd++ = 25 ++d = 27 关系运算符\n假设整数变量A的值为10，整数变量B的值为20：\n位运算符\n假设整数变量A的值为60，整数变量B的值为13：\n逻辑运算符\n假设布尔变量A为真，布尔变量B为假：\n需要注意的是，如果第一个操作数已经能判断结果了，那么就不会执行下一个操作数，比如：\npublic class Test { public static void main(String[] args) { boolean a = false; boolean b = true; boolean c = a \u0026amp;\u0026amp; b; // a已经能判断结果为false，不再执行b boolean d = b || a; // b已经能判断结果为true，不再执行a } } 赋值运算符\n其他运算符\n条件运算符\nvariable x = (expression) ? value if true : value if false 示例：\npublic class Test { public static void main(String[] args){ int a , b; a = 10; // 如果 a 等于 1 成立，则设置 b 为 20，否则为 30 b = (a == 1) ? 20 : 30; System.out.println( \u0026#34;Value of b is : \u0026#34; + b ); // 30 // 如果 a 等于 10 成立，则设置 b 为 20，否则为 30 b = (a == 10) ? 20 : 30; System.out.println( \u0026#34;Value of b is : \u0026#34; + b ); // 20 } } 条件运算符也叫做三元运算符，三元场景可以多用这个运算符简写代码。\ninstanceof 运算符\n( Object reference variable ) instanceof (class/interface type) 用来判断对象是否为类的实例。比如：\nString name = \u0026#34;James\u0026#34;; boolean result = name instanceof String; // 由于 name 是 String 类型，所以返回真 Java运算符优先级\n在编写代码的时候，多用小括号把优先计算的表达式框起来，才不容易出错。\n循环结构 循环结构\nwhile\nwhile( 布尔表达式 ) { // 循环体 } 只要布尔表达式为True，就会一直反复执行循环体。\n示例：\npublic class Test { public static void main(String args[]) { int x = 10; while( x \u0026lt; 20 ) { System.out.print(\u0026#34;value of x : \u0026#34; + x ); x++; System.out.print(\u0026#34;\\n\u0026#34;); } } } do while\ndo { // 循环体 }while( 布尔表达式 ); 无论表达式是否为True，都先执行一次循环体，然后就跟while一样先判断布尔表达式，如果为True再继续执行循环，为False就退出循环。\n示例：\npublic class Test { public static void main(String args[]){ int x = 10; do{ System.out.print(\u0026#34;value of x : \u0026#34; + x ); x++; System.out.print(\u0026#34;\\n\u0026#34;); }while( x \u0026lt; 20 ); } } for\nfor(初始化; 布尔表达式; 更新) { // 循环体 } 初始化：定义一个或多个循环控制变量，也可以为空语句。 布尔表达式：根据True或False决定是否继续执行循环。 更新：更新循环控制变量。 示例：\npublic class Test { public static void main(String args[]) { for(int x = 10; x \u0026lt; 20; x = x+1) { System.out.print(\u0026#34;value of x : \u0026#34; + x ); System.out.print(\u0026#34;\\n\u0026#34;); } } } Java也有更方便从数组遍历元素的for循环：\nfor(声明语句 : 表达式) { // 循环体 } 声明语句：跟数组元素类型匹配的局部变量。 表达式：数组或返回数组的方法。 示例：\npublic class Test { public static void main(String args[]){ int [] numbers = {10, 20, 30, 40, 50}; for(int x : numbers ){ System.out.print( x ); System.out.print(\u0026#34;,\u0026#34;); } System.out.print(\u0026#34;\\n\u0026#34;); String [] names ={\u0026#34;James\u0026#34;, \u0026#34;Larry\u0026#34;, \u0026#34;Tom\u0026#34;, \u0026#34;Lacy\u0026#34;}; for( String name : names ) { System.out.print( name ); System.out.print(\u0026#34;,\u0026#34;); } } } break\n跳出整个循环。\n示例：\npublic class Test { public static void main(String args[]) { int [] numbers = {10, 20, 30, 40, 50}; for(int x : numbers ) { if( x == 30 ) { break; // x等于30时跳出循环，后面都不打印了 } System.out.print( x ); System.out.print(\u0026#34;\\n\u0026#34;); } } } continue\n跳过当前这次循环，执行下一次循环。\n示例：\npublic class Test { public static void main(String args[]) { int [] numbers = {10, 20, 30, 40, 50}; for(int x : numbers ) { if( x == 30 ) { continue; // 不会打印30，但是会继续打印后面元素 } System.out.print( x ); System.out.print(\u0026#34;\\n\u0026#34;); } } } break和continue可以从字面意思来区分，break中断循环，continue继续下次循环。\n条件语句 if\nif(布尔表达式) { //如果布尔表达式为true将执行的语句 } 示例：\npublic class Test { public static void main(String args[]){ int x = 10; if( x \u0026lt; 20 ){ System.out.print(\u0026#34;这是 if 语句\u0026#34;); } } } if else\nif(布尔表达式){ //如果布尔表达式的值为true }else{ //如果布尔表达式的值为false } 示例：\npublic class Test { public static void main(String args[]){ int x = 30; if( x \u0026lt; 20 ){ System.out.print(\u0026#34;这是 if 语句\u0026#34;); }else{ System.out.print(\u0026#34;这是 else 语句\u0026#34;); } } } 也可以跟多个if else：\nif(布尔表达式 1){ //如果布尔表达式 1的值为true执行代码 }else if(布尔表达式 2){ //如果布尔表达式 2的值为true执行代码 }else if(布尔表达式 3){ //如果布尔表达式 3的值为true执行代码 }else { //如果以上布尔表达式都不为true执行代码 } 示例：\npublic class Test { public static void main(String args[]){ int x = 30; if( x == 10 ){ System.out.print(\u0026#34;Value of X is 10\u0026#34;); }else if( x == 20 ){ System.out.print(\u0026#34;Value of X is 20\u0026#34;); }else if( x == 30 ){ System.out.print(\u0026#34;Value of X is 30\u0026#34;); }else{ System.out.print(\u0026#34;这是 else 语句\u0026#34;); } } } 嵌套的if else\nif(布尔表达式 1){ ////如果布尔表达式 1的值为true执行代码 if(布尔表达式 2){ ////如果布尔表达式 2的值为true执行代码 } } 示例：\npublic class Test { public static void main(String args[]){ int x = 30; int y = 10; if( x == 30 ){ if( y == 10 ){ System.out.print(\u0026#34;X = 30 and Y = 10\u0026#34;); } } } } switch case\nswitch(expression){ case value : //语句 break; //可选 case value : //语句 break; //可选 //你可以有任意数量的case语句 default : //可选 //语句 } expression：变量或返回变量的方法，变量类型可以是byte、short、int或char，以及String类型。 value：字符串常量或字面量，且与表达式的变量类型相同。 break：可选，有break时会中断后续匹配跳出switch语句，没有break时会继续执行后面的case。 default：当所有case都没有匹配到时，会执行default语句，一般放在最后的位置。 示例：\npublic class Test { public static void main(String args[]){ //char grade = args[0].charAt(0); char grade = \u0026#39;C\u0026#39;; switch(grade) { case \u0026#39;A\u0026#39; : System.out.println(\u0026#34;优秀\u0026#34;); break; case \u0026#39;B\u0026#39; : case \u0026#39;C\u0026#39; : System.out.println(\u0026#34;良好\u0026#34;); break; case \u0026#39;D\u0026#39; : System.out.println(\u0026#34;及格\u0026#34;); break; case \u0026#39;F\u0026#39; : System.out.println(\u0026#34;你需要再努力努力\u0026#34;); break; default : System.out.println(\u0026#34;未知等级\u0026#34;); } System.out.println(\u0026#34;你的等级是 \u0026#34; + grade); } } Number类 Java是纯面向对象编程语言，为了以对象的方式使用内置数据类型，比如byte、int、long、double等，Java对它们进行了封装，封装后的类称为包装类。这里的封装一般也叫做装箱，反之叫做拆箱。\n所有的数字包装类，都是抽象基类Number的子类，包括Byte、Short、Integer、Long、Float、Double。\n示例：\npublic class Test{ public static void main(String[] args){ Integer x = 5; // 装箱 x = x + 10; // 拆箱 System.out.println(x); } } Math类 为了支持数学运算，Java提供了Math类，可以进行指数、对数、平方根等数学运算。\n示例：\npublic class Test { public static void main (String []args) { System.out.println(\u0026#34;90 度的正弦值：\u0026#34; + Math.sin(Math.PI/2)); System.out.println(\u0026#34;0度的余弦值：\u0026#34; + Math.cos(0)); System.out.println(\u0026#34;60度的正切值：\u0026#34; + Math.tan(Math.PI/3)); System.out.println(\u0026#34;1的反正切值： \u0026#34; + Math.atan(1)); System.out.println(\u0026#34;π/2的角度值：\u0026#34; + Math.toDegrees(Math.PI/2)); System.out.println(Math.PI); } } 对于四舍五入，Math提供了round、floor、ceil三个方法：\nround：四舍五入 floor：向下取整 ceil：向上取整（返回double类型） 示例：\n基本类型与包装类区别\n基本类型不是对象，不需要new关键字创建，包装类需要使用new关键字创建对象。 存储方式不同，基本类型的值存在堆栈中，包装类的实例存在堆中。 初始值不同，包装类的初始值为null，基本类型视具体类型而定，比如int初始值为0，boolean初始值为false。 有些场景下只能使用包装类，比如与集合类交互，使用泛型和反射调用函数，某个字段允许null值，就只能使用包装类。 Character类 Character用于对单个字符进行操作。\n我们知道Java内置了数据类型char，但面向对象的Java在实际处理过程中需要的是对象，于是包装类Character就被设计了出来。\n创建对象代码如下：\nCharacter ch = new Character(\u0026#39;a\u0026#39;); 也可以利用装箱简写代码：\nCharacter ch = \u0026#39;a\u0026#39;; Character类具有以下方法：\nString类 字符串在任何编程语言都是应用非常多的，Java提供了String类来对字符串进行操作。\n创建字符串有两种方式：\n简单方式\nString str = \u0026#34;Runoob\u0026#34;; new关键字\nString str2=new String(\u0026#34;Runoob\u0026#34;); 它们的区别在于，前者创建的字符串存放在公共池中，后者存放在堆上：\n// 简单方式 公共池 String s1 = \u0026#34;Runoob\u0026#34;; String s2 = \u0026#34;Runoob\u0026#34;; String s3 = s1; // new关键字 堆 String s4 = new String(\u0026#34;Runoob\u0026#34;); String s5 = new String(\u0026#34;Runoob\u0026#34;); 如下图所示：\nString有3个常用方法：\n获取长度\nString site = \u0026#34;www.runoob.com\u0026#34;; int len = site.length(); 连接字符串\n// 调用方法 \u0026#34;我的名字是 \u0026#34;.concat(\u0026#34;Runoob\u0026#34;); // “+”操作符 \u0026#34;Hello,\u0026#34; + \u0026#34; runoob\u0026#34; + \u0026#34;!\u0026#34; 创建格式化字符串\nString fs; fs = String.format(\u0026#34;浮点型变量的值为 \u0026#34; + \u0026#34;%f, 整型变量的值为 \u0026#34; + \u0026#34; %d, 字符串变量的值为 \u0026#34; + \u0026#34; %s\u0026#34;, floatVar, intVar, stringVar); StringBuilder类 String创建的字符串对象是不能修改的，如果想修改，那么需要用到StringBuffer和StringBuilder类。\nStringBuilder相对于StringBuffer来说有速度优秀，所以大多数时候使用StringBuilder即可。如果想要保证线程安全，那么只能使用StringBuffer。\nStringBuilder示例：\npublic class RunoobTest{ public static void main(String args[]){ StringBuilder sb = new StringBuilder(10); sb.append(\u0026#34;Runoob..\u0026#34;); System.out.println(sb); sb.append(\u0026#34;!\u0026#34;); System.out.println(sb); sb.insert(8, \u0026#34;Java\u0026#34;); System.out.println(sb); sb.delete(5,8); System.out.println(sb); } StringBuffer类 示例：\npublic class Test{ public static void main(String args[]){ StringBuffer sBuffer = new StringBuffer(\u0026#34;菜鸟教程官网：\u0026#34;); sBuffer.append(\u0026#34;www\u0026#34;); sBuffer.append(\u0026#34;.runoob\u0026#34;); sBuffer.append(\u0026#34;.com\u0026#34;); System.out.println(sBuffer); } } 数组 Java中的数组是用来存储固定大小的同类型元素。\n声明方式：\ndataType[] arrayRefVar; 创建数组：\narrayRefVar = new dataType[arraySize]; 声明和创建可以一行代码搞定：\ndataType[] arrayRefVar = new dataType[arraySize]; 在创建时同时初始化值：\ndataType[] arrayRefVar = {value0, value1, ..., valuek}; 或者创建匿名数组：\nSystem.out.println(Arrays.toString(new int[]{3, 1, 2, 6, 4, 2})); 数组是通过索引来访问元素的，索引值从0到arrayRefVar.length-1。\n可以使用for循环来遍历数组，比如：\npublic class TestArray { public static void main(String[] args) { double[] myArray = {1.9, 2.9, 3.4, 3.5}; // 打印所有数组元素 for (int i = 0; i \u0026lt; myArray.length; i++) { System.out.println(myArray[i] + \u0026#34; \u0026#34;); } // 计算所有元素的总和 double total = 0; for (int i = 0; i \u0026lt; myArray.length; i++) { total += myArray[i]; } System.out.println(\u0026#34;Total is \u0026#34; + total); // 查找最大元素 double max = myArray[0]; for (int i = 1; i \u0026lt; myArray.length; i++) { if (myArray[i] \u0026gt; max) max = myArray[i]; } System.out.println(\u0026#34;Max is \u0026#34; + max); } } 也能使用for each在不使用下标的情况下遍历数组：\nfor(type element: array) { System.out.println(element); } 比如：\npublic class TestArray { public static void main(String[] args) { double[] myArray = {1.9, 2.9, 3.4, 3.5}; // 打印所有数组元素 for (double element: myArray) { System.out.println(element); } } } 前面介绍的都是一维数组，除了一维数组，还有多维数组，比如：\nString[][] str = new String[3][4]; int[][] a = new int[2][3]; java.util.Arrays类提供了很多方法来操作数组，这些方法都是静态的。比如：\ntoString：转换为字符串。 fill：给数组赋值。 sort：对数组排序。 equals：比较数组。 binarySearch：对排序好的数组进行二分查找。 方法 Java中没有函数的概念，只有方法这一说法。但实际上他们的作用是一模一样的，都是把一段代码进行封装后调用。\n方法的命名规则\nJava中的方法采用驼峰命名法，第一个单词首字母小写，后面每个单词首字母均大写，比如addPerson。\n方法定义\nmain方法\npublic static void main(String[] args) { int i = 5; int j = 2; int k = max(i, j); System.out.println( i + \u0026#34; 和 \u0026#34; + j + \u0026#34; 比较，最大值是：\u0026#34; + k); } main方法的头部是不变的，带修饰符public和static，返回void类型值，方法名字是main，此外带一个String[]类型参数args。\nvoid\nvoid是Java基本数据类型之一，表明方法没有返回值。\n值传递\nJava方法的参数传递都是值传递。如果参数是基本类型，传递的值是基本类型字面量的拷贝。如果参数是对象，传递的值是对象引用的拷贝。\n构造方法\n构造方法和类名一模一样，是一种特殊的方法，没有返回值，在对象初始化时调用。一个类可以有多个构造方法，Java会根据参数进行匹配。比如：\n// 一个简单的构造函数 class MyClass { int x; // 构造函数 MyClass() { x = 10; } // 构造函数 MyClass(int i) { x = i; } } 构造方法可以不用显式定义，Java会默认定义一个，一旦你定义了自己的构造方法，默认构造方法就会失效。默认构造方法的访问修饰符和类的访问修饰符相同，类为public构造方法也是public，类为protected构造方法也是protected。\nfinalize()\nfinalize()方式是构造函数的逆向，在对象销毁时调用，比如：\npublic class FinalizationDemo { public static void main(String[] args) { Cake c1 = new Cake(1); Cake c2 = new Cake(2); Cake c3 = new Cake(3); c2 = c3 = null; System.gc(); //调用Java垃圾收集器 } } class Cake extends Object { private int id; public Cake(int id) { this.id = id; System.out.println(\u0026#34;Cake Object \u0026#34; + id + \u0026#34;is created\u0026#34;); } protected void finalize() throws java.lang.Throwable { super.finalize(); System.out.println(\u0026#34;Cake Object \u0026#34; + id + \u0026#34;is disposed\u0026#34;); } } 可变参数\ntypeName... parameterName 一个方法中只能有一个可变参数，并且必须放在最后。比如：\npublic class VarargsDemo { public static void main(String args[]) { // 调用可变参数的方法 printMax(34, 3, 3, 2, 56.5); printMax(new double[]{1, 2, 3}); } // 可变参数 public static void printMax( double... numbers) { if (numbers.length == 0) { System.out.println(\u0026#34;No argument passed\u0026#34;); return; } double result = numbers[0]; for (int i = 1; i \u0026lt; numbers.length; i++){ if (numbers[i] \u0026gt; result) { result = numbers[i]; } } System.out.println(\u0026#34;The max value is \u0026#34; + result); } } 异常处理 Java的异常检测格外的严格，如果没有合适的处理异常，有可能代码都无法编译。Java异常类如下图所示：\nThrowable类有两个类Error和Exception，图中也列举了几个常见的子类，比如OutOfMemoryError内存溢出、NullPointerException空指针异常等。\n捕获异常\ntry { // 程序代码 }catch(ExceptionName e1) { //Catch 块 } 示例：\n// 文件名 : ExcepTest.java import java.io.*; public class ExcepTest{ public static void main(String args[]){ try{ int a[] = new int[2]; System.out.println(\u0026#34;Access element three :\u0026#34; + a[3]); }catch(ArrayIndexOutOfBoundsException e){ System.out.println(\u0026#34;Exception thrown :\u0026#34; + e); } System.out.println(\u0026#34;Out of the block\u0026#34;); } } 多重捕获\n一个try后面可以跟多个catch：\ntry{ // 程序代码 }catch(异常类型1 异常的变量名1){ // 程序代码 }catch(异常类型2 异常的变量名2){ // 程序代码 }catch(异常类型3 异常的变量名3){ // 程序代码 } 示例：\ntry { file = new FileInputStream(fileName); x = (byte) file.read(); } catch(FileNotFoundException f) { // Not valid! f.printStackTrace(); return -1; } catch(IOException i) { i.printStackTrace(); return -1; } throws/throw\nthrows放在方法尾部用来抛出异常，throw放在方法中用来抛出异常。\nimport java.io.*; public class className { public void deposit(double amount) throws RemoteException { // Method implementation throw new RemoteException(); } //Remainder of class definition } 一个方法可以抛出多个异常：\nimport java.io.*; public class className { public void withdraw(double amount) throws RemoteException, InsufficientFundsException { // Method implementation } //Remainder of class definition } finally\n无论是否发生异常，finally代码块中的代码总会被执行。finally代码块不是必须而是可选的。\ntry{ // 程序代码 }catch(异常类型1 异常的变量名1){ // 程序代码 }catch(异常类型2 异常的变量名2){ // 程序代码 }finally{ // 程序代码 } 示例：\npublic class ExcepTest{ public static void main(String args[]){ int a[] = new int[2]; try{ System.out.println(\u0026#34;Access element three :\u0026#34; + a[3]); }catch(ArrayIndexOutOfBoundsException e){ System.out.println(\u0026#34;Exception thrown :\u0026#34; + e); } finally{ a[0] = 6; System.out.println(\u0026#34;First element value: \u0026#34; +a[0]); System.out.println(\u0026#34;The finally statement is executed\u0026#34;); } } } 自定义异常\n继承Exception或RuntimeException类可以自定义异常，比如：\n// 文件名InsufficientFundsException.java import java.io.*; //自定义异常类，继承Exception类 public class InsufficientFundsException extends Exception { //此处的amount用来储存当出现异常（取出钱多于余额时）所缺乏的钱 private double amount; public InsufficientFundsException(double amount) { this.amount = amount; } public double getAmount() { return amount; } } 定义好以后就可以throw new InsufficientFundsException(needs);抛出异常，然后再try{} catch(InsufficientFundsException e){} 捕获异常。\n参考资料：\nJDK维基百科 https://zh.wikipedia.org/zh-hans/JDK\nJava菜鸟教程 https://www.runoob.com/java/java-tutorial.html\nJava 到底是值传递还是引用传递？https://www.zhihu.com/question/31203609\n","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/java/000001-%E5%85%A5%E9%97%A8%E6%B5%8B%E8%AF%95%E4%BA%BA%E5%91%98%E5%AD%A6java%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"目标读者\"\u003e目标读者\u003c/h1\u003e\n\u003cp\u003e本指南特别适合有Python基础的同学学习Java入门，对于没有任何编程经验的同学可能会存在困难。\u003c/p\u003e\n\u003ch1 id=\"为什么测试人员要学java\"\u003e为什么测试人员要学Java\u003c/h1\u003e\n\u003cp\u003e选择Java，应该不错。TesterHome测试论坛知名大佬基本都是以Java技术栈为主，少部分是Golang和Python。我公司2021年校招的两个同济大学研究生，测试开发，也是以Java为主。有一个测开同事跳槽去了美团，拿了高薪，只会Java。有一个测开同事跳槽去了创业公司做了测试架构师，只会Java。招聘网站搜索测试开发20K以上职位，基本都要求会Java。我接触到的开发，Java程序员占80%以上。\u003c/p\u003e\n\u003ch1 id=\"测试人员用java能干嘛\"\u003e测试人员用Java能干嘛\u003c/h1\u003e\n\u003cp\u003e如果你的公司是以Java技术栈为主，那么你可以用Java做所有事情，而且有些事情只能用Java来做。这个问题挺关键的，学习一门语言肯定是要能够学以致用。在自动化测试，Java会显得稍微有点笨重，而Python会适合一点。你可以用Python来写接口自动化脚本，在小范围内使用，但是当团队变大以后，脚本如何规范如何维护，就成了一个很大的问题，而Java由于静态语言特性和面向对象编程，正好可以用来解决这个问题。在测试平台和性能测试，Java有很成熟的Web开发框架和配套的生态。在测试工具开发，公司封装好的Java库可以拿来即用，碰到问题也可以找开发帮忙。白盒测试，基本上都是要靠Java来做的，看代码，用JaCoCo统计代码覆盖率。其他专项比如流量录制回放、全链路压测、Dubbo接口测试，都只能使用Java来做。\u003c/p\u003e\n\u003ch1 id=\"测试人员怎么学java\"\u003e测试人员怎么学Java\u003c/h1\u003e\n\u003cp\u003e我推荐刷完一遍基础语法后，在LeetCode上面刷算法题来加强练习。Java还是有一定的学习门槛的，初学者可能学起来会有些吃力，可以先从Python入手，了解编程语言的底层逻辑，等对编程有些许感知以后，再学习Java。基础语法可以快速浏览一遍，一遍并不会就能记住了，需要在编码中持续加强记忆。光学语法也不行，学习Java和其他编程语言一样，需要多敲多练，刷算法题，既能熟悉Java语法，也能熟悉算法，一举两得，题做不来就先把答案背一遍再做。另外，可以在工作中主动找研发开通下代码权限，尝试Review开发代码，看多了，有些技巧也会知道了。有机会，可以用Java写点工具或者平台，实践出真知。\u003c/p\u003e\n\u003ch1 id=\"搭建环境\"\u003e搭建环境\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e安装Java\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eJava的安装包是个\u003ccode\u003e.exe\u003c/code\u003e文件，特殊地方在于文件名不是\u003ccode\u003ejava-xxx.exe\u003c/code\u003e而是\u003ccode\u003ejdk-xxx.exe\u003c/code\u003e，比如\u003ccode\u003ejdk-8u281-windows-x64.exe\u003c/code\u003e，\u003ccode\u003ejdk\u003c/code\u003e是\u003ccode\u003eJava Development Kit \u003c/code\u003e的缩写，指Java开发工具包，包括以下内容：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/399cd94f18b64811a55a3c7f1176ad96.png\"\u003e\u003c/p\u003e\n\u003cp\u003eJDK的最新版本为15，但是国内大多数公司仍然使用的是JDK8，这是因为JDK8是各方面都很成熟稳定的版本，并且基于JDK8开发的业务系统已经具有一定规模，新版本JDK并不能完全无感知的迁移，需要做代码修改和测试，会是一笔巨大开销，为了降低成本和规避兼容问题风险，JDK8沿用到了至今。JDK8的下载地址如下：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html\"\u003ehttps://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e选择相应的版本下载：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b5ccd5d4a2b842918b280f34839c1ca7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e比如我的电脑是Windows 64位的系统，就选择了\u003ccode\u003eWindows x64\u003c/code\u003e这个版本，会下载到一个\u003ccode\u003ejdk-8u281-windows-x64.exe\u003c/code\u003e文件。Java已经被Oracle收购了，需要登录Oracle账号后才能下载，没有账号注册一个也很方便：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f7613a6a08ec456b8ac622e87f33c5f7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e下载后可以开始安装了：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/907449e94e9348c498085711ebbcbeda.png\"\u003e\u003c/p\u003e\n\u003cp\u003e安装到默认位置即可，直接下一步。中途会让选择JRE（Java Runtime Environment，Java运行环境）的目标文件夹：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fef7be800034400b852bbbccb4e13ace.png\"\u003e\u003c/p\u003e\n\u003cp\u003e依然保持默认即可。安装结束后就能看到两个新文件夹了：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ebd1aee25aad4d61806c52a14be78fed.png\"\u003e\u003c/p\u003e\n\u003cp\u003e以前，还需要配置系统环境变量，设置\u003ccode\u003eJAVA_HOME\u003c/code\u003e和\u003ccode\u003eCLASSPATH\u003c/code\u003e才能使用Java（具体方法百度有很多）。现在，安装完成即可用，省去了不少麻烦，速度加快。打开\u003ccode\u003ecmd\u003c/code\u003e输入\u003ccode\u003ejava -version\u003c/code\u003e，命令能执行成功就表示Java已经安装好了：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f07ae335c31f4acfa2bf72691f984792.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e安装IntelliJ IDEA\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eJetBrains全家桶中也有专门用来写Java的，叫做\u003ccode\u003eIntelliJ IDEA\u003c/code\u003e，一般只说后面一个单词“哎迪儿”就知道是这个东西了。下载地址为：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.jetbrains.com/idea/download/#section=windows\"\u003ehttps://www.jetbrains.com/idea/download/#section=windows\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/d7addb5f165d4de7867d55e46f7408ee.png\"\u003e\u003c/p\u003e\n\u003cp\u003e个人建议选择旗舰版，看着齐全。下载后双击\u003ccode\u003eideaIU-2020.3.2.exe\u003c/code\u003e安装，先别忙着打开。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMaven仓库\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eMaven是用来管理Java第三方包的主流仓库工具，通过\u003ccode\u003epom.xml\u003c/code\u003e文件来配置安装即可，在mvnrepository网站上能检索到所需配置信息：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://mvnrepository.com/\"\u003ehttps://mvnrepository.com/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e官方仓库有时候下载速度很慢，本文用国内镜像替代，提高首次打开时同步仓库速度。我们先把Maven中央仓库从Apache替换成阿里云。打开本地目录\u003ccode\u003eD:\\Program Files\\JetBrains\\IntelliJ IDEA 2020.3.2\\plugins\\maven\\lib\\maven3\\conf\u003c/code\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/8d4c0b2aac3a4296b452eb14a6e57d64.png\"\u003e\u003c/p\u003e\n\u003cp\u003e编辑\u003ccode\u003esettings.xml\u003c/code\u003e文件，找到\u003ccode\u003e\u0026lt;mirrors\u0026gt;\u003c/code\u003e标签，添加代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;mirror\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;id\u0026gt;\u003c/span\u003ealiyunmaven\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/id\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;mirrorOf\u0026gt;\u003c/span\u003e*\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/mirrorOf\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;name\u0026gt;\u003c/span\u003e阿里云公共仓库\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;url\u0026gt;\u003c/span\u003ehttps://maven.aliyun.com/repository/public\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/url\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/mirror\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ddfff549897741da9041b8bf246bd902.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这是新版写法，官网https://maven.aliyun.com/mvn/guide有说明。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e接着可以打开IDEA了，欢迎界面映入眼帘：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3de1c1b47873455b8d3a914ca8d343a5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e新建项目\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e选择新建Maven项目：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/b33cc86e0d1f41b1a071da468039b81d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e填写项目名和存放位置：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/c9bfa1ffaff346c2ba20e855d3a96bfb.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ccode\u003epom.xml\u003c/code\u003e文件中添加仓库配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;repositories\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;repository\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;id\u0026gt;\u003c/span\u003epublic\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/id\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;url\u0026gt;\u003c/span\u003ehttps://maven.aliyun.com/repository/public\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/url\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/repository\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/repositories\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;pluginRepositories\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;pluginRepository\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;id\u0026gt;\u003c/span\u003epublic\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/id\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;url\u0026gt;\u003c/span\u003ehttps://maven.aliyun.com/repository/public\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/url\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/pluginRepository\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#0550ae\"\u003e\u0026lt;/pluginRepositories\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/77932da2d3a343af8017833336c7ece0.png\"\u003e\u003c/p\u003e","title":"测试人员学Java入门指南"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n时间来到2026年夏天，一轮又一轮的手机涨价新闻出现，“华为新款三折叠19999元非常有诚意”，“折叠屏iPhone Ultra的预计标价达到20559元”，“小米18 Fold折叠屏起售价10999元”。我不是以前的我，手机也不是以前的手机了。\n2013年7月31日，随着一句“为发烧而生”，红米带着性价比横空出世。这也是我的第一台触屏手机。拆开外包装，拿到手上，打开屏幕的那一刻，我现在还记得，我终于有了一台真正的手机，我内心澎湃地感叹着。手机上的文字看着特别的细腻。我给这台手机换了3次手机壳，一直坚持用到了毕业。\n手机市场最疯狂的时代是2015年到2017年，我在这3年换了4部手机，HTC、联想ZUK、小米6、小米Note3，最快的是半年就换。手机越来越漂亮，可是人却越来越不懂得珍惜。\n那段时间经常刷中关村在线、IT之家，了解新机的特点，追各种评测。我同学也必看小米发布会，我也是从他那知道，对着小米音响喊一声雷军，它会自动播放are you ok。\n那是在江苏常州出差的下午，天下着暴雨，京东快递小哥电话说你的手机到了。我满怀激动地打开了用工资买的第一台手机HTC。我当时的购机需求是外放音质，低音炮特别带感，金属拉丝后盖也有质感。联想ZUK是小巧旗舰用起来很顺手，小米6没用坏是摔坏了。小米Note3雷军没有耍猴，我第一时间在小米之家拿到了首发现货。这台手机的手感是我用过的手感最舒服的，温润如玉。\n2021年，从小米Note3换到了小米11。用了5年，也让我再也不想买小米任何产品。\n刚开始买小米的时候，我还自嘲说自己要成为米粉了。毕竟有红米1代情节，加上小米Note3的体验加持，我对小米的感官是特别好的。还买了小米电视、小米机顶盒、小米充电宝等一系列小米产品。\n我升级了一次系统之后，WIFI用不了，找到小米之家售后。“你的手机是哪个型号”，“小米11”，店员二话不说，给我免费换了主板。不怎么关注手机新闻的我，那时才知道小米11烧主板的事情。我问那个店员你的手机看着挺不错啊，是哪个系列的，他微微一笑，“苹果”。\n回顾用过的小米产品，确实性价比挺高，但是质量也并没有那么好。当初1999把价格打下来的“土匪”，现在也摇身一变成为了高不可攀的“贵族”。\n2026年，小米11的摄像头聚不起焦了，每次拍照都要甩两下。之前在酒店让前台写评价拍照，我让他甩两下，他开玩笑的说，这么高级。\n我已经很久没关注手机的新闻，最近买手机发现价格已经变得离谱。一个手机的价格都够买一台笔记本电脑了。手机的那些乱七八糟的功能很多也用不上。\n华为手机要6000+，苹果手机要8000+，新一代更是要突破万元。现在的手机，已经贵得难以下手。老婆追求拍照买了Vivo，我顺理成章地继承了她原来的那台iPhone，换个手机壳，还能继续用3年。\n一年又一年，十六年啦。是手机已经买不起了？还是我已经不想买那么贵的手机？\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%9D%82%E8%B0%88/000001-%E4%BB%8E300%E5%88%B019999%E6%88%91%E4%B9%B0%E6%89%8B%E6%9C%BA%E7%9A%84%E5%8D%81%E5%85%AD%E5%B9%B4/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/6c375cfc77b64f3b961537f858c6df50.png\"\u003e\u003c/p\u003e\n\u003cp\u003e时间来到2026年夏天，一轮又一轮的手机涨价新闻出现，“华为新款三折叠19999元非常有诚意”，“折叠屏iPhone Ultra的预计标价达到20559元”，“小米18 Fold折叠屏起售价10999元”。我不是以前的我，手机也不是以前的手机了。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/2d750308f6234ac9b951f3053428fcd7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2013年7月31日，随着一句“为发烧而生”，红米带着性价比横空出世。这也是我的第一台触屏手机。拆开外包装，拿到手上，打开屏幕的那一刻，我现在还记得，我终于有了一台真正的手机，我内心澎湃地感叹着。手机上的文字看着特别的细腻。我给这台手机换了3次手机壳，一直坚持用到了毕业。\u003c/p\u003e\n\u003cp\u003e手机市场最疯狂的时代是2015年到2017年，我在这3年换了4部手机，HTC、联想ZUK、小米6、小米Note3，最快的是半年就换。手机越来越漂亮，可是人却越来越不懂得珍惜。\u003c/p\u003e\n\u003cp\u003e那段时间经常刷中关村在线、IT之家，了解新机的特点，追各种评测。我同学也必看小米发布会，我也是从他那知道，对着小米音响喊一声雷军，它会自动播放are you ok。\u003c/p\u003e\n\u003cp\u003e那是在江苏常州出差的下午，天下着暴雨，京东快递小哥电话说你的手机到了。我满怀激动地打开了用工资买的第一台手机HTC。我当时的购机需求是外放音质，低音炮特别带感，金属拉丝后盖也有质感。联想ZUK是小巧旗舰用起来很顺手，小米6没用坏是摔坏了。小米Note3雷军没有耍猴，我第一时间在小米之家拿到了首发现货。这台手机的手感是我用过的手感最舒服的，温润如玉。\u003c/p\u003e\n\u003cp\u003e2021年，从小米Note3换到了小米11。用了5年，也让我再也不想买小米任何产品。\u003c/p\u003e\n\u003cp\u003e刚开始买小米的时候，我还自嘲说自己要成为米粉了。毕竟有红米1代情节，加上小米Note3的体验加持，我对小米的感官是特别好的。还买了小米电视、小米机顶盒、小米充电宝等一系列小米产品。\u003c/p\u003e\n\u003cp\u003e我升级了一次系统之后，WIFI用不了，找到小米之家售后。“你的手机是哪个型号”，“小米11”，店员二话不说，给我免费换了主板。不怎么关注手机新闻的我，那时才知道小米11烧主板的事情。我问那个店员你的手机看着挺不错啊，是哪个系列的，他微微一笑，“苹果”。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/a733681d5603432a82e3bc4d49a7b193.png\"\u003e\u003c/p\u003e\n\u003cp\u003e回顾用过的小米产品，确实性价比挺高，但是质量也并没有那么好。当初1999把价格打下来的“土匪”，现在也摇身一变成为了高不可攀的“贵族”。\u003c/p\u003e\n\u003cp\u003e2026年，小米11的摄像头聚不起焦了，每次拍照都要甩两下。之前在酒店让前台写评价拍照，我让他甩两下，他开玩笑的说，这么高级。\u003c/p\u003e\n\u003cp\u003e我已经很久没关注手机的新闻，最近买手机发现价格已经变得离谱。一个手机的价格都够买一台笔记本电脑了。手机的那些乱七八糟的功能很多也用不上。\u003c/p\u003e\n\u003cp\u003e华为手机要6000+，苹果手机要8000+，新一代更是要突破万元。现在的手机，已经贵得难以下手。老婆追求拍照买了Vivo，我顺理成章地继承了她原来的那台iPhone，换个手机壳，还能继续用3年。\u003c/p\u003e\n\u003cp\u003e一年又一年，十六年啦。是手机已经买不起了？还是我已经不想买那么贵的手机？\u003c/p\u003e","title":"从300到19999，我买手机的十六年"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nMan In The Middle mitm是Man In The Middle的首字母缩写，意思是位于中间的人，表明mitmproxy是一个代理，可以拦截请求，实现网络抓包。知名的网络抓包工具有Fiddler、Charles、HttpWatch、WireShark、BurpSuite和Postman等，mitmproxy相对来说，没有这么高的知名度，它相比于以上工具的独特优势是，它提供了Python API，可以编写Python代码对网络请求进行流量录制，从而收集接口信息以及转化为接口自动化用例等。mitmproxy官方文档不但有mitmproxy工具使用介绍，还有代理实现原理，对我们学习掌握网络知识有很大帮助。\n安装与使用 pip安装 pip install mitmproxy mitmproxy是Python开发的，用pip就能安装成功。官方给出了二进制安装包等方法，我觉得作为一款用Python开发的工具，用pip直接安装是更Pythonic的。\n命令启动 可以通过3种命令启动mitmproxy：\nmitmproxy 控制台\nmitmweb 网页\nmitmdump 命令行\n它们的内容都是一样的，只是UI不一样而已。\n手动设置代理 打开以后mitmproxy默认会监听http://localhost:8080，需要设置下代理，把代理服务器设置为这个地址。\nWindows可以这样设置：\n设置后一定要记得点击保存按钮，否则代理不会生效。\n实际上，Charles等网络抓包工具会自动配置代理，而mitmproxy需要手动设置代理，开发团队的解释是因为浏览器版本和配置经常会变化，所以他们建议在网上根据环境搜索方法手动设置。\n下载证书 访问http://mitm.it/，如果代理配置没有生效，打开后会是这个界面：\n正常的话，会出现证书下载页面：\n根据平台选择相应证书安装就OK了。\n使用 mitmproxy更其他抓包工具用法类似。\n官方用视频演示了mitmproxy如何使用：\nhttps://docs.mitmproxy.org/stable/mitmproxytutorial-userinterface/\n并且介绍了5种运行模式：\nhttps://docs.mitmproxy.org/stable/concepts-modes/\n读者可以自行阅读下。\nmitmproxy工作原理 客户端（本机）连接代理服务器（http://127.0.0.1:8080），代理服务器连接服务器。 客户端给代理服务器发请求，代理服务器把请求发送到服务器。 所有请求经过mitmproxy，就可以实现请求拦截。\n对于HTTP来说，直接借助mitmproxy传递请求就可以。而对于HTTPS来说，有个SSL/TLS安全认证，必须安装证书，服务器才会认为mitmproxy的请求是可信任的，请求才会成功传递。\nHTTPS的代理过程就要复杂很多，客户端先跟mitmproxy建立连接（12步），然后进行安全认证（36步），最后传递请求（7~8步）。\n反向代理 假设在本机使用FastAPI启动了个Mock服务，地址为http://127.0.0.1:5000，通过Postman调接口，怎么使用mitmproxy来拦截请求呢？这就需要用到反向代理。\n首先以反向代理模式运行mitmproxy，服务器端口为5000，监听端口为8000：\nmitmproxy --mode reverse:http://127.0.0.1:5000 --listen-host 127.0.0.1 --listen-port 8000 然后把请求http://127.0.0.1:5000/login里面的5000端口改成8000，访问：\nhttp://127.0.0.1:8000/login\n就能在mitmproxy看到抓到请求了：\n这样就能在本地使用Mock服务来调试拦截请求的Python代码了。\n反向代理，不需要在本机手动设置代理。\nPython API Python API是mitmproxy的特色功能：\n能够在Python代码中对拦截的请求进行处理。\naddons提供了很多hook函数，比如request：\n\u0026#34;\u0026#34;\u0026#34; Basic skeleton of a mitmproxy addon. Run as follows: mitmproxy -s anatomy.py \u0026#34;\u0026#34;\u0026#34; from mitmproxy import ctx class Counter: def __init__(self): self.num = 0 def request(self, flow): self.num = self.num + 1 ctx.log.info(\u0026#34;We\u0026#39;ve seen %d flows\u0026#34; % self.num) addons = [ Counter() ] response：\n\u0026#34;\u0026#34;\u0026#34;Add an HTTP header to each response.\u0026#34;\u0026#34;\u0026#34; class AddHeader: def __init__(self): self.num = 0 def response(self, flow): self.num = self.num + 1 flow.response.headers[\u0026#34;count\u0026#34;] = str(self.num) addons = [ AddHeader() ] 启动时加上-s参数指定脚本：\nmitmdump -s ./anatomy.py 代码就会在请求拦截时生效了。\n更多hook函数请参考官方说明：\nhttps://docs.mitmproxy.org/stable/api/events.html\n这里就不再详细说明了。\n而关于如何使用mitmproxy录制流量自动生成自动化用例的部分，我会开发完成后集成到tep新版本中，届时再做介绍，敬请期待。\n参考资料：\nmitmproxy官方文档 https://docs.mitmproxy.org/stable/\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E6%B5%81%E9%87%8F%E5%9B%9E%E6%94%BE/000001-%E5%BD%95%E5%88%B6%E5%9B%9E%E6%94%BE%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%AD%A6mitmproxy%E6%8A%93%E5%8C%85%E5%B7%A5%E5%85%B7/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"man-in-the-middle\"\u003eMan In The Middle\u003c/h2\u003e\n\u003cp\u003emitm是Man In The Middle的首字母缩写，意思是位于中间的人，表明mitmproxy是一个代理，可以拦截请求，实现网络抓包。知名的网络抓包工具有Fiddler、Charles、HttpWatch、WireShark、BurpSuite和Postman等，mitmproxy相对来说，没有这么高的知名度，它相比于以上工具的独特优势是，它提供了Python API，可以编写Python代码对网络请求进行流量录制，从而收集接口信息以及转化为接口自动化用例等。mitmproxy官方文档不但有mitmproxy工具使用介绍，还有代理实现原理，对我们学习掌握网络知识有很大帮助。\u003c/p\u003e\n\u003ch2 id=\"安装与使用\"\u003e安装与使用\u003c/h2\u003e\n\u003ch3 id=\"pip安装\"\u003epip安装\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epip install mitmproxy\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003emitmproxy是Python开发的，用pip就能安装成功。官方给出了二进制安装包等方法，我觉得作为一款用Python开发的工具，用pip直接安装是更Pythonic的。\u003c/p\u003e\n\u003ch3 id=\"命令启动\"\u003e命令启动\u003c/h3\u003e\n\u003cp\u003e可以通过3种命令启动mitmproxy：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003emitmproxy 控制台\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/98b5387530e44e849f29b5e0a8d4aee3.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003emitmweb 网页\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/758ae8bcd1b54468a460577672942bc9.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003emitmdump 命令行\u003c/strong\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e它们的内容都是一样的，只是UI不一样而已。\u003c/p\u003e\n\u003ch3 id=\"手动设置代理\"\u003e手动设置代理\u003c/h3\u003e\n\u003cp\u003e打开以后mitmproxy默认会监听http://localhost:8080，需要设置下代理，把代理服务器设置为这个地址。\u003c/p\u003e\n\u003cp\u003eWindows可以这样设置：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/aa92b1c024054923928528b49e0a1c0e.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e设置后一定要记得点击保存按钮，否则代理不会生效。\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e实际上，Charles等网络抓包工具会自动配置代理，而mitmproxy需要手动设置代理，开发团队的解释是因为浏览器版本和配置经常会变化，所以他们建议在网上根据环境搜索方法手动设置。\u003c/p\u003e\n\u003ch3 id=\"下载证书\"\u003e下载证书\u003c/h3\u003e\n\u003cp\u003e访问http://mitm.it/，如果代理配置没有生效，打开后会是这个界面：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/1831e54315fe4f3cb85c6237debc66ad.png\"\u003e\u003c/p\u003e\n\u003cp\u003e正常的话，会出现证书下载页面：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/ad280b644a554e4a9c6f284078953a2d.png\"\u003e\u003c/p\u003e\n\u003cp\u003e根据平台选择相应证书安装就OK了。\u003c/p\u003e\n\u003ch3 id=\"使用\"\u003e使用\u003c/h3\u003e\n\u003cp\u003emitmproxy更其他抓包工具用法类似。\u003c/p\u003e\n\u003cp\u003e官方用视频演示了mitmproxy如何使用：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://docs.mitmproxy.org/stable/mitmproxytutorial-userinterface/\"\u003ehttps://docs.mitmproxy.org/stable/mitmproxytutorial-userinterface/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e并且介绍了5种运行模式：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://docs.mitmproxy.org/stable/concepts-modes/\"\u003ehttps://docs.mitmproxy.org/stable/concepts-modes/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e读者可以自行阅读下。\u003c/p\u003e\n\u003ch2 id=\"mitmproxy工作原理\"\u003emitmproxy工作原理\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/957972d2254347bea38454fb160acede.png\"\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e客户端（本机）连接代理服务器（http://127.0.0.1:8080），代理服务器连接服务器。\u003c/li\u003e\n\u003cli\u003e客户端给代理服务器发请求，代理服务器把请求发送到服务器。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e所有请求经过mitmproxy，就可以实现请求拦截。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e对于HTTP来说，直接借助mitmproxy传递请求就可以。而对于HTTPS来说，有个SSL/TLS安全认证，必须安装证书，服务器才会认为mitmproxy的请求是可信任的，请求才会成功传递。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/41c173ac4307449f9273157e08c6e0c7.png\"\u003e\u003c/p\u003e\n\u003cp\u003eHTTPS的代理过程就要复杂很多，客户端先跟mitmproxy建立连接（1\u003cdel\u003e2步），然后进行安全认证（3\u003c/del\u003e6步），最后传递请求（7~8步）。\u003c/p\u003e\n\u003ch2 id=\"反向代理\"\u003e反向代理\u003c/h2\u003e\n\u003cp\u003e假设在本机使用FastAPI启动了个Mock服务，地址为http://127.0.0.1:5000，通过Postman调接口，怎么使用mitmproxy来拦截请求呢？这就需要用到反向代理。\u003c/p\u003e\n\u003cp\u003e首先以反向代理模式运行mitmproxy，服务器端口为5000，监听端口为8000：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emitmproxy --mode reverse:http://127.0.0.1:5000 --listen-host 127.0.0.1 --listen-port 8000\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后把请求http://127.0.0.1:5000/login里面的5000端口改成8000，访问：\u003c/p\u003e\n\u003cp\u003ehttp://127.0.0.1:8000/login\u003c/p\u003e\n\u003cp\u003e就能在mitmproxy看到抓到请求了：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/4f0e6351ff2d4e7fb7f3c70233918664.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这样就能在本地使用Mock服务来调试拦截请求的Python代码了。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e反向代理，不需要在本机手动设置代理。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"python-api\"\u003ePython API\u003c/h2\u003e\n\u003cp\u003ePython API是mitmproxy的特色功能：\u003c/p\u003e","title":"从零开始学mitmproxy抓包工具"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n《代码变形记》系列文章基于真实案例，记录实际工作中的代码优化方法。\n背景 有5类数据A、B、C、D、E，需要分别进行处理。\n优化前 通过ParamBuilder来分别构造各自参数，然后在Runner中分别调用，分别判断。\npackage TemplateMethodPattern.before; public class Runner { public static void main(String[] args) { Param param = new Param(); ParamBuilder paramBuilder = new ParamBuilder(); // 处理数据A boolean result = paramBuilder.buildA(param); if (!result) { return; } boolean inserted = doIt(param); if (!inserted) { return; } // 处理数据B result = paramBuilder.buildB(param); if (!result) { return; } inserted = doIt(param); if (!inserted) { return; } // 处理数据C result = paramBuilder.buildC(param); if (!result) { return; } inserted = doIt(param); if (!inserted) { return; } // 处理数据D result = paramBuilder.buildD(param); if (!result) { return; } inserted = doIt(param); if (!inserted) { return; } // 处理数据E result = paramBuilder.buildE(param); if (!result) { return; } inserted = doIt(param); if (!inserted) { return; } } public static boolean doIt(Param param) { return param.flag; } } package TemplateMethodPattern.before; import lombok.Data; @Data public class Param { public boolean flag; } package TemplateMethodPattern.before; public class ParamBuilder { public boolean buildA(Param param) { return true; } public boolean buildB(Param param) { return false; } public boolean buildC(Param param) { return true; } public boolean buildD(Param param) { return false; } public boolean buildE(Param param) { return true; } } 存在问题 Runner中存在较多重复代码。\n优化方法 模板方法模式，Template Method Pattern。将重复代码抽象为一个抽象类，定义多个子类继承并实现各自代码。\n优化后 AbstractHandler抽象了重复方法，子类继承后实现build()方法。Runner中调用process()方法来遍历子类对象进行处理。\npackage TemplateMethodPattern.after; import TemplateMethodPattern.after.handler.*; import TemplateMethodPattern.before.Param; import java.util.Arrays; import java.util.List; public class Runner { public static void main(String[] args) { Param param = new Param(); List\u0026lt;AbstractHandler\u0026gt; handlers = Arrays.asList( new AHandler(), new BHandler(), new CHandler(), new DHandler(), new EHandler() ); for (AbstractHandler handler : handlers) { if (!handler.process(param, Runner::doIt)) { return; } } } public static boolean doIt(Param param) { return param.flag; } } package TemplateMethodPattern.after.handler; import TemplateMethodPattern.before.Param; import java.util.function.Function; public abstract class AbstractHandler { protected abstract boolean build(Param param); public boolean process(Param param, Function\u0026lt;Param, Boolean\u0026gt; doIt) { boolean result = build(param); if (!result) { return false; } return doIt.apply(param); } } package TemplateMethodPattern.after.handler; import TemplateMethodPattern.before.Param; public class AHandler extends AbstractHandler{ @Override protected boolean build(Param param) { return true; } } package TemplateMethodPattern.after.handler; import TemplateMethodPattern.before.Param; public class BHandler extends AbstractHandler{ @Override protected boolean build(Param param) { return false; } } package TemplateMethodPattern.after.handler; import TemplateMethodPattern.before.Param; public class CHandler extends AbstractHandler{ @Override protected boolean build(Param param) { return true; } } package TemplateMethodPattern.after.handler; import TemplateMethodPattern.before.Param; public class DHandler extends AbstractHandler{ @Override protected boolean build(Param param) { return false; } } package TemplateMethodPattern.after.handler; import TemplateMethodPattern.before.Param; public class EHandler extends AbstractHandler{ @Override protected boolean build(Param param) { return true; } } 个人感想，因为我是从Python转到Java，在使用设计模式解决这个问题后，我也思考了如果使用Python该怎么处理？Python是动态语言，有鸭子类型，对于重复代码通过定义一个公共函数，动态绑定类型就能解决，感兴趣的同学可以试试。不同的语言都各自的优势，也有各自的限制。也有各自的解决方法。\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/000001-%E4%BB%A3%E7%A0%81%E5%8F%98%E5%BD%A2%E8%AE%B0%E4%B9%8B%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F/","summary":"《代码变形记》系列文章基于真实案例，记录实际工作中的代码优化方法。","title":"代码变形记之模板方法模式"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n0-5岁就有过目不忘的记忆力，6个月能认出客人，2岁能用德语和母亲交流。6岁已经能读古希腊文献，经常随口报两个8位数，心算除法，结果精确到小数点后几位。8岁就自学完成微积分，阅读44卷本的《世界通史》。10岁进入著名的路德中学，这所学校后来培养出多位诺贝尔奖得主。12岁开始研究集合论和超限数，讨论无穷理论。15岁掌握泛函分析、复变函数、测度论。17岁发表第一篇数学论文《论某类最小多项式的零点位置》。18岁考入布达佩斯大学数学博士项目。\n冯·诺依曼是20世纪的“人脉之王”，他与几乎所有改变世界的科学家、政治家、军事家都有过深度交往，包括阿尔伯特·爱因斯坦 (Albert Einstein)、阿兰·图灵 (Alan Turing)、库尔特·哥德尔 (Kurt Gödel)、罗伯特·奥本海默 (Robert Oppenheimer)、理查德·费曼 (Richard Feynman)。\n1944年8月，军方联络官找到他说，自己正在参与一个项目，机器每秒能做5000次加法运算。第二天，冯·诺依曼就出现在了ENIAC项目组门口。计算机的历史从此改变。\nENIAC (Electronic Numerical Integrator and Computer)，是世界上第一台通用电子计算机。冯·诺依曼加入后提出了一个革命性的想法：“为什么不把程序也存到内存里，像数据一样？”\n1945年6月，冯·诺依曼整理了一份《First Draft of a Report on the EDVAC》，首次系统描述了现代计算机的架构。80年过去，从手机到公司服务器，几乎所有计算机仍然是遵循这份架构。这就是冯·诺依曼架构。\n1946年，他启动了IAS 计算机项目，亲自主持设计和建造，在1951年建成第一台冯·诺依曼架构计算机。中国最早的两台计算机（103机、104机），也是仿制苏联 BESM 系列,而 BESM 源头就是 IAS。\n除了硬件，冯·诺依曼还发明了大量算法，归并排序 (Merge Sort) 、 蒙特卡洛方法 (Monte Carlo Method)、 伪随机数生成器、自修改代码 \u0026amp; 子程序概念，这些都是现代编程语言的基石。\n在冯·诺依曼晚年，他还提出了更加超前的问题：“机器能不能自我复制”，对生命科学有重要启示。1958年写下了未完成的遗作，《计算机与人脑》 (The Computer and the Brain）。预见了后来的神经网络、深度学习。60年后，当GPU、TPU、神经网络芯片的兴起，证明他晚年的直觉是对的：串行的冯·诺依曼架构不是唯一答案。\n冯·诺依曼定义了计算机应该怎么造，冯·诺依曼架构是唯一“活着”的架构，今天使用的所有设备，本质上还是它。所以冯·诺依曼又被称为，现代计算机之父。\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%8E%86%E5%8F%B2/000001-%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%8E%86%E5%8F%B2%E4%BA%BA%E7%89%A9%E4%B9%8B%E5%86%AF%E8%AF%BA%E4%BE%9D%E6%9B%BC/","summary":"0-5岁就有过目不忘的记忆力，6个月能认出客人，2岁能用德语和母亲交流。6岁已经能读古希腊文献，经常随口报两个8位数，心算除法，结果精确到小数点后几位。8岁就自学完成微积分，阅读44卷本的《世界通史》。10岁进入著名的路德中学，这所学校后来培养出多位诺贝尔奖得主。12岁开始研究集合论和超限数，讨论无穷理论。15岁掌握泛函分析、复变函数、测度论。17岁发表第一篇数学论文《论某类最小多项式的零点位置》","title":"计算机历史人物之冯·诺依曼"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n设计理念 聚焦于任务调度，接口自动化80%本地编写，20%交由平台管理。\n如果使用pytest做接口自动化，那么个人认为最好的编写工具是PyCharm，任何低代码测试平台都无法取代。当然不会代码，或者不使用pytest，那低代码测试平台，或者yaml，甚至excel写自动化用例，都是可以接受的。而在使用pytest这个特定场景里面，要做平台化，平台的功能就需要仔细斟酌。既然编写用例最好使用PyCharm，平台也就只能专注于用例编排和任务调度，即创建任务，关联用例，批量运行，以及定时，并行，通知等。\npytestx正是基于此理念设计的一款纯粹的任务调度平台。\n平台架构 tep1.0.0的延续 tep1.0.0版本已经封版，在pytestx中对它进行了延续。\n从pypi到git\ntep是上传到pypi的，通过pip命令安装，使用tep startproject命令创建项目脚手架，代码结构分为项目代码和tep代码。改造后的tep只有项目代码，存放在：\nhttps://gitee.com/dongfanger/tep-project.git\n也就是说，在这个项目中，能看到整个tep全貌。之前的tep内核代码，比如fixtures自动寻找、内置fixture、环境变量等，全部迁移到了项目代码里面，尤其是conftest.py的文件内容。pip install tep顺带安装的依赖包，也整理到了requirements.txt，按照说明安装即可。\npytest-html替换allure\n由重到轻，化繁为简，pytest-html能够满足数据+日志的报告需要，最新的V3.2.0版本的界面尚可。诚然allure漂亮，但安装Java、下载Allure、配置环境变量、安装pytest-allure、添加pytest hook还是相对麻烦。更重要的是，要支持平台在线查看报告，减少存储占用，pytest-html无疑是更好的选择。\n下载脚手架\n既可以从使用git clone下载代码，也可以从页面下载，就像spring.io那样：\nfastapi到django\n之前的示例代码是调用的fastapi启动的应用，pytestx则将示例代码的接口换成了Django自身的mock接口。\nteprunner的延续 pytestx的基础代码是从teprunner来的，对数据库设计、代码规范、项目结构做了优化。\n任务管理 一、同步项目\n同步项目，是把项目配置的Git仓库地址和分支，更新到本地沙箱中，项目之间完全隔离。\n并把用例落库：\n二、任务关联用例\n创建任务，任务关联用例，就形成了新的用例集。\n三、容器化运行\n当前是假容器，未真正引入Docker，所谓容器，是指每次运行，都将用例拉取到新的目录，批量执行：\n四、生成报告\npytest命令生成html报告，存放于reports，借助于Django挂载页面，从页面点击，在线查看报告：\n体验平台 下载源码：https://github.com/dongfanger/pytestx\n安装依赖：前端（npm install）后端（pip install -r requirements.txt）\n启动服务：前端（npm run serve）后端（python manage.py runserver）\n即可畅快体验。\n学习交流 添加微信cekaigang，加入交流群，欢快讨论吧。\n参考资料：\nhttps://github.com/dongfanger/pytestx\n","permalink":"https://dongfanger.github.io/posts/%E5%8E%9F%E5%88%9B%E9%A1%B9%E7%9B%AE/pytestx/000001-%E8%81%9A%E7%84%A6%E4%BA%8E%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E7%9A%84%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0pytestx/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"设计理念\"\u003e设计理念\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e聚焦于任务调度\u003c/strong\u003e，接口自动化80%本地编写，20%交由平台管理。\u003c/p\u003e\n\u003cp\u003e如果使用pytest做接口自动化，那么个人认为最好的编写工具是PyCharm，任何低代码测试平台都无法取代。当然不会代码，或者不使用pytest，那低代码测试平台，或者yaml，甚至excel写自动化用例，都是可以接受的。而在使用pytest这个特定场景里面，要做平台化，平台的功能就需要仔细斟酌。既然编写用例最好使用PyCharm，平台也就只能专注于用例编排和任务调度，即创建任务，关联用例，批量运行，以及定时，并行，通知等。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003epytestx\u003c/strong\u003e正是基于此理念设计的一款纯粹的任务调度平台。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/883d930149b2437da1b3f410b1835ffa.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"平台架构\"\u003e平台架构\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/82056831880343ad8a090709ee6abd26.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"tep100的延续\"\u003etep1.0.0的延续\u003c/h1\u003e\n\u003cp\u003etep1.0.0版本已经封版，在pytestx中对它进行了延续。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e从pypi到git\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003etep是上传到pypi的，通过pip命令安装，使用tep startproject命令创建项目脚手架，代码结构分为项目代码和tep代码。改造后的tep只有项目代码，存放在：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://gitee.com/dongfanger/tep-project.git\"\u003ehttps://gitee.com/dongfanger/tep-project.git\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e也就是说，在这个项目中，能看到整个tep全貌。之前的tep内核代码，比如fixtures自动寻找、内置fixture、环境变量等，全部迁移到了项目代码里面，尤其是conftest.py的文件内容。pip install tep顺带安装的依赖包，也整理到了requirements.txt，按照说明安装即可。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003epytest-html替换allure\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e由重到轻，化繁为简，pytest-html能够满足数据+日志的报告需要，最新的V3.2.0版本的界面尚可。诚然allure漂亮，但安装Java、下载Allure、配置环境变量、安装pytest-allure、添加pytest hook还是相对麻烦。更重要的是，要支持平台在线查看报告，减少存储占用，pytest-html无疑是更好的选择。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e下载脚手架\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e既可以从使用git clone下载代码，也可以从页面下载，就像spring.io那样：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/78d828f0b74c457a9a610ff093891909.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003efastapi到django\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e之前的示例代码是调用的fastapi启动的应用，pytestx则将示例代码的接口换成了Django自身的mock接口。\u003c/p\u003e\n\u003ch1 id=\"teprunner的延续\"\u003eteprunner的延续\u003c/h1\u003e\n\u003cp\u003epytestx的基础代码是从teprunner来的，对数据库设计、代码规范、项目结构做了优化。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/89f0add05f4145f586844c02be511aef.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"任务管理\"\u003e任务管理\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f22f47846fc5419284141f72f7259d22.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、同步项目\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e同步项目，是把项目配置的Git仓库地址和分支，更新到本地沙箱中，项目之间完全隔离。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/5422dbfe859f44cf8127e852542d1d17.png\"\u003e\u003c/p\u003e\n\u003cp\u003e并把用例落库：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/481995032b754a7baed495bf29f3cd10.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e二、任务关联用例\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e创建任务，任务关联用例，就形成了新的用例集。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e三、容器化运行\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e当前是假容器，未真正引入Docker，所谓容器，是指每次运行，都将用例拉取到新的目录，批量执行：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/711257c91504476f8679320e277c53e7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e四、生成报告\u003c/p\u003e\n\u003cp\u003epytest命令生成html报告，存放于reports，借助于Django挂载页面，从页面点击，在线查看报告：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/bee4ca83f58a4fad98d3814f98d07729.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"体验平台\"\u003e体验平台\u003c/h1\u003e\n\u003cp\u003e下载源码：https://github.com/dongfanger/pytestx\u003c/p\u003e\n\u003cp\u003e安装依赖：前端（npm install）后端（pip install -r requirements.txt）\u003c/p\u003e\n\u003cp\u003e启动服务：前端（npm run serve）后端（python manage.py runserver）\u003c/p\u003e\n\u003cp\u003e即可畅快体验。\u003c/p\u003e\n\u003ch1 id=\"学习交流\"\u003e学习交流\u003c/h1\u003e\n\u003cp\u003e添加微信cekaigang，加入交流群，欢快讨论吧。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/67f63522e3fb431485eea9bb21983888.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/dongfanger/pytestx\"\u003ehttps://github.com/dongfanger/pytestx\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"聚焦于任务调度的测试平台pytestx"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nUI自动化最麻烦的就是定位元素，好在Playwright支持录制脚本，可以在界面进行操作，快速获取元素定位，生成测试脚本。\n并且Playwright会优先选择稳定的元素定位方式，通常是role, text和test id。\n前置条件：\n安装node.js 执行playwright install安装测试版浏览器 安装完成后执行命令，启动Codegen：\nplaywright codegen 也可以指定URL进行启动：\nplaywright codegen demo.playwright.dev/todomvc 启动后会打开2个窗口：\n1个是测试版浏览器，也就是进行操作的页面。1个是监视器，会实时生成脚本，可以在右上角的Target切换不同语言，Python就选择Pytest。\n更多用法请咨询AI：\n元素定位 提示词： 访问https://playwright.dev/python/docs/locators，提取最重要的10点 Playwright Locators 核心 10 点（官方文档核心提炼）\n定位器核心特性：Locators 是 Playwright 自动等待和重试机制的核心，代表任意时刻查找页面元素的方式，每次执行操作都会重新定位最新的 DOM 元素，适配页面重渲染场景。\n推荐定位器优先级：优先使用面向用户的内置定位器，而非 CSS/XPath，官方推荐顺序核心为：get_by_role() \u0026gt; get_by_test_id() \u0026gt; 文本 / 标签类定位器，避免依赖 DOM 结构的选择器导致用例不稳定。\n核心内置定位器及适用场景\n提供 7 种官方推荐定位器，各有明确适用场景：\nget_by_role()：按 W3C ARIA 角色 / 可访问名称定位（如按钮、复选框），最贴合用户操作视角； get_by_label()：定位带标签的表单控件（如输入框）； get_by_test_id()：按data-testid定位（最稳定，不受页面文本 / 样式变化影响）； 其余：get_by_text()（非交互元素文本）、get_by_placeholder()（无标签表单）、get_by_alt_text()（图片）、get_by_title()（带 title 属性元素）。 定位器可链式调用：所有创建定位器的方法（如get_by_role()/get_by_label()）可在Locator/FrameLocator上调用，支持逐层缩小定位范围（如先定位 iframe，再定位内部按钮）。\n强大的过滤能力：通过locator.filter()实现精准筛选，支持 4 种过滤方式：按文本（has_text/has_not_text）、按子元素（has/has_not）、正则匹配文本，且可多过滤条件链式组合。\n定位器运算符\n支持 3 种复合定位逻辑，满足复杂场景：\nand_()：同时匹配多个定位器（如角色为按钮且含指定 title）； or_()：匹配任意一个定位器（如处理弹窗 / 目标元素二选一场景）； 嵌套定位：在父定位器内匹配子定位器（如对话框内的保存按钮）。 严格模式特性：Locators 默认开启严格模式，单个元素操作若匹配多个元素会抛出异常（如click()）；多元素操作（如count()）可正常执行，可通过first/last/nth()显式取消严格校验（不推荐）。\n原生支持 Shadow DOM：所有定位器默认穿透 Shadow DOM（XPath 除外），闭模式 Shadow DOM 不支持，可直接按普通元素方式定位 Shadow DOM 内的元素。\n列表元素处理方案：针对列表提供全套操作，包括：断言列表数量（to_have_count）、断言列表文本（to_have_text）、精准获取指定项（文本 / Test ID / 过滤），避免直接使用nth()（易因页面顺序变化失效）。\nCSS/XPath 的使用规范：官方不推荐使用CSS/XPath（易因 DOM 结构变化失效），仅在无其他方案时使用page.locator()，且禁止使用超长嵌套的 CSS/XPath 选择器，替代方案为定义 Test ID 或使用角色定位。\n题外话 这篇文章是我第一次使用AI生成文字，写文章的效率提升了很多。但是怎么提高文章质量？如何选择话题？哪些内容能对读者有帮助？是接下来需要重点思考和调整的。纯粹的知识搬运，受众只会越来越少。“能帮助读者拓展认知的好文章”，也许是我努力的方向。\n参考资料\nhttps://playwright.dev/python/docs/codegen\nhttps://playwright.dev/python/docs/locators\nhttps://playwright.dev/python/docs/other-locators\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/playwright/000001-playwright%E5%BD%95%E5%88%B6%E8%84%9A%E6%9C%AC%E4%B8%8E%E5%85%83%E7%B4%A0%E5%AE%9A%E4%BD%8D/","summary":"UI自动化最麻烦的就是定位元素，好在Playwright支持录制脚本，可以在界面进行操作，快速获取元素定位，生成测试脚本。","title":"录制脚本与元素定位"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n序言 这篇文章是转型后发表的第一篇文章，有必要先聊一下我的写文计划。\n串行文章和并行文章\n我会按照发文顺序分为串行文章和并行文章。Python 语言本身的内容，我会按照入门、进阶、原理的顺序依次展开。由于我的水平有限，为了保证质量不乱讲，我选择站在巨人的肩膀上进行二次创作。入门篇主要参考菜鸟教程的基础教程部分。进阶篇主要参考《流畅的Python》这本书，我是买了实体书的，计划后面会送出来，虽然有些破旧，还望不要嫌弃（当时买的时候书页就坏了，京东还赔了我 1000 京豆）。原理篇主要参考官方文档以及外网资料，有一说一，国外资料确实更多更专业，只不过都是英文的，还好现在已经适应了。Django、算法、pytest、tep、测试成长小说等系列专题会并行发表，也包括其他单篇文章。这两天了解到 Python 之父 Guido van Rossum 从 Google 退休后，又加入微软继续搞 Python 了。有点意思。\n有啥不一样？\nPython 文章和视频都非常多，我就在想我写的东西对读者而言价值到底在哪？我不是搞培训的，也不是做教程，内容会更偏向于学习笔记和实用经验。看我的文章，说不定能把以前遗漏或遗忘的知识点再次补上。同时我已经有一段时间的写作经验了，我也尽量深入浅出的把知识讲清楚讲透彻，首先写的文章我要自己都理解透，这样别人看的时候才会觉得一目了然。另外就是认真的态度，见过一些为了流量写的教程，不做评价。只是觉得既然写了，既要对得起自己，也要对得起别人。\n标题\n既为了提高打开率，也是为了新颖，我不会按照比如《Python入门1》、《Python入门2》这种常规的命名，这样的文章实在是太多了。而是会在写完文章后，根据文章内容，选择其中的亮点，作为标题。这个过程也挺有趣，有种点睛之笔的意味。同时我也希望随便摘一篇文章拿出去，也是够看的。\n（突然想起来上学写 2000 多条错题笔记的时候，简单又充实）\n开篇 为了把这个开篇写好，我专门在 B 站上面找了最火的 Python 教程，全站排行榜最高第1名的《花了2万多买的Python教程全套》，质量很高，也推荐大家看看。不过我不搞培训，也不需要宣扬 Python 的好处促进就业，就不写那么多背景介绍了。身边学财会的朋友之前让我教教 Python，干脆我就以这个为契机，作为开篇，保证 5 分钟内，即便是文科生，也能用 Python 编起程序来！\n电脑 Python 并不神秘，它其实就和 QQ 一样，只是一个运行在电脑上的普普通通的软件。只要有一台电脑，就可以写 Python。\n安装 打开这个下载网址：\nhttps://www.python.org/downloads/\n点击 Download Python 进行下载（Mac 土豪请注意选择版本）。下载后傻瓜式一路点击下一步安装就好。\n使用 一般的软件我们都是双击图标打开。Python 在安装以后桌面上不会出现图标。不过可以从开始菜单找到：\n点击后就打开了：\nPython 毕竟是一门编程语言，它也有它自己的尊严，所以它的界面看起来也有专业的一面，比如那一串密密麻麻的英文。\n输入一句话试试：\nprint(\u0026#34;hello\u0026#34;) 敲击回车。它马上就会回应一句，hello：\n没错，是的，你已经在用 Python 编程了！\n小结 本文以极其简单的例子，介绍了如何动手用 Python 编程。我试了下，如果网速快的话，5 分钟内完成是妥妥的。哈哈哈。之所以选择这个简单例子，也是想说明 Python 本来就是很简单的，不用听到编程就被吓到了。先动手，一点一点学习就好。不过言归正传，我们工作中并不是这么写 Python 的，而是用一款叫做 PyCharm 的软件。PyCharm 分为社区免费版和专业旗舰版。日常使用社区版就可以了，个人推荐旗舰版。提示一下，破解的时候注意搜索带上 PyCharm 的版本，比如 2020.2.3，成功率会更高。\n","permalink":"https://dongfanger.github.io/posts/python%E7%94%9F%E6%80%81/python/000001-python%E5%A6%82%E4%BD%95%E8%AE%A9%E6%96%87%E7%A7%91%E7%94%9F5%E5%88%86%E9%92%9F%E5%86%99%E4%B8%8Apython/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"序言\"\u003e序言\u003c/h1\u003e\n\u003cp\u003e这篇文章是转型后发表的第一篇文章，有必要先聊一下我的写文计划。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e串行文章和并行文章\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e我会按照发文顺序分为串行文章和并行文章。Python 语言本身的内容，我会按照入门、进阶、原理的顺序依次展开。由于我的水平有限，为了保证质量不乱讲，我选择站在巨人的肩膀上进行二次创作。入门篇主要参考菜鸟教程的基础教程部分。进阶篇主要参考《流畅的Python》这本书，我是买了实体书的，计划后面会送出来，虽然有些破旧，还望不要嫌弃（当时买的时候书页就坏了，京东还赔了我 1000 京豆）。原理篇主要参考官方文档以及外网资料，有一说一，国外资料确实更多更专业，只不过都是英文的，还好现在已经适应了。Django、算法、pytest、tep、测试成长小说等系列专题会并行发表，也包括其他单篇文章。这两天了解到 Python 之父 Guido van Rossum 从 Google 退休后，又加入微软继续搞 Python 了。有点意思。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e有啥不一样？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003ePython 文章和视频都非常多，我就在想我写的东西对读者而言价值到底在哪？我不是搞培训的，也不是做教程，内容会更偏向于学习笔记和实用经验。看我的文章，说不定能把以前遗漏或遗忘的知识点再次补上。同时我已经有一段时间的写作经验了，我也尽量深入浅出的把知识讲清楚讲透彻，首先写的文章我要自己都理解透，这样别人看的时候才会觉得一目了然。另外就是认真的态度，见过一些为了流量写的教程，不做评价。只是觉得既然写了，既要对得起自己，也要对得起别人。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e标题\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e既为了提高打开率，也是为了新颖，我不会按照比如《Python入门1》、《Python入门2》这种常规的命名，这样的文章实在是太多了。而是会在写完文章后，根据文章内容，选择其中的亮点，作为标题。这个过程也挺有趣，有种点睛之笔的意味。同时我也希望随便摘一篇文章拿出去，也是够看的。\u003c/p\u003e\n\u003cp\u003e（突然想起来上学写 2000 多条错题笔记的时候，简单又充实）\u003c/p\u003e\n\u003ch1 id=\"开篇\"\u003e开篇\u003c/h1\u003e\n\u003cp\u003e为了把这个开篇写好，我专门在 B 站上面找了最火的 Python 教程，全站排行榜最高第1名的《花了2万多买的Python教程全套》，质量很高，也推荐大家看看。不过我不搞培训，也不需要宣扬 Python 的好处促进就业，就不写那么多背景介绍了。身边学财会的朋友之前让我教教 Python，干脆我就以这个为契机，作为开篇，保证 5 分钟内，即便是文科生，也能用 Python 编起程序来！\u003c/p\u003e\n\u003ch1 id=\"电脑\"\u003e电脑\u003c/h1\u003e\n\u003cp\u003ePython 并不神秘，它其实就和 QQ 一样，只是一个运行在电脑上的普普通通的软件。只要有一台电脑，就可以写 Python。\u003c/p\u003e\n\u003ch1 id=\"安装\"\u003e安装\u003c/h1\u003e\n\u003cp\u003e打开这个下载网址：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.python.org/downloads/\"\u003ehttps://www.python.org/downloads/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/643d0a0a685545589a005980ceb3f507.png\"\u003e\u003c/p\u003e\n\u003cp\u003e点击 Download Python 进行下载（Mac 土豪请注意选择版本）。下载后傻瓜式一路点击下一步安装就好。\u003c/p\u003e\n\u003ch1 id=\"使用\"\u003e使用\u003c/h1\u003e\n\u003cp\u003e一般的软件我们都是双击图标打开。Python 在安装以后桌面上不会出现图标。不过可以从开始菜单找到：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/3d74e07e141847c8918007117229bd78.png\"\u003e\u003c/p\u003e\n\u003cp\u003e点击后就打开了：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/f0af6ad0a1df4e2492ad8602a0085ab3.png\"\u003e\u003c/p\u003e\n\u003cp\u003ePython 毕竟是一门编程语言，它也有它自己的尊严，所以它的界面看起来也有专业的一面，比如那一串密密麻麻的英文。\u003c/p\u003e\n\u003cp\u003e输入一句话试试：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eprint(\u0026#34;hello\u0026#34;)\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e敲击回车。它马上就会回应一句，hello：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/fda707a93bcb468692d527e0c6a79b8a.png\"\u003e\u003c/p\u003e\n\u003cp\u003e没错，是的，你已经在用 Python 编程了！\u003c/p\u003e\n\u003ch1 id=\"小结\"\u003e小结\u003c/h1\u003e\n\u003cp\u003e本文以极其简单的例子，介绍了如何动手用 Python 编程。我试了下，如果网速快的话，5 分钟内完成是妥妥的。哈哈哈。之所以选择这个简单例子，也是想说明 Python 本来就是很简单的，不用听到编程就被吓到了。先动手，一点一点学习就好。不过言归正传，我们工作中并不是这么写 Python 的，而是用一款叫做 PyCharm 的软件。PyCharm 分为社区免费版和专业旗舰版。日常使用社区版就可以了，个人推荐旗舰版。提示一下，破解的时候注意搜索带上 PyCharm 的版本，比如 2020.2.3，成功率会更高。\u003c/p\u003e","title":"如何让文科生5分钟写上Python"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n李笑来在书中谈到了事件时间日志的好处：“它会使你对时间的感觉越来越精确。可以调整我对时间的感觉，在估算任何任务的工作量的时候，都更容易确定真正现实可行的目标。相对准确的估算又使得目标基本上都可以达成，由此可以算是战胜了焦虑”。\n为此我在工作中，试验了一周的事件时间日志，记录每天的工作内容，看看这种效果到底如何。意想不到，出奇的好。\n我使用了Sublime Text文本编辑器来做记录，文件名为EventTimeLog.txt。在做一件事情前，先写上事件，“分类，事件名称”，然后再标记上计划时间，以小时为单位。示例如下：\n2023.12.26\n用例评审，XXX 1h\n需求测试，XXX 2h\n工具开发，XXX 1h\n会议，XXX 0.5h\n这样我对时间管理得更好了，在做一件事情前，我划定了一个时间段，在这段时间内，能够专注完成这一件事，工作效率提高得很明显。番茄工作法我也用过，但通过事件时间日志做好记录，能加强对时间的感知。\n对工作量的估算，也确实如李笑来所言，更加精准。比如因为测试过程中有阻塞，当每日汇报进度时，我能清楚知道一天8小时，我实际投入到需求测试中的只有4小时，也能将这个数字反馈出去。量化后汇报也确实减少了在汇报时，讲不清楚的那种焦虑感。\n也许有人会认为，记录工作浪费时间，其实不然。适当的记录，虽然花了一点时间动手，但是能够在总结回溯时，节省回忆的大脑消耗，反而节省了时间。事件时间日志的成本也很低，只需要按照自己的语言，简单写下事件和时间即可。\n","permalink":"https://dongfanger.github.io/posts/%E8%AF%BB%E4%B9%A6%E4%B8%8E%E6%80%9D%E8%80%83/%E4%B8%AA%E4%BA%BA%E6%80%9D%E8%80%83/000001-%E4%BA%8B%E4%BB%B6%E6%97%B6%E9%97%B4%E6%97%A5%E5%BF%97%E7%B2%BE%E5%87%86%E5%B7%A5%E4%BD%9C%E4%B9%8B%E9%81%93/","summary":"李笑来在书中谈到了事件时间日志的好处：“它会使你对时间的感觉越来越精确。可以调整我对时间的感觉，在估算任何任务的工作量的时候，都更容易确定真正现实可行的目标。相对准确的估算又使得目标基本上都可以达成，由此可以算是战胜了焦虑”。","title":"事件时间日志，精准工作之道"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n刷了80道算法题以后，我最大的感受是，自信心倍增。既克服了对算法的恐惧，也加强了Java代码能力，还收集了2068行答案代码。\n我刷算法题的理由很平常，就是想提高自己的竞争力，在面试的时候能过最后那一关。前后离职的两名同事都告诉我，在牛客网上面刷题就可以了。于是这次我就在牛客网上面按照题库→在线编程→全部题目→测试的路径，筛选了一下算法题，搜出来一共80道题：\n我之前面试碰到过的字符串相加和判断字符串是否为IP地址的两道题，都出现在了这80道题之中。并且前同事面试做过的算法题都是在这上面做过的原题，我对比了一下也在这80道题里面。可以毫不夸张的说，掌握这80道题，基本上90%的面试算法题，你都能通过。\n正是来着这样的决心，我就开始做题了，使用的编程语言是Java，借着这个机会也多拿Java练练手。有好几个阶段：\n直接做。做不来。 先背答案，再敲代码，坚持到了第15题。背得费劲。 对着代码，一个一个字母敲一遍，坚持到了第35题。敲的手疼。 直接在题解中找答案代码，复制粘贴后提交。顺利“刷”完了80道题。 所有80道题的答案代码都上传到了我的GitHub上面：\nhttps://github.com/dongfanger/java-util/blob/main/AQ80.java\n接下来把这80道题的答案理解了或者背下来，面试考算法就基本上没多大问题了。\n这80道题包含了简单、中等、困难所有3种难度，困难题可以选择性放弃，简单题必须掌握，中等题根据代码量平衡选择，一般测试考算法，不会让你写代码量特别多那种的。\n算法题也是有套路可寻，重点会考察这些知识点：哈希表、链表、栈、排序、二分法、双指针、递归、动态规划、树的遍历等。对算法感兴趣也可以阅读这个大佬写的攻略：\nhttps://github.com/labuladong/fucking-algorithm\n希望这篇文章对正在求职的小伙伴能有所帮助！\n","permalink":"https://dongfanger.github.io/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E7%AE%97%E6%B3%95/000001-%E5%88%B7%E4%BA%8680%E9%81%93%E7%AE%97%E6%B3%95%E9%A2%98%E4%BB%A5%E5%90%8E/","summary":"刷了80道算法题以后，我最大的感受是，\u003cstrong\u003e自信心倍增\u003c/strong\u003e。既克服了对算法的恐惧，也加强了Java代码能力，还收集了2068行答案代码。","title":"刷了80道算法题以后"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n点进Service，全是接口。点进ServiceImpl，前半段在校验业务规则，后半段调了个stockService.deduct()，又得跳到另一个Service。\n看不懂后端代码，卡点不在语法。\n跟着一个请求走一遍，你会经过五站——每一站都藏着一个\u0026quot;为什么代码长这样\u0026quot;的答案。\n示例：POST /api/v1/orders {sku_id: 1001, count: 2, coupon_id: 5}\n一、请求5站 第1站：路由 你的脚本发一个请求到 POST /api/v1/orders，后端怎么知道该执行哪段代码？答案是查一张表——URL 和函数的对应关系，这个叫路由。\n# 路由：URL 不是文件名，是一张映射表 ROUTES = { \u0026#34;POST /api/v1/orders\u0026#34;: create_order, # ← 请求进来，靠这张表找到函数 \u0026#34;GET /api/v1/orders/{id}\u0026#34;: get_order, } 测试视角：你每天调的是 URL，但 URL 是靠映射表找到函数的。所以线上报错时，别只把报错信息甩给研发——顺手问一句这个 URL 对应哪个函数，你就能自己先定位到出问题的那一层，而不是每次都等研发翻代码。\n第2站：分层 测试的是下单接口，但在后端眼中是Controller → Service → DAO一串调用，中间还可能跳到别的Service（扣库存等）。业务逻辑通常都在Service层，这是测试重点关注的地方。\n# ===== Controller 层 ===== def create_order(req):# Controller：只管校验和返回 if req.count \u0026lt;= 0: return err(\u0026#34;数量非法\u0026#34;) return order_service.create(req) # ← 业务在这儿 # ===== Service 层 ===== def create(req):# Service：业务 + 事务边界 with transaction(): # ← 事务从这儿开始 stock_dao.deduct(req.sku_id, req.count) coupon_dao.use(req.coupon_id) return order_dao.insert(req) # 出了这个函数就提交/回滚 # ===== DAO 层 ===== def insert(order):# DAO：只有 SQL db.execute(\u0026#34;INSERT INTO orders ...\u0026#34;) 分层的本质是用“规矩”换“可维护性”：改动局部化、每层能单独测、逻辑能复用。反过来说，所有逻辑堆一个函数里就是面条代码——改一处可能影响全部，测试也无从下手。\n测试视角：同一个\u0026quot;下单失败\u0026quot;，可能出在 Controller（参数校验没做）、Service（库存判断写反了）、也可能出在 DAO（SQL 少了个条件）。这三层的验证方式完全不同——Controller 层看返回，Service 层要查副作用，DAO 层直接查库。下次提 Bug 前先想想\u0026quot;它大概在哪一层\u0026quot;，你给的复现步骤会精准很多。\n第3站：依赖注入 代码里面写的stockService.deduct()，但你看不到它是怎么被创建的，因为容器在启动时帮你new好了并塞进来。\n# 你看到的：函数里直接用，但没见它从哪来 def create(req): stock.deduct(...) # ← stock 哪来的？签名里没有 # 真相：容器在启动时创建好，挂到了这个模块的变量（或对象的属性）上 # 测试时：容器换个假的挂上去 → 这就是 Mock 的底层原理 简单一句话：容器帮你把依赖的对象创建好了，你直接用。\nService 全是接口，接口和实现类分开，就是为了配合注入：IOrderService 是插座，OrderServiceImpl 是插头，容器想插哪个就插哪个。所以你找不到 new，也找不到\u0026quot;到底调用的哪个实现\u0026quot;。\n测试视角：理解了依赖注入，你就明白——\n为什么换个实现类就能把外部支付、短信、风控 Mock 掉 为什么有些环境能 Mock、有些不能（取决于是不是通过容器注入的） 第4站：ORM 代码里没有UPDATE语句，但数据库变了，因为ORM把对象变更“翻译”成了SQL，在某个时刻自动发出去。\norder = Order.get(order_id) # 从库里取出一个对象 order.status = \u0026#34;PAID\u0026#34; # 只改了对象属性，没有 UPDATE # 没写 SQL，但事务提交时 ORM 自动生成： # UPDATE orders SET status=\u0026#39;PAID\u0026#39; WHERE id=? 测试视角：你遇到过的\u0026quot;我明明在数据库改了数据，接口查出来还是旧的\u0026quot;，根子基本都在这儿——不是数据库没生效，是 ORM 在同一条连接里缓存了这个对象，或者你的改动在另一个没提交的事务里，或者你查的是从库、改的是主库。所以造数据一定要在事务外造、造完要确认提交。另外，如果某个接口突然变慢，先怀疑 N+1：查 10 条订单，ORM 偷偷发了 11 条 SQL。\n第5站：事务边界 事务从Service层开始，方法正常返回→提交，抛异常→回滚，所以“执行到一半失败”，前面扣的库存也会全部撤销。这里有个最坑的点：事务没包住的部分（发消息、调外部接口）不会回滚，这也是容易出bug的地方。\nwith transaction(): # 边界：包住全部 → 失败全回滚 deduct_stock() send_mq() # ← 但 MQ 不在事务里！ raise Exception(\u0026#34;支付失败\u0026#34;) # 库存回滚了，消息却发出去了 那外部调用怎么办？这是分布式最棘手的问题之一，我们在第8篇专门讲。这里先记住一件事：事务管不住数据以外的东西。\n测试视角：这是测试最容易漏的一类 Bug——\u0026ldquo;接口报错不等于什么都没发生\u0026rdquo;。库存回滚了，但 MQ 消息发出去了、短信发了、外部接口调了。所以验失败场景时，别只看响应码和主表，要把所有副作用清单列出来，逐个确认它们复原了没有。\n二、分层的测试策略 把上面五站的\u0026quot;测试视角\u0026quot;汇总一下，就得到这张表：\n一句话：知道代码分几层，你才知道一个Bug该在哪一层验、该看数据库还是看返回。\n三、结尾 以后接手一个接口，搞清三件事：\n这个接口的业务在哪个Service？\n事务边界在哪一行？失败会回滚哪些东西？\n有没有改了数据库但接口读不到的坑？\n这三件事搞明白，你才算真的“看懂”了这个接口，而不只是“会调”。\n下一篇聊一个你天天在调、但可能没细想过的东西：接口设计。为什么返回里要塞冗余字段？为什么加个字段要出 v2，而不是直接改 v1？\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E5%90%8E%E7%AB%AF%E6%B5%8B%E8%AF%95/000001-%E4%B8%80%E4%B8%AA%E8%AF%B7%E6%B1%82%E7%9A%84%E4%B8%80%E7%94%9F/","summary":"点进Service，全是接口。点进ServiceImpl，前半段在校验业务规则，后半段调了个stockService.deduct()，又得跳到另一个Service。","title":"一个请求的一生"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n电子书，简洁专注的阅读体验。对于技术文章来说，电脑端比手机端更适合阅读，尤其是代码。电子书的左侧目录能快速找到文章，虽然公众号也有文章分类功能，但是不能像目录树一样有“位置感”。电子书还能支持搜索。所以把个人网站做成了电子书。\n使用AI，文章重新分类，网站技术重构。新版电子书的一级目录更加聚焦。网站技术从sphinx升级为mkdocs，主题仍然是readthedocs。解决的核心痛点是构建速度，从3分钟提升到了5秒。同时也省去了rst文件，全部使用markdown文件。\n决定名字回归到测试开发刚哥。\n{width=240}\n从这张图能看到7年来对于名字的纠结。写文章的高峰期是在2022年到2024年的两年，名字用的是测试开发刚哥。可惜的是内心不够坚定。可喜的是，仍然还有粉丝朋友关注着，在此表示感谢。等到2027改名次数解除限制，就改回来。\n技术永不落幕。dev.to是最近入坑的技术网站。\n短视频再火爆，AI再厉害，技术网站仍然有它的一席之地。Sylwia Laskowska分享了入驻dev.to一年的感想，其中有一句很有感触：“写文章一半是写给读者，一半是写给自己”。\n基于电子书的知识结构写文章。3月8日公众号发布的《AI驱动的自动化测试框架（OpenClau实战版），看完少走1年弯路》是由AI写的。甚至里面的内容都有点失真。但是阅读量3755、赞26、分享403、涨粉75。写文章不是为了赚钱，事实上赚不了多少钱。与其追求流量，不如静下心来，提升写作水平。电子书是一张很好的个人名片。\n电子书地址：dongfanger.github.io\n","permalink":"https://dongfanger.github.io/posts/ai%E6%8A%80%E6%9C%AF/ai%E5%BC%80%E5%8F%91/000001-ai%E5%BC%80%E5%8F%91%E5%8E%9F%E5%88%9B%E7%94%B5%E5%AD%90%E4%B9%A6%E5%85%A8%E6%96%B0%E9%87%8D%E7%A3%85%E5%8D%87%E7%BA%A7/","summary":"电子书，简洁专注的阅读体验。对于技术文章来说，电脑端比手机端更适合阅读，尤其是代码。电子书的左侧目录能快速找到文章，虽然公众号也有文章分类功能，但是不能像目录树一样有“位置感”。电子书还能支持搜索。所以把个人网站做成了电子书。","title":"原创电子书全新重磅升级"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\n安装过程略过，网上搜一大把。\n介绍 本文会在一个module中开发一个简单的Go package。\n同时介绍go tool（也就是go命令行）。\n以及如何fetch，build和install Go的modules，packages，commands。\n代码组织 Go是按packages来组织代码的。一个package == 一个目录。\n同一个package中的functions，types，variables，和constants是共享的。也就是包访问权限，java默认也是包访问权限。\npackages是放在module中的，module是通过go.mod文件来定义的。典型的，一个repository只有一个go.mod，放在根目录。\n可以使用go mod init name来创建这个文件。在go run后会生成go.sum文件，内容是go.mod的加密哈希。\nrepository也允许有多个module，module的packages是go.mod所在的目录，如果子目录也有go.mod，那么这个子目录的packages就属于子目录module。\n第一个程序 假设module path是example.com/user/hello，\n$ mkdir hello # Alternatively, clone it if it already exists in version control. $ cd hello $ go mod init example.com/user/hello go: creating new go.mod: module example.com/user/hello $ cat go.mod module example.com/user/hello go 1.14 $ Go源文件的第一个语句必须是package name。程序入口必须是package main。\npackage main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello, world.\u0026#34;) } 喜闻乐见Hello World。\n现在可以build和install，\n$ go install example.com/user/hello $ 这条命令会build然后生成可执行二进制文件（这是我比较喜欢Go的一个原因，直接生成可执行文件，省去了安装依赖的麻烦）。\nbuild和install命令都可以生成可执行文件。不同点在于\ngo build 不能生成包文件, go install 可以生成包文件 go build 生成可执行文件在当前目录下， go install 生成可执行文件在bin目录下 install生成文件的bin目录是根据环境变量来的。按以下顺序检查\nGOBIN GOPATH 如果都没有设置，就会生成到默认GOPATH（Linux $HOME/go 或Windows %USERPROFILE%\\go）。\n示例的二进制文件会生成到$HOME/go/bin/hello（Windows的话就是%USERPROFILE%\\go\\bin\\hello.exe）。\n可以查看环境变量GOBIN和GOPATH的值\ngo env 也可以设置GOBIN\n$ go env -w GOBIN=/somewhere/else/bin $ 设置后可以重置\n$ go env -u GOBIN $ GOPATH需要到系统环境变量进行修改。\ninstall等命令需要在源文件目录下执行，准确点说是“当前工作目录”。否则会报错。\n在当前目录执行，以下等价\n$ go install example.com/user/hello $ go install . $ go install 验证下结果，为了方便，添加install目录到PATH\n## Windows users should consult https://github.com/golang/go/wiki/SettingGOPATH ## for setting %PATH%. $ export PATH=$PATH:$(dirname $(go list -f \u0026#39;{{.Target}}\u0026#39; .)) $ hello Hello, world. $ 如果cd到了install的bin目录，也可以直接\n$ hello Hello, world. $ 现阶段Go的很多库都是放在GitHub等代码托管网站上面的，使用Git进行提交\n$ git init Initialized empty Git repository in /home/user/hello/.git/ $ git add go.mod hello.go $ git commit -m \u0026#34;initial commit\u0026#34; [master (root-commit) 0b4507d] initial commit 1 file changed, 7 insertion(+) create mode 100644 go.mod hello.go $ Go命令通过请求相应的HTTPS URL，并读取嵌入在HTML响应中的元数据\u0026lt;meta\u0026gt;标签，来定位包含给定module path的repository\nBitbucket (Git, Mercurial) import \u0026#34;bitbucket.org/user/project\u0026#34; import \u0026#34;bitbucket.org/user/project/sub/directory\u0026#34; GitHub (Git) import \u0026#34;github.com/user/project\u0026#34; import \u0026#34;github.com/user/project/sub/directory\u0026#34; Launchpad (Bazaar) import \u0026#34;launchpad.net/project\u0026#34; import \u0026#34;launchpad.net/project/series\u0026#34; import \u0026#34;launchpad.net/project/series/sub/directory\u0026#34; import \u0026#34;launchpad.net/~user/project/branch\u0026#34; import \u0026#34;launchpad.net/~user/project/branch/sub/directory\u0026#34; IBM DevOps Services (Git) import \u0026#34;hub.jazz.net/git/user/project\u0026#34; import \u0026#34;hub.jazz.net/git/user/project/sub/directory\u0026#34; 很多托管网站已经为Go的repository提供了元数据，为了共享module，最简单的办法就是让module path匹配repository的URL。\n从module import packages 先在名字为morestrings的package中创建一个reverse.go文件，实现字符串反转\n// Package morestrings implements additional functions to manipulate UTF-8 // encoded strings, beyond what is provided in the standard \u0026#34;strings\u0026#34; package. package morestrings // ReverseRunes returns its argument string reversed rune-wise left to right. func ReverseRunes(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i \u0026lt; len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } 由于ReverseRunes函数是大写的，所以是公有的，可以被其他packages import。\n先build测试下编译成功\n$ cd $HOME/hello/morestrings $ go build $ 因为只是在package中，不是在module根目录，go build不会生成文件，而是会把compile后的package保存到local build cache中。\n接着在hello.go中import\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;example.com/user/hello/morestrings\u0026#34; ) func main() { fmt.Println(morestrings.ReverseRunes(\u0026#34;!oG ,olleH\u0026#34;)) } 然后install hello\n$ go install example.com/user/hello 验证，import成功，字符串反转\n$ hello Hello, Go! 从远程remore modules import packages 可以用import path通过版本控制系统来获取package源码，如Git或Mercurial。\n示例，使用github.com/google/go-cmp/cmp\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;example.com/user/hello/morestrings\u0026#34; \u0026#34;github.com/google/go-cmp/cmp\u0026#34; ) func main() { fmt.Println(morestrings.ReverseRunes(\u0026#34;!oG ,olleH\u0026#34;)) fmt.Println(cmp.Diff(\u0026#34;Hello World\u0026#34;, \u0026#34;Hello Go\u0026#34;)) } 当运行命令go install go build go run的时候，go命令会自动下载远程module，然后写到go.mod文件中\n$ go install example.com/user/hello go: finding module for package github.com/google/go-cmp/cmp go: downloading github.com/google/go-cmp v0.4.0 go: found github.com/google/go-cmp/cmp in github.com/google/go-cmp v0.4.0 $ hello Hello, Go! string( - \u0026#34;Hello World\u0026#34;, + \u0026#34;Hello Go\u0026#34;, ) $ cat go.mod module example.com/user/hello go 1.14 require github.com/google/go-cmp v0.4.0 $ 国内容易超时，可以使用代理走国内镜像\n七牛云\ngo env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct 阿里云\ngo env -w GO111MODULE=on go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct module依赖会自动下载到GOPATH指定目录的pkg/mod子目录。\nmodule指定版本的下载内容，是在所有其他require这个版本的modules中共享的，所以go命令会标记这些文件和目录为只读的。\n可以使用命令删除所有下载的modules\n$ go clean -modcache $ 测试 Go有个轻量的测试框架，go test和testing package。\n测试框架识别以_test.go结尾的文件，包含TestXXX命名的函数，函数签名func (t *testing.T)。如果函数调用失败如t.Error 或 t.Fail，那么test就会失败。\n示例，新建*$HOME/hello/morestrings/reverse_test.go*文件，添加morestrings package的测试代码\npackage morestrings import \u0026#34;testing\u0026#34; func TestReverseRunes(t *testing.T) { cases := []struct { in, want string }{ {\u0026#34;Hello, world\u0026#34;, \u0026#34;dlrow ,olleH\u0026#34;}, {\u0026#34;Hello, 世界\u0026#34;, \u0026#34;界世 ,olleH\u0026#34;}, {\u0026#34;\u0026#34;, \u0026#34;\u0026#34;}, } for _, c := range cases { got := ReverseRunes(c.in) if got != c.want { t.Errorf(\u0026#34;ReverseRunes(%q) == %q, want %q\u0026#34;, c.in, got, c.want) } } } 运行测试\n$ go test PASS ok example.com/user/morestrings 0.165s $ ","permalink":"https://dongfanger.github.io/posts/java%E7%94%9F%E6%80%81/golang/000001-golang%E6%80%8E%E4%B9%88%E5%86%99go%E4%BB%A3%E7%A0%81/","summary":"安装过程略过，网上搜一大把。","title":"怎么写Go代码"},{"content":" 本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\nRPC接口 我所在的部门是技术中台，主要负责toB业务，上游是业务条线，包括PC、APP、小程序等终端，这些端的后台是HTTP接口层，而在HTTP接口层之下，就是技术中台的RPC接口层。\nHTTP是一种超文本传输协议，基于TCP/IP来传输文本、图片、音频、视频等数据。数据包的传输能力不是HTTP提供的，而是TCP/IP干的。HTTP的本质是客户端和服务端约定好的一种通信格式，是一种全世界认可的通用范本。\nRPC是一种远程调用，也就是进程间通信，既可以用HTTP协议，也可以用自定义协议。RPC的好处是没有HTTP那么多约束，简化了写法，可以根据公司自定义规范，内部统一即可。 所以在公司内部特别流行，比如阿里的Dubbo接口就是一种RPC接口，我们公司用的JSF接口，自研的服务框架，简称杰夫。\n谈谈京东的服务框架 https://juejin.cn/post/6948351920088186888\n举个例子，本地调用，是调用的本地方法：\npublic User getUserById(Long id) { return userDao.getUserById(id); // 这叫本地调用 } 而RPC远程调用，调用的是另外一个服务的方法：\npublic User getUserById(Long id) { return userConsumer.getUserById(id); // 这是远程调用，逻辑是用户小组的服务实现的。 } 本地调用和远程调用的比较如下图所示：\n左边的本地调用，只在应用内部调用。右边的远程调用，会跨应用在应用之间调用。\nRPC和HTTP用法区别 HTTP接口在调用时，输入URL，Method，Headers，Body等等等就可以发送请求了。\nRPC接口的调用大同小异，输入：\nJSF地址：方法引用路径，比如com.dongfanger.api.service.ShopProvider。\n方法名：方法名称，比如queryShopList。\n别名：既可以是分组别名，也可以直连IP地址。\n方法入参：JSON串。\n等等等，就可以发起请求了。\n从用法能看出来，RPC和HTTP接口的结构体是差不多的，但是RPC不需要Method和Headers，精简了许多。\n接口自动化 首先需要考虑的是目录结构设计，HTTP可以模块菜单创建用例目录，比如：\n一级目录：管理端\n二级目录：发票管理\n三级目录：发票查询\n而RPC是无页面的，一般存在很多业务线，所以可以按照功能域和业务线来组织目录，比如：\n一级目录：发票\n二级目录：提交申请\n三级目录：分销\n其次就得考虑技术实现了，公司基建比较完善，无论是HTTP还是JSF接口，都能在接口测试平台上编写自动化用例。虽然是Web平台写用例，但是得益于测试平台的优秀设计和使用体验，用起来很方便，效率也很高。交互方式跟JMeter大同小异，只是在细节上做了很多打磨。\n然后就是参数构造了，建议是用流量回放的方式来做，省去手工编写参数的崩溃感。流量用例创建好以后，需要做一些参数化处理和接口关联。这样才能够把多个接口串起来。\n对于测试数据，通过预设变量来统一管理，自动化用例也尽可能设计成闭环，增加，修改，查询，删除，用例跑完后产生的数据也跟着清理掉。\n最后就是断言了。没有断言的自动化用例，不叫做自动化用例。断言一定要加上，否则定时任务跑出结果后，无法判断到底有没有问题。而断言内容不能只是简单的code200，需要加上业务校验。\n中台RPC接口自动化最难处理的是接口依赖问题，链路特别长，依赖特别多。我一般通过Mock来解决这个问题，公司的测试平台也提供了这个功能。\n需要注意的是，核心用例定时运行监控起来，而一次性用例要及时维护或清理，防止变成僵尸用例。\n最后，我准备基于Vue+FastAPI开发一套新的pytest脚本管理平台，集成流量录制创建用例和Mock等功能，并借鉴公司测试平台的设计，感兴趣的同学可以添加微信cekaigang加群讨论哟。\n参考资料：\nHTTP 的本质？HTTP 和 RPC 的区别？ https://www.jianshu.com/p/fe5ccfc5d7bd\n谈谈京东的服务框架 https://juejin.cn/post/6948351920088186888\n","permalink":"https://dongfanger.github.io/posts/%E6%B5%8B%E8%AF%95%E4%B8%93%E9%A2%98/%E8%87%AA%E5%8A%A8%E5%8C%96/000001-%E4%B8%AD%E5%8F%B0rpc%E6%8E%A5%E5%8F%A3%E8%87%AA%E5%8A%A8%E5%8C%96%E6%80%8E%E4%B9%88%E5%81%9A/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本书作者：测试开发刚哥，文章版权归个人所有，未经授权请勿转载。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"rpc接口\"\u003eRPC接口\u003c/h1\u003e\n\u003cp\u003e我所在的部门是技术中台，主要负责toB业务，上游是业务条线，包括PC、APP、小程序等终端，这些端的后台是HTTP接口层，而在HTTP接口层之下，就是技术中台的RPC接口层。\u003c/p\u003e\n\u003cp\u003eHTTP是一种超文本传输协议，基于TCP/IP来传输文本、图片、音频、视频等数据。数据包的传输能力不是HTTP提供的，而是TCP/IP干的。\u003cstrong\u003eHTTP的本质是客户端和服务端约定好的一种通信格式，是一种全世界认可的通用范本。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eRPC是一种远程调用，也就是进程间通信，既可以用HTTP协议，也可以用自定义协议。\u003cstrong\u003eRPC的好处是没有HTTP那么多约束，简化了写法，可以根据公司自定义规范，内部统一即可。\u003c/strong\u003e 所以在公司内部特别流行，比如阿里的Dubbo接口就是一种RPC接口，我们公司用的JSF接口，自研的服务框架，简称杰夫。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e谈谈京东的服务框架 \u003ca href=\"https://juejin.cn/post/6948351920088186888\"\u003ehttps://juejin.cn/post/6948351920088186888\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e举个例子，本地调用，是调用的本地方法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eUser\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetUserById\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eLong\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e       \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003euserDao\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetUserById\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 这叫本地调用\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e而RPC远程调用，调用的是另外一个服务的方法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eUser\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003egetUserById\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eLong\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e       \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003euserConsumer\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003egetUserById\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eid\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 这是远程调用，逻辑是用户小组的服务实现的。\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e本地调用和远程调用的比较如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"../assets/e0c812e4fc6b43eda732c1edfba24398.png\"\u003e\u003c/p\u003e\n\u003cp\u003e左边的本地调用，只在应用内部调用。右边的远程调用，会跨应用在应用之间调用。\u003c/p\u003e\n\u003ch1 id=\"rpc和http用法区别\"\u003eRPC和HTTP用法区别\u003c/h1\u003e\n\u003cp\u003eHTTP接口在调用时，输入URL，Method，Headers，Body等等等就可以发送请求了。\u003c/p\u003e\n\u003cp\u003eRPC接口的调用大同小异，输入：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJSF地址\u003c/strong\u003e：方法引用路径，比如\u003ccode\u003ecom.dongfanger.api.service.ShopProvider\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e方法名\u003c/strong\u003e：方法名称，比如\u003ccode\u003equeryShopList\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e别名\u003c/strong\u003e：既可以是分组别名，也可以直连IP地址。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e方法入参\u003c/strong\u003e：JSON串。\u003c/p\u003e\n\u003cp\u003e等等等，就可以发起请求了。\u003c/p\u003e\n\u003cp\u003e从用法能看出来，\u003cstrong\u003eRPC和HTTP接口的结构体是差不多的，但是RPC不需要Method和Headers\u003c/strong\u003e，精简了许多。\u003c/p\u003e\n\u003ch1 id=\"接口自动化\"\u003e接口自动化\u003c/h1\u003e\n\u003cp\u003e首先需要考虑的是\u003cstrong\u003e目录结构设计\u003c/strong\u003e，HTTP可以模块菜单创建用例目录，比如：\u003c/p\u003e\n\u003cp\u003e一级目录：管理端\u003c/p\u003e\n\u003cp\u003e二级目录：发票管理\u003c/p\u003e\n\u003cp\u003e三级目录：发票查询\u003c/p\u003e\n\u003cp\u003e而RPC是无页面的，一般存在很多业务线，所以可以按照功能域和业务线来组织目录，比如：\u003c/p\u003e\n\u003cp\u003e一级目录：发票\u003c/p\u003e\n\u003cp\u003e二级目录：提交申请\u003c/p\u003e\n\u003cp\u003e三级目录：分销\u003c/p\u003e\n\u003cp\u003e其次就得考虑\u003cstrong\u003e技术实现\u003c/strong\u003e了，公司基建比较完善，无论是HTTP还是JSF接口，都能在接口测试平台上编写自动化用例。虽然是Web平台写用例，但是得益于测试平台的优秀设计和使用体验，用起来很方便，效率也很高。交互方式跟JMeter大同小异，只是在细节上做了很多打磨。\u003c/p\u003e\n\u003cp\u003e然后就是\u003cstrong\u003e参数构造\u003c/strong\u003e了，建议是用流量回放的方式来做，省去手工编写参数的崩溃感。流量用例创建好以后，需要做一些参数化处理和接口关联。这样才能够把多个接口串起来。\u003c/p\u003e\n\u003cp\u003e对于\u003cstrong\u003e测试数据\u003c/strong\u003e，通过预设变量来统一管理，自动化用例也尽可能设计成闭环，增加，修改，查询，删除，用例跑完后产生的数据也跟着清理掉。\u003c/p\u003e\n\u003cp\u003e最后就是\u003cstrong\u003e断言了\u003c/strong\u003e。没有断言的自动化用例，不叫做自动化用例。断言一定要加上，否则定时任务跑出结果后，无法判断到底有没有问题。而断言内容不能只是简单的code200，需要加上业务校验。\u003c/p\u003e\n\u003cp\u003e中台RPC接口自动化最难处理的是接口依赖问题，链路特别长，依赖特别多。我一般通过Mock来解决这个问题，公司的测试平台也提供了这个功能。\u003c/p\u003e\n\u003cp\u003e需要注意的是，核心用例定时运行监控起来，而一次性用例要及时维护或清理，防止变成僵尸用例。\u003c/p\u003e\n\u003cp\u003e最后，我准备基于Vue+FastAPI开发一套新的pytest脚本管理平台，集成流量录制创建用例和Mock等功能，并借鉴公司测试平台的设计，感兴趣的同学可以添加微信\u003cstrong\u003ecekaigang\u003c/strong\u003e加群讨论哟。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考资料：\u003c/p\u003e\n\u003cp\u003eHTTP 的本质？HTTP 和 RPC 的区别？ \u003ca href=\"https://www.jianshu.com/p/fe5ccfc5d7bd\"\u003ehttps://www.jianshu.com/p/fe5ccfc5d7bd\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e谈谈京东的服务框架 \u003ca href=\"https://juejin.cn/post/6948351920088186888\"\u003ehttps://juejin.cn/post/6948351920088186888\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"中台RPC接口自动化怎么做"},{"content":"我是谁 王刚，10 年测试开发老兵。\n2014 年毕业于北京理工大学，计算机科学与技术专业，在 500 强、中厂、小公司都有测试工作经历，从功能测试一路做到测试开发。\n写作与成就 2019 年开始写作，专注测试开发领域，累计 260+ 篇原创文章。\n累计粉丝 3000+ 技术交流群 400+ 技术分享 5 次 · 参与 350 人 咨询交流 60 次 🏆 2020 年度最佳作者奖 🏆 2022 年度优秀作者奖 《平凡之路》 徘徊着的，在路上的，你要走吗\n易碎的，骄傲着，那也曾是我的模样\n沸腾着的，不安着的，你要去哪\n谜一样的，沉默着的，故事你真的，在听吗\n我曾经跨过山和大海，也穿过人山人海\n我曾经拥有着的一切，转眼都飘散如烟\n我曾经失落失望，失掉所有方向\n直到看见平凡，才是唯一的答案\n我曾经毁了我的一切，只想永远地离开\n我曾经堕入无边黑暗，想挣扎无法自拔\n我曾经像你像他，像那野草野花\n绝望着，渴望着，也哭也笑的平凡着\n联系我 GitHub: https://github.com/dongfanger\n微信公众号\n个人微信 / 技术交流群\n","permalink":"https://dongfanger.github.io/about/","summary":"关于测试开发刚哥","title":"关于"}]