返回测试开发学习路线
Test Development / Tutorial 01

测试开发编程基础教程

从测试工程师的视角出发,掌握把业务规则写成可运行脚本的编程能力:数据处理、HTTP 请求、数据库校验、日志留痕,最终串成一个订单服务回归脚本。

8 个章节贯穿案例:订单服务回归Python + requests + SQL
01

为什么测试工程师需要编程能力

从点到线
测试开发的工作闭环
发现问题用例执行或线上反馈
设计验证把场景翻译成逻辑
编码脚本与数据
自动执行一键回归
持续反馈日志与报告

普通测试与测试开发的区别

环节普通测试测试开发
发现问题执行用例,记录缺陷执行用例,记录缺陷
定位原因把现象写进 Bug 单判断是数据、接口还是环境问题
设计验证等待修复后手工重测把场景翻译成可重复执行的验证逻辑
执行回归手工重跑,依赖人的记忆脚本自动执行,一键回归
持续反馈Bug 单闭环日志 + 报告,失败可定位

回到贯穿案例:订单服务回归

接口越来越多,每次发版都要手工点一遍“下单、查询、取消”。订单服务回归脚本把这件事变成:自动读取一批订单数据 → 调用下单接口 → 校验响应 → 核对数据库 → 输出报告。人只负责看结论,机器负责重复劳动。

会写代码不是目的,把一条业务规则变成“今天能跑、下个月还能跑、失败了能定位”的自动回归,才是测试开发要解决的工程问题。
02

测试代码与业务代码的区别

读者不同

业务代码与测试代码的关注点

维度业务代码测试代码
关注点功能、性能、稳定性可读性、可维护性、可扩展性、可复用性
主要读者开发者和编译器测试工程师自己和团队
运行频率部署后长期运行每次回归都运行,一次编写多次执行
失败含义系统出了 Bug业务与预期不一致,要能快速定位
演进方式随功能迭代修改随用例集沉淀,持续扩充

为什么测试代码最看重可读性

业务代码要扛住高并发,测试代码要扛住“三个月后还有人维护”。下一轮回归跑你的脚本的,可能是另一位测试同事:他不需要知道你调了什么框架,但他必须能一眼看出——数据从哪来、接口调哪个、断言断什么、失败看哪里

业务代码追求“把功能做对”,测试代码追求“把验证说清楚”。同样一段逻辑,写在测试里要优先让下一步骤的人三分钟读懂。
03

数据与文件处理:测试数据的来源

JSON / CSV / Excel
test_data/orders.json
[
  {
    "orderId": "SO-20240301-001",
    "skuId": "SKU-1001",
    "quantity": 2,
    "expectedStatus": "PENDING_PAYMENT",
    "payAmount": "199.80"
  },
  {
    "orderId": "SO-20240301-002",
    "skuId": "SKU-1002",
    "quantity": 1,
    "expectedStatus": "PAID",
    "payAmount": "59.90"
  }
]
读取与断言
import json
import csv

def load_orders_from_json(path):
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)

def load_orders_from_csv(path):
    with open(path, "r", encoding="utf-8") as f:
        return list(csv.DictReader(f))

def assert_order_status(order, expected_status):
    assert order["status"] == expected_status, (
        "订单 %s 状态异常: 期望 %s, 实际 %s"
        % (order["orderId"], expected_status, order["status"])
    )

测试数据的三条纪律

  • 数据独立成文件,不写死在断言里。
  • 金额用字符串或 Decimal 存储,避免浮点误差。
  • 每条数据都要能说清它验证什么:正常、边界还是异常。
Excel 场景用 pandas 一行读取:pandas.read_excel(path, dtype=str) 再转成字典列表。注意把金额读成字符串而不是浮点数,否则 199.80 会变成 199.8。
04

HTTP 请求与响应:与接口对话

requests
封装下单请求
import requests

BASE_URL = "https://api.example.com"
TOKEN = "test-token-xxx"

def create_order(payload):
    response = requests.post(
        BASE_URL + "/orders",
        json=payload,
        headers={
            "Authorization": "Bearer " + TOKEN,
            "Content-Type": "application/json",
        },
        timeout=10,
    )
    return response

def assert_business_success(response):
    assert response.status_code == 200, "HTTP 状态异常: %s" % response.status_code
    body = response.json()
    assert body["code"] == "SUCCESS", "业务码异常: %s" % body
    assert body["data"]["orderId"], "响应缺少订单号"

三层检查,缺一不可

检查项含义例子
HTTP 状态码网络与服务器处理结果200 / 500
业务 code业务规则是否通过SUCCESS / ORDER_CLOSED
响应体 data真正的业务数据orderId、payAmount、status
HTTP 200 只说明“服务器处理了这个请求”,不代表下单成功。断言必须分层:先看 HTTP 状态,再看业务 code,最后取 data 里的业务字段。
05

数据库操作:用另一只眼看订单

交叉校验
查询订单表
import sqlite3

DB_PATH = "orders.db"

def fetch_order(order_id):
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.cursor()
        cur.execute(
            "SELECT order_id, status, pay_amount FROM orders WHERE order_id = ?",
            (order_id,),
        )
        row = cur.fetchone()
        if row is None:
            return None
        return {"orderId": row[0], "status": row[1], "payAmount": str(row[2])}
    finally:
        conn.close()
接口返回与数据库交叉校验
def cross_check(api_order, db_order):
    assert db_order is not None, "订单未写入数据库: %s" % api_order["orderId"]
    assert db_order["status"] == api_order["status"], (
        "状态不一致: 接口 %s vs 数据库 %s"
        % (api_order["status"], db_order["status"])
    )
    assert db_order["payAmount"] == api_order["payAmount"], (
        "金额不一致: 接口 %s vs 数据库 %s"
        % (api_order["payAmount"], db_order["payAmount"])
    )

为什么要交叉校验

接口层的返回是系统自己“说”的,数据库落库才是业务真正发生的证据。接口说下单成功、数据库却没有订单——这种问题只测接口永远发现不了,交叉校验能把它抓出来。

测试环境连库只做只读查询;如果确实要写数据,优先在事务里回滚。参数化 SQL 一律用占位符 ?,永远不要字符串拼接,防止注入也避免引号转义。
06

日志与异常:失败时要能三分钟定位

留痕
统一日志配置
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
)

logger = logging.getLogger("order-regression")

def log_api_call(action, payload, response):
    logger.info(
        "action=%s payload=%s status=%s code=%s",
        action,
        payload,
        response.status_code,
        response.json().get("code"),
    )
异常上下文要带订单号
import requests

try:
    response = create_order(payload)
    assert_business_success(response)
except AssertionError:
    logger.error(
        "订单 %s 断言失败, 响应体=%s",
        payload.get("orderId"),
        response.text,
    )
    raise
except requests.RequestException as exc:
    logger.error("订单 %s 请求异常: %s", payload.get("orderId"), exc)
    raise

一次失败至少留下这些

  • 用例名与测试数据编号。
  • 订单号、商品号等业务主键。
  • 请求方法、URL 与请求体。
  • HTTP 状态、业务 code 与响应体。
  • 发生时间与环境标识;token 和密码绝不进日志。
日志不是写给别人看的装饰,是失败时唯一的“事故现场”。先设计好“失败时能看到什么”,再写断言,比事后补日志高效得多。
07

串成一个小工具:订单服务回归脚本

可运行
一条回归数据从文件到报告
读取数据orders.json
调接口POST /orders
校验响应HTTP + code
核对数据库交叉校验
输出报告PASS / FAIL
order_regression.py
import json
import logging
import sqlite3

import requests

BASE_URL = "https://api.example.com"
TOKEN = "test-token-xxx"
DB_PATH = "orders.db"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
)
logger = logging.getLogger("order-regression")

def load_orders(path):
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)

def create_order(payload):
    return requests.post(
        BASE_URL + "/orders",
        json=payload,
        headers={
            "Authorization": "Bearer " + TOKEN,
            "Content-Type": "application/json",
        },
        timeout=10,
    )

def fetch_order(order_id):
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.cursor()
        cur.execute(
            "SELECT status, pay_amount FROM orders WHERE order_id = ?",
            (order_id,),
        )
        row = cur.fetchone()
        return {"status": row[0], "payAmount": str(row[1])} if row else None
    finally:
        conn.close()

def verify_order(payload, response):
    assert response.status_code == 200, "HTTP 状态: %s" % response.status_code
    body = response.json()
    assert body["code"] == "SUCCESS", "业务码异常: %s" % body
    order_id = body["data"]["orderId"]

    db_order = fetch_order(order_id)
    assert db_order is not None, "订单未写入数据库: %s" % order_id
    assert db_order["status"] == body["data"]["status"], (
        "状态不一致: 接口 %s vs 数据库 %s"
        % (body["data"]["status"], db_order["status"])
    )
    return order_id

def run_regression(path):
    total = 0
    passed = 0
    for payload in load_orders(path):
        total += 1
        try:
            response = create_order(payload)
            order_id = verify_order(payload, response)
            logger.info("PASS order_id=%s", order_id)
            passed += 1
        except Exception:
            logger.exception("FAIL payload=%s", payload)
    logger.info("报告: 共 %s 条, 通过 %s 条", total, passed)

if __name__ == "__main__":
    run_regression("test_data/orders.json")

运行步骤

  1. 安装依赖:pip install requests。
  2. 准备 test_data/orders.json,放 3 到 5 条订单数据。
  3. 执行 python order_regression.py。
  4. 观察日志中的 PASS / FAIL 与最终统计。
  5. 故意把一条数据的状态改错,确认失败信息能定位到具体订单。
这就是“发现一条 Bug”到“持续回归”的最小闭环:读数据、调接口、做断言、留证据。脚本先跑通,后面可以换 pytest、加参数化、接 CI——骨架不变。
08

练习与检查

动手

练习:完成订单服务回归脚本

  1. 准备 5 条订单测试数据(含 1 条异常状态),存成 JSON。
  2. 写函数读取数据并逐条断言期望状态。
  3. 用 requests 封装下单接口,正确处理 token。
  4. 把 HTTP 状态、业务 code、响应体 data 分开断言。
  5. 查询数据库,校验接口返回与落库数据一致。
  6. 每条用例的前后打印请求摘要与响应摘要。
  7. 故意制造一条失败,确认日志能定位到具体订单号。
  8. 把以上整理成 order_regression.py 一个脚本并跑通。

数据可靠

  • 测试数据独立成文件
  • 金额用字符串存储
  • 正常与异常数据并存
  • 每条数据说明验证意图

校验可信

  • HTTP 状态与业务 code 分开断言
  • 接口结果与数据库交叉校验
  • 失败信息包含订单号
  • 断言失败信息可读

工程可用

  • 脚本可重复运行
  • 日志可追溯定位
  • token 不进日志
  • 报告统计通过率

你已经能把一条业务规则写成可运行的回归脚本。下一步把脚本升级成真正的自动化测试工程。

回到测试开发学习路线