前言
你是否曾遇到过这样的困境:自动化测试运行失败,但终端输出的信息杂乱无章,难以定位问题?或者当你想回顾上周的测试结果时,却发现没有任何历史记录可寻?
一个专业、健壮的日志系统,是衡量一个自动化测试框架成熟度的关键标准。它不仅是调试的利器,更是质量追溯的基石。
本文将以一个真实的电商API测试项目为例,手把手带你走过从零开始集成日志,到解决 pytest
环境下的各种“水土不服”,最终实现一个完美的、可用于生产环境的日志系统的全过程。
第一步:明确我们的目标
在开始写代码前,我们先定义一个完美的日志系统应该具备哪些特质:
双重输出:日志既要在控制台实时显示,方便我们观察运行过程;也要能自动写入日志文件,以便于长期保存和分析。
按天归档:日志文件应按日期自动创建(如
2025-09-24.log
),方便按天回溯问题。日志追加:当天多次运行测试时,新的日志应该追加到当天的文件中,而不是覆盖旧内容。
内容详细:日志信息应包含时间戳、级别、来源(哪个文件、哪个函数、哪一行)、以及最重要的——当前正在运行的是哪个测试用例。
格式优美:通过分隔符和层次化的信息,让日志清晰易读。
第二步:遭遇“拦路虎”——Pytest的日志捕获机制
很多初学者(包括我们最初的尝试)会直接使用Python内置的logging
模块在代码中配置日志。然而,当我们在终端运行pytest
时,会发现一个棘手的问题:终端有日志,但日志文件要么不生成,要么就是空的。
这是因为pytest
为了更好地展示测试过程,会全面接管Python的日志系统。我们自己手动在代码里添加的文件处理器(FileHandler
)会与pytest
自身的捕获机制产生冲突,导致发往文件的日志流被“劫持”或“丢弃”。
第三步:最终解决方案——与Pytest和谐共存
解决这个问题的最佳方式,不是与pytest
的机制对抗,而是利用它提供的工具,将我们的日志需求完美地融入它的生命周期。
最终,我们确立了以 pytest.ini
和 conftest.py
为核心的解决方案。
3.1 pytest.ini
- 只负责“终端显示”
我们首先明确分工,让 pytest.ini
只负责它最擅长的控制台日志。文件日志的管理将完全交给 conftest.py
。
文件路径: Litemall_ApiTest/pytest.ini
[pytest]
base_url = http://112.***.**.***
# 只保留与控制台输出相关的配置
log_cli = true
log_cli_level = INFO
# 定义我们想要的、包含自定义字段的控制台日志格式
log_cli_format = %(asctime)s - [%(levelname)s] - [%(test_case)s] - [%(module)s.%(funcName)s:%(lineno)d] - %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S
# 移除所有 log_file 相关的配置,避免冲突
3.2 utils/logger.py
- 提供日志“加工”工具
这个文件现在变得非常简单,它唯一的任务就是提供一个LogAdapter
,用来给日志动态地“贴上”当前测试用例名称的标签。
文件路径: Litemall_ApiTest/utils/logger.py
import logging
class LogAdapter(logging.LoggerAdapter):
"""
这个 Adapter 用于在日志信息中动态插入 'test_case' 字段。
"""
def process(self, msg, kwargs):
# 拷贝一份上下文,避免污染 self.extra
extra = self.extra.copy()
# 如果 kwargs 里有额外的 extra,就合并进来
if "extra" in kwargs:
extra.update(kwargs["extra"])
# 确保一定有 test_case 字段
if "test_case" not in extra:
extra["test_case"] = "general_context"
# 写回 kwargs,保证 Formatter 能取到
kwargs["extra"] = extra
return msg, kwargs
3.3 tests/conftest.py
- 成为真正的“日志总指挥”
这是整个解决方案的核心。我们使用pytest
的钩子函数(hooks),在测试会话的开始和结束,手动、精准地控制文件日志的创建和销毁。
文件路径: Litemall_ApiTest/tests/conftest.py
import pytest
import logging
from pathlib import Path
from datetime import datetime
from utils.logger import LogAdapter
# ... 其他 import ...
# 定义一个全局变量来持有我们的文件处理器
file_handler = None
def pytest_configure(config):
"""
在所有测试执行开始前,由 pytest 调用的钩子函数。
我们在这里进行文件日志的全局配置。
"""
global file_handler
# 动态生成文件名,例如:logs/2025-09-24.log
current_date = datetime.now().strftime("%Y-%m-%d")
log_dir = Path.cwd() / 'logs'
log_dir.mkdir(exist_ok=True)
log_file = log_dir / f"{current_date}.log"
# 创建一个文件处理器,模式为 'a' (追加)
file_handler = logging.FileHandler(log_file, mode='a', encoding='utf-8')
# 定义我们想要的日志格式 (与 .ini 中的控制台格式一致)
formatter = logging.Formatter(
'%(asctime)s - [%(levelname)s] - [%(test_case)s] - [%(module)s.%(funcName)s:%(lineno)d] - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
# 获取根 logger,并将我们的文件处理器添加进去
root_logger = logging.getLogger()
root_logger.addHandler(file_handler)
root_logger.setLevel(logging.INFO)
def pytest_unconfigure(config):
"""
在所有测试执行结束后,由 pytest 调用的钩子函数。
我们在这里进行清理工作。
"""
global file_handler
if file_handler:
logging.getLogger().removeHandler(file_handler)
file_handler.close()
@pytest.fixture
def logger(request):
"""
为每个测试用例创建一个带有上下文(测试用例名称)的 logger。
"""
# 直接获取根 logger
log_instance = logging.getLogger()
test_case_name = request.node.name
adapter = LogAdapter(log_instance, {'test_case': test_case_name})
return adapter
3.4 在业务和测试代码中优雅地使用
现在,我们只需要在keywords.py
和所有测试用例(如test_add_shopptcart.py
)中,通过pytest
的fixture机制请求并使用我们定义好的logger
即可。
# keywords.py 中的例子
class Keywords:
def __init__(self, base_url, logger):
self.api = APIEndpoints(base_url, logger)
self.logger = logger
# ...
def login(self, username, password):
self.logger.info("--------- 关键字: login [开始] ---------")
# ...
# test_add_shopptcart.py 中的例子
class TestShoppingCart:
def test_add_to_cart_scenarios(self, logged_in_keywords, logger, case_id, ...):
logger.info(f"******************** 测试场景: {case_id} [开始] ********************")
# ...
结论
通过以上步骤,我们构建了一个堪称完美的日志系统:
职责清晰:
pytest.ini
负责控制台,conftest.py
负责文件,互不干扰。生命周期绑定:通过
pytest_configure
和pytest_unconfigure
钩子,文件日志的生命周期与测试会话完全同步,确保了日志的完整性和可靠性。信息丰富:通过
LogAdapter
和统一的日志格式,每一条日志都包含了完整的上下文,极大地提升了可读性和调试效率。
为pytest
项目添加日志,关键不在于代码有多复杂,而在于要理解并顺应pytest
的框架机制。希望这篇由真实项目经验总结而来的博客,能为你未来的测试开发工作带来启发。