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能帮你:

  1. 从现有代码反向生成完整单元测试
  2. 按TDD流程先写测试再写代码
  3. Mock所有外部依赖,测试纯快不依赖外部
  4. 生成conftest.py共享fixture,减少重复代码

记住:测试覆盖率不是目标,快速定位Bug才是。优先测试核心业务逻辑和边界条件。

posted @ 2026-05-29 21:49  机房管理员  阅读(30)  评论(0)    收藏  举报