【python 第三方库】pytest

2024-01-25 00:00:00

目录:

pytest 模块介绍

pytest 是一个 Python 测试框架,用于测试代码,支持单元测试、集成测试、功能测试等。

安装依赖

$ pip3 install pytest

命名规则

文件名:文件名应以 test_ 开头或以 _test 结尾。例如 test_example.py 或 example_test.py;

测试类:类名应以 Test 开头(例如 TestExample),且类中不应包含 __init__ 方法;

测试函数:函数名或方法名应以 test_ 开头(例如 test_addition)。不符合此命名模式的函数将不会被执行;

运行规则

运行所有用例:在项目根目录直接执行 pytest;

运行指定目录:pytest tests/;

运行指定文件:pytest test_sample.py;

运行指定类:pytest test_demo2.py::TestDemo;

运行指定方法:pytest test_demo2.py::TestDemo::test_demo1;

在代码中运行:可以在脚本中调用 pytest.main() 来执行:

if __name__ == '__main__':
  pytest.main(["-s", "test_sample.py"])  # -s 用于显示打印输出

其中:pytest 命令有一些常见参数,介绍如下:

  • -v:显示详细的测试信息,包括每个用例的名称和结果;
  • -s:显示测试用例中 print 等语句的输出信息;
  • -k:只运行名称中包含指定关键词的用例,如 pytest -k “demo”;
  • -m:运行被特定标记(mark)装饰的用例,需要先在 pytest.ini 配置文件中注册标记

函数测试用例

# test_sample.py
def inc(x):
  return x + 1

def test_answer():
  assert inc(3) == 5  # 此断言会失败

在包含 test_sample.py 文件的目录下执行 pytest 命令。

$ pytest
================================= test session starts ==================================
platform darwin -- Python 3.11.1, pytest-8.3.3, pluggy-1.5.0
sensitiveurl: .*
rootdir: /Users/dkvirus/Documents
plugins: variables-3.1.0, html-4.1.1, metadata-3.1.1, allure-pytest-2.13.5, ordering-0.6, rerunfailures-15.0, base-url-2.1.0, anyio-3.6.2, xdist-3.6.1, selenium-4.1.0
collected 1 item                                                                       

test_sample.py F                                                                 [100%]

======================================= FAILURES =======================================
_____________________________________ test_answer ______________________________________

    def test_answer():
>       assert inc(3) == 5  # 此断言会失败
E       assert 4 == 5
E        +  where 4 = inc(3)

test_sample.py:5: AssertionError
=============================== short test summary info ================================
FAILED test_sample.py::test_answer - assert 4 == 5
================================== 1 failed in 0.06s ===================================

输出信息中,一个点 . 代表一个测试通过,F 代表失败,并会显示详细的断言错误信息。

类测试用例

class TestDemo:
    def test_demo1(self):
        assert 11 == 11  # 通过

    def test_demo2(self):
        assert 22 == 21  # 失败

运行 pytest 后,会清晰展示类中每个测试方法的结果。

$ pytest
================================= test session starts ==================================
platform darwin -- Python 3.11.1, pytest-8.3.3, pluggy-1.5.0
sensitiveurl: .*
rootdir: /Users/dkvirus/Documents/vscode-docs
plugins: variables-3.1.0, html-4.1.1, metadata-3.1.1, allure-pytest-2.13.5, ordering-0.6, rerunfailures-15.0, base-url-2.1.0, anyio-3.6.2, xdist-3.6.1, selenium-4.1.0
collected 2 items                                                                      

test_sample.py .F                                                                [100%]

======================================= FAILURES =======================================
_________________________________ TestDemo.test_demo2 __________________________________

self = <test_sample.TestDemo object at 0x107aa5410>

    def test_demo2(self):
>       assert 22 == 21  # 失败
E       assert 22 == 21

test_sample.py:6: AssertionError
=============================== short test summary info ================================
FAILED test_sample.py::TestDemo::test_demo2 - assert 22 == 21
============================= 1 failed, 1 passed in 0.06s ==============================

输出信息最后一行,可以看到 1failed, 1 passed 表示类中的两个方法,一个测试通过,一个测试失败。

参数化测试

@pytest.mark.parametrize,参数化可以避免为多组输入输出数据编写重复的测试代码,极大提高效率。

import pytest

@pytest.mark.parametrize(
    "test_input, expected", 
    [("3+5", 8), ("'2'+'4'", "24"), ("6*9", 54)]
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected

执行时,test_eval 函数会分别使用三组参数各运行一次。

Fixture

@pytest.fixture 通过 yield 语句将测试执行过程分为 “前置” 和 “后置” 两部分,前置代码在测试用例执行前运行,用于准备测试环境;后置代码在测试用例执行后运行,用于清理资源。

import pytest
import tempfile
import os

@pytest.fixture
def temp_file():
    # 前置:创建临时文件
    file = tempfile.NamedTemporaryFile(mode='w', delete=False)
    file.write("测试数据")
    file.close()
    
    yield file.name  # 将文件名传递给测试用例
    
    # 后置:清理临时文件
    os.unlink(file.name)

def test_file_operations(temp_file):
    """测试文件读取操作"""
    with open(temp_file, 'r') as f:
        content = f.read()
    assert content == "测试数据"
    print(f"测试文件:{temp_file}")

在这个示例中:

  • temp_file fixture 创建了一个临时文件并写入数据;
  • yield file.name 将文件名传递给 test_file_operations 测试函数;
  • 测试结束后自动执行 os.unlink(file.name) 删除文件;
  • 测试函数只需关注业务逻辑,无需管理文件的创建和清理;

全局 Fixture

当多个测试文件需要共享相同的 Fixture 时,可以将其放在 conftest.py 文件中,pytest 会自动发现并加载这个文件中的 Fixture。

项目目录结构如下:

project/
├── conftest.py      # 共享 Fixture 定义,文件名不能改变
├── test_api.py      # API测试
└── test_db.py       # 数据库测试

conftest.py 内容如下:

import pytest
import requests

@pytest.fixture
def api_session():
    """创建共享的HTTP会话"""
    session = requests.Session()
    # 可以在这里设置通用请求头
    session.headers.update({'User-Agent': 'pytest-test-client'})
    
    yield session
    
    # 测试结束后关闭会话
    session.close()

@pytest.fixture(scope='module')
def db_connection():
    """模块级别的数据库连接"""
    import sqlite3
    conn = sqlite3.connect(':memory:')
    
    # 创建测试表
    conn.execute('''CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)''')
    conn.execute("INSERT INTO users (name) VALUES ('测试用户')")
    conn.commit()
    
    yield conn
    
    conn.close()

test_api.py 中使用共享 Fixture:

def test_get_user(api_session):
    """测试获取用户信息"""
    # 使用共享的api_session,无需重复创建
    response = api_session.get(' https://api.example.com/users')
    assert response.status_code == 200

def test_create_user(api_session):
    """测试创建用户"""
    data = {'name': '新用户'}
    response = api_session.post(' https://api.example.com/users', json=data)
    assert response.status_code == 201

test_db.py 中使用共享 Fixture:

def test_user_query(db_connection):
    """测试数据库查询"""
    cursor = db_connection.cursor()
    cursor.execute("SELECT * FROM users WHERE name = '测试用户'")
    result = cursor.fetchone()
    assert result is not None
    assert result[1] == '测试用户'

Fixture 作用域

Fixture 可以通过 scope 参数指定作用域,默认为 ‘function’。

function 作用域

每个测试函数都会重新创建 Fixture,适合需要完全隔离的测试环境。

@pytest.fixture(scope='function')
def fresh_user():
    """每个测试函数都创建一个新用户"""
    user = {'id': 1, 'name': '临时用户', 'score': 0}
    yield user
    print(f"清理用户:{user['name']}")

def test_user_score_1(fresh_user):
    fresh_user['score'] += 10
    assert fresh_user['score'] == 10

def test_user_score_2(fresh_user):
    # 这是一个全新的用户对象,score为初始值0
    fresh_user['score'] += 20
    assert fresh_user['score'] == 20

class 作用域

同一个测试类中的所有测试方法共享同一个 Fixture 实例。

@pytest.fixture(scope='class')
class TestCalculator:
    @pytest.fixture(scope='class')
    def calculator(self):
        """整个测试类共享一个计算器实例"""
        calc = Calculator()
        print("创建计算器实例")
        yield calc
        print("清理计算器实例")
    
    def test_addition(self, calculator):
        result = calculator.add(2, 3)
        assert result == 5
    
    def test_subtraction(self, calculator):
        # 使用同一个calculator实例
        result = calculator.subtract(5, 2)
        assert result == 3

module 作用域

同一个模块(文件)中的所有测试共享 Fixture。

# test_module.py
@pytest.fixture(scope='module')
def config_data():
    """整个模块共享配置数据"""
    config = load_config('test_config.json')
    print("加载模块配置")
    yield config
    print("清理模块配置")

def test_feature_a(config_data):
    assert config_data['feature_a'] is True

def test_feature_b(config_data):
    assert config_data['feature_b_enabled'] is False

session 作用域

整个测试会话(一次 pytest 执行)期间只创建一次 Fixture。

# conftest.py
@pytest.fixture(scope='session')
def global_config():
    """全局配置,整个测试会话只加载一次"""
    config = {
      'api_base_url': 'https://api.example.com',
      'timeout': 30,
      'max_retries': 3
    }
    print("=== 加载全局配置 ===")
    yield config
    print("=== 清理全局配置 ===")

Fixture 参数化

Fixture 可以通过 params 参数实现参数化,为同一个测试逻辑提供多组测试数据。

import pytest

@pytest.fixture(params=[
  {'username': 'user1', 'password': 'pass123', 'expected': True},
  {'username': 'user2', 'password': 'wrong', 'expected': False},
  {'username': '', 'password': 'pass123', 'expected': False},
  {'username': 'user3', 'password': '', 'expected': False}
])
def login_test_data(request):
  """参数化的登录测试数据"""
  data = request.param
  yield data

def test_login(login_test_data):
  """使用参数化数据测试登录功能"""
  result = login(
      username=login_test_data['username'],
      password=login_test_data['password']
  )
  assert result == login_test_data['expected']
  print(f"测试:{login_test_data['username']} -> {'成功' if result else '失败'}")

运行这个测试时,pytest 会自动执行 4 次 test_login,每次使用不同的测试数据。

Fixture 自动应用

Fixture 可以通过 autouse=True 参数实现自动应用。

import pytest
import logging
import time

@pytest.fixture(autouse=True)
def setup_logging():
    """自动设置日志,所有测试都会应用"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    print("=== 测试开始,日志已配置 ===")

@pytest.fixture(autouse=True, scope='function')
def test_timer():
    """自动计时每个测试的执行时间"""
    start_time = time.time()
    yield
    end_time = time.time()
    duration = end_time - start_time
    print(f"测试执行时间: {duration:.3f}秒")

def test_quick_operation():
    time.sleep(0.1)
    assert True

def test_slow_operation():
    time.sleep(0.5)
    assert True

执行 pytest -s 可以看到打印了每个测试案例的执行时间。

Fixture 嵌套使用

Fixture 可以依赖其他 Fixture,形成嵌套结构,用于构建复杂的测试环境。

import pytest

@pytest.fixture
def database():
    """基础数据库连接"""
    db = Database('test.db')
    db.connect()
    yield db
    db.disconnect()

@pytest.fixture
def empty_table(database):
    """清空用户表"""
    database.execute("DELETE FROM users")
    yield database
    # 测试后不需要特殊清理

@pytest.fixture
def sample_users(database):
    """预置样本用户数据"""
    users = [
        {'id': 1, 'name': 'Alice', 'age': 25},
        {'id': 2, 'name': 'Bob', 'age': 30},
        {'id': 3, 'name': 'Charlie', 'age': 35}
    ]
    for user in users:
        database.insert('users', user)
    yield database
    database.execute("DELETE FROM users")

def test_empty_database(empty_table):
    """测试空数据库"""
    count = empty_table.count('users')
    assert count == 0

def test_sample_data(sample_users):
    """测试有样本数据的数据库"""
    count = sample_users.count('users')
    assert count == 3
    
    user = sample_users.query('users', id=2)
    assert user['name'] == 'Bob'

Fixture 在 Web 自动化测试中应用

import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By

@pytest.fixture(scope='session')
def browser():
    """全局浏览器实例"""
    driver = webdriver.Chrome()
    driver.implicitly_wait(10)
    driver.maximize_window()
    
    yield driver
    
    driver.quit()

@pytest.fixture
def logged_in_user(browser):
    """已登录的用户会话"""
    # 访问登录页面
    browser.get(' https://example.com/login')
    
    # 输入凭据
    browser.find_element(By.ID, 'username').send_keys('testuser')
    browser.find_element(By.ID, 'password').send_keys('testpass')
    browser.find_element(By.ID, 'login-btn').click()
    
    # 验证登录成功
    assert 'Dashboard' in browser.title
    
    yield browser
    
    # 登出
    browser.find_element(By.ID, 'logout-btn').click()

def test_user_profile(logged_in_user):
    """测试用户个人资料页面"""
    browser = logged_in_user
    browser.get(' https://example.com/profile')
    
    # 验证页面元素
    username = browser.find_element(By.CLASS_NAME, 'username').text
    assert username == 'testuser'
    
    # 测试个人资料更新
    browser.find_element(By.ID, 'edit-profile').click()
    # ... 更多测试操作

Fixture 在 API 测试中应用

import pytest
import requests

@pytest.fixture
def auth_token():
    """获取认证令牌"""
    response = requests.post(
        ' https://api.example.com/auth/login',
        json={'username': 'test', 'password': 'test123'}
    )
    assert response.status_code == 200
    token = response.json()['token']
    
    yield token
    
    # 注销
    requests.post(
        ' https://api.example.com/auth/logout',
        headers={'Authorization': f'Bearer {token}'}
    )

@pytest.fixture
def authenticated_session(auth_token):
    """带认证的请求会话"""
    session = requests.Session()
    session.headers.update({
        'Authorization': f'Bearer {auth_token}',
        'Content-Type': 'application/json'
    })
    
    yield session
    
    session.close()

@pytest.fixture
def test_user(authenticated_session):
    """创建测试用户"""
    user_data = {
        'name': 'Fixture测试用户',
        'email': 'fixture_test@example.com',
        'role': 'tester'
    }
    
    response = authenticated_session.post(
        ' https://api.example.com/users',
        json=user_data
    )
    assert response.status_code == 201
    user_id = response.json()['id']
    
    yield user_id
    
    # 清理:删除测试用户
    authenticated_session.delete(f' https://api.example.com/users/ {user_id}')

def test_user_operations(test_user, authenticated_session):
    """测试用户相关操作"""
    user_id = test_user
    
    # 1. 获取用户信息
    response = authenticated_session.get(
        f' https://api.example.com/users/ {user_id}'
    )
    assert response.status_code == 200
    user_info = response.json()
    assert user_info['email'] == 'fixture_test@example.com'
    
    # 2. 更新用户信息
    update_data = {'name': '更新后的名称'}
    response = authenticated_session.patch(
        f' https://api.example.com/users/ {user_id}',
        json=update_data
    )
    assert response.status_code == 200
    
    # 3. 验证更新
    response = authenticated_session.get(
        f' https://api.example.com/users/ {user_id}'
    )
    assert response.json()['name'] == '更新后的名称'

生成 HTML 测试报告

安装依赖

$ pip3 install pytest-html

在执行测试时添加 –html 参数:

$ pytest --html=report.html

返回首页

本文总阅读量  次
皖ICP备17026209号-3
总访问量: 
总访客量: