Selenium 实战 —— 抽奖系统 UI 自动化测试框架搭建

文章目录

    • 前言
    • 一、自动化测试的整体思路
    • 二、测试用例的设计(脑图)
    • 三、测试框架的设计
      • [3.1 整体架构](#3.1 整体架构)
      • [3.2 公共工具类的设计](#3.2 公共工具类的设计)
      • [3.3 需要依赖](#3.3 需要依赖)
    • 四、主要测试类
      • [4.1 登录页 LoginPage](#4.1 登录页 LoginPage)
      • [4.2 后台管理页 AdminPage](#4.2 后台管理页 AdminPage)
      • [4.3 奖品列表页 PrizeListPage](#4.3 奖品列表页 PrizeListPage)
      • [4.4 人员列表页 UserListPage](#4.4 人员列表页 UserListPage)
      • [4.5 活动列表页 ActivityListPage](#4.5 活动列表页 ActivityListPage)
      • [4.6 奖品创建页 CreatePrizePage](#4.6 奖品创建页 CreatePrizePage)
      • [4.7 人员创建页](#4.7 人员创建页)
      • [4.8 活动创建页 CreateActivityPage](#4.8 活动创建页 CreateActivityPage)
      • [4.9 抽奖页DrawPage](#4.9 抽奖页DrawPage)
    • 五、关键技术点的处理
      • [5.1 iframe 嵌套页面的处理](#5.1 iframe 嵌套页面的处理)
      • [5.2 新窗口的处理](#5.2 新窗口的处理)
      • [5.3 表单输入的处理](#5.3 表单输入的处理)
      • [5.4 弹窗的处理](#5.4 弹窗的处理)
      • [5.5 下拉选择和模态框的处理](#5.5 下拉选择和模态框的处理)
    • 六、测试执行与结果分析
      • [6.1 测试执行流程](#6.1 测试执行流程)
      • [6.2 结果分析](#6.2 结果分析)
    • 七、总结与展望

前言

抽奖系统是一个典型的后台管理系统,主要功能包括用户登录、奖品管理、人员管理、活动创建和抽奖执行等。通过自动化测试,可以在每次代码更新后快速验证核心功能是否正常,大大提高测试效率。

本次自动化测试使用 WebDriverManager 自动管理浏览器驱动,避免了手动下载和配置驱动的繁琐过程。整个测试框架遵循 Page Object 设计模式,将每个页面的操作封装到独立的类中,提高了代码的可维护性和可复用性。

  • 使用技术栈:Java + selenium + WebDriverManager + commons-io + Chrom浏览器

一、自动化测试的整体思路

在开始编写测试代码之前,需要先梳理清楚测试的整体思路。首先要明确测试的范围和边界,哪些功能适合自动化测试,哪些适合手动测试。一般来说,重复执行频率高、操作步骤固定、预期结果明确的功能最适合自动化测试。

对于抽奖系统来说,核心业务流程是:用户登录后台管理系统,创建奖品信息,添加参与抽奖的人员,创建抽奖活动并关联奖品和人员,最后执行抽奖操作。这个流程中的每个环节都需要进行验证,包括正常场景和异常场景。

自动化测试的执行步骤通常包括:打开浏览器并导航到目标页面,定位页面元素并执行操作,验证操作结果是否符合预期,最后记录测试结果。在整个过程中,需要处理各种可能出现的情况,比如网络延迟导致的加载缓慢、弹窗提示、页面跳转等。

二、测试用例的设计(脑图)

测试用例是自动化测试的基础,好的测试用例设计能够帮助全面覆盖功能点。在设计测试用例时,需要考虑正常流程和异常流程两个方面。

基于抽奖系统主流程设计测试用例:

三、测试框架的设计

3.1 整体架构

测试框架采用分层设计,最底层是公共工具类,负责浏览器驱动的创建和管理、截图功能、通用等待方法等。中间层是各个页面的测试类,每个类对应一个页面,封装该页面的所有操作方法。最上层是测试入口类,负责按照业务流程组织测试用例的执行顺序。

这种分层设计的好处是:公共功能统一管理,避免代码重复;页面操作封装后可以在多个测试用例中复用;测试入口类只关注流程编排,代码结构清晰。

3.2 公共工具类的设计

公共工具类是整个测试框架的基础,主要提供以下几个核心功能。

首先是浏览器驱动的创建和管理。使用单例模式确保整个测试过程中只有一个浏览器实例,这样既能节省资源,又能保证测试上下文的连续性。WebDriverManager 会自动下载和配置对应版本的浏览器驱动,省去了手动管理的麻烦。

其次是显式等待和隐式等待的配置。隐式等待作用于全局,当查找元素时如果元素不存在,会等待指定时间后再抛出异常。显式等待则更加灵活,可以等待特定条件成立,比如元素可见、元素可点击、弹窗出现等。两种等待方式配合使用,能够有效处理页面加载延迟的问题。

最后是截图功能。在测试的关键步骤进行截图,可以帮助追溯测试过程,定位问题原因。截图文件按照日期分目录存储,文件名包含测试方法名和时间戳,方便查找和识别。

工具类示例:

java 复制代码
/**
 * Selenium WebDriver 工具类
 * 提供浏览器驱动管理、截图、弹窗处理等公共方法
 */
public class Utils {

    /** WebDriver 驱动对象,静态变量保证全局只有一个驱动实例 */
    public static WebDriver driver = null;

    /** 显式等待对象,用于等待特定条件成立 */
    public WebDriverWait wait = null;

    /** 测试基础URL地址 */
    public static String baseUrl = "url";

    /**
     * 构造函数,初始化驱动并打开指定URL
     * @param url 要访问的页面地址
     */
    public Utils(String url) {
        driver = createDriver();
        driver.get(url);
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    /**
     * 默认构造函数,不打开新页面
     * 用于已经切换到目标页面的场景
     */
    public Utils() {
        if (driver == null) {
            driver = createDriver();
        }
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    /**
     * 创建 WebDriver 驱动对象
     * 使用单例模式,确保整个测试过程只创建一个浏览器实例
     * @return WebDriver 实例
     */
    public static WebDriver createDriver() {
        if (null == driver) {
            // 使用 WebDriverManager 自动下载并配置 ChromeDriver
            // 指定 Chrome 版本号,确保驱动版本匹配
            WebDriverManager.chromedriver().driverVersion("146.0.7680.178").setup();

            // 配置 Chrome 浏览器选项
            ChromeOptions options = new ChromeOptions();
            // 允许远程源访问,解决跨域问题
            options.addArguments("--remote-allow-origins=*");
            // 禁用浏览器通知弹窗
            options.addArguments("--disable-notifications");

            // 创建 Chrome 驱动实例
            driver = new ChromeDriver(options);

            // 设置隐式等待时间(全局等待,查找元素时最多等待5秒)
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
            // 最大化浏览器窗口
            driver.manage().window().maximize();
        }
        return driver;
    }

    /**
     * 屏幕截图方法
     * 将截图保存到 ./src/test/java/images/日期/方法名-时间.png
     * @param str 截图文件名前缀,通常使用测试方法名
     * @throws IOException 文件操作异常
     */
    public void ScreenShot(String str) throws IOException {
        // 日期格式化器,用于生成目录名(年-月-日)
        SimpleDateFormat sim1 = new SimpleDateFormat("yyyy-MM-dd");
        // 时间格式化器,用于生成文件名后缀(时分秒毫秒)
        SimpleDateFormat sim2 = new SimpleDateFormat("HHmmssSS");

        String dirTime = sim1.format(System.currentTimeMillis());
        String fileTime = sim2.format(System.currentTimeMillis());

        // 构建截图文件完整路径
        String fileName = "./src/test/java/images/" + dirTime + "/" + str + "-" + fileTime + ".png";

        // 执行截图并保存到文件
        File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        FileUtils.copyFile(srcFile, new File(fileName));
    }
}

3.3 需要依赖

xml 复制代码
		<!-- Selenium WebDriver -->
		<dependency>
			<groupId>org.seleniumhq.selenium</groupId>
			<artifactId>selenium-java</artifactId>
			<version>4.27.0</version>
			<scope>test</scope>
		</dependency>
		<!-- WebDriverManager -->
		<dependency>
			<groupId>io.github.bonigarcia</groupId>
			<artifactId>webdrivermanager</artifactId>
			<version>5.9.2</version>
			<scope>test</scope>
		</dependency>
		<!-- Commons IO -->
		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.15.1</version>
			<scope>test</scope>
		</dependency>

四、主要测试类

每个页面测试类都继承自公共工具类,这样可以直接使用父类提供的驱动对象和通用方法。页面测试类的设计遵循以下原则。

  1. 每个类对应一个页面,类中包含该页面的 URL 地址、页面元素定位器、操作方法等。这样设计的好处是页面相关的所有内容都集中在一个类中,修改时只需要改一个地方。

  2. 操作方法的命名要清晰明确,能够直观反映方法的功能。比如登录成功的方法命名为 loginSuc,创建奖品失败的方法命名为 createPrizeFail,这样在测试入口类中调用时,代码的可读性很强。

  3. 每个操作方法内部要处理好等待和异常情况。比如点击按钮前要等待按钮可点击,处理弹窗时要先判断弹窗是否存在,操作完成后可以截图留证。

4.1 登录页 LoginPage

登录页是整个系统的入口,该类主要包含三个核心测试方法:

  • checkPageRight 方法用于验证页面元素是否正常显示,包括用户名输入框、密码输入框和登录按钮;
  • loginFail 方法测试登录失败的场景,输入错误的用户名和密码后验证系统是否给出正确的错误提示;
  • loginSuc 方法测试登录成功的场景,验证正确登录后是否能跳转到后台管理页面。

示例代码:

java 复制代码
public class LoginPage extends Utils {

    /** 登录页面URL地址 */
    public static String url = baseUrl + "/blogin.html";

    /**
     * 构造函数,打开登录页面
     */
    public LoginPage() {
        super(url);
    }

    /**
     * 检查登录页面元素是否正常显示
     * 验证:手机号输入框、密码输入框、登录按钮、验证码登录Tab
     */
    public void checkPageRight() {
        // 检查手机号输入框
        driver.findElement(By.cssSelector("#phoneNumber"));
        // 检查密码输入框
        driver.findElement(By.cssSelector("#password"));
        // 检查登录按钮
        driver.findElement(By.cssSelector("#loginForm > button"));
        // 检查验证码登录Tab
        driver.findElement(By.cssSelector("body > div.login-box > div.login-container > div.tab-box > span:nth-child(2)"));
    }

    /**
     * 测试密码登录成功场景
     * 使用正确的手机号和密码登录,验证是否成功跳转到管理页面
     * @throws IOException 截图异常
     */
    public void loginSuc() throws IOException {
        // 清空输入框
        driver.findElement(By.cssSelector("#phoneNumber")).clear();
        driver.findElement(By.cssSelector("#password")).clear();

        // 输入正确的账号密码
        driver.findElement(By.cssSelector("#phoneNumber")).sendKeys("...");
        driver.findElement(By.cssSelector("#password")).sendKeys("...");
        // 点击登录按钮
        driver.findElement(By.cssSelector("#loginForm > button")).click();

        // 截图保存登录结果
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName());

        // 等待页面跳转到管理页面
        wait.until(ExpectedConditions.urlContains("admin.html"));
    }

    /**
     * 测试登录失败场景
     *
     * @throws IOException 截图异常
     */
    public void loginFail() throws IOException {

        // 输入为空
        sendKeys("","");

        // 电话为空,密码不为空
        sendKeys("","wrongpassword");

        // 电话不为空,密码为空
        sendKeys("...","");

        // 输入特殊字符串
        sendKeys("SSSSSSSSSS","wrongpassword");

		// ......................

    }

    /**
     * 输入异常登录信息
     * @param phone 手机号
     * @param password  密码
     * @throws IOException 截图异常
     */
    public void sendKeys(String phone, String password) throws IOException {
        // 清空输入框
        driver.findElement(By.cssSelector("#phoneNumber")).clear();
        driver.findElement(By.cssSelector("#password")).clear();


        driver.findElement(By.cssSelector("#phoneNumber")).sendKeys(phone);
        driver.findElement(By.cssSelector("#password")).sendKeys(password);

        // 截图保存登录前的状态
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName());
        // 点击登录按钮
        driver.findElement(By.cssSelector("#loginForm > button")).click();

        // 等待弹窗出现
        wait.until(ExpectedConditions.alertIsPresent());

        // 处理弹窗
        Alert alert = driver.switchTo().alert();
        alert.accept();
    }

4.2 后台管理页 AdminPage

后台管理页是登录成功后的主页面,采用左侧菜单栏加右侧内容区的布局方式。AdminPage 类主要负责测试菜单导航功能和 iframe 切换处理。

该页面的核心特点是使用 iframe 嵌入子页面,点击不同的菜单项会在右侧 iframe 中加载对应的页面。测试时需要先点击菜单项,然后切换到 iframe 中才能操作子页面元素。

AdminPage 类还包含退出登录的测试方法 logout,验证点击退出按钮后是否能正确弹出确认提示,确认后是否能跳转回登录页面。

代码示例:

java 复制代码
public class AdminPage extends Utils {

    /** 后台管理页面URL地址 */
    public static String url = baseUrl + "/admin.html";

    /**
     * 构造函数,打开后台管理页面
     */
    public AdminPage() {
        super(url);
    }

    /**
     * 检查侧边栏导航元素是否正常显示
     * 验证:侧边栏容器、活动管理、奖品管理、人员管理菜单
     */
    public void checkSidebar() {
        // 检查侧边栏容器
        driver.findElement(By.cssSelector("body > div.cont-box > div.sidebar"));
        // 检查活动管理菜单
        driver.findElement(By.cssSelector("body > div.cont-box > div.sidebar > ul > li:nth-child(1) > div"));
        String acList = driver.findElement(By.cssSelector("#activitiesList")).getText();
        String crAc = driver.findElement(By.cssSelector("#createActivity")).getText();
		
		//.........................

        assert acList.equals("活动列表");
        assert crAc.equals("新建抽奖活动");

    }

    /**
     * 检查页面头部元素是否正常显示
     * 验证:Logo、用户信息区域
     */
    public void checkHeader() {
        // 检查Logo区域
        driver.findElement(By.cssSelector("body > div.header-box > div.logo-box"));
        // 检查用户信息区域(退出按钮)
        driver.findElement(By.cssSelector("body > div.header-box > div.user-box > div"));
    }

    /**
     * 测试展开活动管理菜单
     * 点击活动管理,验证子菜单是否正确显示
     * @throws IOException 截图异常
     */
    public void expandActivityMenu() throws IOException {
        // 点击活动管理菜单
        driver.findElement(By.cssSelector("body > div.cont-box > div.sidebar > ul > li:nth-child(1) > div")).click();
        // 等待动画完成
        sleep(500);

        // 验证子菜单显示
        driver.findElement(By.cssSelector("#activitiesList"));
        driver.findElement(By.cssSelector("#createActivity"));

        // 截图保存菜单展开状态
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName());
    }

    /**
     * 测试展开奖品管理菜单
     * 点击奖品管理,验证子菜单是否正确显示
     * @throws IOException 截图异常
     */
    public void expandPrizeMenu() throws IOException {

    }

    /**
     * 测试展开人员管理菜单
     * 点击人员管理,验证子菜单是否正确显示
     * @throws IOException 截图异常
     */
    public void expandUserMenu() throws IOException {

    }

    /**
     * 跳转到活动列表页面
     * 点击活动列表菜单,切换到对应的iframe
     */
    public void goToActivityList() {
        // 点击活动列表菜单
        driver.findElement(By.cssSelector("#activitiesList")).click();
        // 等待iframe加载并切换进去
        wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("#contentFrame")));
    }

    /**
     * 从 iframe 切换回主页面
     * 在 iframe 中操作完成后,需要切换回主页面才能继续操作侧边栏等元素
     */
    public void switchToDefaultContent() {
        driver.switchTo().defaultContent();
    }

    /**
     * 测试退出登录功能
     * 点击退出按钮,验证是否正确跳转到登录页面
     */
    public void logout() {
        // 点击退出按钮
        driver.findElement(By.cssSelector("body > div.header-box > div.user-box > div")).click();
        // 等待页面跳转到登录页面
        wait.until(ExpectedConditions.urlContains("blogin.html"));
    }
}

4.3 奖品列表页 PrizeListPage

奖品列表页展示系统中所有的奖品信息,支持分页显示。PrizeListPage 类主要测试列表展示和分页功能。

  • checkPageRight 方法验证列表页的基本元素,包括表格、分页控件等。
  • checkPrizeList 方法检查奖品列表数据是否正确显示,验证奖品名称、价格、图片等信息。
  • testPagination 方法测试分页功能,点击下一页按钮后验证页面是否正确切换,数据是否正确加载。

代码示例:

java 复制代码
public class PrizeListPage extends Utils {

    /** 奖品列表页面URL地址 */
    public static String url = baseUrl + "/prizes-list.html";

    /**
     * 构造函数,打开奖品列表页面
     */
    public PrizeListPage() {
        super(url);
    }

    /**
     * 检查奖品列表页面元素是否正常显示
     * 验证:页面标题、奖品列表容器、分页控件
     */
    public void checkPageRight() {
        // 检查页面标题
        driver.findElement(By.cssSelector("body > div.prize-table > h2"));
        // 检查奖品列表容器
        driver.findElement(By.cssSelector("#prizeList"));
        // 检查分页控件
        driver.findElement(By.cssSelector("body > div.prize-table > div.pagination"));
    }

    /**
     * 检查奖品列表是否正常加载
     * 验证奖品项是否显示,包括奖品ID、图片、名称
     * @throws IOException 截图异常
     */
    public void checkPrizeList() throws IOException {
        // 等待列表加载
        sleep(1000);
        // 截图保存列表状态
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName());

        // 检查是否有奖品数据
        boolean hasPrize = driver.findElements(By.cssSelector("#prizeList > tr")).size() > 0;
        if (hasPrize) {
            // 验证第一个奖品项的各列数据
            driver.findElement(By.cssSelector("#prizeList > tr:nth-child(1) > td:nth-child(1)"));  // 奖品ID
            driver.findElement(By.cssSelector("#prizeList > tr:nth-child(1) > td:nth-child(2)"));  // 奖品图片
            driver.findElement(By.cssSelector("#prizeList > tr:nth-child(1) > td:nth-child(3)"));  // 奖品名称
            driver.findElement(By.cssSelector("#prizeList > tr:nth-child(1) > td:nth-child(4)"));  // 奖品描述
            driver.findElement(By.cssSelector("#prizeList > tr:nth-child(1) > td:nth-child(5)"));  // 奖品价格

        }
    }
    
	/**
     * 测试分页功能
     * 点击"下一页"按钮,验证分页是否正常工作
     */
    public void testPagination() {
        // 点击下一页按钮
        driver.findElement(By.cssSelector("body > div.prize-table > div.pagination > button:nth-child(4)")).click();
        // 等待页面加载
        sleep(1000);
    }
}

4.4 人员列表页 UserListPage

人员列表页展示参与抽奖的所有人员信息,同样支持分页显示。UserListPage 类的结构与奖品列表页类似,主要测试人员数据的展示和分页功能。

人员信息包括姓名、手机号、创建时间等字段。测试时需要验证这些字段是否正确显示。人员列表页的数据是创建抽奖活动时选择参与人员的数据来源,因此该页面的测试非常重要。此处代码和奖品列表页类似

4.5 活动列表页 ActivityListPage

活动列表页是管理抽奖活动的核心页面,展示所有已创建的活动,包括进行中的活动和已结束的活动。ActivityListPage 类不仅测试列表展示功能,还负责跳转到抽奖页面的测试。

此处代码除了抽奖逻辑外,和奖品列表页基本类似。

4.6 奖品创建页 CreatePrizePage

奖品创建页用于添加新的奖品信息,需要填写奖品名称、价格、描述并上传图片。CreatePrizePage 类测试奖品创建的各种场景。

  • createPrizeFail 方法测试创建失败的场景,包括奖品名称为空、价格为空等情况,验证系统的输入校验是否正确。
  • createPrizeSuc 方法测试创建成功的场景,填写完整信息后提交,验证是否能成功创建并跳转到列表页。

奖品创建页测试的一个技术难点是 number 类型输入框的处理。价格输入框是 number 类型,直接使用 sendKeys 方法可能无法正确输入。解决方案是使用 JavaScript 临时修改输入框的 type 属性为 text,输入完成后再改回 number,或者直接使用 JavaScript 设置输入框的值。

另一个难点是图片上传。Selenium 处理文件上传需要找到 type 为 file 的 input 元素,然后调用 sendKeys 方法传入文件的绝对路径。注意这里传入的是文件路径而不是点击上传按钮。

核心代码:

java 复制代码
	/**
     * 测试创建奖品成功场景
     * 输入完整的奖品信息,验证是否创建成功
     * @throws IOException 截图异常
     */
    public void createPrizeSuc() throws IOException {
        driver.findElement(By.cssSelector("#prizeName")).clear();
        driver.findElement(By.cssSelector("#price")).clear();
        driver.findElement(By.cssSelector("#description")).clear();

        SimpleDateFormat sim = new SimpleDateFormat("HHmmssSS");

        String titleTime = sim.format(System.currentTimeMillis());
        String title = "自动化测试发布" + titleTime;

        String imagePath = "C:\\Users\\wuhaoxiang\\Desktop\\csdn\\抽奖系统\\a6b78ff78f2945d69653c859995e4d9a.png";

        driver.findElement(By.cssSelector("#prizeName")).sendKeys(title);
        
        JavascriptExecutor js = (JavascriptExecutor) driver;
        js.executeScript("formData = new FormData();");
        
        testImageUpload(imagePath);
        driver.findElement(By.cssSelector("#description")).sendKeys(title + "description");

        WebElement priceInput = driver.findElement(By.cssSelector("#price"));
        
        js.executeScript("arguments[0].setAttribute('type', 'text');", priceInput);
        
        priceInput.clear();
        priceInput.sendKeys("111");
        
        js.executeScript("arguments[0].setAttribute('type', 'number');", priceInput);
        js.executeScript("$(arguments[0]).val('111').trigger('input').trigger('change').trigger('blur');", priceInput);

        sleep(500);

        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName());

        driver.findElement(By.cssSelector("button[onclick='submitPrizes()']")).click();

        wait.until(ExpectedConditions.alertIsPresent());

        Alert alert = driver.switchTo().alert();
        alert.accept();

        wait.until(ExpectedConditions.urlContains("prizes-list.html"));
    }

4.7 人员创建页

人员创建页用于添加参与抽奖的人员信息,需要填写姓名和手机号。该页面的测试相对简单,主要验证输入校验和创建流程。

创建人员时需要验证姓名和手机号的必填校验,手机号的格式校验等。创建成功后应该能在人员列表页看到新增的人员信息。由于人员信息相对简单,测试用例主要关注正常创建流程和基本的输入验证。

4.8 活动创建页 CreateActivityPage

活动创建页是整个系统最复杂的页面之一,需要填写活动基本信息,选择奖品和参与人员。CreateActivityPage 类测试活动创建的各种场景,包括多个业务规则的验证。

该页面的测试方法包括:

  • createActivityFailWithoutPrizes 测试未选择奖品时提交的情况;
  • createActivityFailWithoutUsers 测试未选择人员时提交的情况;
  • createActivityFailWithPrizeMoreThanUser 测试奖品数量超过人员数量的情况,这是一个重要的业务规则验证;
  • createActivitySuc 测试正常创建活动的流程。

选择奖品和人员是通过模态框完成的,点击选择按钮后弹出模态框,在模态框中勾选需要的项目后确认。测试时需要处理模态框的显示和隐藏,以及模态框内元素的操作。

核心代码:

java 复制代码
	/**
     * 测试打开奖品选择模态框
     * 点击"圈选奖品"按钮,验证模态框是否正常弹出
     */
    public void openPrizesModal() {
        // 点击圈选奖品按钮
        driver.findElement(By.cssSelector("#buttonPrizes")).click();

        // 等待模态框显示
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#prizesModal")));

        // 验证模态框标题
        driver.findElement(By.cssSelector("#prizesModal h2"));
    }

	/**
     * 测试创建活动成功场景 - 多个奖品多个人员(带参数版本)
     * 选择多个奖品和多个人员,验证创建成功
     * @param activityName 活动名称
     * @param description 活动描述
     * @param prizeQuantities 奖品数量数组
     * @param userCount 人员数量
     * @throws IOException 截图异常
     */
    public void createActivityWithMultiplePrizes(String activityName, String description, int[] prizeQuantities, int userCount) throws IOException {
        // 清空输入框
        driver.findElement(By.cssSelector("#activityName")).clear();
        driver.findElement(By.cssSelector("#description")).clear();

        // 输入活动信息
        driver.findElement(By.cssSelector("#activityName")).sendKeys(activityName);
        driver.findElement(By.cssSelector("#description")).sendKeys(description);

        // 选择多个奖品
        selectMultiplePrizes(prizeQuantities);

        // 选择人员
        selectUsers(userCount);

        // 截图保存创建前的状态
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName());

        // 点击创建活动按钮
        driver.findElement(By.cssSelector("#createActivity")).click();

        // 等待弹窗出现
        wait.until(ExpectedConditions.alertIsPresent());

        // 处理弹窗
        Alert alert = driver.switchTo().alert();
        alert.accept();
    }

4.9 抽奖页DrawPage

抽奖页是执行抽奖操作的页面,会在新窗口中打开。DrawPage 类负责测试完整的抽奖流程,是整个自动化测试的核心环节。

drawAllPrizes 方法执行完整的抽奖流程。它会循环检测按钮状态,当按钮显示"开始抽奖"时点击开始抽奖,等待名字滚动后点击确定,然后进入下一个奖项的抽取,直到按钮显示"已全部抽完"为止。这个方法模拟了真实的抽奖操作流程。

switchToOriginalWindow 方法用于抽奖完成后切换回原窗口。由于抽奖页面是在新窗口中打开的,测试完成后需要关闭新窗口并切换回活动列表页。这个方法使用保存的原窗口句柄来实现切换。

核心抽奖代码:

java 复制代码
	/**
     * 执行完整的抽奖流程(所有奖项)
     * 循环执行抽奖,直到所有奖项抽完
     * 
     * @throws IOException 截图异常
     */
    public void drawAllPrizes() throws IOException {
        System.out.println("========== 开始执行完整抽奖流程 ==========");

        // 等待页面加载
        sleep(1000);

        int prizeIndex = 1;
        while (true) {
            String buttonText = getNextButtonText();
            System.out.println("当前按钮文本: " + buttonText);

            // 如果按钮文本是"已全部抽完",说明抽奖结束
            if (buttonText.equals("已全部抽完")) {
                System.out.println(">>> 所有奖项已抽完!");
                break;
            }

            // 如果按钮文本是"开始抽奖",说明是新奖项
            if (buttonText.equals("开始抽奖")) {
                System.out.println(">>> 开始抽取第 " + prizeIndex + " 个奖项...");
                System.out.println(">>> 当前奖品: " + getPrizeDescription());
            }

            // 执行一次抽奖
            drawOnePrize();
            prizeIndex++;
     }

	/**
     * 执行一次完整的抽奖流程
     * 1. 点击"开始抽奖"(名字滚动)
     * 2. 点击"点我确定"(确定中奖者)
     * 3. 点击"已抽完,下一步"(进入下一奖项)
     * 
     * @throws IOException 截图异常
     */
    public void drawOnePrize() throws IOException {
        // 等待页面加载
        sleep(1000);

        // 截图保存初始状态
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName() + "_1_initial");

        // 第一步:点击"开始抽奖",开始滚动名字
        clickNextButton();
        System.out.println(">>> 点击开始抽奖,名字滚动中...");
        sleep(2000);  // 等待名字滚动

        // 截图保存滚动状态
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName() + "_2_rolling");

        // 第二步:点击"点我确定",确定中奖者
        clickNextButton();
        System.out.println(">>> 点击确定,中奖者已确定");
        sleep(1000);

        // 截图保存中奖结果
        ScreenShot(Thread.currentThread().getStackTrace()[1].getMethodName() + "_3_winner");

        // 第三步:点击"已抽完,下一步",进入下一奖项
        clickNextButton();
        System.out.println(">>> 进入下一奖项");
    }

五、关键技术点的处理

5.1 iframe 嵌套页面的处理

后台管理系统通常使用 iframe 来嵌入子页面,这种情况下直接定位 iframe 内部的元素会失败,必须先切换到对应的 iframe 中。

处理 iframe 的方法是先定位到 iframe 元素,然后使用 switchTo().frame() 方法切换进去。切换后就可以正常操作 iframe 内部的元素了。操作完成后,需要使用 switchTo().defaultContent() 方法退出 iframe,回到主页面。

在实际测试中,使用显式等待来确保 iframe 加载完成后再切换,这样可以避免因为页面加载慢导致的切换失败问题。

5.2 新窗口的处理

抽奖功能会在新窗口中打开,这就涉及到窗口切换的问题。处理新窗口的关键是获取窗口句柄,通过对比切换前后的窗口句柄集合,找出新增的窗口句柄,然后切换过去。

需要注意的是,有些情况下点击链接可能会导致当前窗口也被导航,这时候使用 JavaScript 的 window.open 方法来打开新窗口会更加可靠。通过 JavaScript 获取链接的 href 属性,然后调用 window.open 在新窗口打开,这样可以避免影响当前窗口。

切换到新窗口后,要保存原窗口的句柄,以便后续切换回去。在测试类中可以定义一个成员变量来存储原窗口句柄,切换回去时直接使用这个变量即可。

5.3 表单输入的处理

在 Web 自动化测试中,表单输入是最常见的操作之一。对于普通的文本输入框,使用 sendKeys 方法直接输入即可。但是对于一些特殊类型的输入框,可能需要特殊处理。

比如 number 类型的输入框,在某些浏览器中 sendKeys 可能无法正确输入。这时候可以使用 JavaScript 来设置输入框的值,或者临时修改输入框的 type 属性为 text,输入完成后再改回 number。

另外,有些表单使用了前端框架或库,输入框的值变化需要触发特定的事件才能被识别。这种情况下,设置值后需要手动触发 input、change 或 blur 事件,确保前端框架能够正确响应值的变化。

5.4 弹窗的处理

弹窗在 Web 应用中很常见,有浏览器原生的 alert 弹窗,也有自定义的模态框。原生弹窗需要使用 switchTo().alert() 来切换,然后调用 accept() 或 dismiss() 来确认或取消。

处理弹窗时要注意时机问题,弹窗可能在任何时候出现,比如页面加载时、操作完成后等。一个好的做法是在关键操作后都检查一下是否有弹窗,如果有就处理掉。可以写一个通用的弹窗处理方法,使用循环来处理可能出现的多个弹窗。

5.5 下拉选择和模态框的处理

创建活动时需要选择奖品和人员,这些选择通常通过模态框来完成。点击选择按钮后,模态框弹出,里面显示可选项列表,勾选后确认。

处理模态框首先要等待模态框显示,然后定位模态框内的元素进行操作。模态框内的表格或列表可能很长,需要滚动才能看到所有选项。操作完成后点击确认按钮,关闭模态框。


六、测试执行与结果分析

6.1 测试执行流程

测试入口类按照业务流程组织测试用例的执行顺序。首先是登录测试,验证登录功能正常后才能进行后续操作。然后是奖品和人员的准备工作,创建测试数据。接着是活动创建测试,验证活动的创建流程。最后是抽奖测试,验证核心的抽奖功能。

每个测试步骤执行后都会输出日志信息,方便了解测试进度。关键步骤会进行截图,保存测试证据。如果某个步骤失败,会输出错误信息,但不会影响后续步骤的执行。

6.2 结果分析

测试完成后,可以通过截图和日志来分析测试结果。截图记录了每个关键步骤的页面状态,可以直观地看到测试过程是否符合预期。日志记录了测试的执行路径和关键信息,可以帮助定位问题。

如果测试失败,首先要查看失败时的截图,了解页面状态。然后查看日志,确定失败发生在哪个步骤。根据错误信息,分析是定位器失效、等待超时、还是业务逻辑问题,然后针对性地修复。


七、总结与展望

通过本次抽奖系统的 UI 自动化测试实践,我们搭建了一套完整的测试框架,覆盖了登录、奖品管理、活动管理、抽奖等核心功能。测试框架采用 Page Object 设计模式,代码结构清晰,易于维护和扩展。

在实际测试过程中,遇到了 iframe 嵌套、新窗口切换、特殊输入框处理、弹窗处理等技术难点,通过查阅文档和实践探索,都找到了相应的解决方案。这些经验对于其他项目的自动化测试同样适用。

自动化测试的价值在于提高测试效率,支持频繁的回归测试。在项目迭代过程中,每次代码更新后运行自动化测试,可以快速发现回归问题,保证产品质量。

该测试脚本仍然有许多可以改进的地方:

运行速度:

  1. 在某些地方使用线程休眠的方式等待元素加载可能会使得脚本运行时间加长,可以优化为等待基本元素或关键元素加载完成后即可进行相关操作。

运行方式: 当前脚本运行方法仍然是在main方法中执行,后续可以从以下几个方面进行优化:

  1. 引入单元测试框架如 TestNG 或 JUnit,使用注解来管理测试用例,支持断言和测试报告生成;
  2. 实现数据驱动测试,从外部文件读取测试数据,提高测试覆盖率;
  3. 集成持续集成工具,实现自动化测试的定时执行和结果通知。
相关推荐
姬成韶几秒前
BUUCTF--[RoarCTF 2019]Easy Java
java·网络安全
组合缺一几秒前
Solon AI Harness 首次发版
java·人工智能·ai·llm·agent·solon
AlunYegeer31 分钟前
MyBatis 传参核心:#{ } 与 ${ } 区别详解(避坑+面试重点)
java·mybatis
少许极端43 分钟前
算法奇妙屋(四十)-贪心算法学习之路7
java·学习·算法·贪心算法
危笑ioi1 小时前
helm部署skywalking链路追踪 java
java·开发语言·skywalking
夕除1 小时前
Mysql--15
java·数据库·mysql
smileNicky1 小时前
Linux 系列从多节点的catalina 日志中统计设备调用频次
java·linux·服务器
赵丙双1 小时前
spring boot 排除自动配置类的方式和原理
java·spring boot·自动配置
8Qi81 小时前
LeetCode热题100--45.跳跃游戏 II
java·算法·leetcode·贪心算法·编程
bilI LESS2 小时前
Spring Boot接收参数的19种方式
java·spring boot·后端