C#转Python第4.5篇:单元测试:pytest vs xUnit/NUnit

在 C# 里写单元测试,用 xUnitNUnit------[Fact][Theory][InlineData]Assert.Equal(),测试方法要标记特性,测试类要遵循命名规范,测试项目要单独创建。

在 Python 里写单元测试,用 pytest------函数名以 test_ 开头就行,没有特性标记,没有测试类要求,测试文件名以 test_ 开头就行。简单得像在写普通函数。

当我第一次在 Python 里用 pytest 写测试的时候,我的内心是:"这就完了?我的 [Fact] 呢?我的 Assert.Equal() 呢?我的测试类呢?"

后来我才明白,Python 的测试哲学是:测试就是普通函数,不需要额外的仪式感 。而 C# 的测试哲学是:测试需要明确的标记和结构,便于发现和执行

Python 的测试哲学是:测试就是普通函数,不需要额外的仪式感。

基础语法对比

C# 版本(xUnit):

复制代码
public class CalculatorTests
{
    [Fact]
    public void Add_TwoNumbers_ReturnsSum()
    {
        var calculator = new Calculator();
        int result = calculator.Add(2, 3);
        Assert.Equal(5, result);
    }

    [Theory]
    [InlineData(1, 1, 2)]
    [InlineData(2, 3, 5)]
    [InlineData(-1, 1, 0)]
    public void Add_MultipleCases_ReturnsCorrectSum(int a, int b, int expected)
    {
        var calculator = new Calculator();
        int result = calculator.Add(a, b);
        Assert.Equal(expected, result);
    }
}

Python 版本(pytest):

复制代码
def add(a, b):
    return a + b

def test_add_two_numbers():
    result = add(2, 3)
    assert result == 5

def test_add_multiple_cases():
    assert add(1, 1) == 2
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

对比一下:

对比项 C#(xUnit) Python(pytest)
测试标记 [Fact][Theory] 函数名 test_ 开头
参数化 [InlineData] @pytest.mark.parametrize
断言 Assert.Equal(expected, actual) assert expected == actual
测试类 必须,但可以没有 可选
测试文件 必须单独项目 文件名 test_ 开头
运行 dotnet test pytest

Python 的测试像在写普通函数------函数名以 test_ 开头就行,没有额外的仪式感。C# 的测试像在写合同------每个测试方法都得标记 [Fact],参数化测试得用 [Theory] + [InlineData]

参数化测试

C# 版本(xUnit):

复制代码
[Theory]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
public void Add_ReturnsCorrectSum(int a, int b, int expected)
{
    var calculator = new Calculator();
    int result = calculator.Add(a, b);
    Assert.Equal(expected, result);
}

Python 版本(pytest):

复制代码
import pytest

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

C# 用 [InlineData] 一行一个测试用例,Python 用 @pytest.mark.parametrize 传入参数列表。Python 的方式更灵活------你可以用函数生成参数:

复制代码
def generate_test_cases():
    return [(i, i, i*2) for i in range(10)]

@pytest.mark.parametrize("a, b, expected", generate_test_cases())
def test_add_generated_cases(a, b, expected):
    assert add(a, b) == expected

C# 做不到这么灵活------[InlineData] 的参数必须是编译时常量。

Fixture(测试夹具)

C# 版本(xUnit):

复制代码
public class DatabaseTests : IDisposable
{
    private readonly DbContext _context;

    public DatabaseTests()
    {
        _context = new DbContext();
        _context.Database.EnsureCreated();
    }

    public void Dispose()
    {
        _context.Database.EnsureDeleted();
        _context.Dispose();
    }

    [Fact]
    public void AddUser_SavesToDatabase()
    {
        _context.Users.Add(new User { Name = "Alice" });
        _context.SaveChanges();

        Assert.Single(_context.Users);
    }
}

Python 版本(pytest):

复制代码
import pytest

@pytest.fixture
def db_session():
    session = create_session()
    yield session
    session.close()

def test_add_user_saves_to_database(db_session):
    db_session.add(User(name="Alice"))
    db_session.commit()

    assert db_session.query(User).count() == 1

C# 用构造函数和 IDisposable 来管理测试夹具,Python 用 @pytest.fixtureyield。Python 的方式更灵活------你可以在 yield 前后做任何清理工作。

Mock(模拟)

C# 版本(Moq):

复制代码
public class UserServiceTests
{
    [Fact]
    public void GetUser_ReturnsUserFromRepository()
    {
        var mockRepo = new Mock<IUserRepository>();
        mockRepo.Setup(r => r.GetById(1)).Returns(new User { Name = "Alice" });

        var service = new UserService(mockRepo.Object);
        var user = service.GetUser(1);

        Assert.Equal("Alice", user.Name);
        mockRepo.Verify(r => r.GetById(1), Times.Once);
    }
}

Python 版本(pytest-mock):

复制代码
from unittest.mock import Mock, patch

def test_get_user_returns_user_from_repository():
    mock_repo = Mock()
    mock_repo.get_by_id.return_value = User(name="Alice")

    service = UserService(mock_repo)
    user = service.get_user(1)

    assert user.name == "Alice"
    mock_repo.get_by_id.assert_called_once_with(1)

C# 用 Moq 库,Python 用内置的 unittest.mock。Python 的 Mock 更灵活------你可以用 patch 来替换整个模块的函数:

复制代码
@patch('my_module.requests.get')
def test_api_call(mock_get):
    mock_get.return_value.json.return_value = {"status": "ok"}
    result = call_api()
    assert result["status"] == "ok"

异常测试

C# 版本(xUnit):

复制代码
[Fact]
public void Divide_ByZero_ThrowsDivideByZeroException()
{
    var calculator = new Calculator();
    Assert.Throws<DivideByZeroException>(() => calculator.Divide(10, 0));
}

Python 版本(pytest):

复制代码
import pytest

def test_divide_by_zero_raises_exception():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

C# 用 Assert.Throws<T>(),Python 用 pytest.raises()。两者都支持断言异常消息:

复制代码
// C#
var ex = Assert.Throws<DivideByZeroException>(() => calculator.Divide(10, 0));
Assert.Contains("除以零", ex.Message);

# Python
with pytest.raises(ZeroDivisionError, match="除以零"):
    divide(10, 0)

测试覆盖率

C# 用 coverlet

复制代码
<!-- .csproj -->
<ItemGroup>
    <PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>

dotnet test /p:CollectCoverage=true

Python 用 pytest-cov

复制代码
pip install pytest-cov
pytest --cov=my_module --cov-report=html

两者都能生成覆盖率报告,但 Python 的方式更简单------不需要修改项目文件。

测试发现

C# 的测试发现:

复制代码
dotnet test  # 自动发现所有 [Fact] 和 [Theory] 方法

Python 的测试发现:

复制代码
pytest  # 自动发现所有 test_*.py 文件中的 test_* 函数

Python 的测试发现更灵活------你可以用 conftest.py 来配置测试发现规则。

参数化测试的高级用法

C# 版本(xUnit):

复制代码
[Theory]
[MemberData(nameof(TestCases))]
public void Add_ReturnsCorrectSum(int a, int b, int expected)
{
    var calculator = new Calculator();
    int result = calculator.Add(a, b);
    Assert.Equal(expected, result);
}

public static IEnumerable<object[]> TestCases => new List<object[]>
{
    new object[] { 1, 1, 2 },
    new object[] { 2, 3, 5 },
    new object[] { -1, 1, 0 }
};

Python 版本(pytest):

复制代码
import pytest

def generate_test_cases():
    return [(i, i, i*2) for i in range(10)]

@pytest.mark.parametrize("a, b, expected", generate_test_cases())
def test_add_returns_correct_sum(a, b, expected):
    assert add(a, b) == expected

Python 的参数化更灵活------你可以用函数生成参数,C# 的 [MemberData] 也能做到,但语法更复杂。

迁移指南:C# 开发者最容易犯的错

  1. 测试函数命名 :C# 的测试方法名可以很长(Add_TwoNumbers_ReturnsSum),Python 的测试函数名用下划线分隔(test_add_two_numbers_returns_sum

  2. 测试类可选:Python 的测试类不是必须的,函数也能做测试

  3. fixture 用 yield :Python 的 fixture 用 yield 来分隔 setup 和 teardown

  4. Mock 用内置库 :Python 的 unittest.mock 已经足够强大,不需要第三方库

坑点提醒

测试函数没有 test_ 前缀------pytest 不会发现:

复制代码
def add_test():  # 这不是测试函数!
    assert add(1, 1) == 2

def test_add():  # 这才是测试函数
    assert add(1, 1) == 2

fixture 作用域------默认是函数级别:

复制代码
@pytest.fixture
def db_session():
    session = create_session()
    yield session
    session.close()  # 每个测试函数都会执行一次

@pytest.fixture(scope="module")
def db_session():
    session = create_session()
    yield session
    session.close()  # 整个模块只执行一次

Mock 的 side_effect------模拟异常:

复制代码
mock_repo.get_by_id.side_effect = Exception("数据库错误")

真实案例 :我有个同事从 C# 转 Python,写了一个 API 测试脚本。他用 unittest.TestCase 来组织测试,每个测试方法都继承 TestCase。后来发现用 pytest 可以直接写函数,不需要继承任何类,代码量减少了一半。

测试插件生态

Python 的 pytest 有丰富的插件:

插件 功能
pytest-cov 覆盖率
pytest-xdist 并行测试
pytest-mock Mock
pytest-asyncio 异步测试
pytest-django Django 测试
pytest-flask Flask 测试

C# 的 xUnit/NUnit 也有插件,但 Python 的插件生态更丰富。

一句话总结

C# 的测试像在写合同------每个测试方法都得标记 [Fact];Python 的测试像在写普通函数------函数名以 test_ 开头就行,简单直接。

第四阶段的五篇文章到这里就全部完成了。下一篇咱们进入第五阶段,聊聊异步编程和高级特性------C# 的 async/await vs Python 的 asyncio,两种语言的"异步哲学"又有啥不同。


📦 示例代码:C# 转 Python 全系列配套练习代码(含 48 章示例)

💬 欢迎点赞、收藏、转发,你的支持是我持续创作的动力!

相关推荐
2601_962300811 小时前
Python + Requests + Pytest + Excel + Allure:接口自动化测试项目实战
python·excel·pytest·allure·requests
Zane19941 小时前
两个线程算两遍循环,为什么只快了一点点不是两倍?GIL连环追问
后端·python
重生之小比特1 小时前
【Java SE】程序逻辑控制
java·开发语言·python
小灰灰搞电子1 小时前
Python 函数参数分隔符 *:Keyword-Only Arguments 原理与实践
开发语言·python
2601_962297251 小时前
Authlib 0.13通用Python认证授权库wheel安装包(支持Python 2/3)
python·jwt·oauth2.0·authlib·openidconnect
SamChan901 小时前
大文件多语言PDF翻译性能实测:300页文档的耗时、内存占用与失败率分析
python·ai·pdf·机器翻译
GIS数据转换器1 小时前
遥感GIS一体化技术应用平台
大数据·人工智能·python·安全·数据挖掘
2601_966949651 小时前
批量 K 线不只是提速:因子研究中的数据质量、复权与回测偏差
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
一位正在转型AI全栈的前端工程师1 小时前
AI 全栈学习之旅 -Week 11:从 CLI 到浏览器:用 FastAPI、SSE 和 Vue 3 做一个可审核的 ReAct Agent
前端·python