写一个测试

新建文件 test_xxx.py,用 test_ 开头的函数:

# test_calc.py
def add(a, b):
    return a + b

def test_add():
    assert add(1, 2) == 3

def test_add_negative():
    assert add(-1, -1) == -2

运行:

pip install pytest
pytest                    # 自动发现并跑所有 test_*.py

用 assert,不用 self.assertEqual

pytest 比 unittest 简洁多了:

# unittest(老)
self.assertEqual(actual, expected)
self.assertTrue(x)
self.assertIn(item, container)

# pytest(新)
assert actual == expected
assert x
assert item in container

pytest 报错时会自动展示对比信息——不需要手写 message。

fixture:测试用例的"准备工作"

import pytest

@pytest.fixture
def sample_user():
    return {"name": "WadeLy", "age": 30}

def test_name(sample_user):
    assert sample_user["name"] == "WadeLy"

def test_age(sample_user):
    assert sample_user["age"] == 30

把 fixture 名当函数参数,pytest 自动注入。

fixture 的清理

@pytest.fixture
def db():
    conn = connect()
    yield conn          # 测试用例拿到的是 conn
    conn.close()        # 测试结束后自动清理

跟上下文管理器异曲同工。

conftest.py:跨文件共享 fixture

放一个 conftest.py 在测试目录里,里面的 fixture 自动被所有测试看见:

# conftest.py
import pytest

@pytest.fixture
def app():
    return create_test_app()

# 任意 test_*.py 都能直接用 app
def test_index(app):
    ...

parametrize:一个测试跑多组数据

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

跑出来是 4 个测试用例,分别打印通过/失败。

测试异常

def divide(a, b):
    if b == 0:
        raise ValueError("b 不能为 0")
    return a / b

def test_divide_zero():
    with pytest.raises(ValueError, match="不能为 0"):
        divide(10, 0)

mock:替换外部依赖

from unittest.mock import patch

def fetch_user(uid):
    return requests.get(f"https://api/{uid}").json()

def test_fetch_user():
    with patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"name": "WadeLy"}
        result = fetch_user(1)
        assert result["name"] == "WadeLy"

不真发 HTTP——把 requests.get 换成假货。

或者用 fixture 形式:

def test_fetch_user(monkeypatch):
    monkeypatch.setattr("requests.get", lambda url: FakeResponse(...))
    ...

标记 skip / xfail

@pytest.mark.skip(reason="还没实现")
def test_future_feature(): ...

@pytest.mark.skipif(sys.platform == "win32", reason="只在 Linux 跑")
def test_unix_only(): ...

@pytest.mark.xfail
def test_known_bug(): ...

覆盖率

pip install pytest-cov
pytest --cov=mypackage --cov-report=html

生成 HTML 报告,告诉你哪些行没被测到。

项目结构推荐

myproject/
├── src/
│   └── mypkg/
│       ├── __init__.py
│       └── calc.py
└── tests/
    ├── conftest.py
    ├── test_calc.py
    └── test_api.py

pytest tests/ 跑全部。

下一篇讲调试技巧