实战对象:SauceDemo 网站
SauceDemo 是一个由 Sauce Labs 公司维护的、完全模拟电商流程(登录-浏览商品-加购-下单)的测试网站。它前端稳定,后端逻辑简单但完整,是学习 Web 自动化的“标准教具”。
用户名 | 密码 | 用途 |
---|---|---|
standard_user | secret_sauce | 标准用户,可成功登录并完成所有操作 |
locked_out_user | secret_sauce | 被锁定用户,用于测试登录失败场景 |
problem_user | secret_sauce | 问题用户,部分页面元素加载异常,用于测试异常处理 |
performance_glitch_user | secret_sauce | 性能问题用户,页面响应慢,用于测试超时等待 |
在本篇实战文章中,我将带你一步步构建一个完整的Python自动化测试框架,用于测试Web应用。我们将使用Selenium进行浏览器自动化,pytest作为测试框架,并实现页面对象模型模式。
环境准备
首先,确保你已经安装了Python 3.6+。然后安装必要的依赖:
# 创建项目目录
mkdir python-automation-framework
cd python-automation-framework
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境 (Windows)
venv\Scripts\activate
# 激活虚拟环境 (Mac/Linux)
source venv/bin/activate
# 安装依赖包
pip install selenium pytest pytest-html
项目结构
创建以下目录结构:
python-automation-framework/
│
├── config/
│ ├── __init__.py
│ └── settings.yaml
│
├── pages/
│ ├── __init__.py
│ ├── base_page.py
│ └── login_page.py
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_login.py
│
├── utils/
│ ├── __init__.py
│ └── logger.py
│
└── run_tests.py
配置文件 (config/settings.yaml)
# 浏览器配置
browser:
name: "chrome"
headless: false
implicit_wait: 10
# 日志配置
log:
dir: "logs"
console_level: "INFO"
file_level: "DEBUG"
# 测试账户配置
test_account:
valid_username: "standard_user"
valid_password: "secret_sauce"
invalid_username: "locked_out_user"
invalid_password: "wrong_password"
# 应用配置
app:
base_url: "https://www.saucedemo.com"
日志工具 (utils/logger.py)
# -*- coding: utf-8 -*-
"""
日志工具模块
提供统一的日志记录功能
"""
import logging
import os
from pathlib import Path
from datetime import datetime
class Logger:
"""日志记录类"""
def __init__(self, name=__name__):
"""初始化日志记录器"""
# 创建日志记录器
self.logger = logging.getLogger(name)
self.logger.setLevel(logging.DEBUG)
# 防止日志重复输出
if not self.logger.handlers:
# 创建日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# 创建文件处理器
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
# 生成带时间戳的日志文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
log_file = log_dir / f"test_{timestamp}.log"
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# 添加处理器到日志记录器
self.logger.addHandler(console_handler)
self.logger.addHandler(file_handler)
def get_logger(self):
"""获取配置好的日志记录器"""
return self.logger
# 创建全局日志实例
logger = Logger(__name__).get_logger()
页面基类 (pages/base_page.py)
# -*- coding: utf-8 -*-
"""
页面对象基类
所有页面对象都应继承此类
"""
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from utils.logger import logger
class BasePage:
"""页面对象基类"""
def __init__(self, driver):
"""初始化基类"""
self.driver = driver
self.timeout = 10
def find_element(self, locator, timeout=None):
"""查找单个元素,支持显式等待"""
if timeout is None:
timeout = self.timeout
try:
logger.info(f"查找元素: {locator}")
element = WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located(locator)
)
return element
except TimeoutException:
logger.error(f"元素查找超时: {locator}")
raise
def click(self, locator, timeout=None):
"""点击元素"""
element = self.find_element(locator, timeout)
logger.info(f"点击元素: {locator}")
element.click()
def input_text(self, locator, text, timeout=None):
"""在输入框中输入文本"""
element = self.find_element(locator, timeout)
logger.info(f"在元素 {locator} 中输入文本: {text}")
element.clear()
element.send_keys(text)
def get_text(self, locator, timeout=None):
"""获取元素文本"""
element = self.find_element(locator, timeout)
text = element.text
logger.info(f"获取元素 {locator} 的文本: {text}")
return text
def is_element_visible(self, locator, timeout=None):
"""检查元素是否可见"""
try:
element = self.find_element(locator, timeout)
return element.is_displayed()
except (TimeoutException, Exception):
return False
登录页面对象 (pages/login_page.py)
# -*- coding: utf-8 -*-
"""
登录页面对象
封装SauceDemo登录页面的元素和操作
"""
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
from utils.logger import logger
class LoginPage(BasePage):
"""登录页面对象类"""
# 页面元素定位器
USERNAME_INPUT = (By.ID, "user-name")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "login-button")
ERROR_MESSAGE = (By.CLASS_NAME, "error-message-container")
def __init__(self, driver):
"""初始化登录页面"""
super().__init__(driver)
self.url = "https://www.saucedemo.com"
def open(self):
"""打开登录页面"""
logger.info(f"打开登录页面: {self.url}")
self.driver.get(self.url)
return self
def enter_username(self, username):
"""输入用户名"""
self.input_text(self.USERNAME_INPUT, username)
return self
def enter_password(self, password):
"""输入密码"""
self.input_text(self.PASSWORD_INPUT, password)
return self
def click_login(self):
"""点击登录按钮"""
self.click(self.LOGIN_BUTTON)
return self
def login(self, username, password):
"""执行完整登录操作"""
logger.info(f"执行登录操作,用户名: {username}")
return self.open().enter_username(username).enter_password(password).click_login()
def get_error_message(self):
"""获取错误消息"""
if self.is_element_visible(self.ERROR_MESSAGE):
return self.get_text(self.ERROR_MESSAGE)
return ""
Pytest配置 (tests/conftest.py)
# -*- coding: utf-8 -*-
"""
Pytest配置文件
定义全局fixture
"""
import pytest
import yaml
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@pytest.fixture(scope="session")
def config():
"""配置fixture,提供全局配置访问"""
# 读取配置文件
with open('config/settings.yaml', 'r') as file:
return yaml.safe_load(file)
@pytest.fixture(scope="function")
def browser(config):
"""浏览器fixture,为每个测试函数提供浏览器实例"""
# 根据配置选择浏览器
browser_name = config["browser"]["name"]
if browser_name.lower() == "chrome":
# Chrome浏览器选项
chrome_options = Options()
if config["browser"]["headless"]:
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=1920,1080")
# 初始化Chrome浏览器
driver = webdriver.Chrome(options=chrome_options)
else:
raise ValueError(f"不支持的浏览器: {browser_name}")
# 设置隐式等待时间
driver.implicitly_wait(config["browser"]["implicit_wait"])
# 返回浏览器实例给测试函数
yield driver
# 测试结束后关闭浏览器
driver.quit()
测试用例 (tests/test_login.py)
# -*- coding: utf-8 -*-
"""
登录功能测试用例
测试SauceDemo网站的登录功能
"""
import pytest
import yaml
from pages.login_page import LoginPage
from utils.logger import logger
# 读取配置文件
with open('config/settings.yaml', 'r') as file:
config = yaml.safe_load(file)
class TestLogin:
"""登录功能测试类"""
def test_successful_login(self, browser):
"""测试成功登录"""
logger.info("测试成功登录")
# 获取正确的用户名和密码
username = config["test_account"]["valid_username"]
password = config["test_account"]["valid_password"]
# 执行登录
login_page = LoginPage(browser)
login_page.login(username, password)
# 验证登录成功后跳转到库存页面
assert "inventory" in browser.current_url
logger.info("成功登录测试通过")
def test_login_with_invalid_password(self, browser):
"""测试使用错误密码登录"""
logger.info("测试使用错误密码登录")
# 获取正确的用户名和错误的密码
username = config["test_account"]["valid_username"]
password = config["test_account"]["invalid_password"]
# 执行登录
login_page = LoginPage(browser)
login_page.login(username, password)
# 验证显示错误消息
error_message = login_page.get_error_message()
assert "Username and password do not match" in error_message
logger.info("错误密码登录测试通过")
def test_login_with_locked_user(self, browser):
"""测试使用锁定用户登录"""
logger.info("测试使用锁定用户登录")
# 获取锁定的用户名和正确的密码
username = config["test_account"]["invalid_username"]
password = config["test_account"]["valid_password"]
# 执行登录
login_page = LoginPage(browser)
login_page.login(username, password)
# 验证显示错误消息
error_message = login_page.get_error_message()
assert "Sorry, this user has been locked out" in error_message
logger.info("锁定用户登录测试通过")
测试运行脚本 (run_tests.py)
# -*- coding: utf-8 -*-
"""
测试运行入口脚本
提供命令行界面来运行测试并生成报告
"""
import argparse
import subprocess
import sys
from datetime import datetime
def run_tests(test_path=None, browser=None, headless=None, report=None):
"""运行测试并生成报告"""
# 构建pytest命令
cmd = [sys.executable, "-m", "pytest"]
# 添加测试路径
if test_path:
cmd.append(test_path)
else:
cmd.append("tests/")
# 添加浏览器选项
if browser:
cmd.extend(["--browser", browser])
# 添加无头模式选项
if headless is not None:
cmd.extend(["--headless", str(headless)])
# 添加HTML报告选项
if report == "html":
# 创建报告目录
import os
report_dir = "reports"
if not os.path.exists(report_dir):
os.makedirs(report_dir)
# 添加HTML报告参数
html_report = os.path.join(report_dir, "report.html")
cmd.extend(["--html", html_report, "--self-contained-html"])
print(f"执行命令: {' '.join(cmd)}")
# 运行测试
result = subprocess.run(cmd)
# 返回退出码
return result.returncode
def main():
"""主函数,解析命令行参数并运行测试"""
parser = argparse.ArgumentParser(description="自动化测试运行器")
parser.add_argument("test_path", nargs="?", help="测试文件或目录路径")
parser.add_argument("--browser", choices=["chrome", "firefox"], help="指定浏览器")
parser.add_argument("--headless", action="store_true", help="使用无头模式")
parser.add_argument("--report", choices=["html"], help="生成测试报告")
args = parser.parse_args()
# 运行测试
exit_code = run_tests(
test_path=args.test_path,
browser=args.browser,
headless=args.headless,
report=args.report
)
# 退出程序,返回测试结果
sys.exit(exit_code)
if __name__ == "__main__":
main()
运行测试
方法一:使用 Python 直接运行(最简单)
在终端或命令行中,进入项目目录,直接运行这个 Python 文件:
python test_saucedemo_login.py
方法二:使用 Pytest 命令运行(更标准)
在终端或命令行中,进入项目目录,运行:
pytest test_saucedemo_login.py -v -s
参数解释:
-v
: 显示详细输出,会打印每个测试用例的名称。-s
: 允许将print
语句的内容输出到控制台。
测试报告
运行测试后,如果使用了--report html
参数,你会在reports
目录下找到生成的HTML测试报告。报告包含测试结果详情、通过率、失败原因等信息。
预期结果
如果一切设置正确(网络通畅、驱动路径正确),您将看到浏览器自动打开,访问 SauceDemo 网站,执行登录操作,然后浏览器自动关闭。最后在命令行中看到类似以下的输出:
test_saucedemo_login.py::TestSauceDemoLogin::test_successful_login
Setting up for test: test_successful_login
测试通过!用户成功登录并看到了商品列表。
Tearing down after test: test_successful_login
PASSED
test_saucedemo_login.py::TestSauceDemoLogin::test_locked_out_user
Setting up for test: test_locked_out_user
测试通过!被锁定用户登录失败,正确显示了错误信息。
Tearing down after test: test_locked_out_user
PASSED
THE END