# 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
```
### 第四步 查看测试报告

### 第五步 查看异常Trace

## 打包部署
### 第一步 本地打包并上传
```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给关了,否则会报错。