开篇:从”测试地狱”到”自动化天堂”的真实经历
说实话,在我刚开始做Java项目的时候,测试真的是件头疼的事。记得有一次,我们团队要在周五下午发布一个紧急版本,但我还没有跑完所有的测试用例——因为手工测试太慢了。最后我们决定不跑全量测试就上线,结果第二天就出了bug,那感觉真是……你懂的。
从那天起,我就下定决心要建立一个可靠的自动化测试体系。经过无数次的踩坑、调试、重构,现在我终于可以把这套Maven+Selenium+JUnit的集成方案毫无保留地分享给你了。
为什么选择Maven + Selenium + JUnit这个组合?
在深入具体配置之前,我想先说说为什么这个组合这么受欢迎:
- Maven:Java生态中最成熟的构建工具,依赖管理、生命周期、插件体系都非常完善
- Selenium:浏览器自动化测试的事实标准,支持几乎所有主流浏览器
- JUnit 5:现代Java单元测试框架,注解灵活、扩展性强、与Maven集成无缝
这三个家伙凑在一起,简直就是自动化测试界的”铁三角”。
项目结构:整洁的代码从好的结构开始
在我分享具体代码之前,先来看看一个标准的项目结构应该是怎样的:
my-test-project/
├── pom.xml # Maven配置文件
├── src/
│ ├── main/
│ │ └── java/
│ │ └── com/example/app/
│ │ └── App.java # 主程序代码
│ └── test/
│ └── java/
│ └── com/example/test/
│ ├── base/
│ │ └── BaseTest.java # 测试基类
│ ├── pages/
│ │ ├── LoginPage.java # 登录页面对象
│ │ └── DashboardPage.java # 仪表盘页面对象
│ ├── tests/
│ │ ├── LoginTest.java # 登录测试用例
│ │ └── DashboardTest.java # 仪表盘测试用例
│ └── utils/
│ └── WebDriverManager.java # WebDriver管理工具
├── test-output/ # 测试报告输出目录
└── reports/ # 测试报告HTML目录
你看,这样的结构是不是非常清晰?base包放基类,pages包放页面对象,tests包放测试用例,utils包放工具类。这样分工明确,后期维护起来也方便。
Maven配置:pom.xml的详细解读
好了,废话不多说,直接上干货。先看最核心的pom.xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>automation-test-project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Automation Test Project</name>
<description>Maven集成Selenium和JUnit自动化测试项目</description>
<properties>
<!-- 统一版本管理,避免版本冲突 -->
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 核心依赖版本 -->
<selenium.version>4.15.0</selenium.version>
<junit.version>5.10.1</junit.version>
<log4j.version>2.21.1</log4j.version>
<allure.version>2.24.0</allure.version>
<testng.version>7.7.1</testng.version>
<maven-surefire.version>3.2.2</maven-surefire.version>
<maven-failsafe.version>3.2.2</maven-failsafe.version>
<allure-maven.version>2.12.0</allure-maven.version>
<webdrivermanager.version>5.6.2</webdrivermanager.version>
</properties>
<dependencies>
<!-- Selenium WebDriver -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Selenium依赖的测试运行器 -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-testcontainers</artifactId>
<version>${selenium.version}</version>
<scope>test</scope>
</dependency>
<!-- WebDriverManager:自动管理浏览器驱动版本 -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>${webdrivermanager.version}</version>
</dependency>
<!-- Log4j2日志框架 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- Allure测试报告 -->
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>${allure.version}</version>
<scope>test</scope>
</dependency>
<!-- TestNG(可选,用于更复杂的测试场景) -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>2.14.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Surefire Plugin:运行单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire.version}</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/*IT.java</exclude>
<exclude>**/*IntegrationTest.java</exclude>
</excludes>
<!-- 并行执行测试 -->
<parallel>methods</parallel>
<threadCount>4</threadCount>
<!-- 报告格式 -->
<reportFormat>plain</reportFormat>
<!-- 输出详细日志 -->
<consoleOutputReporter>
<disable>true</disable>
</consoleOutputReporter>
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
-Dallure.results.directory=${project.build.directory}/allure-results
</argLine>
</configuration>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.21</version>
</dependency>
</dependencies>
</plugin>
<!-- Failsafe Plugin:运行集成测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe.version}</version>
<configuration>
<includes>
<include>**/*IT.java</include>
<include>**/*IntegrationTest.java</include>
</includes>
<!-- 集成测试结束后保持浏览器打开,便于调试 -->
<systemPropertyVariables>
<keepBrowserOpen>true</keepBrowserOpen>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Allure Maven Plugin:生成测试报告 -->
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>${allure-maven.version}</version>
<configuration>
<reportVersion>2.24.0</reportVersion>
<allureDownloadUrl>
https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/${allure.version}/allure-commandline-${allure.version}.zip
</allureDownloadUrl>
</configuration>
<executions>
<execution>
<goals>
<goal>report</goal>
<goal>publish</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Maven Assembly Plugin:打包测试JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
这个pom.xml看起来有点长,但其实都是必要的配置。让我给你解释一下几个关键点:
- 版本管理:我在
<properties>部分统一管理所有依赖的版本,这样以后升级版本只需要改一个地方。 - Surefire和Failsafe分离:Surefire运行单元测试,Failsafe运行集成测试。这样你可以分开运行两种测试。
- Allure集成:Allure是目前最流行的测试报告工具之一,支持丰富的功能和漂亮的界面。
- WebDriverManager:这个库会自动下载和管理浏览器驱动,避免手动下载驱动的麻烦。
测试基类:BaseTest的设计艺术
基类是自动化测试框架的核心,它决定了整个测试框架的架构。来看看我是怎么设计的:
”`java package com.example.test.base;
import io.github.bonigarcia.wdm.WebDriverManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.support.events.EventFiringWebDriver; import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration; import java.util.HashMap; import java.util.Map;
/**
测试基类,所有测试用例的父类
负责WebDriver的初始化和清理工作 */ public abstract class BaseTest {
private static final Logger logger = LogManager.getLogger(BaseTest.class);
// 使用ThreadLocal保证多线程环境下的线程安全 protected static ThreadLocal
driver = new ThreadLocal<>(); protected static ThreadLocal wait = new ThreadLocal<>(); // 测试环境配置 private static final String BASE_URL = System.getProperty(“baseUrl”, “http://localhost:8080”); private static final String BROWSER = System.getProperty(“browser”, “chrome”);
@BeforeAll public static void setUpClass() {
logger.info("========== 测试类开始 =========="); logger.info("基础URL: {}", BASE_URL); logger.info("浏览器: {}", BROWSER);}
@BeforeEach public void setUp() {
logger.info("---------- 测试方法开始: {} ----------", Thread.currentThread().getStackTrace()[1].getMethodName()); // 初始化WebDriver initWebDriver(); // 设置隐式等待 getDriver().manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); // 设置显式等待 WebDriverWait webDriverWait = new WebDriverWait(getDriver(), Duration.ofSeconds(30)); wait.set(webDriverWait); // 最大化浏览器窗口 getDriver().manage().window().maximize(); // 访问基础URL getDriver().get(BASE_URL);}
@AfterEach public void tearDown() {
logger.info("---------- 测试方法结束 ----------"); // 关闭浏览器,但不退出驱动 if (getDriver() != null) { try { getDriver().quit(); } catch (Exception e) { logger.error("关闭浏览器时出错: {}", e.getMessage(), e); } } driver.remove(); wait.remove();}
@AfterAll public static void tearDownClass() {
logger.info("========== 测试类结束 ==========");}
/**
- 初始化WebDriver
- 根据配置选择合适的浏览器 */ protected void initWebDriver() { switch (BROWSER.toLowerCase()) { case “chrome”: setupChromeDriver(); break; case “firefox”: setupFirefoxDriver(); break; case “edge”: setupEdgeDriver(); break; case “headless”: setupHeadlessChromeDriver(); break; default: setupChromeDriver(); break; } }
/**
配置Chrome驱动 */ private void setupChromeDriver() { WebDriverManager.chromedriver().setup(); ChromeOptions options = new ChromeOptions();
// 添加常用选项 options.addArguments(“–disable-extensions”); options.addArguments(“–disable-gpu”); options.addArguments(“–no-sandbox”); options.addArguments(“–disable-dev-shm-usage”); options.addArguments(“–remote-allow-origins=*”);
// 添加实验性选项 Map
prefs = new HashMap<>(); prefs.put(“download.default_directory”, System.getProperty(“user.dir”) + “/downloads”); prefs.put(“profile.default_content_settings.popups”, 0); options.setExperimentalOption(“prefs”, prefs); driver.set(new ChromeDriver(options)); logger.info(“Chrome浏览器初始化完成”); }
/**
- 配置Firefox驱动 */ private void setupFirefoxDriver() { WebDriverManager.firefoxdriver().setup(); driver.set(new org.openqa.selenium.fire
