Python利用pytest和selenium实现自动化测试完整指南
作者:天天进步2015
自动化测试是现代软件开发中不可或缺的一环,Python作为一门简洁优雅的编程语言,配合pytest测试框架和selenium自动化工具,为我们提供了强大的自动化测试解决方案,下面小编就来和大家简单介绍一下吧
前言
自动化测试是现代软件开发中不可或缺的一环。Python作为一门简洁优雅的编程语言,配合pytest测试框架和selenium自动化工具,为我们提供了强大的自动化测试解决方案。
本教程将从零开始,带领大家掌握Python自动化测试的核心技能,通过实战项目学会如何构建稳定、高效的自动化测试体系。
环境搭建
安装Python和依赖包
# 创建虚拟环境 python -m venv test_env source test_env/bin/activate # Windows: test_env\Scripts\activate # 安装核心依赖 pip install pytest selenium webdriver-manager pytest-html allure-pytest
浏览器驱动配置
# 使用webdriver-manager自动管理驱动 from webdriver_manager.chrome import ChromeDriverManager from selenium import webdriver from selenium.webdriver.chrome.service import Service def setup_driver(): service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) return driver
项目结构搭建
automation_project/
├── tests/
│ ├── __init__.py
│ ├── test_login.py
│ └── test_search.py
├── pages/
│ ├── __init__.py
│ ├── base_page.py
│ └── login_page.py
├── utils/
│ ├── __init__.py
│ └── config.py
├── drivers/
├── reports/
├── conftest.py
├── pytest.ini
└── requirements.txt
pytest基础教程
pytest核心概念
pytest是Python中最流行的测试框架,具有以下特点:
- 简单易用的断言语法
- 丰富的插件生态系统
- 强大的fixture机制
- 灵活的测试发现和执行
基础测试示例
# test_basic.py import pytest def test_simple_assert(): """基础断言测试""" assert 1 + 1 == 2 def test_string_operations(): """字符串操作测试""" text = "Hello, World!" assert "Hello" in text assert text.startswith("Hello") assert text.endswith("!") class TestCalculator: """测试类示例""" def test_addition(self): assert 2 + 3 == 5 def test_division(self): assert 10 / 2 == 5 def test_division_by_zero(self): with pytest.raises(ZeroDivisionError): 10 / 0
fixture机制深入
# conftest.py import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager @pytest.fixture(scope="session") def driver(): """会话级别的浏览器驱动""" options = webdriver.ChromeOptions() options.add_argument("--headless") # 无头模式 driver = webdriver.Chrome( service=Service(ChromeDriverManager().install()), options=options ) yield driver driver.quit() @pytest.fixture def test_data(): """测试数据fixture""" return { "username": "test@example.com", "password": "password123" }
参数化测试
# test_parametrize.py import pytest @pytest.mark.parametrize("a,b,expected", [ (2, 3, 5), (1, 1, 2), (0, 5, 5), (-1, 1, 0) ]) def test_addition(a, b, expected): assert a + b == expected @pytest.mark.parametrize("url", [ "https://www.baidu.com", "https://www.google.com" ]) def test_website_accessibility(driver, url): driver.get(url) assert driver.title
selenium基础教程
元素定位策略
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class ElementLocator: """元素定位封装类""" def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def find_element_safely(self, locator): """安全查找元素""" try: element = self.wait.until(EC.presence_of_element_located(locator)) return element except TimeoutException: print(f"元素定位失败: {locator}") return None def click_element(self, locator): """点击元素""" element = self.wait.until(EC.element_to_be_clickable(locator)) element.click() def input_text(self, locator, text): """输入文本""" element = self.find_element_safely(locator) if element: element.clear() element.send_keys(text)
常用操作封装
# utils/selenium_helper.py from selenium.webdriver.support.ui import Select from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys class SeleniumHelper: """Selenium操作助手类""" def __init__(self, driver): self.driver = driver def scroll_to_element(self, element): """滚动到指定元素""" self.driver.execute_script("arguments[0].scrollIntoView();", element) def select_dropdown_by_text(self, locator, text): """通过文本选择下拉框""" select = Select(self.driver.find_element(*locator)) select.select_by_visible_text(text) def hover_element(self, element): """鼠标悬停""" actions = ActionChains(self.driver) actions.move_to_element(element).perform() def switch_to_iframe(self, iframe_locator): """切换到iframe""" iframe = self.driver.find_element(*iframe_locator) self.driver.switch_to.frame(iframe) def take_screenshot(self, filename): """截图""" self.driver.save_screenshot(f"screenshots/{filename}")
pytest + selenium实战项目
实战项目:电商网站测试
让我们以一个电商网站为例,构建完整的自动化测试项目。
配置文件设置
# utils/config.py class Config: """测试配置类""" BASE_URL = "https://example-shop.com" TIMEOUT = 10 BROWSER = "chrome" HEADLESS = False # 测试账户信息 TEST_USER = { "email": "test@example.com", "password": "password123" } # 测试数据 TEST_PRODUCT = { "name": "iPhone 14", "price": "999.99" }
基础页面类
# pages/base_page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By class BasePage: """基础页面类""" def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def open(self, url): """打开页面""" self.driver.get(url) def find_element(self, locator): """查找元素""" return self.wait.until(EC.presence_of_element_located(locator)) def click(self, locator): """点击元素""" element = self.wait.until(EC.element_to_be_clickable(locator)) element.click() def input_text(self, locator, text): """输入文本""" element = self.find_element(locator) element.clear() element.send_keys(text) def get_text(self, locator): """获取元素文本""" element = self.find_element(locator) return element.text def is_element_visible(self, locator): """检查元素是否可见""" try: self.wait.until(EC.visibility_of_element_located(locator)) return True except: return False
登录页面类
# pages/login_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): """登录页面""" # 页面元素定位 EMAIL_INPUT = (By.ID, "email") PASSWORD_INPUT = (By.ID, "password") LOGIN_BUTTON = (By.XPATH, "//button[@type='submit']") ERROR_MESSAGE = (By.CLASS_NAME, "error-message") SUCCESS_MESSAGE = (By.CLASS_NAME, "success-message") def login(self, email, password): """执行登录操作""" self.input_text(self.EMAIL_INPUT, email) self.input_text(self.PASSWORD_INPUT, password) self.click(self.LOGIN_BUTTON) def get_error_message(self): """获取错误信息""" if self.is_element_visible(self.ERROR_MESSAGE): return self.get_text(self.ERROR_MESSAGE) return None def is_login_successful(self): """检查登录是否成功""" return self.is_element_visible(self.SUCCESS_MESSAGE)
登录测试用例
# tests/test_login.py import pytest from pages.login_page import LoginPage from utils.config import Config class TestLogin: """登录功能测试类""" @pytest.fixture(autouse=True) def setup(self, driver): """测试前置条件""" self.driver = driver self.login_page = LoginPage(driver) self.login_page.open(f"{Config.BASE_URL}/login") def test_valid_login(self): """测试有效登录""" self.login_page.login( Config.TEST_USER["email"], Config.TEST_USER["password"] ) assert self.login_page.is_login_successful() @pytest.mark.parametrize("email,password,expected_error", [ ("", "password123", "邮箱不能为空"), ("invalid-email", "password123", "邮箱格式不正确"), ("test@example.com", "", "密码不能为空"), ("wrong@example.com", "wrongpass", "用户名或密码错误") ]) def test_invalid_login(self, email, password, expected_error): """测试无效登录""" self.login_page.login(email, password) error_message = self.login_page.get_error_message() assert expected_error in error_message def test_login_form_elements(self): """测试登录表单元素存在性""" assert self.login_page.is_element_visible(self.login_page.EMAIL_INPUT) assert self.login_page.is_element_visible(self.login_page.PASSWORD_INPUT) assert self.login_page.is_element_visible(self.login_page.LOGIN_BUTTON)
商品搜索测试
# pages/search_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage class SearchPage(BasePage): """搜索页面""" SEARCH_INPUT = (By.NAME, "search") SEARCH_BUTTON = (By.CLASS_NAME, "search-btn") SEARCH_RESULTS = (By.CLASS_NAME, "product-item") NO_RESULTS_MESSAGE = (By.CLASS_NAME, "no-results") PRODUCT_TITLE = (By.CLASS_NAME, "product-title") def search_product(self, keyword): """搜索商品""" self.input_text(self.SEARCH_INPUT, keyword) self.click(self.SEARCH_BUTTON) def get_search_results_count(self): """获取搜索结果数量""" results = self.driver.find_elements(*self.SEARCH_RESULTS) return len(results) def get_first_product_title(self): """获取第一个商品标题""" return self.get_text(self.PRODUCT_TITLE) # tests/test_search.py import pytest from pages.search_page import SearchPage from utils.config import Config class TestSearch: """搜索功能测试类""" @pytest.fixture(autouse=True) def setup(self, driver): self.driver = driver self.search_page = SearchPage(driver) self.search_page.open(Config.BASE_URL) def test_valid_search(self): """测试有效搜索""" self.search_page.search_product("iPhone") assert self.search_page.get_search_results_count() > 0 def test_search_no_results(self): """测试无结果搜索""" self.search_page.search_product("不存在的商品") assert self.search_page.is_element_visible(self.search_page.NO_RESULTS_MESSAGE) @pytest.mark.parametrize("keyword", [ "iPhone", "Samsung", "小米", "华为" ]) def test_multiple_searches(self, keyword): """测试多个关键词搜索""" self.search_page.search_product(keyword) results_count = self.search_page.get_search_results_count() assert results_count >= 0 # 至少返回0个结果
页面对象模式(POM)
页面对象模式是自动化测试中的重要设计模式,它将页面元素和操作封装在独立的类中。
完整的POM实现
# pages/product_page.py from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import Select from pages.base_page import BasePage class ProductPage(BasePage): """商品详情页""" # 商品信息元素 PRODUCT_TITLE = (By.H1, "product-title") PRODUCT_PRICE = (By.CLASS_NAME, "price") PRODUCT_DESCRIPTION = (By.CLASS_NAME, "description") # 购买相关元素 QUANTITY_SELECT = (By.NAME, "quantity") ADD_TO_CART_BUTTON = (By.ID, "add-to-cart") CART_SUCCESS_MESSAGE = (By.CLASS_NAME, "cart-success") # 评论相关元素 REVIEWS_SECTION = (By.ID, "reviews") REVIEW_INPUT = (By.NAME, "review") SUBMIT_REVIEW_BUTTON = (By.ID, "submit-review") def get_product_info(self): """获取商品信息""" return { "title": self.get_text(self.PRODUCT_TITLE), "price": self.get_text(self.PRODUCT_PRICE), "description": self.get_text(self.PRODUCT_DESCRIPTION) } def add_to_cart(self, quantity=1): """添加到购物车""" # 选择数量 quantity_select = Select(self.find_element(self.QUANTITY_SELECT)) quantity_select.select_by_value(str(quantity)) # 点击添加到购物车 self.click(self.ADD_TO_CART_BUTTON) # 等待成功消息 return self.is_element_visible(self.CART_SUCCESS_MESSAGE) def submit_review(self, review_text): """提交评论""" self.input_text(self.REVIEW_INPUT, review_text) self.click(self.SUBMIT_REVIEW_BUTTON)
购物车页面测试
# tests/test_cart.py import pytest from pages.product_page import ProductPage from pages.cart_page import CartPage from utils.config import Config class TestShoppingCart: """购物车功能测试""" @pytest.fixture(autouse=True) def setup(self, driver): self.driver = driver self.product_page = ProductPage(driver) self.cart_page = CartPage(driver) def test_add_single_product_to_cart(self): """测试添加单个商品到购物车""" # 打开商品页面 self.product_page.open(f"{Config.BASE_URL}/product/1") # 添加到购物车 success = self.product_page.add_to_cart(quantity=1) assert success # 验证购物车 self.cart_page.open(f"{Config.BASE_URL}/cart") assert self.cart_page.get_cart_items_count() == 1 def test_add_multiple_quantities(self): """测试添加多数量商品""" self.product_page.open(f"{Config.BASE_URL}/product/1") success = self.product_page.add_to_cart(quantity=3) assert success self.cart_page.open(f"{Config.BASE_URL}/cart") total_quantity = self.cart_page.get_total_quantity() assert total_quantity == 3 def test_cart_total_calculation(self): """测试购物车总价计算""" # 添加多个商品 products = [ {"id": 1, "quantity": 2, "price": 99.99}, {"id": 2, "quantity": 1, "price": 149.99} ] expected_total = 0 for product in products: self.product_page.open(f"{Config.BASE_URL}/product/{product['id']}") self.product_page.add_to_cart(product["quantity"]) expected_total += product["price"] * product["quantity"] self.cart_page.open(f"{Config.BASE_URL}/cart") actual_total = self.cart_page.get_total_price() assert actual_total == expected_total
测试报告生成
HTML报告配置
# pytest.ini [tool:pytest] minversion = 6.0 addopts = -v --strict-markers --html=reports/report.html --self-contained-html testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* markers = smoke: 冒烟测试 regression: 回归测试 slow: 慢速测试
Allure报告集成
# conftest.py 添加allure配置 import allure import pytest from selenium import webdriver @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """生成测试报告钩子""" outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: # 测试失败时自动截图 driver = item.funcargs.get('driver') if driver: allure.attach( driver.get_screenshot_as_png(), name="失败截图", attachment_type=allure.attachment_type.PNG ) # 在测试中使用allure装饰器 import allure class TestLoginWithAllure: """带Allure报告的登录测试""" @allure.epic("用户管理") @allure.feature("用户登录") @allure.story("正常登录流程") @allure.severity(allure.severity_level.CRITICAL) def test_valid_login_with_allure(self, driver): """测试有效登录 - Allure版本""" with allure.step("打开登录页面"): login_page = LoginPage(driver) login_page.open(f"{Config.BASE_URL}/login") with allure.step("输入登录凭证"): login_page.login( Config.TEST_USER["email"], Config.TEST_USER["password"] ) with allure.step("验证登录结果"): assert login_page.is_login_successful() allure.attach( driver.get_screenshot_as_png(), name="登录成功截图", attachment_type=allure.attachment_type.PNG )
持续集成配置
GitHub Actions配置
# .github/workflows/test.yml name: 自动化测试 on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: 设置Python环境 uses: actions/setup-python@v3 with: python-version: '3.9' - name: 安装Chrome uses: browser-actions/setup-chrome@latest - name: 安装依赖 run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: 运行测试 run: | pytest tests/ --html=reports/report.html --alluredir=allure-results - name: 生成Allure报告 uses: simple-elf/allure-report-action@master if: always() with: allure_results: allure-results allure_history: allure-history - name: 上传测试报告 uses: actions/upload-artifact@v3 if: always() with: name: test-reports path: | reports/ allure-report/
Docker配置
# Dockerfile FROM python:3.9-slim # 安装系统依赖 RUN apt-get update && apt-get install -y \ wget \ gnupg \ unzip \ curl # 安装Chrome RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \ && apt-get update \ && apt-get install -y google-chrome-stable # 设置工作目录 WORKDIR /app # 复制项目文件 COPY requirements.txt . RUN pip install -r requirements.txt COPY . . # 运行测试 CMD ["pytest", "tests/", "--html=reports/report.html"]
最佳实践和进阶技巧
测试数据管理
# utils/test_data.py import json import yaml from pathlib import Path class TestDataManager: """测试数据管理器""" def __init__(self, data_dir="test_data"): self.data_dir = Path(data_dir) def load_json_data(self, filename): """加载JSON测试数据""" file_path = self.data_dir / f"{filename}.json" with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) def load_yaml_data(self, filename): """加载YAML测试数据""" file_path = self.data_dir / f"{filename}.yaml" with open(file_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) # test_data/login_data.yaml valid_users: - email: "user1@example.com" password: "password123" expected_result: "success" - email: "user2@example.com" password: "password456" expected_result: "success" invalid_users: - email: "" password: "password123" expected_error: "邮箱不能为空" - email: "invalid-email" password: "password123" expected_error: "邮箱格式不正确"
失败重试机制
# conftest.py import pytest @pytest.fixture(autouse=True) def retry_failed_tests(request): """失败测试重试机制""" if request.node.rep_call.failed: # 重试逻辑 pass # 使用pytest-rerunfailures插件 # pip install pytest-rerunfailures # pytest --reruns 3 --reruns-delay 2
并行测试执行
# 安装pytest-xdist # pip install pytest-xdist # 并行执行测试 # pytest -n auto # 自动检测CPU核心数 # pytest -n 4 # 使用4个进程
测试环境管理
# utils/environment.py import os from enum import Enum class Environment(Enum): DEV = "dev" TEST = "test" STAGING = "staging" PROD = "prod" class EnvironmentConfig: """环境配置管理""" def __init__(self): self.current_env = Environment(os.getenv('TEST_ENV', 'test')) def get_base_url(self): """获取当前环境的基础URL""" urls = { Environment.DEV: "http://dev.example.com", Environment.TEST: "http://test.example.com", Environment.STAGING: "http://staging.example.com", Environment.PROD: "http://example.com" } return urls[self.current_env] def get_database_config(self): """获取数据库配置""" configs = { Environment.TEST: { "host": "test-db.example.com", "database": "test_db" }, Environment.STAGING: { "host": "staging-db.example.com", "database": "staging_db" } } return configs.get(self.current_env, {})
性能测试集成
# tests/test_performance.py import time import pytest from selenium.webdriver.support.ui import WebDriverWait class TestPerformance: """性能测试""" def test_page_load_time(self, driver): """测试页面加载时间""" start_time = time.time() driver.get("https://example.com") WebDriverWait(driver, 10).until( lambda d: d.execute_script("return document.readyState") == "complete" ) load_time = time.time() - start_time assert load_time < 5.0, f"页面加载时间过长: {load_time}秒" def test_search_response_time(self, driver, search_page): """测试搜索响应时间""" search_page.open("https://example.com") start_time = time.time() search_page.search_product("iPhone") # 等待搜索结果出现 WebDriverWait(driver, 10).until( lambda d: len(d.find_elements(*search_page.SEARCH_RESULTS)) > 0 ) response_time = time.time() - start_time assert response_time < 3.0, f"搜索响应时间过长: {response_time}秒"
数据库验证
# utils/database.py import sqlite3 import pymongo from contextlib import contextmanager class DatabaseHelper: """数据库操作助手""" def __init__(self, db_config): self.config = db_config @contextmanager def get_connection(self): """获取数据库连接""" if self.config['type'] == 'sqlite': conn = sqlite3.connect(self.config['path']) elif self.config['type'] == 'mysql': import mysql.connector conn = mysql.connector.connect(**self.config) try: yield conn finally: conn.close() def verify_user_created(self, email): """验证用户是否创建成功""" with self.get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM users WHERE email = ?", (email,)) result = cursor.fetchone() return result is not None # 在测试中使用数据库验证 def test_user_registration_with_db_verification(driver, db_helper): """测试用户注册并验证数据库""" # 执行注册操作 registration_page = RegistrationPage(driver) test_email = f"test_{int(time.time())}@example.com" registration_page.register_user( email=test_email, password="password123" ) # 验证UI显示成功 assert registration_page.is_registration_successful() # 验证数据库中确实创建了用户 assert db_helper.verify_user_created(test_email)
总结
本教程全面介绍了使用pytest和selenium进行Python自动化测试的完整流程,从环境搭建到高级技巧,涵盖了实际项目中的各个方面。
关键要点回顾
- 环境搭建: 正确配置Python环境、浏览器驱动和项目结构
- pytest框架: 掌握基础测试、fixture机制和参数化测试
- selenium操作: 学会元素定位、常用操作和等待机制
- 页面对象模式: 使用POM提高代码复用性和维护性
- 测试报告: 生成专业的HTML和Allure测试报告
- 持续集成: 配置CI/CD流程实现自动化测试
- 最佳实践: 应用进阶技巧提升测试质量和效率
以上就是Python利用pytest和selenium实现自动化测试完整指南的详细内容,更多关于Python自动化测试的资料请关注脚本之家其它相关文章!