Python自动化测试实战:构建你的第一个Web自动化测试框架

实战对象:SauceDemo 网站

SauceDemo 是一个由 Sauce Labs 公司维护的、完全模拟电商流程(登录-浏览商品-加购-下单)的测试网站。它前端稳定,后端逻辑简单但完整,是学习 Web 自动化的“标准教具”。

用户名密码用途
standard_usersecret_sauce标准用户,可成功登录并完成所有操作
locked_out_usersecret_sauce被锁定用户,用于测试登录失败场景
problem_usersecret_sauce问题用户,部分页面元素加载异常,用于测试异常处理
performance_glitch_usersecret_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
喜欢就支持一下吧
赞赏 分享