Redmine系统Web自动化测试¶
1. Web自动化测试概述¶
1.1 自动化测试目标¶
Web自动化测试的目标是通过自动化工具和技术,提高测试效率,减少重复性工作,确保Redmine系统Web界面的功能稳定性和用户体验质量。
1.2 自动化测试范围¶
- 用户登录和权限验证
- 项目管理功能自动化测试
- 问题跟踪功能自动化测试
- 过滤器功能自动化测试
- 界面交互自动化测试
- 回归测试自动化
1.3 自动化测试原则¶
- 可维护性:测试脚本易于维护和更新
- 可复用性:测试脚本可在不同场景下复用
- 稳定性:测试脚本执行稳定可靠
- 效率性:提高测试执行效率
- 覆盖性:覆盖主要功能场景
2. 自动化测试框架¶
2.1 技术选型¶
2.1.1 测试框架¶
- Selenium WebDriver:Web自动化测试核心框架
- Python:主要编程语言
- pytest:测试执行框架
- Page Object Model:页面对象模式
2.1.2 工具选择¶
- 浏览器驱动:ChromeDriver、FirefoxDriver
- 测试报告:Allure、HTMLTestRunner
- 数据管理:Excel、JSON、CSV
- 版本控制:Git
2.2 框架架构¶
2.2.1 目录结构¶
RedmineWebTestProject/
├── config/ # 配置文件
│ ├── config.py # 基础配置
│ └── test_data.json # 测试数据
├── pages/ # 页面对象
│ ├── base_page.py # 基础页面类
│ ├── login_page.py # 登录页面
│ ├── project_page.py # 项目页面
│ └── issue_page.py # 问题页面
├── tests/ # 测试用例
│ ├── test_login.py # 登录测试
│ ├── test_project.py # 项目测试
│ └── test_issue.py # 问题测试
├── utils/ # 工具类
│ ├── driver_manager.py # 驱动管理
│ ├── data_utils.py # 数据工具
│ └── report_utils.py # 报告工具
├── reports/ # 测试报告
├── logs/ # 日志文件
└── requirements.txt # 依赖包
2.2.2 设计模式¶
- Page Object Model:页面对象模式
- 数据驱动:测试数据与测试逻辑分离
- 关键字驱动:关键字驱动的测试框架
- 模块化设计:功能模块化设计
3. 页面对象设计¶
3.1 基础页面类¶
3.1.1 BasePage类设计¶
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
class BasePage:
"""基础页面类,提供通用方法"""
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def find_element(self, locator):
"""查找元素"""
try:
element = self.wait.until(EC.presence_of_element_located(locator))
return element
except TimeoutException:
raise Exception(f"元素未找到: {locator}")
def click_element(self, locator):
"""点击元素"""
element = self.find_element(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_present(self, locator):
"""检查元素是否存在"""
try:
self.find_element(locator)
return True
except:
return False
3.2 登录页面对象¶
3.2.1 LoginPage类设计¶
from pages.base_page import BasePage
class LoginPage(BasePage):
"""登录页面对象"""
# 页面元素定位器
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.NAME, "login")
ERROR_MESSAGE = (By.CLASS_NAME, "error")
LOGOUT_LINK = (By.LINK_TEXT, "登出")
def __init__(self, driver):
super().__init__(driver)
self.url = "http://192.168.100.200:3000/login"
def open_login_page(self):
"""打开登录页面"""
self.driver.get(self.url)
def input_username(self, username):
"""输入用户名"""
self.input_text(self.USERNAME_INPUT, username)
def input_password(self, password):
"""输入密码"""
self.input_text(self.PASSWORD_INPUT, password)
def click_login_button(self):
"""点击登录按钮"""
self.click_element(self.LOGIN_BUTTON)
def login(self, username, password):
"""执行登录操作"""
self.open_login_page()
self.input_username(username)
self.input_password(password)
self.click_login_button()
def get_error_message(self):
"""获取错误信息"""
if self.is_element_present(self.ERROR_MESSAGE):
return self.get_text(self.ERROR_MESSAGE)
return None
def is_logged_in(self):
"""检查是否已登录"""
return self.is_element_present(self.LOGOUT_LINK)
3.3 项目页面对象¶
3.3.1 ProjectPage类设计¶
from pages.base_page import BasePage
class ProjectPage(BasePage):
"""项目页面对象"""
# 页面元素定位器
NEW_PROJECT_BUTTON = (By.LINK_TEXT, "新建项目")
PROJECT_NAME_INPUT = (By.ID, "project_name")
PROJECT_IDENTIFIER_INPUT = (By.ID, "project_identifier")
PROJECT_DESCRIPTION_INPUT = (By.ID, "project_description")
CREATE_BUTTON = (By.NAME, "commit")
PROJECT_LIST = (By.CLASS_NAME, "projects")
PROJECT_LINK = (By.CSS_SELECTOR, "a[href*='/projects/']")
EDIT_PROJECT_BUTTON = (By.LINK_TEXT, "编辑项目")
SAVE_BUTTON = (By.NAME, "commit")
def __init__(self, driver):
super().__init__(driver)
self.url = "http://192.168.100.200:3000/projects"
def open_project_page(self):
"""打开项目页面"""
self.driver.get(self.url)
def click_new_project(self):
"""点击新建项目按钮"""
self.click_element(self.NEW_PROJECT_BUTTON)
def input_project_name(self, name):
"""输入项目名称"""
self.input_text(self.PROJECT_NAME_INPUT, name)
def input_project_identifier(self, identifier):
"""输入项目标识符"""
self.input_text(self.PROJECT_IDENTIFIER_INPUT, identifier)
def input_project_description(self, description):
"""输入项目描述"""
self.input_text(self.PROJECT_DESCRIPTION_INPUT, description)
def click_create_button(self):
"""点击创建按钮"""
self.click_element(self.CREATE_BUTTON)
def create_project(self, name, identifier, description=""):
"""创建项目"""
self.open_project_page()
self.click_new_project()
self.input_project_name(name)
self.input_project_identifier(identifier)
if description:
self.input_project_description(description)
self.click_create_button()
def get_project_list(self):
"""获取项目列表"""
projects = self.driver.find_elements(*self.PROJECT_LINK)
return [project.text for project in projects]
def click_project(self, project_name):
"""点击指定项目"""
project_link = (By.LINK_TEXT, project_name)
self.click_element(project_link)
def click_edit_project(self):
"""点击编辑项目按钮"""
self.click_element(self.EDIT_PROJECT_BUTTON)
def edit_project(self, new_name, new_description=""):
"""编辑项目"""
self.click_edit_project()
self.input_project_name(new_name)
if new_description:
self.input_project_description(new_description)
self.click_element(self.SAVE_BUTTON)
3.4 问题页面对象¶
3.4.1 IssuePage类设计¶
from pages.base_page import BasePage
class IssuePage(BasePage):
"""问题页面对象"""
# 页面元素定位器
NEW_ISSUE_BUTTON = (By.LINK_TEXT, "新建问题")
ISSUE_SUBJECT_INPUT = (By.ID, "issue_subject")
ISSUE_DESCRIPTION_INPUT = (By.ID, "issue_description")
TRACKER_SELECT = (By.ID, "issue_tracker_id")
PRIORITY_SELECT = (By.ID, "issue_priority_id")
CREATE_ISSUE_BUTTON = (By.NAME, "commit")
ISSUE_LIST = (By.CLASS_NAME, "issues")
ISSUE_LINK = (By.CSS_SELECTOR, "a[href*='/issues/']")
EDIT_ISSUE_BUTTON = (By.LINK_TEXT, "编辑")
STATUS_SELECT = (By.ID, "issue_status_id")
UPDATE_BUTTON = (By.NAME, "commit")
def __init__(self, driver):
super().__init__(driver)
self.url = "http://192.168.100.200:3000/issues"
def open_issue_page(self):
"""打开问题页面"""
self.driver.get(self.url)
def click_new_issue(self):
"""点击新建问题按钮"""
self.click_element(self.NEW_ISSUE_BUTTON)
def input_issue_subject(self, subject):
"""输入问题标题"""
self.input_text(self.ISSUE_SUBJECT_INPUT, subject)
def input_issue_description(self, description):
"""输入问题描述"""
self.input_text(self.ISSUE_DESCRIPTION_INPUT, description)
def select_tracker(self, tracker):
"""选择跟踪标签"""
from selenium.webdriver.support.ui import Select
select = Select(self.find_element(self.TRACKER_SELECT))
select.select_by_visible_text(tracker)
def select_priority(self, priority):
"""选择优先级"""
from selenium.webdriver.support.ui import Select
select = Select(self.find_element(self.PRIORITY_SELECT))
select.select_by_visible_text(priority)
def click_create_issue_button(self):
"""点击创建问题按钮"""
self.click_element(self.CREATE_ISSUE_BUTTON)
def create_issue(self, subject, tracker="缺陷", priority="普通", description=""):
"""创建问题"""
self.open_issue_page()
self.click_new_issue()
self.input_issue_subject(subject)
self.select_tracker(tracker)
self.select_priority(priority)
if description:
self.input_issue_description(description)
self.click_create_issue_button()
def get_issue_list(self):
"""获取问题列表"""
issues = self.driver.find_elements(*self.ISSUE_LINK)
return [issue.text for issue in issues]
def click_issue(self, issue_subject):
"""点击指定问题"""
issue_link = (By.LINK_TEXT, issue_subject)
self.click_element(issue_link)
def click_edit_issue(self):
"""点击编辑问题按钮"""
self.click_element(self.EDIT_ISSUE_BUTTON)
def update_issue_status(self, status):
"""更新问题状态"""
self.click_edit_issue()
from selenium.webdriver.support.ui import Select
select = Select(self.find_element(self.STATUS_SELECT))
select.select_by_visible_text(status)
self.click_element(self.UPDATE_BUTTON)
4. 测试用例设计¶
4.1 登录功能测试¶
4.1.1 正常登录测试¶
import pytest
from pages.login_page import LoginPage
from utils.driver_manager import DriverManager
class TestLogin:
"""登录功能测试类"""
@pytest.fixture(scope="class")
def driver(self):
"""初始化驱动"""
driver_manager = DriverManager()
driver = driver_manager.get_driver()
yield driver
driver.quit()
@pytest.fixture
def login_page(self, driver):
"""初始化登录页面"""
return LoginPage(driver)
def test_successful_login(self, login_page):
"""测试正常登录"""
# 测试数据
username = "admin"
password = "password"
# 执行登录
login_page.login(username, password)
# 验证结果
assert login_page.is_logged_in(), "登录失败"
def test_invalid_username(self, login_page):
"""测试无效用户名"""
# 测试数据
username = "invalid_user"
password = "password"
# 执行登录
login_page.login(username, password)
# 验证结果
error_message = login_page.get_error_message()
assert error_message is not None, "应该显示错误信息"
assert "用户名或密码错误" in error_message, "错误信息不正确"
def test_invalid_password(self, login_page):
"""测试无效密码"""
# 测试数据
username = "admin"
password = "wrong_password"
# 执行登录
login_page.login(username, password)
# 验证结果
error_message = login_page.get_error_message()
assert error_message is not None, "应该显示错误信息"
assert "用户名或密码错误" in error_message, "错误信息不正确"
def test_empty_username(self, login_page):
"""测试空用户名"""
# 测试数据
username = ""
password = "password"
# 执行登录
login_page.login(username, password)
# 验证结果
error_message = login_page.get_error_message()
assert error_message is not None, "应该显示错误信息"
def test_empty_password(self, login_page):
"""测试空密码"""
# 测试数据
username = "admin"
password = ""
# 执行登录
login_page.login(username, password)
# 验证结果
error_message = login_page.get_error_message()
assert error_message is not None, "应该显示错误信息"
4.2 项目管理功能测试¶
4.2.1 项目创建测试¶
import pytest
from pages.login_page import LoginPage
from pages.project_page import ProjectPage
from utils.driver_manager import DriverManager
class TestProject:
"""项目管理功能测试类"""
@pytest.fixture(scope="class")
def driver(self):
"""初始化驱动"""
driver_manager = DriverManager()
driver = driver_manager.get_driver()
yield driver
driver.quit()
@pytest.fixture
def login_page(self, driver):
"""初始化登录页面"""
return LoginPage(driver)
@pytest.fixture
def project_page(self, driver):
"""初始化项目页面"""
return ProjectPage(driver)
@pytest.fixture(autouse=True)
def setup(self, login_page):
"""测试前置条件"""
login_page.login("admin", "password")
def test_create_project_success(self, project_page):
"""测试成功创建项目"""
# 测试数据
project_name = "自动化测试项目"
project_identifier = "auto_test_project"
project_description = "这是一个自动化测试项目"
# 执行创建项目
project_page.create_project(project_name, project_identifier, project_description)
# 验证结果
project_list = project_page.get_project_list()
assert project_name in project_list, "项目创建失败"
def test_create_project_empty_name(self, project_page):
"""测试空项目名称"""
# 测试数据
project_name = ""
project_identifier = "empty_name_test"
# 执行创建项目
project_page.create_project(project_name, project_identifier)
# 验证结果 - 应该显示错误信息
# 这里需要根据实际页面实现来验证错误信息
assert True, "需要根据实际页面实现验证错误信息"
def test_create_project_duplicate_identifier(self, project_page):
"""测试重复项目标识符"""
# 测试数据
project_name = "重复标识符测试"
project_identifier = "auto_test_project" # 使用已存在的标识符
# 执行创建项目
project_page.create_project(project_name, project_identifier)
# 验证结果 - 应该显示错误信息
# 这里需要根据实际页面实现来验证错误信息
assert True, "需要根据实际页面实现验证错误信息"
def test_edit_project(self, project_page):
"""测试编辑项目"""
# 测试数据
project_name = "自动化测试项目"
new_name = "修改后的项目名称"
new_description = "修改后的项目描述"
# 点击项目
project_page.click_project(project_name)
# 编辑项目
project_page.edit_project(new_name, new_description)
# 验证结果
# 这里需要根据实际页面实现来验证编辑结果
assert True, "需要根据实际页面实现验证编辑结果"
4.3 问题跟踪功能测试¶
4.3.1 问题创建测试¶
import pytest
from pages.login_page import LoginPage
from pages.issue_page import IssuePage
from utils.driver_manager import DriverManager
class TestIssue:
"""问题跟踪功能测试类"""
@pytest.fixture(scope="class")
def driver(self):
"""初始化驱动"""
driver_manager = DriverManager()
driver = driver_manager.get_driver()
yield driver
driver.quit()
@pytest.fixture
def login_page(self, driver):
"""初始化登录页面"""
return LoginPage(driver)
@pytest.fixture
def issue_page(self, driver):
"""初始化问题页面"""
return IssuePage(driver)
@pytest.fixture(autouse=True)
def setup(self, login_page):
"""测试前置条件"""
login_page.login("admin", "password")
def test_create_issue_success(self, issue_page):
"""测试成功创建问题"""
# 测试数据
subject = "自动化测试问题"
tracker = "缺陷"
priority = "高"
description = "这是一个自动化测试问题"
# 执行创建问题
issue_page.create_issue(subject, tracker, priority, description)
# 验证结果
issue_list = issue_page.get_issue_list()
assert subject in issue_list, "问题创建失败"
def test_create_issue_empty_subject(self, issue_page):
"""测试空问题标题"""
# 测试数据
subject = ""
tracker = "缺陷"
priority = "普通"
# 执行创建问题
issue_page.create_issue(subject, tracker, priority)
# 验证结果 - 应该显示错误信息
# 这里需要根据实际页面实现来验证错误信息
assert True, "需要根据实际页面实现验证错误信息"
def test_update_issue_status(self, issue_page):
"""测试更新问题状态"""
# 测试数据
subject = "自动化测试问题"
new_status = "已确认"
# 点击问题
issue_page.click_issue(subject)
# 更新状态
issue_page.update_issue_status(new_status)
# 验证结果
# 这里需要根据实际页面实现来验证状态更新结果
assert True, "需要根据实际页面实现验证状态更新结果"
5. 测试数据管理¶
5.1 测试数据设计¶
5.1.1 用户数据¶
{
"users": {
"admin": {
"username": "admin",
"password": "password",
"role": "管理员"
},
"user1": {
"username": "user1",
"password": "password",
"role": "普通用户"
},
"testuser": {
"username": "testuser",
"password": "password",
"role": "测试用户"
}
}
}
5.1.2 项目数据¶
{
"projects": {
"valid_project": {
"name": "测试项目",
"identifier": "test_project",
"description": "这是一个测试项目"
},
"invalid_project": {
"name": "",
"identifier": "invalid_project",
"description": "无效项目"
}
}
}
5.1.3 问题数据¶
{
"issues": {
"bug_issue": {
"subject": "测试缺陷",
"tracker": "缺陷",
"priority": "高",
"description": "这是一个测试缺陷"
},
"feature_issue": {
"subject": "测试功能",
"tracker": "功能",
"priority": "普通",
"description": "这是一个测试功能"
}
}
}
5.2 数据驱动测试¶
5.2.1 参数化测试¶
import pytest
from pages.login_page import LoginPage
class TestLoginDataDriven:
"""数据驱动的登录测试"""
@pytest.fixture(scope="class")
def driver(self):
"""初始化驱动"""
driver_manager = DriverManager()
driver = driver_manager.get_driver()
yield driver
driver.quit()
@pytest.fixture
def login_page(self, driver):
"""初始化登录页面"""
return LoginPage(driver)
@pytest.mark.parametrize("username,password,expected", [
("admin", "password", True),
("invalid_user", "password", False),
("admin", "wrong_password", False),
("", "password", False),
("admin", "", False)
])
def test_login_data_driven(self, login_page, username, password, expected):
"""数据驱动的登录测试"""
# 执行登录
login_page.login(username, password)
# 验证结果
if expected:
assert login_page.is_logged_in(), f"登录应该成功: {username}"
else:
error_message = login_page.get_error_message()
assert error_message is not None, f"应该显示错误信息: {username}"
6. 测试执行和报告¶
6.1 测试执行配置¶
6.1.1 pytest配置文件¶
# pytest.ini
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--html=reports/report.html
--self-contained-html
--alluredir=reports/allure-results
-v
markers =
smoke: 冒烟测试
regression: 回归测试
login: 登录测试
project: 项目测试
issue: 问题测试
6.1.2 测试执行脚本¶
# run_tests.py
import subprocess
import sys
import os
def run_tests():
"""执行测试"""
# 执行冒烟测试
print("执行冒烟测试...")
subprocess.run([
sys.executable, "-m", "pytest",
"-m", "smoke",
"--html=reports/smoke_report.html",
"--self-contained-html"
])
# 执行完整测试
print("执行完整测试...")
subprocess.run([
sys.executable, "-m", "pytest",
"--html=reports/full_report.html",
"--self-contained-html",
"--alluredir=reports/allure-results"
])
# 生成Allure报告
print("生成Allure报告...")
subprocess.run(["allure", "generate", "reports/allure-results", "-o", "reports/allure-report"])
subprocess.run(["allure", "open", "reports/allure-report"])
if __name__ == "__main__":
run_tests()
6.2 测试报告生成¶
6.2.1 HTML报告¶
# report_utils.py
import pytest
from datetime import datetime
import os
class HTMLReportGenerator:
"""HTML报告生成器"""
def __init__(self):
self.report_dir = "reports"
self.ensure_report_dir()
def ensure_report_dir(self):
"""确保报告目录存在"""
if not os.path.exists(self.report_dir):
os.makedirs(self.report_dir)
def generate_report(self, test_results):
"""生成HTML报告"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
report_file = f"{self.report_dir}/test_report_{timestamp}.html"
html_content = self.create_html_content(test_results)
with open(report_file, 'w', encoding='utf-8') as f:
f.write(html_content)
return report_file
def create_html_content(self, test_results):
"""创建HTML内容"""
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>Redmine Web自动化测试报告</title>
<meta charset="utf-8">
<style>
body {font-family: Arial, sans-serif; margin: 20px; }
.header {background-color: #f0f0f0; padding: 20px; }
.summary {margin: 20px 0; }
.test-case {margin: 10px 0; padding: 10px; border: 1px solid #ddd; }
.passed {background-color: #d4edda; }
.failed {background-color: #f8d7da; }
.skipped {background-color: #fff3cd; }
</style>
</head>
<body>
<div class="header">
<h1>Redmine Web自动化测试报告</h1>
<p>生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
</div>
<div class="summary">
<h2>测试概要</h2>
<p>总测试用例数: {test_results['total']}</p>
<p>通过: {test_results['passed']}</p>
<p>失败: {test_results['failed']}</p>
<p>跳过: {test_results['skipped']}</p>
<p>通过率: {test_results['pass_rate']:.2f}%</p>
</div>
<div class="test-cases">
<h2>测试用例详情</h2>
{self.create_test_cases_html(test_results['test_cases'])}
</div>
</body>
</html>
"""
return html
def create_test_cases_html(self, test_cases):
"""创建测试用例HTML"""
html = ""
for test_case in test_cases:
status_class = test_case['status'].lower()
html += f"""
<div class="test-case {status_class}">
<h3>{test_case['name']}</h3>
<p>状态: {test_case['status']}</p>
<p>执行时间: {test_case['duration']}</p>
{f"<p>错误信息: {test_case['error']}</p>" if test_case['error'] else ""}
</div>
"""
return html
7. 持续集成¶
7.1 CI/CD配置¶
7.1.1 Jenkins Pipeline¶
// Jenkinsfile
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Setup Environment') {
steps {
sh 'python -m venv venv'
sh 'source venv/bin/activate && pip install -r requirements.txt'
}
}
stage('Run Tests') {
steps {
sh 'source venv/bin/activate && python -m pytest tests/ --html=reports/report.html --self-contained-html'
}
}
stage('Generate Report') {
steps {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'reports',
reportFiles: 'report.html',
reportName: 'Web自动化测试报告'
])
}
}
}
post {
always {
archiveArtifacts artifacts: 'reports/**', fingerprint: true
}
failure {
emailext (
subject: "Redmine Web自动化测试失败",
body: "测试执行失败,请查看报告",
to: "test-team@company.com"
)
}
}
}
7.2 自动化部署¶
7.2.1 部署脚本¶
#!/bin/bash
# deploy.sh
echo "开始部署Redmine Web自动化测试..."
# 检查Python环境
if ! command -v python3 &> /dev/null; then
echo "Python3未安装,请先安装Python3"
exit 1
fi
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装依赖
pip install -r requirements.txt
# 下载浏览器驱动
python -c "from selenium import webdriver; webdriver.Chrome()"
echo "部署完成!"
echo "运行测试: python run_tests.py"
8. 最佳实践¶
8.1 测试脚本最佳实践¶
8.1.1 代码规范¶
- 使用清晰的命名规范
- 添加适当的注释
- 遵循PEP 8代码风格
- 使用类型提示
8.1.2 错误处理¶
- 添加异常处理机制
- 使用重试机制
- 记录详细的错误信息
- 提供有意义的错误消息
8.2 维护最佳实践¶
8.2.1 页面对象维护¶
- 及时更新页面对象
- 使用稳定的定位策略
- 避免硬编码的等待时间
- 使用页面工厂模式
8.2.2 测试数据维护¶
- 使用外部数据文件
- 定期更新测试数据
- 使用数据生成工具
- 保持数据的有效性
8.3 执行最佳实践¶
8.3.1 测试执行¶
- 使用并行执行
- 优化测试顺序
- 使用测试标记
- 监控测试执行
8.3.2 结果分析¶
- 分析失败原因
- 跟踪测试趋势
- 优化测试用例
- 持续改进