MonkeyCode写单元测试:从零到高覆盖率
不写测试的代码是负债。MonkeyCode帮你把测试覆盖率从0%提到80%+,而且写起来不痛苦。
为什么程序员不写测试?
三个字:懒、烦、慢。
- 写测试的时间比写业务代码还长
- 改一行业务代码,要改10个测试
- 不知道测什么、怎么Mock、怎么断言
MonkeyCode把这三点全解决了。
快速上手:让MonkeyCode生成测试
假设你有这样一个业务函数:
# app/order_service.py
import db
async def create_order(user_id: int, items: list, shipping_address: str) -> dict:
"""创建订单"""
# 验证用户
user = await db.fetch_one("SELECT * FROM users WHERE id = ?", user_id)
if not user:
raise ValueError("用户不存在")
# 验证库存
for item in items:
inv = await db.fetch_one("SELECT stock FROM inventory WHERE sku = ?", item["sku"])
if not inv or inv["stock"] < item["qty"]:
raise ValueError(f"库存不足: {item['sku']}")
# 计算总价
total = sum(item["price"] * item["qty"] for item in items)
# 创建订单
order_id = await db.execute(
"INSERT INTO orders (user_id, total_amount, address) VALUES (?, ?, ?)",
(user_id, total, shipping_address)
)
# 扣库存
for item in items:
await db.execute(
"UPDATE inventory SET stock = stock - ? WHERE sku = ?",
(item["qty"], item["sku"])
)
return {"order_id": order_id, "total": total}
直接对MonkeyCode说:
给 create_order 函数写完整的pytest单元测试,包含:
1. 正常流程测试
2. 用户不存在的异常测试
3. 库存不足的异常测试
4. Mock数据库依赖(用pytest-mock)
5. 参数化测试(多组输入)
6. 覆盖率100%
MonkeyCode生成:
# tests/test_order_service.py
import pytest
from unittest.mock import AsyncMock, MagicMock
import sys
sys.path.insert(0, ".")
from app.order_service import create_order
# ─── 正常流程 ───
@pytest.mark.asyncio
async def test_create_order_success(mocker):
"""测试正常创建订单"""
# Mock db
mock_db = mocker.patch("app.order_service.db")
# Mock用户查询
mock_db.fetch_one.side_effect = [
{"id": 1, "name": "张三"}, # 用户存在
{"stock": 100}, # 库存充足
]
# Mock写入
mock_db.execute.return_value = 10086
result = await create_order(
user_id=1,
items=[{"sku": "SKU001", "qty": 2, "price": 50}],
shipping_address="北京市朝阳区"
)
assert result["order_id"] == 10086
assert result["total"] == 100 # 50 * 2
mock_db.execute.assert_called_once()
# ─── 异常:用户不存在 ───
@pytest.mark.asyncio
async def test_create_order_user_not_found(mocker):
mock_db = mocker.patch("app.order_service.db")
mock_db.fetch_one.return_value = None # 用户不存在
with pytest.raises(ValueError, match="用户不存在"):
await create_order(1, [], "")
# ─── 异常:库存不足 ───
@pytest.mark.asyncio
async def test_create_order_insufficient_stock(mocker):
mock_db = mocker.patch("app.order_service.db")
mock_db.fetch_one.side_effect = [
{"id": 1, "name": "张三"}, # 用户存在
None, # 库存记录不存在
]
with pytest.raises(ValueError, match="库存不足"):
await create_order(1, [{"sku": "SKU001", "qty": 1, "price": 50}], "")
# ─── 参数化测试:多组输入 ───
@pytest.mark.parametrize("items,expected_total", [
([{"sku": "A", "qty": 1, "price": 100}], 100),
([{"sku": "A", "qty": 3, "price": 50}], 150),
([{"sku": "A", "qty": 2, "price": 10}, {"sku": "B", "qty": 1, "price": 80}], 100),
])
@pytest.mark.asyncio
async def test_create_order_total_calculation(mocker, items, expected_total):
mock_db = mocker.patch("app.order_service.db")
mock_db.fetch_one.side_effect = [
{"id": 1},
{"stock": 999},
]
mock_db.execute.return_value = 1
result = await create_order(1, items, "addr")
assert result["total"] == expected_total
# ─── Mock调用次数验证 ───
@pytest.mark.asyncio
async def test_create_order_db_calls(mocker):
"""验证数据库调用次数和参数"""
mock_db = mocker.patch("app.order_service.db")
mock_db.fetch_one.side_effect = [{"id": 1}, {"stock": 100}]
mock_db.execute.return_value = 1
await create_order(1, [{"sku": "A", "qty": 1, "price": 10}], "addr")
# 验证:1次用户查询 + 1次库存查询 + 1次写入 + 1次扣库存
assert mock_db.fetch_one.call_count == 2
assert mock_db.execute.call_count == 2
运行测试 + 覆盖率报告
pip install pytest pytest-mock pytest-cov
# 运行测试
pytest tests/ -v
# 覆盖率报告
pytest tests/ --cov=app --cov-report=html --cov-report=term
# 查看HTML报告
open htmlcov/index.html
FastAPI接口的测试
# tests/test_api.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_create_order_api(mocker):
# Mock业务函数
mocker.patch("app.order_service.create_order", return_value={"order_id": 1, "total": 100})
resp = client.post("/orders", json={
"user_id": 1,
"items": [{"sku": "A", "qty": 1, "price": 100}],
"shipping_address": "北京"
}, headers={"Authorization": "Bearer faketoken"})
assert resp.status_code == 200
assert resp.json()["order_id"] == 1
def test_create_order_api_unauthorized():
resp = client.post("/orders", json={})
assert resp.status_code == 401 # 未授权
数据库测试的夹具(Fixture)
# tests/conftest.py
import pytest
import asyncio
from app.db import get_db
@pytest.fixture
def event_loop():
"""创建事件循环,供所有异步测试使用"""
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.fixture
async def db_session():
"""创建测试数据库会话(事务回滚,不污染真实数据)"""
async with TestTransaction() as tx:
yield tx
@pytest.fixture
def override_get_db(app, db_session):
"""依赖注入覆盖:用测试DB替换真实DB"""
app.dependency_overrides[get_db] = lambda: db_session
yield
app.dependency_overrides.clear()
测试金字塔
/¯¯\
/ E2E \ ← 最少(关键路径,慢)
/_______\
/___________\
/ 集成测试 \ ← 适量(API+DB,中等速度)
/___________\
/____________\ ← 最多(纯函数,快)
/ 单元测试 \
/____________\
MonkeyCode帮你生成金字塔底层80%的单元测试,集成测试和E2E只写关键路径。
测试驱动开发(TDD):让MonkeyCode先做TDD
用TDD方式实现[功能描述]:
1. 先写失败的测试(Red)
2. 写最少代码让测试通过(Green)
3. 重构(Refactor)
请按TDD循环给出完整代码
常见测试场景速查
| 场景 | Mock工具 | 示例 |
|---|---|---|
| HTTP请求 | unittest.mock.patch + respx |
respx.get(url).mock(return_value=...) |
| 数据库 | pytest-mock + 事务回滚 |
mocker.patch("db.fetch_one") |
| Redis | pytest-redis + fixture |
fake_redis.get(...) |
| 时间 | freezegun |
@freeze_time("2024-01-01") |
| 环境变量 | monkeypatch |
monkeypatch.setenv("KEY", "val") |
| 文件IO | tmp_path fixture |
with open(tmp_path/"f.txt") as f: |
MonkeyCode Prompt模板
给以下Python函数写完整的pytest单元测试:
[粘贴函数代码]
要求:
1. 正常流程:覆盖所有分支
2. 异常流程:覆盖所有except分支
3. 边界条件:空输入、None、极大值
4. 参数化测试:多组输入输出
5. Mock所有外部依赖(DB/Redis/HTTP)
6. 覆盖率目标:100%行覆盖
7. 包含conftest.py共享fixture
踩坑实录
| 坑 | 表现 | 解决方案 |
|---|---|---|
| Mock对象类型不对 | assert_called_with失败 |
用isinstance检查Mock参数类型 |
| 异步测试报错 | RuntimeWarning: coroutine not awaited |
用@pytest.mark.asyncio装饰器 |
| 测试间数据污染 | 测试A的修改影响了测试B | 每个测试用独立fixture,事务回滚 |
| Mock不生效 | patch路径不对 | patch的是使用处不是定义处 |
| 覆盖率造假 | __init__.py拉低覆盖率 |
--cov-report=term-missing找出未覆盖行 |
总结
单元测试的核心价值是让你敢重构。MonkeyCode能帮你:
- 从现有代码反向生成完整单元测试
- 按TDD流程先写测试再写代码
- Mock所有外部依赖,测试纯快不依赖外部
- 生成
conftest.py共享fixture,减少重复代码
记住:测试覆盖率不是目标,快速定位Bug才是。优先测试核心业务逻辑和边界条件。

浙公网安备 33010602011771号