Python高级技术之:`unittest.TestCase`和`pytest`:两种测试框架的优劣对比。

各位观众老爷们,晚上好!我是今天的讲师,今天咱们聊聊Python测试界两大扛把子:unittest.TestCasepytest。这两个框架就像武林中的少林和武当,各有千秋,今天咱们就好好比划比划,看看谁更适合你。

开场白:测试,代码的保险丝

话说程序员写代码,就像盖房子。房子盖得再漂亮,地基不稳,迟早塌。测试就是给代码上保险,确保它按预期工作,不出幺蛾子。没有测试的代码,就跟没买保险的房子一样,住着心里没底。

第一回合:出身背景大PK

  • unittest.TestCase Python 内置模块,老牌劲旅,根正苗红。

  • pytest 第三方库,后起之秀,社区力量强大。

简单来说,unittest 是 Python “亲儿子”,安装完 Python 就能直接用;pytest 是“干儿子”,需要 pip install pytest 才能用。

第二回合:代码风格大比拼

  • unittest.TestCase 遵循 xUnit 架构,面向对象,继承 unittest.TestCase 类,使用 assert 方法进行断言。

  • pytest 更加灵活,函数式风格,不需要继承,使用 assert 语句进行断言,配合各种插件,能玩出花来。

咱们上代码,让大家看得更明白:

unittest 的例子:

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split() still works correctly
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

pytest 的例子:

def test_upper():
    assert 'foo'.upper() == 'FOO'

def test_isupper():
    assert 'FOO'.isupper()
    assert not 'Foo'.isupper()

def test_split():
    s = 'hello world'
    assert s.split() == ['hello', 'world']
    # pytest 会自动捕捉异常,所以不需要特殊处理
    # with pytest.raises(TypeError):  # pytest 的写法,但这里不需要
    try:
        s.split(2)
    except TypeError:
        pass  # 期望的异常
    else:
        assert False, "TypeError was not raised"

代码解读:

  • unittest 需要定义一个类,继承 unittest.TestCase,测试方法以 test_ 开头。断言使用 self.assertEqual, self.assertTrue 等方法。
  • pytest 直接定义函数,函数名以 test_ 开头,断言使用简单的 assert 语句。

结论: pytest 的代码更简洁,更 Pythonic。

第三回合:断言方式大对决

  • unittest.TestCase 提供一系列 assert 方法,例如 assertEqual, assertTrue, assertRaises 等。

  • pytest 直接使用 Python 的 assert 语句,更加灵活,可以配合 pytest.raises 处理异常。

再来个例子:

unittest 的异常处理:

import unittest

def divide(x, y):
    if y == 0:
        raise ValueError("Cannot divide by zero")
    return x / y

class TestDivide(unittest.TestCase):

    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

    def test_divide_positive(self):
        self.assertEqual(divide(10, 2), 5)

if __name__ == '__main__':
    unittest.main()

pytest 的异常处理:

import pytest

def divide(x, y):
    if y == 0:
        raise ValueError("Cannot divide by zero")
    return x / y

def test_divide_by_zero():
    with pytest.raises(ValueError) as excinfo:
        divide(10, 0)
    assert str(excinfo.value) == "Cannot divide by zero"  # 可以验证异常信息

def test_divide_positive():
    assert divide(10, 2) == 5

代码解读:

  • unittest 使用 self.assertRaises 上下文管理器来断言异常。
  • pytest 使用 pytest.raises 上下文管理器,并且可以捕获异常信息,进行更详细的断言。

结论: pytest 在异常处理方面更胜一筹,可以验证异常的具体内容。

第四回合:Fixture 大比拼

  • unittest.TestCase 使用 setUptearDown 方法进行测试环境的准备和清理。

  • pytest 使用 fixture 装饰器,更加灵活,可以定义作用域(function, class, module, session),并且可以自动注入到测试函数中。

代码示例:

unittest 的 setup 和 teardown:

import unittest

class TestDatabase(unittest.TestCase):

    def setUp(self):
        # 连接数据库,创建测试表
        self.db_conn = ... # 假设这里是数据库连接代码
        print("Setup: Connecting to database and creating test table")

    def tearDown(self):
        # 删除测试表,关闭数据库连接
        self.db_conn.close() # 假设这里是关闭数据库连接的代码
        print("Teardown: Dropping test table and closing database connection")

    def test_insert_data(self):
        # 测试插入数据
        print("Testing insert data")
        ... # 测试代码

    def test_query_data(self):
        # 测试查询数据
        print("Testing query data")
        ... # 测试代码

pytest 的 fixture:

import pytest

@pytest.fixture(scope="module")  # module 级别,整个模块只执行一次
def db_conn():
    # 连接数据库,创建测试表
    conn = ...  # 假设这里是数据库连接代码
    print("Setup: Connecting to database and creating test table")
    yield conn  # 将连接对象返回给测试函数
    # 删除测试表,关闭数据库连接
    conn.close() # 假设这里是关闭数据库连接的代码
    print("Teardown: Dropping test table and closing database connection")

def test_insert_data(db_conn):  # 自动注入 fixture
    # 测试插入数据
    print("Testing insert data")
    ... # 测试代码

def test_query_data(db_conn):  # 自动注入 fixture
    # 测试查询数据
    print("Testing query data")
    ... # 测试代码

代码解读:

  • unittestsetUptearDown 只能在测试类的开始和结束时执行,作用域有限。
  • pytestfixture 可以定义不同的作用域(function, class, module, session),并且可以自动注入到测试函数中,代码更简洁,更易于维护。yield 关键字实现了setup和teardown的功能。

结论: pytest 的 fixture 机制更加强大,灵活,代码可读性更高。

第五回合:参数化测试大比拼

  • unittest.TestCase 需要手动循环或者使用第三方库来实现参数化测试。

  • pytest 内置参数化功能,使用 pytest.mark.parametrize 装饰器,非常方便。

代码示例:

unittest 实现参数化:

import unittest

class TestAdd(unittest.TestCase):

    def test_add(self):
        test_cases = [
            (1, 2, 3),
            (0, 0, 0),
            (-1, 1, 0),
        ]
        for a, b, expected in test_cases:
            with self.subTest(a=a, b=b):
                self.assertEqual(a + b, expected)

if __name__ == '__main__':
    unittest.main()

pytest 实现参数化:

import pytest

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

代码解读:

  • unittest 需要手动循环测试用例,并且使用 subTest 来区分不同的测试用例。
  • pytest 使用 pytest.mark.parametrize 装饰器,代码更简洁,可读性更高。

结论: pytest 的参数化功能更加方便,易用。

第六回合:插件生态大比拼

  • unittest.TestCase 插件生态相对较弱,需要自己编写或者寻找第三方库。

  • pytest 拥有强大的插件生态系统,各种插件应有尽有,例如 pytest-cov (代码覆盖率), pytest-django (Django 测试), pytest-xdist (并行测试) 等。

例子:代码覆盖率

使用 pytest-cov 可以轻松生成代码覆盖率报告:

  1. 安装插件:pip install pytest-cov
  2. 运行测试:pytest --cov=. --cov-report term-missing

运行完成后,会生成代码覆盖率报告,告诉你哪些代码被测试覆盖,哪些没有被覆盖。

结论: pytest 的插件生态更加丰富,可以满足各种测试需求。

第七回合:测试发现大比拼

  • unittest.TestCase 需要手动编写测试套件或者使用 unittest.TestLoader 来发现测试用例。

  • pytest 自动发现测试用例,只需要按照约定命名测试文件和函数即可。

代码示例:

假设有以下目录结构:

my_project/
├── src/
│   └── my_module.py
└── tests/
    └── test_my_module.py

unittest 需要在 tests/test_my_module.py 中手动加载测试用例:

import unittest
from src import my_module

class TestMyModule(unittest.TestCase):
    ... # 测试用例

if __name__ == '__main__':
    unittest.main()

或者使用 unittest.TestLoader

import unittest

loader = unittest.TestLoader()
suite = loader.discover('tests') # 自动发现 tests 目录下的测试用例

runner = unittest.TextTestRunner()
runner.run(suite)

pytest 只需要运行 pytest 命令,它会自动发现 tests 目录下的 test_*.py*_test.py 文件,并执行其中的测试用例。

结论: pytest 的测试发现机制更加简单,方便。

第八回合:兼容性大比拼

  • unittest.TestCase 兼容性好,Python 内置模块,无需额外安装。

  • pytest 需要安装,但兼容性也不错,支持 Python 2.7+ 和 Python 3+。

总结:

特性 unittest.TestCase pytest 优势
出身 Python 内置 第三方库 无需安装
代码风格 面向对象 函数式 更简洁,更 Pythonic
断言 assert 方法 assert 语句 更灵活,可以验证异常信息
Fixture setUp/tearDown fixture 装饰器 作用域更灵活,可以自动注入
参数化 手动循环 pytest.mark.parametrize 更方便,易用
插件生态 较弱 强大 各种插件应有尽有
测试发现 手动加载 自动发现 更简单,方便
兼容性 都很好

选择建议:

  • 新手入门: 如果你是 Python 测试新手,建议从 unittest 开始,掌握基本的测试概念和方法。
  • 项目迭代: 如果你的项目已经使用了 unittest,并且运行良好,可以继续使用。
  • 拥抱社区: 如果你想体验更强大的功能和更丰富的插件生态,或者你喜欢简洁的代码风格,建议选择 pytest

最后的忠告:

无论选择哪个框架,最重要的是编写高质量的测试代码,确保你的代码稳定可靠。 记住,测试不是负担,而是保障。

好了,今天的讲座就到这里,感谢各位的观看!希望大家都能写出高质量的 Python 代码!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注