随着测试用例数量增长,散乱的脚本、重复的代码、难以追溯的测试结果会严重拖慢团队效率。一个设计良好的UI自动化测试框架,应该具备分层清晰、配置灵活、报告美观、易于维护的特点。本文将带你从零设计一个企业级UI自动化测试框架,整合TestNG(Java)或JUnit + Allure报告,并提供可落地的代码结构。
一、为什么需要框架设计?
没有统一框架的UI自动化项目,通常会遇到:
每个测试类都重复写WebDriver初始化、等待、截图代码
元素定位器和测试数据散落在各处,修改成本高
测试执行结果难以分析,失败时不知道是环境问题还是bug
无法并行执行,整体运行时间过长
好的框架应该做到:
分层:驱动层 → 页面层 → 用例层 → 数据/配置层
可配置:浏览器类型、URL、超时时间等可通过配置文件修改
可扩展:添加新的浏览器支持、新的报告组件不破坏现有结构
可观测:自动生成详细的测试报告,失败时自动截图、记录日志
标题二、框架整体架构
text
├── src/main/java
│ ├── config/ # 配置管理(读取properties/yaml)
│ ├── driver/ # WebDriver工厂(管理浏览器启动)
│ ├── pages/ # Page Object类
│ │ ├── BasePage.java
│ │ ├── LoginPage.java
│ │ └── HomePage.java
│ └── utils/ # 工具类(截图、日志、数据读取)
├── src/test/java
│ ├── base/ # 测试基类(初始化、清理、监听器)
│ ├── tests/ # 测试用例类
│ └── suites/ # TestNG XML套件文件
├── src/test/resources
│ ├── config.properties # 环境配置
│ ├── testdata/ # Excel/JSON测试数据
│ └── log4j2.xml # 日志配置
├── allure-results/ # Allure原始数据(自动生成)
├── logs/ # 日志文件
├── screenshots/ # 失败截图
└── pom.xml
三、核心模块实现(Java + TestNG + Allure)
3.1 配置管理
使用.properties文件管理环境相关配置。
src/test/resources/config.properties:
properties
browser=chrome
base.url=https://example.com
implicit.wait=10
explicit.wait=10
headless=false
ConfigReader.java:
java
import java.io.InputStream;
import java.util.Properties;
public class ConfigReader {
private static Properties properties = new Properties();
static {
try (InputStream input = ConfigReader.class.getClassLoader()
.getResourceAsStream("config.properties")) {
properties.load(input);
} catch (Exception e) {
throw new RuntimeException("配置文件加载失败", e);
}
}
public static String getBrowser() { return properties.getProperty("browser"); }
public static String getBaseUrl() { return properties.getProperty("base.url"); }
public static int getImplicitWait() { return Integer.parseInt(properties.getProperty("implicit.wait")); }
public static int getExplicitWait() { return Integer.parseInt(properties.getProperty("explicit.wait")); }
public static boolean isHeadless() { return Boolean.parseBoolean(properties.getProperty("headless")); }
}
3.2 WebDriver工厂(支持多浏览器)
DriverFactory.java:
java
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
public class DriverFactory {
private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
public static WebDriver getDriver() {
if (driver.get() == null) {
driver.set(createDriver());
}
return driver.get();
}
private static WebDriver createDriver() {
String browser = ConfigReader.getBrowser().toLowerCase();
boolean headless = ConfigReader.isHeadless();
switch (browser) {
case "chrome":
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
if (headless) options.addArguments("--headless");
return new ChromeDriver(options);
case "firefox":
WebDriverManager.firefoxdriver().setup();
return new FirefoxDriver();
case "edge":
WebDriverManager.edgedriver().setup();
return new EdgeDriver();
default:
throw new IllegalArgumentException("不支持的浏览器: " + browser);
}
}
public static void quitDriver() {
if (driver.get() != null) {
driver.get().quit();
driver.remove();
}
}
}
3.3 BasePage(封装通用操作和等待)
BasePage.java:
java
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
public abstract class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
public BasePage() {
this.driver = DriverFactory.getDriver();
this.wait = new WebDriverWait(driver, Duration.ofSeconds(ConfigReader.getExplicitWait()));
}
protected void waitForVisibility(By locator) {
wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
protected void click(By locator) {
wait.until(ExpectedConditions.elementToBeClickable(locator)).click();
}
protected void sendKeys(By locator, String text) {
waitForVisibility(locator);
driver.findElement(locator).clear();
driver.findElement(locator).sendKeys(text);
}
protected String getText(By locator) {
waitForVisibility(locator);
return driver.findElement(locator).getText();
}
}
3.4 具体Page Object(以登录页为例)
LoginPage.java:
java
import org.openqa.selenium.By;
public class LoginPage extends BasePage {
private By usernameInput = By.id("username");
private By passwordInput = By.id("password");
private By loginBtn = By.cssSelector("button[type='submit']");
private By errorMsg = By.className("error");
public LoginPage inputUsername(String username) {
sendKeys(usernameInput, username);
return this;
}
public LoginPage inputPassword(String password) {
sendKeys(passwordInput, password);
return this;
}
public HomePage clickLoginSuccess() {
click(loginBtn);
return new HomePage();
}
public LoginPage clickLoginExpectingError() {
click(loginBtn);
return this;
}
public String getErrorMessage() {
return getText(errorMsg);
}
}
3.5 测试基类(初始化、清理、截图监听)
BaseTest.java:
java
import io.qameta.allure.Attachment;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.testng.ITestResult;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeSuite;
import java.lang.reflect.Method;
public class BaseTest {
@BeforeSuite
public void beforeSuite() {
// 可在此做全局初始化,如清理报告目录
System.out.println("=== 测试套件开始 ===");
}
@BeforeMethod
public void setUp(Method method) {
WebDriver driver = DriverFactory.getDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(ConfigReader.getImplicitWait()));
driver.get(ConfigReader.getBaseUrl());
System.out.println("开始执行: " + method.getName());
}
@AfterMethod
public void tearDown(ITestResult result) {
if (result.getStatus() == ITestResult.FAILURE) {
captureScreenshot(result.getMethod().getMethodName());
}
DriverFactory.quitDriver();
}
@Attachment(value = "{methodName} - 失败截图", type = "image/png")
public byte[] captureScreenshot(String methodName) {
TakesScreenshot ts = (TakesScreenshot) DriverFactory.getDriver();
return ts.getScreenshotAs(OutputType.BYTES);
}
@AfterSuite
public void afterSuite() {
System.out.println("=== 测试套件结束 ===");
}
}
3.6 具体测试用例
LoginTest.java:
java
import io.qameta.allure.Description;
import io.qameta.allure.Epic;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.testng.Assert;
import org.testng.annotations.Test;
@Epic("用户管理")
@Feature("登录功能")
public class LoginTest extends BaseTest {
@Test
@Description("使用正确的用户名和密码登录成功")
@Story("正常登录")
public void testLoginSuccess() {
LoginPage loginPage = new LoginPage();
HomePage homePage = loginPage.inputUsername("admin")
.inputPassword("123456")
.clickLoginSuccess();
Assert.assertTrue(homePage.isWelcomeDisplayed(), "登录后应显示欢迎信息");
}
@Test
@Description("使用错误的密码登录,应提示错误")
@Story("异常登录")
public void testLoginWrongPassword() {
LoginPage loginPage = new LoginPage();
loginPage.inputUsername("admin")
.inputPassword("wrong")
.clickLoginExpectingError();
String error = loginPage.getErrorMessage();
Assert.assertTrue(error.contains("密码错误"), "错误信息应包含'密码错误'");
}
}
3.7 TestNG套件配置
testng.xml:
xml
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="UI自动化测试套件" parallel="methods" thread-count="4">
<test name="登录模块测试">
<classes>
<class name="tests.LoginTest"/>
</classes>
</test>
</suite>
四、Allure报告集成
4.1 Maven依赖
xml
<!-- Allure TestNG适配器 -->
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-testng</artifactId>
<version>2.24.0</version>
</dependency>
<!-- AspectJ(用于运行时注解处理) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
4.2 Maven Surefire插件配置
xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M9</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
</suiteXmlFiles>
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/1.9.19/aspectjweaver-1.9.19.jar"
</argLine>
<systemProperties>
<property>
<name>allure.results.directory</name>
<value>${project.build.directory}/allure-results</value>
</property>
</systemProperties>
</configuration>
</plugin>
</plugins>
</build>
4.3 执行与生成报告
bash
# 执行测试
mvn clean test
# 生成并打开Allure报告
allure serve target/allure-results
五、JUnit 5 版本概要
如果偏好JUnit 5,可以用类似的分层结构,但注意:
JUnit 5 没有原生ITestListener,需借助TestWatcher扩展或AllureJUnit5。
并行执行通过junit-platform.properties配置。
示例测试类:
java
import org.junit.jupiter.api.*;
import io.qameta.allure.*;
@Epic("用户管理")
class LoginTestJunit extends BaseTestJunit {
@Test
@DisplayName("登录成功测试")
void testLoginSuccess() {
// 与TestNG类似
}
}
六、框架最佳实践总结
配置外置:所有环境相关(URL、浏览器、超时)放在配置文件,避免硬编码。
页面对象封装:每个页面一个类,方法返回其他Page Object或this。
测试基类:统一管理driver生命周期、截图、日志。
报告增强:使用Allure的@Step、@Attachment让报告更清晰。
并行执行:TestNG的parallel="methods"可大幅提升执行效率,但需确保测试用例间无依赖。
数据驱动:结合Excel/JSON,避免为每个数据组合写单独的测试方法。