简历上那句「熟悉接口测试」,背后该是一个什么样的项目

举报
霍格沃兹测试开发学社 发表于 2026/09/22 21:05:49 2026/09/22
【摘要】 应届生投测试开发岗,简历最缺的往往不是关键词,是一个能讲透的小项目。关键词谁都会写:pytest、requests、接口自动化、postman、Jenkins——一串排下来看着挺唬人,面试官一追问「你这些是在什么项目里用的?断言怎么分层?异常路径怎么覆盖?CI 怎么挂?」,很多人当场就卡住了。原因很简单:简历上写的是名词,脑子里没有项目。一般经验:面试官看到「熟悉接口自动化」这几个字,通常会...
应届生投测试开发岗,简历最缺的往往不是关键词,是一个能讲透的小项目。

关键词谁都会写:pytest、requests、接口自动化、postman、Jenkins——一串排下来看着挺唬人,面试官一追问「你这些是在什么项目里用的?断言怎么分层?异常路径怎么覆盖?CI 怎么挂?」,很多人当场就卡住了。

原因很简单:简历上写的是名词,脑子里没有项目。

一般经验:面试官看到「熟悉接口自动化」这几个字,通常会追问三个问题——你的用例是怎么分层的?断言只断状态码还是断到业务字段?出错了怎么定位、怎么让别人复现? 这三个问题背后,其实是在筛掉「跟着教程抄一遍就写进简历」的候选人,留下「有一个真正属于自己的项目、能一层层剥开讲清楚」的人。

这篇只钉死一个场景:给一个公开的 REST API(jsonplaceholder.typicode.com),从零写一套 pytest + requests 的接口自动化,做到能放进简历、能对着屏幕讲透每一步为什么这么写。 分四步走:环境搭建、用例分层、数据驱动与断言、报告与 CI 挂钩。每一步都会给出可直接跑的代码,以及这一步「为什么这么做、不这么做会怎样」的取舍说明。全篇不引用任何就业或薪资数据,只讲工程做法;涉及求职建议的地方,一律标「一般经验」。

一、场景钉死:给 jsonplaceholder 写接口自动化

jsonplaceholder 是一个公开、免费、无需注册的 REST API mock 服务,提供 /posts/comments/users 等资源的标准 CRUD 接口,返回结构稳定,非常适合做教学演示。我们把它当作「被测系统」,目标很朴素:

  • 覆盖 GET 单资源、GET 列表、POST 创建三类接口;
  • 冒烟用例、正常路径、异常路径、边界,各来一份;
  • 参数化跑数据驱动,不写重复代码;
  • 断言不仅看状态码,还要看响应结构与业务字段;
  • 一份 README 讲清怎么跑,一段 GitHub Actions 让 PR 自动跑一遍。

这个规模不大,一个下午能做完,但五脏俱全——「小而完整」远比「大而浅」更能撑住追问(一般经验)。


二、第一步:环境搭建(venv + requirements.txt + 目录约定)

先用虚拟环境把依赖隔离干净,别把包塞进全局 Python:

python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate

pip install pytest requests pytest-html jsonschema
pip freeze > requirements.txt

项目目录按「配置 / fixture / 用例 / 数据 / 报告」五块分:

api-auto-demo/
├── .github/
│   └── workflows/
│       └── pytest.yml           # CI 配置
├── config/
│   └── settings.py              # BASE_URL、超时等
├── tests/
│   ├── conftest.py              # 全局 fixture
│   ├── test_smoke.py            # 冒烟:核心接口能通
│   ├── test_posts_happy.py      # 正常路径
│   ├── test_posts_error.py      # 异常路径
│   └── test_posts_boundary.py   # 边界
├── data/
│   └── post_cases.yaml          # 参数化数据(可选)
├── reports/                     # pytest-html 输出
├── requirements.txt
├── pytest.ini
└── README.md

pytest.ini 只做最小配置:

[pytest]
testpaths = tests
addopts = -v --html=reports/report.html --self-contained-html

config/settings.py

BASE_URL = "https://jsonplaceholder.typicode.com"
TIMEOUT = 10

这一步看着简单,但很多应届生的项目就是从这一步开始乱的——所有代码塞在一个 test.py 里,跑一次得手动改 URL,别人拿到手不知道怎么复现。目录约定就是可复现性的第一道门槛:一个陌生人 clone 下来,pip install -r requirements.txt 再 pytest,能一次跑通,这个项目才算立住了。

两个细节顺带说一下。第一,requirements.txt 里的版本要不要锁死?一般经验是锁大版本、放开小版本,比如 pytest>=8.0,<9.0,避免半年后 pytest 出了大版本改语法,别人 clone 下来直接跑不起来;如果你追求极致可复现,也可以用 pip freeze 输出精确到小版本号的锁文件。第二,README 至少要写清三件事:这是什么项目、怎么装依赖、怎么跑用例——三行字就够,但没有这三行,面试官打开仓库第一眼就是「这人不会写文档」,印象分先扣一档。

三、第二步:conftest.py 与用例分层

conftest.py 是 pytest 的「共享配置中心」,把 session 对象、BASE_URL、公共断言函数都放进去,避免每条用例重复写:

# tests/conftest.py
import pytest
import requests
from config.settings import BASE_URL, TIMEOUT


@pytest.fixture(scope="session")
def api():
    """整个测试会话共用一个 Session,复用 TCP 连接。"""
    s = requests.Session()
    s.headers.update({"Content-Type""application/json"})
    yield s
    s.close()


@pytest.fixture
def base_url():
    return BASE_URL


@pytest.fixture
def timeout():
    return TIMEOUT


def assert_fields(payload, required_fields):
    """最简 schema 校验:字段存在 + 类型正确。"""
    for name, typ in required_fields.items():
        assert name in payload, f"缺少字段 {name}"
        assert isinstance(payload[name], typ), f"{name} 类型不对,期望 {typ}"

用例按四层分文件写:

  • 冒烟test_smoke.py):一两条,只验「服务活着、核心接口能通」,每次部署完先跑;
  • 正常路径test_posts_happy.py):主流程,如 GET 单条 post、GET 某用户下所有 post、POST 新建;
  • 异常路径test_posts_error.py):404、非法 id、方法不允许;
  • 边界test_posts_boundary.py):id=0、id 超大、空 body、极长字符串。

分层的价值不是文件好看,是让面试官问「你怎么覆盖异常路径」的时候,你能直接翻出一个文件给他看——用例的物理组织,就是你脑子里测试策略的映射。

四、第三步:parametrize 数据驱动 + 断言三层

数据驱动是应届生项目里最容易被追问、也最容易做假的一环。做假的写法是把 5 条用例复制粘贴改参数;做真的写法是用 @pytest.mark.parametrize 把数据从代码里抽出来:

# tests/test_smoke.py
def test_smoke_service_alive(api, base_url, timeout):
    """冒烟:核心接口可达,服务活着。"""
    resp = api.get(f"{base_url}/posts/1", timeout=timeout)
    assert resp.status_code == 200
# tests/test_posts_happy.py
import pytest
from tests.conftest import assert_fields


@pytest.mark.parametrize("post_id,expected_user_id", [
    (11),
    (505),
    (10010),
])
def test_get_post_by_id(api, base_url, timeout, post_id, expected_user_id):
    """正常路径:按 id 拿 post,三层断言。"""
    resp = api.get(f"{base_url}/posts/{post_id}", timeout=timeout)
    # 第 1 层:状态码
    assert resp.status_code == 200
    data = resp.json()
    # 第 2 层:响应 schema
    assert_fields(data, {"userId": int, "id": int, "title": str, "body": str})
    # 第 3 层:业务字段
    assert data["id"] == post_id
    assert data["userId"] == expected_user_id
    assert data["title"].strip(), "title 不应为空字符串"
# tests/test_posts_error.py
import pytest


@pytest.mark.parametrize("bad_id", [0, -1, 99999])
def test_get_post_not_found(api, base_url, timeout, bad_id):
    """异常路径:不存在或非法 id 应返回 404。"""
    resp = api.get(f"{base_url}/posts/{bad_id}", timeout=timeout)
    assert resp.status_code == 404
# tests/test_posts_boundary.py
def test_create_post_with_empty_body(api, base_url, timeout):
    """边界:POST 空 body,服务应仍能返回 201 并给出资源 id。"""
    resp = api.post(f"{base_url}/posts", json={}, timeout=timeout)
    assert resp.status_code == 201
    assert "id" in resp.json()


def test_create_post_with_long_title(api, base_url, timeout):
    """边界:title 5000 字符,验证服务不因长度拒收。"""
    long_title = "a" * 5000
    payload = {"title": long_title, "body""x""userId"1}
    resp = api.post(f"{base_url}/posts", json=payload, timeout=timeout)
    assert resp.status_code == 201
    assert resp.json()["title"] == long_title

这四段合起来满足「4—6 条示例用例」的口径:冒烟 1 条 + 正常路径 3 条(参数化展开)+ 异常路径 3 条(参数化展开)+ 边界 2 条,总共 9 个 test item,但代码只有 4 个函数。

断言按「状态码 → schema → 业务字段」三层写,是这个项目里最值得讲的一件事——很多应届生只断言 status_code == 200,一旦服务返回 200 但 body 结构变了、字段值错了,用例就成了摆设。三层断言的意思,是把「接口通不通」「结构对不对」「值准不准」分开验证,任何一层挂了都能立刻定位是哪一类问题。

关于 parametrize 再多说两句。第一,参数化的价值不在「代码短」,而在「数据变更不动代码」——想加一组新的 post_id,只在装饰器列表里加一行,用例函数本身不用改,这在真实项目里意味着"新增覆盖"是一次低风险动作。第二,参数化默认会用 post_id0post_id1 这样的自动生成 id,报告里看不出来是哪组数据挂了;生产实践里通常会加 ids=[...] 参数给每组数据起个可读名字,比如 ids=["first_post", "middle_post", "last_post"],这是"可讲性"里的一个小加分项。第三,assert_fields 抽成公共函数,是为了让"schema 断言"这件事只写一次——真实项目里往往会换成 jsonschema 库做正式 schema 校验,但抽函数这一步的思路是一样的:同一件事不要在 10 条用例里写 10 遍

五、第四步:报告与 CI 挂钩

本地跑完出一份 HTML 报告:

pytest --html=reports/report.html --self-contained-html

--self-contained-html 会把 CSS/JS 内联进单个 HTML 文件,扔给面试官或部署到 GitHub Pages 都能直接打开,不需要额外资源。

再挂一段 GitHub Actions,让每次 push / PR 自动跑:

# .github/workflows/pytest.yml
name: pytest-api-auto

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install deps
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run pytest
        run: pytest -v --html=reports/report.html --self-contained-html

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: pytest-report
          path: reports/

三个要点值得单独讲:if: always() 保证用例失败也能把报告上传出来,不然红了就啥也看不到;cache: 'pip' 让依赖装得快,节省 CI 分钟数;pull_request 触发意味着这个仓库只要收到 PR,接口用例就会自动跑一遍——这就是「可复现性」从口号变成动作的地方:不是你本地能跑就行,是任何人、任何机器、任何一次提交,都能自动跑一遍并留下证据。

CI 挂上之后,还有三个"顺手就能做"的动作,一般经验值得做进项目里:第一,在 README 顶部挂一个 GitHub Actions 的状态徽章(![CI](https://github.com/<你>/<仓库>/actions/workflows/pytest.yml/badge.svg)),任何人打开仓库第一眼就能看到"这个项目是活的、CI 是绿的";第二,故意提一个 PR 让某条用例挂掉,然后把 Actions 的运行日志和上传的 HTML 报告截图放进 README 的"演示失败定位"章节——能演示怎么定位失败,比只演示成功更能撑住追问;第三,面试现场如果允许打开电脑,直接把仓库地址给面试官,让他随机挑一条用例点进去看代码、看 CI 运行记录,这个动作比讲十分钟都管用。

六、只会写脚本 vs 能讲透:五维度对照

同一句「熟悉接口自动化」,落到项目上差别有多大?下面这张表是带应届生做项目时常用的自检表(一般经验,不代表任何统计口径):

维度
「只会写脚本」的应届生项目
「能讲透」的应届生项目
结构
一个 test.py 打天下,URL、数据、断言全糊在一起
config / tests / data / reports
 分目录,conftest 抽公共 fixture
断言
只断 status_code == 200
三层断言:状态码 → 响应 schema → 业务字段
异常处理
没有异常路径用例,或只写一条「404」
404 / 非法 id / 空 body / 超长字段 分开覆盖,parametrize 列数据
可复现性
依赖靠口口相传,别人 clone 下来跑不起来
requirements.txt + pytest.ini + README + CI,一条命令跑通
可讲性
面试官问「为什么这么写」,答「网上抄的」
每一层能说出取舍:为什么用 Session、为什么参数化、为什么分四层

这五维度不是要你逐条打分,是给你一个自检清单。做完项目、推上 GitHub 之前,把这五个格子逐格问自己一遍——能答上来的,才写进简历;答不上来的,回炉补一遍再来。

七、把它讲透,比把它做大更重要

一般经验:应届生做第一个项目,最容易犯的错不是「做得简单」,而是「做得又多又浅」——十几个用例堆在一起,看着挺满,被追问一层就露馅。

真正让面试官记住的,是一个小而完整的项目:目录清爽、用例分层、参数化用得起、断言写得深、CI 挂得上、README 说得清。jsonplaceholder 这种公开 API 就够用,你完全不需要自己搭一套后端。

写完推上 GitHub 之后,练三件事:

  1. 用三分钟把这个项目讲一遍:背景是什么、分了哪四层、断言策略是什么、CI 怎么挂的;
  2. 现场改一条用例,让 parametrize 多跑一组数据,讲清「为什么这组数据值得加」;
  3. 打开 GitHub Actions 的运行记录,指着日志说「这一步为什么失败 / 为什么通过」。

这三件事能顺畅做下来,简历上那句「熟悉 pytest + requests 接口自动化」,才算真正立住。项目不是给招聘方看的展品,是给你自己练「怎么讲清楚一件事」的稿子——稿子练熟了,面试就是照着念。

关键词能替你打开筛选,但只有能讲透的项目,能替你撑过追问。

如果你也在做自己的第一个测试项目,欢迎把仓库地址留在评论区,一起看看彼此的分层与断言怎么写。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。