各位观众,欢迎来到今天的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_creation
和 test_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_creation
和 test_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_creation
和 test_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 的作用域:
function
、class
、module
、session
。 - 学会使用
conftest.py
:集中管理 Fixture。 - 灵活运用 Fixture 的参数化:使用不同的参数运行测试用例。
- 利用 Fixture 的依赖关系:构建复杂的测试环境。
- 谨慎使用
autouse=True
:避免意想不到的副作用。
掌握了这些技巧,你就可以像一位 Fixture 大师一样,轻松驾驭 Pytest,编写高质量的测试代码!
今天的讲座就到这里。希望大家有所收获!感谢收看!