# EasyAutoUITest **Repository Path**: palinlin/easy-auto-uitest ## Basic Information - **Project Name**: EasyAutoUITest - **Description**: 端到端自动化测试脚手架,不是工具也不是平台。 Not a tool, not a platform, just a framework. 基于Java8+SpringBoot2.7+TestNG7.4+Playwright1.42开发。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: https://laker.blog.csdn.net - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 10 - **Created**: 2024-07-12 - **Last Updated**: 2024-08-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # EasyAutoTest ## 介绍 端到端自动化测试框架,基于Java8+SpringBoot2.7+TestNG7.4+Playwright1.41.2开发,使用了**页面对象模式**和**数据驱动模式**方便后期版本迭代维护。 **变大变长,再创辉煌** ```ini ,------. ,---. ,--. ,--------. ,--. | .---' ,--,--. ,---.,--. ,--./ O \ ,--.,--.,-' '-. ,---.'--. .--',---. ,---.,-' '-. | `--, ' ,-. |( .-' \ ' /| .-. || || |'-. .-'| .-. | | | .-. :( .-''-. .-' | `---.\ '-' |.-' `) \ ' | | | |' '' ' | | ' '-' | | \ --..-' `) | | `------' `--`--'`----'.-' / `--' `--' `----' `--' `---' `--' `----'`----' `--' ``` ## 目录解释 ```ini - src - main - config 配置类 - core 基类和一些扩展,例如截图监听,发邮件,报告 - job 定时任务 - dev 开发过程中会使用的工具,安装浏览器,脚本录制,查看trace.zip - utils 工具类的代码 - testcase 存放测试case,测试类都已Test结尾,继承BasePageTest类 - page 存放页面对象,每个页面一个对象,包含页面元素定位和控件操作,不要把对象中元素暴露出去。 - AutoTestApplication.java 启动类 - resources 配置文件 - application.yaml 配置文件 - testng.xml testng配置文件 - data 存放测试过程中使用到的数据,json,excel等 - report 存放测试报告 - screenshot 存放错误测试case的截图 - trace 存放错误case录制追踪 - videos 存放错误case录制视频 ``` ## 系统要求 > https://playwright.dev/java/docs/intro#system-requirements - Java 8 or higher. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - MacOS 12 Monterey, MacOS 13 Ventura, or MacOS 14 Sonoma. - Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04. **注意**:**playwright在centos上部署官方是不支持的**,[github issue](https://github.com/microsoft/playwright/issues/9194) > 实在想跑就用docker跑吧 ## 开发阶段 > 如果你是一个图快的人,直接去`Record.java`去执行脚本录制吧! > > 然后代码贴进来就能跑,但是这个代码基本不可迭代。 ### 第一步 修改配置 > 根据自己的需求修改如下配置,也可以使用**默认**,**默认**,**默认**的。 **application.yaml** ```yaml spring: #邮箱配置 mail: host: smtp.qq.com # 邮箱 username: 93xxx66@qq.com # 邮箱的授权码 password: nsssssssssss #默认UTF-8 default-encoding: UTF-8 properties: mail: smtp: auth: true starttls: enable: true required: true easy: # 是否开启发送邮件,默认false send-email: false # 收件人邮箱列表 send-to: - example1@example.com - example2@example.com # 是否用自定义增强报告 enhanced-report: false # playwright启动参数 launch-options: # 是否启用无头模式 headless: true # 等待浏览器实例启动时间 timeout: 10000 # 慢模式毫秒数,模拟人的等待操作时间 slow-mo: 100 context-options: # 设置默认操作超时时间为10秒 timeout: 10000 # 定时任务 cronjob: # 是否开启定时任务,默认开启 enabled: true # 指定两次任务执行之间的延迟时间,单位毫秒 fixedDelayMillis: 30000 trace-options: # 是否开启错误trace trace-enable: true # 是否开启错误截图,鸡肋 经常截图不准 screenshot-enable: false # 是否开启错误录屏,这个有点儿鸡肋 video-enable: false retry-options: # 最大重试次数 max-retry-count: 3 cleanfile: # 定时删除几天前的文件 days: 1 ``` **testng.xml** > 目前并行级别只支持"classes"级别。 ```xml ``` ### 第二步 写测试Case > 在框架中我已经预写了几个case供大家参考。 以百度搜索“lakernote”示例,分为如下几步。 **第一步** 确认有几个页面对象,这里只有一个页面 - 新建`BaiduHomePage.java`继承`BasePage` - 当前页面的Url`https://www.baidu.com` ```java public class BaiduHomePage extends BasePage { private final String URL = "https://www.baidu.com"; public BaiduHomePage(Page initPage) { super(initPage); } @Override protected String pageUrl() { return URL; } ``` **第二步** 确认每个页面的元素有哪些,如何定位以及对应的操作 - 搜索框 `searchTermInput` - 结果列表内容 `contentLeft` - 搜索操作 `search(searchTerm)` > 注意: > > 1.页面对象中的元素设计时不要被外部访问 > > 2.当前每个元素都是在对象初始化时定位的,对于动态页面有可能不对,在具体的元素操作时 > > 可以调用waitfor(); ```java public class BaiduHomePage extends BasePage { private final String URL = "https://www.baidu.com"; private final Locator searchTermInput; private final Locator contentLeft; public BaiduHomePage(Page initPage) { super(initPage); // 搜索框 searchTermInput = getComponentById("kw"); // 结果列表内容 contentLeft = getComponentById("content_left"); } @Override protected String pageUrl() { return URL; } public void search(String searchTerm) { searchTermInput.click(); searchTermInput.fill(searchTerm); searchTermInput.press("Enter"); } public String getContentLeftContent() { return contentLeft.innerText(); } } ``` **第三步** 新建测试case 注意继承`BasePageTest` ```java public class BaiduPageTest extends BasePageTest { @Test public void search() { BaiduHomePage baiduHomePage = new BaiduHomePage(page); baiduHomePage.search("lakernote"); String contentLeftContent = baiduHomePage.getContentLeftContent(); Assert.assertTrue(contentLeftContent.contains("lakernote-CSDN博客")); } } ``` ### 第三步 启动测试 运行`AutoTestApplication.java`中main函数即可。 **观察日志如下**: ```ini 2024-03-12 21:58:24.916 INFO 6756 --- [ scheduling-1] com.laker.autotest.job.CronJob : Starting TestNG job at Tue Mar 12 21:58:24 GMT+08:00 2024 ... ... TestNG 7.4.0 by Cédric Beust (cedric@beust.com) ... Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set 2024-03-12 21:58:31.910 INFO 6756 --- [t=autoui-test-2] com.laker.autotest.core.BasePageTest : CsdnPageTest 1993142577 Browser launched 2024-03-12 21:58:32.083 INFO 6756 --- [t=autoui-test-3] com.laker.autotest.core.BasePageTest : GiteePageTest 1692776273 Browser launched 2024-03-12 21:58:32.243 INFO 6756 --- [t=autoui-test-1] com.laker.autotest.core.BasePageTest : CsdnPageTest 1993142577 Context and page created ... 2024-03-12 21:59:22.694 WARN 6756 --- [t=autoui-test-3] c.l.autotest.core.retry.RetryAnalyzer : Retrying test 自动化端到端测试 com.laker.autotest.testcase.GiteePageTest.搜索并star,remaining 0 attempts. 2024-03-12 21:59:31.093 INFO 6756 --- [t=autoui-test-3] com.laker.autotest.core.BasePageTest : GiteePageTest 1692776273 Browser closed =============================================== 自动化端到端测试 Total tests run: 7, Passes: 4, Failures: 0, Skips: 0, Retries: 3 =============================================== 2024-03-12 21:59:31.133 INFO 6756 --- [ scheduling-1] com.laker.autotest.job.CronJob : TestNG job completed at Tue Mar 12 21:59:31 GMT+08:00 2024 ``` ### 第四步 查看测试报告 ![](docs/images/report.png) ### 第五步 查看异常Trace ![](docs/images/trace.png) ## 打包部署 ### 第一步 本地打包并上传 ```sh mvn clean package ``` 然后把`easy-autotest.jar`上传到服务器 > 目录为`target/easy-autotest.jar` ### 第二步 服务器安装Chrome浏览器和其依赖 > **注意不支持Centos**。 ```sh # 程序会自动安装 chromium # 或者执行如下命令,观察安装进度 java -jar easy-autotest.jar install --with-deps chromium ``` ### 第三步 启动自动化测试 ```sh nohup java -jar easy-autotest.jar & ``` ## FQA ### 定时执行和发邮件 这里在框架中自带了定时执行和发邮件,也可以用Jenkins来替代这两个功能。 ### 脚本录制 参考:Record.java ### 失败截图 参考:ScreenshotListener.java ### 失败重试 参考: - RetryAnnotationTransformer.java - RetryAnalyzer.java ### 内置浏览器下载加速 **playwright**默认会从Azure `https://playwright.azureedge.net `下载,国内网访问有时下载很慢。 设置`PLAYWRIGHT_DOWNLOAD_HOST`为国内的下载源即可。 ```java Playwright.CreateOptions createOptions = new Playwright.CreateOptions(); Map env = new HashMap<>(); env.put("PLAYWRIGHT_DOWNLOAD_HOST", "https://registry.npmmirror.com/-/binary/playwright"); createOptions.setEnv(env); Playwright.create(createOptions); ``` ### 自动等待问题 在执行操作之前,Playwright会对元素进行一系列可操作性检查,以确保这些操作按预期执行。它会自动等待所有相关检查通过,然后才执行请求的操作。如果在给定的超时时间内未通过所需的检查,操作将因超时错误而失败。 例如,对于 Locator.click(),Playwright 将确保: - 定位器解析为恰好一个元素 - 元素是可见的 - 元素是稳定的,意味着不在动画过程中或已完成动画 - 元素可接收事件,意味着不被其他元素遮挡 元 - 素是启用的 以下是每个操作执行的所有可操作性检查的完整列表: | Action | [Visible](https://playwright.dev/java/docs/actionability#visible) | [Stable](https://playwright.dev/java/docs/actionability#stable) | [Receives Events](https://playwright.dev/java/docs/actionability#receives-events) | [Enabled](https://playwright.dev/java/docs/actionability#enabled) | [Editable](https://playwright.dev/java/docs/actionability#editable) | | :----------------------------------------------------------- | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | | [Locator.check()](https://playwright.dev/java/docs/api/class-locator#locator-check) | Yes | Yes | Yes | Yes | - | | [Locator.click()](https://playwright.dev/java/docs/api/class-locator#locator-click) | Yes | Yes | Yes | Yes | - | | [Locator.dblclick()](https://playwright.dev/java/docs/api/class-locator#locator-dblclick) | Yes | Yes | Yes | Yes | - | | [Locator.setChecked()](https://playwright.dev/java/docs/api/class-locator#locator-set-checked) | Yes | Yes | Yes | Yes | - | | [Locator.tap()](https://playwright.dev/java/docs/api/class-locator#locator-tap) | Yes | Yes | Yes | Yes | - | | [Locator.uncheck()](https://playwright.dev/java/docs/api/class-locator#locator-uncheck) | Yes | Yes | Yes | Yes | - | | [Locator.hover()](https://playwright.dev/java/docs/api/class-locator#locator-hover) | Yes | Yes | Yes | - | - | | [Locator.dragTo()](https://playwright.dev/java/docs/api/class-locator#locator-drag-to) | Yes | Yes | Yes | - | - | | [Locator.screenshot()](https://playwright.dev/java/docs/api/class-locator#locator-screenshot) | Yes | Yes | - | - | - | | [Locator.fill()](https://playwright.dev/java/docs/api/class-locator#locator-fill) | Yes | - | - | Yes | Yes | | [Locator.clear()](https://playwright.dev/java/docs/api/class-locator#locator-clear) | Yes | - | - | Yes | Yes | | [Locator.selectOption()](https://playwright.dev/java/docs/api/class-locator#locator-select-option) | Yes | - | - | Yes | - | | [Locator.selectText()](https://playwright.dev/java/docs/api/class-locator#locator-select-text) | Yes | - | - | - | - | | [Locator.scrollIntoViewIfNeeded()](https://playwright.dev/java/docs/api/class-locator#locator-scroll-into-view-if-needed) | - | Yes | - | - | - | | [Locator.blur()](https://playwright.dev/java/docs/api/class-locator#locator-blur) | - | - | - | - | - | | [Locator.dispatchEvent()](https://playwright.dev/java/docs/api/class-locator#locator-dispatch-event) | - | - | - | - | - | | [Locator.focus()](https://playwright.dev/java/docs/api/class-locator#locator-focus) | - | - | - | - | - | | [Locator.press()](https://playwright.dev/java/docs/api/class-locator#locator-press) | - | - | - | - | - | | [Locator.pressSequentially()](https://playwright.dev/java/docs/api/class-locator#locator-press-sequentially) | - | - | - | - | - | | [Locator.setInputFiles()](https://playwright.dev/java/docs/api/class-locator#locator-set-input-files) | - | - | - | - | - | ### 断言相关问题 Playwright 包括自动重试的断言功能,通过等待条件满足来消除不稳定性,类似于在操作之前进行自动等待。 | Assertion | Description | | :----------------------------------------------------------- | :-------------------------------- | | [assertThat(locator).isAttached()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-attached) | Element is attached | | [assertThat(locator).isChecked()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-checked) | Checkbox is checked | | [assertThat(locator).isDisabled()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-disabled) | Element is disabled | | [assertThat(locator).isEditable()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-editable) | Element is editable | | [assertThat(locator).isEmpty()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-empty) | Container is empty | | [assertThat(locator).isEnabled()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-enabled) | Element is enabled | | [assertThat(locator).isFocused()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-focused) | Element is focused | | [assertThat(locator).isHidden()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-hidden) | Element is not visible | | [assertThat(locator).isInViewport()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-in-viewport) | Element intersects viewport | | [assertThat(locator).isVisible()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-be-visible) | Element is visible | | [assertThat(locator).containsText()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-contain-text) | Element contains text | | [assertThat(locator).hasAttribute()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-attribute) | Element has a DOM attribute | | [assertThat(locator).hasClass()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-class) | Element has a class property | | [assertThat(locator).hasCount()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-count) | List has exact number of children | | [assertThat(locator).hasCSS()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-css) | Element has CSS property | | [assertThat(locator).hasId()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-id) | Element has an ID | | [assertThat(locator).hasJSProperty()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-js-property) | Element has a JavaScript property | | [assertThat(locator).hasText()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-text) | Element matches text | | [assertThat(locator).hasValue()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-value) | Input has a value | | [assertThat(locator).hasValues()](https://playwright.dev/java/docs/api/class-locatorassertions#locator-assertions-to-have-values) | Select has options selected | | [assertThat(page).hasTitle()](https://playwright.dev/java/docs/api/class-pageassertions#page-assertions-to-have-title) | Page has a title | | [assertThat(page).hasURL()](https://playwright.dev/java/docs/api/class-pageassertions#page-assertions-to-have-url) | Page has a URL | | [assertThat(response).isOK()](https://playwright.dev/java/docs/api/class-apiresponseassertions#api-response-assertions-to-be-ok) | Response has an OK status | Learn more in the [assertions guide](https://playwright.dev/java/docs/test-assertions). ### 安装浏览器相关问题 #### 1.安装浏览器 支持参数为: - `install` 运行无参数的命令将安装默认浏览器 - `install webkit` 安装特定浏览器 - `install --help` 查看所有支持的浏览器 #### 2.安装系统依赖 支持参数为: - `install-deps` 系统依赖项可以自动安装 - `install-deps chromium` 为单个浏览器安装依赖项 - `install --with-deps chromium` 将install-deps与install结合使用,以便通过一条命令安装浏览器和操作系统依赖项 #### 3.管理浏览器二进制文件 Playwright 将 Chromium、WebKit 和 Firefox 浏览器下载到操作系统特定的缓存文件夹中: - `%USERPROFILE%\AppData\Local\ms-playwright` on Windows - `~/Library/Caches/ms-playwright` on MacOS - `~/.cache/ms-playwright` on Linux 这些浏览器安装后将占用几百兆的磁盘空间: ```sh du -hs ~/Library/Caches/ms-playwright/* 281M chromium-XXXXXX 187M firefox-XXXX 180M webkit-XXXX ``` 您可以使用环境变量来覆盖默认行为。在安装 Playwright 时,要求它将浏览器下载到特定位置: > PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers ### 复用登录状态 >- https://blog.csdn.net/abu935009066/article/details/136496682 1.在Record.java中录制登录操作,录制后会自动更新或生成`auth.json`文件。 > - 关闭录制器一定要在Playwright Inspector点击关闭,否则可能存储到`auth.json失败。 > - 如果录制器没有关闭,可以手动删除`auth.json`文件,然后重新录制登录操作。 > - `auth.json`文件存储在项目根目录下。 > - `auth.json`文件中存储了登录状态的cookie信息,可以用于后续的测试。 > - 确保`auth.json`仅在本地使用,因为它包含敏感信息。将其添加到`.gitignore`。 2.在application.yaml中启用 `easy.context-options.login-state: true`。 ### 同时测试UI和API 参考:[CsdnApiAndPageTest.java](https://gitee.com/lakernote/easy-auto-test/blob/master/src/main/java/com/laker/autotest/testcase/CsdnApiAndPageTest.java) > page对象有很多内置的事件监听,可以监听到几乎所有的页面事件,例如:弹窗,network请求,元素的加载完成等事件,可以在这些事件中做定制开发。 ### 制作docker镜像 - https://github.com/microsoft/playwright-java/blob/main/utils/docker/README.md - https://playwright.dev/java/docs/docker - https://github.com/microsoft/playwright-java/blob/main/utils/docker/Dockerfile.jammy - https://mcr.microsoft.com/en-us/product/playwright/java/about > 此 Docker 映像包含启动浏览器所需的所有依赖项以及执行测试所需的 Java 运行时。 ```dockerfile # 基于 Playwright for Java 官方 Docker 镜像 FROM mcr.microsoft.com/playwright/java:v1.40.0-jammy ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles # 设置工作目录 WORKDIR /app # 复制打包好的 JAR 文件到工作目录 COPY easy-autotest.jar /app/easy-autotest.jar # 设置默认启动命令 CMD ["java", "-jar", "easy-autotest.jar"] # 提供执行权限 RUN chmod +x /app/easy-autotest.jar ``` ### 单独设置某次操作/等待超时时间 在 Playwright 的 Java API 中,可以通过传递一个xxxOptions(ClickOptions,WaitForSelectorOptions)对象来设置超时时间。 ```java page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Save")) .click(new Locator.ClickOptions().setTimeout(60000)); ``` ### 模拟文件上传 ```java // 等待文件选择器 FileChooser fileChooser = page.waitForFileChooser(() -> { // 触发文件上传 page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Browse files")).click(); }); final Path videoPath = Paths.get("C:\\Test resource\\mp4\\file_example_MP4_1280_10MG.mp4"); // 选择文件 fileChooser.setFiles(videoPath); // 注意这里并不会等着上传完成,所以你要找个元素等着它上传完成。 // 例如 page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Save")).click(new Locator.ClickOptions().setTimeout(60000)); ``` ### 模拟文件下载 ```java final Page.WaitForDownloadOptions waitForDownloadOptions = new Page.WaitForDownloadOptions(); // 设置超时时间 waitForDownloadOptions.setTimeout(60000); // 等待下载 Download download = page.waitForDownload(waitForDownloadOptions, () -> { // 触发下载 page.getByLabel("Disclaimer").getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Accept")).click(); }); // 保存文件 download.saveAs(Paths.get("./", download.suggestedFilename())); ``` ### 切换浏览器 - 用程序下载chromium,firefox,webkit浏览器,然后使用在`easy.browser`中设置浏览器类型。 - 用电脑上用户下载的chrome,egde等浏览器,然后在`easy.launch-options.channel`中设置浏览器路径。 > 注意使用chrome和egde时,一定 一定 一定要把已经打开的chrome和edge给关了,否则会报错。