Python高级技术之:`pytest`的`fixture`:高级用法,如模块级、会话级`fixture`和自动发现。

各位观众,欢迎来到今天的Pytest高级技巧讲座!今天我们要聊的是Pytest中的“灵魂人物”——fixture,而且是高级用法哦! 准备好了吗?让我们一起深入fixture的世界,解锁模块级、会话级fixture以及自动发现的秘密!

1. 什么是Fixture?为什么要用它?

首先,给还没完全搞明白fixture的同学简单普及一下。fixture,顾名思义,就是测试用例的“固定装置”、“夹具”。它可以帮你做测试前的准备工作,比如初始化数据库连接、创建测试数据、启动服务器等等。

为什么需要fixture? 试想一下,如果没有fixture,每个测试用例都要写重复的初始化代码,那简直是程序员的噩梦!有了fixture,我们可以把这些重复的代码提取出来,集中管理,让测试用例更加简洁、易读、易维护。

举个例子,假设我们有一个测试模块,需要连接数据库才能进行测试。没有fixture的话,每个测试用例都要写连接数据库的代码:

import pytest
import sqlite3

def test_user_creation():
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
    conn.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Alice'")
    result = cursor.fetchone()
    assert result[1] == 'Alice'
    conn.close()

def test_user_deletion():
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
    conn.commit()

    cursor.execute("DELETE FROM users WHERE name = 'Bob'")
    conn.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Bob'")
    result = cursor.fetchone()
    assert result is None
    conn.close()

可以看到,test_user_creationtest_user_deletion 中重复了连接数据库、创建表、关闭连接的代码。有了 fixture,我们可以把这些代码提取出来:

import pytest
import sqlite3

@pytest.fixture
def db_connection():
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    yield conn  # 注意这里用了 yield,这是 fixture 的关键!
    conn.close() # Teardown 放在 yield 之后

def test_user_creation(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Alice'")
    result = cursor.fetchone()
    assert result[1] == 'Alice'

def test_user_deletion(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
    db_connection.commit()

    cursor.execute("DELETE FROM users WHERE name = 'Bob'")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Bob'")
    result = cursor.fetchone()
    assert result is None

现在,test_user_creationtest_user_deletion 简洁多了!它们只需要告诉 Pytest:“嘿,我需要一个 db_connection fixture”,Pytest 就会自动帮我们准备好数据库连接。

2. Fixture的作用域 (Scope):模块级、会话级

刚才的例子中,db_connection fixture的作用域默认是 function 级别的,也就是说,每个测试用例都会创建一个新的数据库连接。但有时候,我们希望同一个模块或整个测试会话只创建一个数据库连接,这样可以提高测试效率。这时候,就要用到 fixture 的作用域了。

fixture 的作用域有以下几种:

  • function (默认): 每个测试用例都会调用一次 fixture。
  • class: 每个测试类只会调用一次 fixture。
  • module: 每个测试模块只会调用一次 fixture。
  • package: 每个测试包只会调用一次 fixture。
  • session: 整个测试会话只会调用一次 fixture。

可以通过 scope 参数来指定 fixture 的作用域。

2.1 模块级 Fixture (module)

如果希望同一个模块中的测试用例共享同一个数据库连接,可以将 db_connection fixture 的作用域设置为 module

import pytest
import sqlite3

@pytest.fixture(scope="module")
def db_connection():
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    yield conn
    conn.close()

def test_user_creation(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Alice'")
    result = cursor.fetchone()
    assert result[1] == 'Alice'

def test_user_deletion(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
    db_connection.commit()

    cursor.execute("DELETE FROM users WHERE name = 'Bob'")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Bob'")
    result = cursor.fetchone()
    assert result is None

现在,test_user_creationtest_user_deletion 共享同一个数据库连接,只有在模块中的第一个测试用例执行之前才会创建连接,模块中最后一个测试用例执行完毕后才会关闭连接。

2.2 会话级 Fixture (session)

如果希望整个测试会话共享同一个数据库连接,可以将 db_connection fixture 的作用域设置为 session

import pytest
import sqlite3

@pytest.fixture(scope="session")
def db_connection():
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    yield conn
    conn.close()

def test_user_creation(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Alice'")
    result = cursor.fetchone()
    assert result[1] == 'Alice'

def test_user_deletion(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
    db_connection.commit()

    cursor.execute("DELETE FROM users WHERE name = 'Bob'")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Bob'")
    result = cursor.fetchone()
    assert result is None

现在,整个测试会话只会创建一个数据库连接。这对于需要长时间运行的测试非常有用,可以避免频繁创建和关闭连接的开销。

不同Scope的区别总结:

Scope 描述 创建次数
function 每个测试函数都会调用一次 fixture 每次调用测试函数都会重新创建。这是默认作用域。
class 每个测试类只会调用一次 fixture 每个测试类创建一次。所有该类内的测试函数共享同一个 fixture 实例。
module 每个测试模块只会调用一次 fixture 每个测试模块创建一次。模块中的所有测试函数共享同一个 fixture 实例。
package 每个测试包只会调用一次 fixture 每个测试包创建一次。包中的所有测试模块共享同一个 fixture 实例。 需要在conftest.py中使用,并且测试通过pytest --pyargs mypackage或者 --import-mode=importlib运行。
session 整个测试会话只会调用一次 fixture 在整个测试会话期间只创建一次。所有测试函数、类和模块共享同一个 fixture 实例。

3. Fixture的自动发现 (Autouse Fixture)

有时候,我们希望某些 fixture 在所有测试用例中自动生效,不需要显式地在测试用例中声明。这时候,可以使用 autouse=True 参数。

例如,假设我们希望在所有测试用例开始之前都打印一条日志:

import pytest
import logging

logging.basicConfig(level=logging.INFO)

@pytest.fixture(autouse=True)
def log_start():
    logging.info("Starting test...")
    yield
    logging.info("Test finished.")

def test_addition():
    assert 1 + 1 == 2

def test_subtraction():
    assert 2 - 1 == 1

现在,每次运行测试用例,都会在控制台看到 "Starting test…" 和 "Test finished." 的日志。

注意: 谨慎使用 autouse=True,因为它可能会导致一些意想不到的副作用。建议只在确实需要在所有测试用例中自动生效的 fixture 上使用。

4. conftest.py:Fixture的集中营

为了更好地组织和管理 fixture,我们可以把它们放在 conftest.py 文件中。conftest.py 是 Pytest 的一个特殊文件,它可以用来存放 fixture、插件和其他配置信息。Pytest 会自动发现 conftest.py 文件,并加载其中的 fixture。

例如,我们可以把 db_connection fixture 放在 conftest.py 文件中:

# conftest.py
import pytest
import sqlite3

@pytest.fixture(scope="session")
def db_connection():
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    yield conn
    conn.close()

然后,在测试模块中,可以直接使用 db_connection fixture,而不需要显式地导入:

# test_users.py
def test_user_creation(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Alice'")
    result = cursor.fetchone()
    assert result[1] == 'Alice'

def test_user_deletion(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
    db_connection.commit()

    cursor.execute("DELETE FROM users WHERE name = 'Bob'")
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = 'Bob'")
    result = cursor.fetchone()
    assert result is None

conftest.py 的好处是,它可以让测试代码更加简洁,避免重复定义 fixture。 另外,它可以让你更方便地在不同的测试模块之间共享 fixture。

5. Fixture的参数化 (Parametrization)

有时候,我们需要用不同的参数来运行同一个测试用例。例如,我们可能需要测试不同的数据库后端,或者使用不同的输入数据。这时候,可以使用 fixture 的参数化功能。

可以通过 pytest.mark.parametrize 装饰器来实现 fixture 的参数化。

例如,假设我们想测试不同的数据库连接字符串:

import pytest
import sqlite3

@pytest.fixture(params=[':memory:', 'test.db'])
def db_connection(request):
    conn = sqlite3.connect(request.param)
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    yield conn
    conn.close()

@pytest.mark.parametrize("user_name", ["Alice", "Bob"])
def test_user_creation(db_connection, user_name):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES (?)", (user_name,))
    db_connection.commit()

    cursor.execute("SELECT * FROM users WHERE name = (?)", (user_name,))
    result = cursor.fetchone()
    assert result[1] == user_name

在这个例子中,db_connection fixture 被参数化为 ':memory:''test.db' 两个值。test_user_creation@pytest.mark.parametrize 也进行了参数化,使用"Alice""Bob"两个名字。Pytest 会自动生成 2 * 2 = 4 个测试用例,分别使用不同的数据库连接字符串和用户名来运行。

6. Fixture的依赖 (Dependency)

Fixture 之间是可以相互依赖的。也就是说,一个 fixture 可以使用另一个 fixture 的返回值。

例如,假设我们有一个 api_client fixture,用于创建一个 API 客户端。我们还有一个 user fixture,它依赖于 api_client fixture,用于创建一个测试用户:

import pytest

@pytest.fixture
def api_client():
    # 模拟API客户端创建
    class MockApiClient:
        def create_user(self, username):
            print(f"Creating user {username} via API client")
            return {"id": 123, "username": username}  # 模拟API返回
    return MockApiClient()

@pytest.fixture
def user(api_client):
    user_data = api_client.create_user("testuser")
    return user_data

def test_user_creation(user):
    assert user["username"] == "testuser"
    assert user["id"] == 123

在这个例子中,user fixture 依赖于 api_client fixture。Pytest 会先执行 api_client fixture,然后把它的返回值传递给 user fixture。

7. 总结:Fixture的精髓

Fixture 是 Pytest 中非常强大的功能,它可以帮助我们编写更加简洁、易读、易维护的测试代码。

掌握 Fixture 的精髓:

  • 理解 Fixture 的作用:准备测试环境,提供测试数据。
  • 掌握 Fixture 的作用域:functionclassmodulesession
  • 学会使用 conftest.py:集中管理 Fixture。
  • 灵活运用 Fixture 的参数化:使用不同的参数运行测试用例。
  • 利用 Fixture 的依赖关系:构建复杂的测试环境。
  • 谨慎使用 autouse=True:避免意想不到的副作用。

掌握了这些技巧,你就可以像一位 Fixture 大师一样,轻松驾驭 Pytest,编写高质量的测试代码!

今天的讲座就到这里。希望大家有所收获!感谢收看!

发表回复

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