PyCharm集成Selenium实战教程一步步教你搭建Web自动化测试框架解决实际测试难题
引言
Web自动化测试是现代软件开发流程中不可或缺的一环,它能显著提高测试效率,减少人工测试的工作量,并确保Web应用的质量。Selenium作为目前最流行的Web自动化测试工具之一,配合强大的Python IDE PyCharm,可以构建出高效、稳定的自动化测试框架。本教程将带你从零开始,一步步搭建一个完整的Web自动化测试框架,并解决实际测试过程中可能遇到的各种难题。
1. 环境准备
1.1 安装Python
首先,我们需要安装Python环境。推荐使用Python 3.8或更高版本,因为它们具有更好的性能和更多的特性支持。
- 访问Python官网(https://www.python.org/downloads/)下载最新版Python
- 安装时记得勾选”Add Python to PATH”选项
- 安装完成后,在命令行中输入以下命令验证安装:
python --version
1.2 安装PyCharm
PyCharm是JetBrains开发的一款Python IDE,分为社区版(免费)和专业版(付费)。社区版已经足够我们进行Selenium自动化测试开发。
- 访问PyCharm官网(https://www.jetbrains.com/pycharm/download/)下载社区版
- 安装过程按照向导完成即可
- 首次启动时,可以进行一些基本的配置,如主题、字体等
1.3 安装Selenium和相关库
打开PyCharm,创建一个新项目,然后通过以下方式安装必要的库:
- 打开PyCharm的Terminal(终端)
- 输入以下命令安装Selenium:
pip install selenium
- 安装其他有用的库:
pip install pytest # 测试框架 pip install pytest-html # HTML测试报告 pip install allure-pytest # 更漂亮的测试报告 pip install openpyxl # Excel操作 pip install pandas # 数据处理 pip install webdriver-manager # 自动管理WebDriver驱动
1.4 浏览器驱动配置
Selenium需要浏览器驱动来控制浏览器。过去,我们需要手动下载对应版本的驱动,但现在可以使用webdriver-manager
自动管理。
# 安装webdriver-manager pip install webdriver-manager # 使用示例 from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(ChromeDriverManager().install())
2. 项目结构设计
一个良好的项目结构是测试框架成功的基础。我们将采用如下的目录结构:
web_automation_framework/ │ ├── config/ # 配置文件 │ ├── __init__.py │ └── config.py # 全局配置 │ ├── pages/ # 页面元素及操作 │ ├── __init__.py │ ├── base_page.py # 基础页面类 │ ├── login_page.py # 登录页面 │ └── home_page.py # 主页 │ ├── tests/ # 测试用例 │ ├── __init__.py │ ├── test_login.py # 登录测试 │ └── test_home.py # 主页功能测试 │ ├── data/ # 测试数据 │ ├── test_data.xlsx # Excel测试数据 │ └── test_data.json # JSON测试数据 │ ├── utils/ # 工具类 │ ├── __init__.py │ ├── excel_utils.py # Excel操作工具 │ ├── screenshot_utils.py # 截图工具 │ └── logger_utils.py # 日志工具 │ ├── reports/ # 测试报告 │ └── html/ # HTML报告存放位置 │ ├── requirements.txt # 项目依赖 └── pytest.ini # pytest配置文件
3. 基础Selenium操作
3.1 创建基础页面类
首先,我们创建一个基础页面类,封装一些通用的Selenium操作:
# pages/base_page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from utils.screenshot_utils import take_screenshot from utils.logger_utils import get_logger logger = get_logger(__name__) class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def open(self, url): """打开URL""" logger.info(f"Opening URL: {url}") self.driver.get(url) def find_element(self, *locator): """查找单个元素""" logger.debug(f"Finding element with locator: {locator}") return self.driver.find_element(*locator) def find_elements(self, *locator): """查找多个元素""" logger.debug(f"Finding elements with locator: {locator}") return self.driver.find_elements(*locator) def click(self, locator): """点击元素""" element = self.wait.until(EC.element_to_be_clickable(locator)) logger.info(f"Clicking element: {locator}") element.click() def input_text(self, locator, text): """输入文本""" element = self.wait.until(EC.visibility_of_element_located(locator)) logger.info(f"Inputting text '{text}' into element: {locator}") element.clear() element.send_keys(text) def get_text(self, locator): """获取元素文本""" element = self.wait.until(EC.visibility_of_element_located(locator)) text = element.text logger.debug(f"Getting text '{text}' from element: {locator}") return text def is_visible(self, locator): """判断元素是否可见""" result = self.wait.until(EC.visibility_of_element_located(locator)).is_displayed() logger.debug(f"Element {locator} visibility: {result}") return result def take_screenshot(self, name): """截图""" take_screenshot(self.driver, name)
3.2 日志工具
创建一个日志工具类,用于记录测试过程中的信息:
# utils/logger_utils.py import logging import os from datetime import datetime def get_logger(name): """创建并配置日志记录器""" logger = logging.getLogger(name) # 如果logger已经有处理器,直接返回 if logger.handlers: return logger logger.setLevel(logging.DEBUG) # 创建logs目录 log_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs') if not os.path.exists(log_dir): os.makedirs(log_dir) # 文件处理器 log_file = os.path.join(log_dir, f"test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log") file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.DEBUG) # 控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 格式化器 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # 添加处理器 logger.addHandler(file_handler) logger.addHandler(console_handler) return logger
3.3 截图工具
创建一个截图工具类,用于在测试失败时捕获屏幕:
# utils/screenshot_utils.py import os from datetime import datetime def take_screenshot(driver, name): """截图并保存到指定目录""" # 创建screenshots目录 screenshot_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'screenshots') if not os.path.exists(screenshot_dir): os.makedirs(screenshot_dir) # 生成文件名 timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"{name}_{timestamp}.png" filepath = os.path.join(screenshot_dir, filename) # 截图 driver.save_screenshot(filepath) return filepath
4. 页面对象模型(Page Object Model)实现
Page Object Model是一种设计模式,它将每个页面表示为一个类,页面的元素作为类的属性,页面的操作作为类的方法。这种模式使测试代码更加清晰、可维护。
4.1 登录页面实现
# pages/login_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage from utils.logger_utils import get_logger logger = get_logger(__name__) class LoginPage(BasePage): # 页面元素定位器 USERNAME_INPUT = (By.ID, "username") PASSWORD_INPUT = (By.ID, "password") LOGIN_BUTTON = (By.ID, "login-btn") ERROR_MESSAGE = (By.CLASS_NAME, "error-message") def __init__(self, driver): super().__init__(driver) self.url = "https://example.com/login" def open(self): """打开登录页面""" super().open(self.url) logger.info("Login page opened") def login(self, username, password): """执行登录操作""" logger.info(f"Attempting to login with username: {username}") self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.LOGIN_BUTTON) def get_error_message(self): """获取错误消息""" if self.is_visible(self.ERROR_MESSAGE): return self.get_text(self.ERROR_MESSAGE) return None def is_login_successful(self): """判断登录是否成功""" # 假设登录成功后会跳转到主页,可以通过URL变化判断 return "dashboard" in self.driver.current_url
4.2 主页实现
# pages/home_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage from utils.logger_utils import get_logger logger = get_logger(__name__) class HomePage(BasePage): # 页面元素定位器 USER_PROFILE = (By.CLASS_NAME, "user-profile") LOGOUT_BUTTON = (By.ID, "logout-btn") WELCOME_MESSAGE = (By.CLASS_NAME, "welcome-message") def __init__(self, driver): super().__init__(driver) def get_welcome_message(self): """获取欢迎消息""" return self.get_text(self.WELCOME_MESSAGE) def logout(self): """执行登出操作""" logger.info("Logging out") self.click(self.USER_PROFILE) self.click(self.LOGOUT_BUTTON) def is_logout_successful(self): """判断登出是否成功""" # 假设登出后会跳转到登录页面 return "login" in self.driver.current_url
5. 测试用例设计
现在,我们将使用pytest框架编写测试用例。pytest是一个强大的Python测试框架,它支持简单灵活的函数式测试,以及复杂的测试场景。
5.1 配置pytest
首先,创建一个pytest配置文件:
# pytest.ini [pytest] addopts = -v --html=./reports/html/report.html --self-contained-html testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* markers = smoke: 冒烟测试 regression: 回归测试 login: 登录相关测试
5.2 创建conftest.py
conftest.py是pytest的配置文件,可以在这里定义fixture,供测试用例使用:
# conftest.py import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.firefox import GeckoDriverManager from pages.login_page import LoginPage from pages.home_page import HomePage import time def pytest_addoption(parser): """添加命令行选项""" parser.addoption("--browser", action="store", default="chrome", help="Type of browser: chrome or firefox") parser.addoption("--headless", action="store_true", help="Run tests in headless mode") @pytest.fixture(scope="session") def browser(request): """根据命令行参数选择浏览器""" return request.config.getoption("--browser") @pytest.fixture(scope="session") def headless(request): """是否使用无头模式""" return request.config.getoption("--headless") @pytest.fixture(scope="function") def driver(browser, headless): """初始化WebDriver""" if browser.lower() == "chrome": options = webdriver.ChromeOptions() if headless: options.add_argument("--headless") options.add_argument("--disable-gpu") options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(ChromeDriverManager().install(), options=options) elif browser.lower() == "firefox": options = webdriver.FirefoxOptions() if headless: options.add_argument("--headless") driver = webdriver.Firefox(executable_path=GeckoDriverManager().install(), options=options) else: raise ValueError(f"Unsupported browser: {browser}") # 设置隐式等待 driver.implicitly_wait(10) yield driver # 测试结束后关闭浏览器 driver.quit() @pytest.fixture(scope="function") def login_page(driver): """提供LoginPage实例""" return LoginPage(driver) @pytest.fixture(scope="function") def home_page(driver): """提供HomePage实例""" return HomePage(driver) @pytest.fixture(scope="function") def logged_in_driver(driver, login_page): """提供已登录的driver""" login_page.open() login_page.login("standard_user", "secret_sauce") time.sleep(1) # 等待登录完成 return driver
5.3 登录测试用例
# tests/test_login.py import pytest from pages.login_page import LoginPage from pages.home_page import HomePage from utils.logger_utils import get_logger logger = get_logger(__name__) class TestLogin: """登录功能测试""" def test_valid_login(self, login_page, home_page): """测试有效登录""" login_page.open() login_page.login("standard_user", "secret_sauce") # 验证登录成功 assert login_page.is_login_successful(), "Login should be successful with valid credentials" # 验证欢迎消息 welcome_message = home_page.get_welcome_message() assert "Welcome" in welcome_message, f"Welcome message should contain 'Welcome', but got: {welcome_message}" logger.info("Valid login test passed") @pytest.mark.parametrize("username, password, expected_error", [ ("invalid_user", "secret_sauce", "Invalid username or password"), ("standard_user", "wrong_password", "Invalid username or password"), ("", "secret_sauce", "Username is required"), ("standard_user", "", "Password is required") ]) def test_invalid_login(self, login_page, username, password, expected_error): """测试无效登录""" login_page.open() login_page.login(username, password) # 验证登录失败 assert not login_page.is_login_successful(), "Login should fail with invalid credentials" # 验证错误消息 error_message = login_page.get_error_message() assert error_message == expected_error, f"Expected error message: {expected_error}, but got: {error_message}" logger.info(f"Invalid login test passed for username: {username}, password: {password}") def test_logout(self, logged_in_driver, home_page): """测试登出功能""" home_page.logout() # 验证登出成功 assert home_page.is_logout_successful(), "Logout should be successful" logger.info("Logout test passed")
5.4 主页功能测试用例
# tests/test_home.py import pytest from pages.home_page import HomePage from utils.logger_utils import get_logger logger = get_logger(__name__) class TestHome: """主页功能测试""" def test_welcome_message_displayed(self, logged_in_driver, home_page): """测试欢迎消息显示""" welcome_message = home_page.get_welcome_message() assert welcome_message is not None, "Welcome message should be displayed" assert len(welcome_message) > 0, "Welcome message should not be empty" logger.info("Welcome message display test passed") def test_user_profile_accessible(self, logged_in_driver, home_page): """测试用户档案可访问""" # 这里假设点击用户档案会打开一个下拉菜单 # 实际实现可能因应用而异 user_profile = home_page.find_element(*home_page.USER_PROFILE) assert user_profile.is_displayed(), "User profile should be accessible" logger.info("User profile accessibility test passed")
6. 数据驱动测试
数据驱动测试允许我们使用不同的数据集运行相同的测试用例,这对于验证各种场景非常有用。
6.1 Excel数据读取工具
# utils/excel_utils.py import openpyxl from utils.logger_utils import get_logger logger = get_logger(__name__) def get_excel_data(file_path, sheet_name): """ 从Excel文件读取测试数据 返回一个列表,每个元素是一个字典,代表一行数据 """ logger.info(f"Reading data from Excel file: {file_path}, sheet: {sheet_name}") try: workbook = openpyxl.load_workbook(file_path) sheet = workbook[sheet_name] # 获取表头 headers = [cell.value for cell in sheet[1]] # 获取数据 data = [] for row in sheet.iter_rows(min_row=2, values_only=True): row_data = {} for i, value in enumerate(row): row_data[headers[i]] = value data.append(row_data) logger.info(f"Successfully read {len(data)} rows from Excel") return data except Exception as e: logger.error(f"Error reading Excel file: {e}") raise
6.2 使用Excel数据进行测试
# tests/test_data_driven.py import pytest from pages.login_page import LoginPage from utils.excel_utils import get_excel_data import os from utils.logger_utils import get_logger logger = get_logger(__name__) # 获取测试数据 current_dir = os.path.dirname(os.path.abspath(__file__)) data_dir = os.path.join(os.path.dirname(current_dir), 'data') test_data_file = os.path.join(data_dir, 'login_test_data.xlsx') login_data = get_excel_data(test_data_file, 'LoginTests') class TestDataDrivenLogin: """数据驱动的登录测试""" @pytest.mark.parametrize("test_data", login_data) def test_login_with_excel_data(self, login_page, test_data): """使用Excel数据进行登录测试""" username = test_data['Username'] password = test_data['Password'] expected_result = test_data['ExpectedResult'] login_page.open() login_page.login(username, password) if expected_result == "Success": assert login_page.is_login_successful(), f"Login should succeed with username: {username}" logger.info(f"Login succeeded as expected for username: {username}") else: assert not login_page.is_login_successful(), f"Login should fail with username: {username}" error_message = login_page.get_error_message() assert expected_result in error_message, f"Expected error message to contain '{expected_result}', but got: {error_message}" logger.info(f"Login failed as expected for username: {username}, error: {error_message}")
7. 测试报告生成
7.1 HTML报告
pytest-html插件可以生成美观的HTML测试报告。我们在pytest.ini中已经配置了基本选项,现在让我们创建一个更高级的配置:
# conftest.py 添加以下内容 @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """ 为HTML报告添加截图和日志 """ outcome = yield report = outcome.get_result() # 只在测试失败时添加截图 if report.when == 'call' and report.failed: # 获取driver实例 driver = item.funcargs.get('driver') if driver: # 截图 screenshot_path = take_screenshot(driver, item.name) # 将截图添加到HTML报告中 if hasattr(report, 'extra'): report.extra.append(pytest_html.extras.png(screenshot_path)) else: report.extra = [pytest_html.extras.png(screenshot_path)]
7.2 Allure报告
Allure是一个更强大的测试报告框架,提供了丰富的图表和分类功能。
安装Allure命令行工具:
- 下载地址:https://github.com/allure-framework/allure2/releases
- 解压并将bin目录添加到系统PATH
修改pytest.ini以支持Allure:
# pytest.ini [pytest] addopts = -v --alluredir=./reports/allure-results testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* markers = smoke: 冒烟测试 regression: 回归测试 login: 登录相关测试
- 在测试用例中添加Allure装饰器:
# tests/test_login.py 添加Allure装饰器 import allure class TestLogin: """登录功能测试""" @allure.feature("登录功能") @allure.story("有效登录") @allure.severity(allure.severity_level.CRITICAL) def test_valid_login(self, login_page, home_page): """测试有效登录""" with allure.step("打开登录页面"): login_page.open() with allure.step("输入用户名和密码"): login_page.login("standard_user", "secret_sauce") with allure.step("验证登录成功"): assert login_page.is_login_successful(), "Login should be successful with valid credentials" with allure.step("验证欢迎消息"): welcome_message = home_page.get_welcome_message() assert "Welcome" in welcome_message, f"Welcome message should contain 'Welcome', but got: {welcome_message}" logger.info("Valid login test passed") @allure.feature("登录功能") @allure.story("无效登录") @allure.severity(allure.severity_level.NORMAL) @pytest.mark.parametrize("username, password, expected_error", [ ("invalid_user", "secret_sauce", "Invalid username or password"), ("standard_user", "wrong_password", "Invalid username or password"), ("", "secret_sauce", "Username is required"), ("standard_user", "", "Password is required") ]) def test_invalid_login(self, login_page, username, password, expected_error): """测试无效登录""" with allure.step(f"使用用户名: {username}, 密码: {password} 尝试登录"): login_page.open() login_page.login(username, password) with allure.step("验证登录失败"): assert not login_page.is_login_successful(), "Login should fail with invalid credentials" with allure.step("验证错误消息"): error_message = login_page.get_error_message() assert error_message == expected_error, f"Expected error message: {expected_error}, but got: {error_message}" logger.info(f"Invalid login test passed for username: {username}, password: {password}")
- 生成Allure报告:
# 运行测试 pytest # 生成Allure报告 allure generate reports/allure-results -o reports/allure-report # 打开报告 allure open reports/allure-report
8. 持续集成
将自动化测试集成到CI/CD流程中,可以在每次代码变更时自动运行测试,及时发现问题。
8.1 Jenkins集成
- 安装Jenkins
- 安装必要的插件:Allure Jenkins Plugin, Pipeline
- 创建Jenkinsfile:
pipeline { agent any environment { PYTHONPATH = "${WORKSPACE}" } stages { stage('Checkout') { steps { git 'https://github.com/yourusername/web_automation_framework.git' } } stage('Setup Environment') { steps { sh 'pip install -r requirements.txt' } } stage('Run Tests') { steps { sh 'pytest --alluredir=allure-results' } } } post { always { script { allure([ includeProperties: false, jdk: '', properties: [], reportBuildPolicy: 'ALWAYS', results: [[path: 'allure-results']] ]) } } } }
8.2 GitHub Actions集成
创建.github/workflows/tests.yml
文件:
name: Run Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run tests run: | pytest --alluredir=allure-results - name: Get Allure history uses: actions/checkout@v2 if: always() continue-on-error: true with: ref: gh-pages path: gh-pages - name: Generate Allure report uses: simple-elf/allure-report-action@master if: always() with: gh_pages: gh-pages allure_history: allure-history allure_results: allure-results keep_reports: 20 - name: Deploy report to GitHub Pages if: always() uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: allure-history
9. 常见问题与解决方案
9.1 元素定位问题
问题:元素无法定位或定位不稳定。
解决方案:
使用更稳定的定位策略:
- ID > Name > CSS > XPath
- 避免使用绝对XPath
使用显式等待代替隐式等待:
# 不推荐 time.sleep(5) # 硬编码等待 # 推荐 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "myElement")) )
- 封装更健壮的元素查找方法:
# utils/element_utils.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import NoSuchElementException, TimeoutException def find_element_with_retry(driver, locator, max_attempts=3, wait_time=10): """重试机制查找元素""" last_exception = None for attempt in range(1, max_attempts + 1): try: element = WebDriverWait(driver, wait_time).until( EC.presence_of_element_located(locator) ) return element except (NoSuchElementException, TimeoutException) as e: last_exception = e logger.warning(f"Attempt {attempt} failed to find element {locator}") if attempt < max_attempts: # 可以在这里添加一些操作,比如刷新页面或滚动 driver.refresh() raise last_exception
9.2 动态元素处理
问题:页面元素是动态生成的,ID或属性会变化。
解决方案:
- 使用更稳定的属性定位:
# 使用多个属性组合定位 element = driver.find_element(By.XPATH, "//input[@type='text' and @name='username']") # 使用文本内容定位 element = driver.find_element(By.XPATH, "//button[contains(text(), 'Login')]") # 使用父元素定位子元素 parent_element = driver.find_element(By.ID, "container") child_element = parent_element.find_element(By.CLASS_NAME, "submit-button")
- 使用XPath轴:
# 查找包含特定文本的元素的兄弟元素 element = driver.find_element(By.XPATH, "//td[text()='Product Name']/following-sibling::td[1]") # 查找具有特定属性的祖先元素 element = driver.find_element(By.XPATH, "//input[@name='email']/ancestor::div[@class='form-group']")
9.3 弹窗处理
问题:测试过程中出现各种弹窗,如alert、confirm、prompt等。
解决方案:
# utils/alert_utils.py from selenium.webdriver.common.alert import Alert from utils.logger_utils import get_logger logger = get_logger(__name__) def handle_alert(driver, action="accept", text=None): """ 处理弹窗 :param driver: WebDriver实例 :param action: 动作类型,accept/dismiss/send_keys :param text: 如果是prompt弹窗,输入的文本 :return: 弹窗文本 """ try: alert = Alert(driver) alert_text = alert.text logger.info(f"Alert text: {alert_text}") if action == "accept": alert.accept() logger.info("Alert accepted") elif action == "dismiss": alert.dismiss() logger.info("Alert dismissed") elif action == "send_keys" and text: alert.send_keys(text) alert.accept() logger.info(f"Text '{text}' sent to alert and accepted") return alert_text except Exception as e: logger.error(f"Error handling alert: {e}") raise
9.4 文件上传处理
问题:如何处理文件上传功能。
解决方案:
# utils/file_upload_utils.py import os from utils.logger_utils import get_logger logger = get_logger(__name__) def upload_file(driver, file_input_locator, file_path): """ 上传文件 :param driver: WebDriver实例 :param file_input_locator: 文件输入元素定位器 :param file_path: 要上传的文件路径 """ try: # 确保文件路径是绝对路径 abs_file_path = os.path.abspath(file_path) # 检查文件是否存在 if not os.path.exists(abs_file_path): raise FileNotFoundError(f"File not found: {abs_file_path}") # 找到文件输入元素 file_input = driver.find_element(*file_input_locator) # 输入文件路径 file_input.send_keys(abs_file_path) logger.info(f"File uploaded successfully: {abs_file_path}") except Exception as e: logger.error(f"Error uploading file: {e}") raise
9.5 验证码处理
问题:如何处理登录或操作时的验证码。
解决方案:
验证码是为了防止自动化程序,所以最好的解决方案是在测试环境中禁用验证码或使用测试专用的验证码。
禁用验证码(推荐):
- 与开发团队合作,在测试环境中添加一个开关,可以禁用验证码
- 或者使用固定的测试验证码
如果无法禁用验证码,可以尝试以下方法:
# utils/captcha_utils.py import time import base64 import requests from io import BytesIO from PIL import Image from pytesseract import pytesseract from utils.logger_utils import get_logger logger = get_logger(__name__) def solve_captcha(driver, captcha_image_locator): """ 尝试识别验证码 注意:这种方法不一定可靠,成功率取决于验证码的复杂度 """ try: # 找到验证码图片元素 captcha_element = driver.find_element(*captcha_image_locator) # 获取图片base64编码 captcha_src = captcha_element.get_attribute("src") # 如果是base64编码的图片 if "base64" in captcha_src: # 提取base64数据 base64_data = captcha_src.split(",")[1] # 解码为二进制数据 binary_data = base64.b64decode(base64_data) # 转换为Image对象 image = Image.open(BytesIO(binary_data)) else: # 如果是图片URL,下载图片 response = requests.get(captcha_src) image = Image.open(BytesIO(response.content)) # 使用OCR识别验证码 captcha_text = pytesseract.image_to_string(image) # 清理识别结果 captcha_text = captcha_text.strip().replace(" ", "").replace("n", "") logger.info(f"Captcha recognized as: {captcha_text}") return captcha_text except Exception as e: logger.error(f"Error solving captcha: {e}") raise
- 使用第三方验证码识别服务(如2Captcha):
# utils/captcha_utils.py import requests import time from utils.logger_utils import get_logger logger = get_logger(__name__) def solve_captcha_with_service(api_key, captcha_image_locator): """ 使用第三方服务识别验证码 :param api_key: 2Captcha API密钥 :param captcha_image_locator: 验证码图片元素定位器 :return: 识别的验证码文本 """ try: # 这里以2Captcha为例 # 1. 提交验证码 # 实际实现需要根据具体服务的API文档进行 # 2. 等待识别结果 # 通常需要轮询服务API,直到识别完成 # 3. 返回识别结果 captcha_text = "recognized_captcha_text" # 这里应该是从服务获取的识别结果 logger.info(f"Captcha recognized as: {captcha_text}") return captcha_text except Exception as e: logger.error(f"Error solving captcha with service: {e}") raise
10. 实战案例:完整的Web应用自动化测试
让我们以一个电子商务网站为例,实现一个完整的自动化测试流程。
10.1 项目结构扩展
ecommerce_automation/ │ ├── config/ │ ├── __init__.py │ └── config.py │ ├── pages/ │ ├── __init__.py │ ├── base_page.py │ ├── home_page.py │ ├── login_page.py │ ├── product_page.py │ ├── cart_page.py │ └── checkout_page.py │ ├── tests/ │ ├── __init__.py │ ├── test_login.py │ ├── test_search.py │ ├── test_cart.py │ ├── test_checkout.py │ └── test_order_history.py │ ├── data/ │ ├── user_credentials.xlsx │ ├── product_data.json │ └── credit_card_data.csv │ ├── utils/ │ ├── __init__.py │ ├── excel_utils.py │ ├── json_utils.py │ ├── csv_utils.py │ ├── screenshot_utils.py │ ├── logger_utils.py │ ├── payment_utils.py │ └── order_utils.py │ ├── reports/ │ ├── html/ │ └── allure-results/ │ ├── requirements.txt └── pytest.ini
10.2 产品页面实现
# pages/product_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage from utils.logger_utils import get_logger logger = get_logger(__name__) class ProductPage(BasePage): # 页面元素定位器 PRODUCT_NAME = (By.CLASS_NAME, "product-name") PRODUCT_PRICE = (By.CLASS_NAME, "product-price") PRODUCT_DESCRIPTION = (By.CLASS_NAME, "product-description") ADD_TO_CART_BUTTON = (By.ID, "add-to-cart-btn") QUANTITY_INPUT = (By.ID, "quantity") SIZE_SELECT = (By.ID, "size-select") COLOR_SELECT = (By.ID, "color-select") SUCCESS_MESSAGE = (By.CLASS_NAME, "success-message") def __init__(self, driver): super().__init__(driver) def get_product_name(self): """获取产品名称""" return self.get_text(self.PRODUCT_NAME) def get_product_price(self): """获取产品价格""" price_text = self.get_text(self.PRODUCT_PRICE) # 移除货币符号,转换为数字 return float(price_text.replace("$", "").replace(",", "")) def get_product_description(self): """获取产品描述""" return self.get_text(self.PRODUCT_DESCRIPTION) def select_size(self, size): """选择产品尺寸""" logger.info(f"Selecting size: {size}") self.select_by_visible_text(self.SIZE_SELECT, size) def select_color(self, color): """选择产品颜色""" logger.info(f"Selecting color: {color}") self.select_by_visible_text(self.COLOR_SELECT, color) def set_quantity(self, quantity): """设置产品数量""" logger.info(f"Setting quantity: {quantity}") self.input_text(self.QUANTITY_INPUT, str(quantity)) def add_to_cart(self): """添加产品到购物车""" logger.info("Adding product to cart") self.click(self.ADD_TO_CART_BUTTON) # 等待成功消息出现 if self.is_visible(self.SUCCESS_MESSAGE): success_message = self.get_text(self.SUCCESS_MESSAGE) logger.info(f"Success message: {success_message}") return success_message return None def add_product_to_cart(self, size=None, color=None, quantity=1): """添加产品到购物车的完整流程""" if size: self.select_size(size) if color: self.select_color(color) if quantity > 1: self.set_quantity(quantity) return self.add_to_cart()
10.3 购物车页面实现
# pages/cart_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage from utils.logger_utils import get_logger logger = get_logger(__name__) class CartPage(BasePage): # 页面元素定位器 CART_ITEMS = (By.CLASS_NAME, "cart-item") ITEM_NAME = (By.CLASS_NAME, "item-name") ITEM_PRICE = (By.CLASS_NAME, "item-price") ITEM_QUANTITY = (By.CLASS_NAME, "item-quantity") ITEM_TOTAL = (By.CLASS_NAME, "item-total") REMOVE_BUTTON = (By.CLASS_NAME, "remove-btn") UPDATE_CART_BUTTON = (By.ID, "update-cart-btn") CHECKOUT_BUTTON = (By.ID, "checkout-btn") CART_TOTAL = (By.ID, "cart-total") EMPTY_CART_MESSAGE = (By.CLASS_NAME, "empty-cart-message") def __init__(self, driver): super().__init__(driver) def get_cart_items(self): """获取购物车中的所有商品""" items = self.find_elements(*self.CART_ITEMS) cart_items = [] for item in items: item_data = { 'name': item.find_element(*self.ITEM_NAME).text, 'price': float(item.find_element(*self.ITEM_PRICE).text.replace("$", "").replace(",", "")), 'quantity': int(item.find_element(*self.ITEM_QUANTITY).get_attribute("value")), 'total': float(item.find_element(*self.ITEM_TOTAL).text.replace("$", "").replace(",", "")) } cart_items.append(item_data) return cart_items def get_cart_total(self): """获取购物车总金额""" total_text = self.get_text(self.CART_TOTAL) return float(total_text.replace("$", "").replace(",", "")) def is_cart_empty(self): """判断购物车是否为空""" return self.is_visible(self.EMPTY_CART_MESSAGE) def remove_item(self, item_name): """从购物车中移除指定商品""" logger.info(f"Removing item from cart: {item_name}") # 找到所有购物车商品 items = self.find_elements(*self.CART_ITEMS) for item in items: name_element = item.find_element(*self.ITEM_NAME) if name_element.text == item_name: # 找到匹配的商品,点击移除按钮 remove_button = item.find_element(*self.REMOVE_BUTTON) remove_button.click() logger.info(f"Item {item_name} removed from cart") return True logger.warning(f"Item {item_name} not found in cart") return False def update_item_quantity(self, item_name, new_quantity): """更新购物车中商品的数量""" logger.info(f"Updating quantity of {item_name} to {new_quantity}") # 找到所有购物车商品 items = self.find_elements(*self.CART_ITEMS) for item in items: name_element = item.find_element(*self.ITEM_NAME) if name_element.text == item_name: # 找到匹配的商品,更新数量 quantity_input = item.find_element(*self.ITEM_QUANTITY) quantity_input.clear() quantity_input.send_keys(str(new_quantity)) # 点击更新购物车按钮 self.click(self.UPDATE_CART_BUTTON) logger.info(f"Quantity of {item_name} updated to {new_quantity}") return True logger.warning(f"Item {item_name} not found in cart") return False def proceed_to_checkout(self): """进入结账流程""" logger.info("Proceeding to checkout") self.click(self.CHECKOUT_BUTTON)
10.4 结账页面实现
# pages/checkout_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage from utils.logger_utils import get_logger logger = get_logger(__name__) class CheckoutPage(BasePage): # 页面元素定位器 SHIPPING_ADDRESS_FORM = (By.ID, "shipping-address-form") FIRST_NAME_INPUT = (By.ID, "first-name") LAST_NAME_INPUT = (By.ID, "last-name") ADDRESS_INPUT = (By.ID, "address") CITY_INPUT = (By.ID, "city") STATE_SELECT = (By.ID, "state") ZIP_CODE_INPUT = (By.ID, "zip-code") COUNTRY_SELECT = (By.ID, "country") PAYMENT_METHOD_FORM = (By.ID, "payment-method-form") CREDIT_CARD_RADIO = (By.ID, "credit-card") PAYPAL_RADIO = (By.ID, "paypal") CREDIT_CARD_FORM = (By.ID, "credit-card-form") CARD_NUMBER_INPUT = (By.ID, "card-number") CARD_NAME_INPUT = (By.ID, "card-name") EXPIRY_DATE_INPUT = (By.ID, "expiry-date") CVV_INPUT = (By.ID, "cvv") PLACE_ORDER_BUTTON = (By.ID, "place-order-btn") ORDER_CONFIRMATION = (By.CLASS_NAME, "order-confirmation") ORDER_NUMBER = (By.CLASS_NAME, "order-number") def __init__(self, driver): super().__init__(driver) def fill_shipping_address(self, first_name, last_name, address, city, state, zip_code, country): """填写收货地址""" logger.info("Filling shipping address") with logger.context("Shipping Address"): self.input_text(self.FIRST_NAME_INPUT, first_name) self.input_text(self.LAST_NAME_INPUT, last_name) self.input_text(self.ADDRESS_INPUT, address) self.input_text(self.CITY_INPUT, city) self.select_by_visible_text(self.STATE_SELECT, state) self.input_text(self.ZIP_CODE_INPUT, zip_code) self.select_by_visible_text(self.COUNTRY_SELECT, country) def select_payment_method(self, method="credit_card"): """选择支付方式""" logger.info(f"Selecting payment method: {method}") if method.lower() == "credit_card": self.click(self.CREDIT_CARD_RADIO) elif method.lower() == "paypal": self.click(self.PAYPAL_RADIO) else: raise ValueError(f"Unsupported payment method: {method}") def fill_credit_card_info(self, card_number, card_name, expiry_date, cvv): """填写信用卡信息""" logger.info("Filling credit card information") with logger.context("Credit Card"): self.input_text(self.CARD_NUMBER_INPUT, card_number) self.input_text(self.CARD_NAME_INPUT, card_name) self.input_text(self.EXPIRY_DATE_INPUT, expiry_date) self.input_text(self.CVV_INPUT, cvv) def place_order(self): """下订单""" logger.info("Placing order") self.click(self.PLACE_ORDER_BUTTON) def is_order_successful(self): """判断订单是否成功""" return self.is_visible(self.ORDER_CONFIRMATION) def get_order_number(self): """获取订单号""" if self.is_order_successful(): return self.get_text(self.ORDER_NUMBER) return None def complete_checkout(self, shipping_info, payment_info): """完成结账流程""" # 填写收货地址 self.fill_shipping_address( shipping_info['first_name'], shipping_info['last_name'], shipping_info['address'], shipping_info['city'], shipping_info['state'], shipping_info['zip_code'], shipping_info['country'] ) # 选择支付方式 self.select_payment_method(payment_info['method']) # 如果是信用卡支付,填写信用卡信息 if payment_info['method'].lower() == "credit_card": self.fill_credit_card_info( payment_info['card_number'], payment_info['card_name'], payment_info['expiry_date'], payment_info['cvv'] ) # 下订单 self.place_order() # 返回订单号 return self.get_order_number()
10.5 完整的购物流程测试
# tests/test_complete_purchase.py import pytest import json import os from pages.home_page import HomePage from pages.product_page import ProductPage from pages.cart_page import CartPage from pages.checkout_page import CheckoutPage from utils.logger_utils import get_logger logger = get_logger(__name__) class TestCompletePurchase: """完整的购物流程测试""" @pytest.fixture(scope="function") def product_data(self): """加载产品数据""" current_dir = os.path.dirname(os.path.abspath(__file__)) data_dir = os.path.join(os.path.dirname(current_dir), 'data') product_data_file = os.path.join(data_dir, 'product_data.json') with open(product_data_file, 'r') as f: return json.load(f) @pytest.fixture(scope="function") def shipping_info(self): """收货地址信息""" return { 'first_name': 'John', 'last_name': 'Doe', 'address': '123 Main St', 'city': 'New York', 'state': 'NY', 'zip_code': '10001', 'country': 'United States' } @pytest.fixture(scope="function") def payment_info(self): """支付信息""" return { 'method': 'credit_card', 'card_number': '4111111111111111', 'card_name': 'John Doe', 'expiry_date': '12/25', 'cvv': '123' } @allure.feature("购物流程") @allure.story("完整购物流程") @allure.severity(allure.severity_level.CRITICAL) def test_complete_purchase(self, logged_in_driver, home_page, product_page, cart_page, checkout_page, product_data, shipping_info, payment_info): """测试完整的购物流程""" with allure.step("浏览并选择产品"): # 假设我们有一个产品URL product_url = product_data['product_url'] home_page.open() home_page.driver.get(product_url) # 验证产品页面加载成功 product_name = product_page.get_product_name() assert product_name == product_data['name'], f"Product name should be {product_data['name']}" product_price = product_page.get_product_price() assert product_price == product_data['price'], f"Product price should be {product_data['price']}" with allure.step("添加产品到购物车"): # 添加产品到购物车 success_message = product_page.add_product_to_cart( size=product_data.get('size'), color=product_data.get('color'), quantity=product_data.get('quantity', 1) ) assert success_message is not None, "Success message should be displayed after adding to cart" assert "added to cart" in success_message.lower(), "Success message should confirm item was added to cart" with allure.step("查看购物车"): # 进入购物车页面 cart_url = product_data['cart_url'] home_page.driver.get(cart_url) # 验证购物车中的产品 cart_items = cart_page.get_cart_items() assert len(cart_items) > 0, "Cart should not be empty" # 验证产品信息 cart_item = cart_items[0] assert cart_item['name'] == product_data['name'], f"Product name in cart should be {product_data['name']}" assert cart_item['price'] == product_data['price'], f"Product price in cart should be {product_data['price']}" assert cart_item['quantity'] == product_data.get('quantity', 1), f"Product quantity in cart should be {product_data.get('quantity', 1)}" # 验证总价 expected_total = product_data['price'] * product_data.get('quantity', 1) cart_total = cart_page.get_cart_total() assert cart_total == expected_total, f"Cart total should be {expected_total}, but got {cart_total}" with allure.step("进入结账流程"): # 进入结账页面 cart_page.proceed_to_checkout() # 验证结账页面加载成功 assert checkout_page.is_visible(checkout_page.SHIPPING_ADDRESS_FORM), "Checkout page should be loaded" with allure.step("填写收货地址和支付信息"): # 完成结账流程 order_number = checkout_page.complete_checkout(shipping_info, payment_info) # 验证订单成功 assert order_number is not None, "Order number should be generated after successful checkout" assert order_number.startswith("ORD-"), "Order number should start with 'ORD-'" logger.info(f"Order placed successfully. Order number: {order_number}") with allure.step("验证订单历史"): # 这里可以添加验证订单历史的代码 # 例如,进入订单历史页面,验证新订单出现在列表中 pass
11. 总结与最佳实践
11.1 框架优势总结
我们构建的Web自动化测试框架具有以下优势:
模块化设计:使用Page Object Model模式,将页面元素和操作封装在单独的类中,提高了代码的可维护性和可重用性。
清晰的目录结构:按照功能划分目录,使项目结构清晰,易于理解和扩展。
丰富的工具支持:集成了日志记录、截图、数据驱动等功能,便于调试和分析测试结果。
灵活的测试执行:支持多种浏览器、无头模式、参数化测试等,满足不同测试需求。
详细的测试报告:集成了HTML和Allure报告,提供直观的测试结果展示。
持续集成支持:可以轻松集成到Jenkins、GitHub Actions等CI/CD工具中,实现自动化测试流程。
11.2 最佳实践建议
保持测试独立性:每个测试用例应该是独立的,不依赖于其他测试用例的执行结果。
使用等待机制:避免使用硬编码的
time.sleep()
,而是使用显式等待或隐式等待。合理组织测试数据:将测试数据与测试代码分离,使用外部文件存储测试数据。
实现重试机制:对于不稳定的测试,可以实现重试机制,提高测试的稳定性。
定期维护测试:随着应用的变化,定期更新和维护测试用例,确保测试的有效性。
使用版本控制:使用Git等版本控制工具管理测试代码,跟踪变更历史。
编写清晰的文档:为框架和测试用例编写清晰的文档,便于团队成员理解和维护。
11.3 未来扩展方向
API测试集成:将Web UI测试与API测试结合,实现更全面的测试覆盖。
性能测试支持:集成性能测试工具,如JMeter或Locust,进行性能测试。
移动端测试扩展:使用Appium扩展框架,支持移动应用测试。
AI辅助测试:探索使用AI技术辅助测试用例生成和缺陷检测。
测试数据管理:实现更完善的测试数据管理,如测试数据工厂、数据清理等。
通过本教程,你已经学会了如何使用PyCharm和Selenium构建一个完整的Web自动化测试框架。这个框架不仅可以帮助你解决实际的测试难题,还可以根据项目需求进行灵活扩展。希望这个教程对你的工作有所帮助!