1
背景
目前市場上 UI 自動化覆寫率最廣的莫過于 Selenium,而且 Selenium 上手很快,資料豐富,社群活躍。在發現 Playwright 之前,觀遠資料一直使用 Selenium 作為 web 自動化架構。但是我們在編寫自動化用例的過程中還是發現了 Selenium 一些問題:
- 依賴各種不同的 driver,随着浏覽器的不斷更新,也需要不斷的去維護這些 driver;
- Selenium IDE 錄屏代碼不穩定;
- 穩定性不夠好;
- ...
然後我們發現了 Playwright。Playwright 是微軟開源的 Web 自動化操作的架構,它的功能類似于 Selenium、Pyppeteer 等,都可以驅動浏覽器進行各種自動化操作,對市面上的主流浏覽器都提供了支援。API 功能簡潔又強大,僅用一個 API 即可執行 Chromium、Firefox、WebKit 等主流浏覽器自動化操作,并同時支援以無頭模式、有頭模式運作。
下面我們将對比 Playwright 和 Selenium 優缺點,以及分享我們基于 Playwright 所産生的一些經驗。
2
Playwright和Selenium對比
- 支援語言(都支援主流語言)
- Playwright:JavaScript & TypeScript\python\C#\Go\Java
- Selenium:java\python\ruby\C#\C++\JavaScript
- 操作浏覽器方式以及性能
- Playwright:通過開發者工具與浏覽器互動,安裝簡潔,不需要安裝各種 Driver,啟動浏覽器速度快。
- Selenium:需要通過各種WebDriver操作不同浏覽器,啟動浏覽器速度慢。
- 支援浏覽器
- Playwright:支援所有主流浏覽器,Chromium/WebKit/Firefox,不支援 IE11。
- Selenium:運作在目前所有主流浏覽器上(不包括國内套皮的浏覽器)。
- 快速可靠執行
- Playwright:自動等待(等待元素出現/等待事件發生)、基于 Websocket(雙向通訊)可自動擷取浏覽器實際情況。
- Selenium:需要代碼中加入等待,甚至元素狀态輪訓判斷,增加運作時間、Selenium 基于 HTTP 協定(單向通訊)。
- 代碼錄屏
- Playwright:可以使用基于 css、xpath、text 這些常用的元素定位方式進行錄制生成代碼,能大幅度的減少寫代碼的時間,同時代碼穩定性也可以保證。
- Selenium:Selenium IDE 錄制的代碼是基于 coordinate 或者 DOM 層級結構,是以極其不穩定,也就導緻 IDE 基本無人問津。
- 異步方式
- Playwright 支援異步方式。
- Selenium 不支援異步方式。
- headless模式
- Playwright 和 Selenium 均支援 headless 模式,是以在 Linux 系統或缺少顯示裝置的場景下也可以跑 UI 自動化。
- 移動端浏覽器
- Playwright 和 Selenium 均支援移動端浏覽器的模拟測試,不支援真機測試。
3
Playwright環境搭建
3.1 前置環境搭建
JDK 8、Git、Maven、TestNg、jUnit等。
3.2 pom.xml添加代碼
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>assertions</artifactId>
<version>1.17.2</version>
</dependency>
3.3 Playwright依賴安裝
#首先安裝可以支援的浏覽器
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install"
#如果有特殊浏覽器需要安裝可以先檢視
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install --help"
#安裝特殊浏覽器
# Install WebKit
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install webkit"
3.4 代碼錄制
#打開浏覽器進行錄屏(保留已經驗證的狀态)
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen --load-storage=auth.json https://www.guandata.com"
#如果要模仿手機操作,可以通過 playwright.devices 清單操作
# Emulate iPhone 11.
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args='open --device="iPhone 11" https://www.guandata.com'
4
實用操作
4.1 錄屏示例
使用 playwright 針對固定場景進行錄屏回放,觀察錄屏效果。
通過以下錄屏過程和錄屏代碼可以看到:
- Playwright 支援無頭浏覽器模式,且較為推薦(headless 預設值為 True);
- 可以使用傳統定位方式(CSS,XPATH 等),也有自定義的新的定位方式(如文字定位),元素定位和操作方法更加簡單;
- 操作方法中傳入了元素定位,定位和操作同時進行(playwright 也提供了單獨的定位方法,作為可選) --selenium 隻能先定位元素,再進行操作;
- 不需要為每個浏覽器下載下傳 webdriver,可以随意切換不同浏覽器;
- 錄屏方式簡單。
,時長00:46
// 輸入賬号密碼登入觀遠BI系統
// 進入資料中心,建立一個test2名稱的目錄
public class Example {
public static void main(String[] args) {
// 執行個體化page對象
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(false));
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("auth.json")));
Page page = context.newPage();
// 通路BI系統位址
page.navigate("http://192.168.199.6:8087/auth/index");
// 輸入域名、賬号、密碼進行登入
page.click("[placeholder=\"公司域\"]");
page.fill("[placeholder=\"公司域\"]", "testing");
page.click("[placeholder=\"郵箱位址\"]");
page.fill("[placeholder=\"郵箱位址\"]", "[email protected]");
page.click("[placeholder=\"密碼\"]");
page.fill("[placeholder=\"密碼\"]", "123456");
page.waitForNavigation(() -> {
page.press("[placeholder=\"密碼\"]", "Enter");
});
// 點選資料中心
page.waitForNavigation(() -> {
page.click("#rc-tabs-0-tab-datacenter div:has-text(\"資料中心\")");
});
// 點選建立檔案夾進行檔案夾建立
page.click("button:has-text(\"建立檔案夾\")");
page.fill("text=檔案夾名稱檔案夾位置:根目錄 >> input[type=\"text\"]", "test-playwright");
page.click("button:has-text(\"确定\")");
}
}
}
4.2 切換浏覽器以及 headless 設定
Playwright 支援切換不同的浏覽器,在實際的自動化過程中可以進行多個浏覽器的相容性測試。
日常自動化用例執行可以采用 headless 模式(無頭模式,浏覽器不會打開界面),提升用例執行效率。而當我們進行用例開發的時候,使用無 headless 模式友善代碼調試(有頭模式,浏覽器會打開界面),提升用例編寫效率。
// 使用此Browser 對象,可以使用launch()方法啟動浏覽器執行個體
Playwright playwright = Playwright.create();
// 使用firefox浏覽器,也可以改為webkit浏覽器
Browser browser = playwright.firefox().launch();
// false為有頭模式(有界面的浏覽器),true為無頭模式(沒有界面的浏覽器)
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false));
4.3 封裝父類方法
把共用的方法封裝在父類中:浏覽器預設為 chromium、headless 模式、設定浏覽器寬和高、環境的預設位址等。測試類中引用這個父類後,在執行測試用例時會先觸發父類中的操作,避免代碼重複編寫。
public class UiAbstract {
public static Page page;
public static BrowserContext context;
@BeforeSuite
public void setUp() {
Playwright playwright = Playwright.create();
//設定浏覽器為chromium,headless模式
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(Env.HEADLESS));
Browser.NewContextOptions contextOptions = new Browser.NewContextOptions();
// 設定浏覽器寬和高
contextOptions.setViewportSize(1280, 720);
//設定環境的預設位址
contextOptions.setBaseURL(Env.TESTSERVER);
context = browser.newContext(contextOptions);
}
}
4.4 常用斷言方法
介紹一些常用的 UI 自動化斷言的方法,觀遠資料中使用最多的是截圖對比斷言,詳見 4.5。
擷取文本内容,進行斷言:
String content = page.text_content(":nth-match(i, 2)");
Assert.assertEquals(content, expected, "[" + description + "]測試案例失敗!");
等待易消失元素出現,再擷取文本内容,進行斷言:
// 等待元素出現
page.waitForSelector(":nth-match(i, 2)");
// 擷取文本内容,進行斷言
String content = page.text_content(":nth-match(i, 2)");
Assert.assertEquals(content, expected, "[" + description + "]測試案例失敗!");
擷取内部文字,進行斷言:
String text = page.inner_text(":nth-match(i, 2)");
Assert.assertEquals(text, expected, "[" + description + "]測試案例失敗!");
擷取屬性值,進行斷言:
String attribute = page.getAttribute("#su", "value");
Assert.assertEquals(attribute, expected, "[" + description + "]測試案例失敗!");
元素可見性,進行斷言:
boolean visible = page.isVisible('#su');
Assert.assertTrue(visible, expected, description);
啟用狀态(元素存在可點選),進行斷言:
boolean enabled = page.isEnabled('#su');
Assert.assertTrue(visible, expected, description);
title,進行斷言:
String title = page.title();
Assert.assertEquals(title, expected, "[" + description + "]測試案例失敗!");
url,進行斷言:
String url = page.url();
Assert.assertEquals(url, expected, "[" + description + "]測試案例失敗!");
4.5 截圖對比斷言
由于通過文本方式進行斷言工作量也是非常大的,固通過二次開發增加截圖與 base 進行像素對比進行斷言,提高UI自動化編寫效率。主要流程:
- 編寫用例時,把需要斷言的頁面進行截圖,圖檔上傳到 Samba 伺服器相應的路徑下;
- 測試類中引用截圖斷言方法(可以通過元素定位截圖,也可以全螢幕進行截圖),并設定像素相似度(0-1 之間),生成的截圖結果會放到 Samba 伺服器對應路徑下;
- 截圖斷言方法會針對兩張圖檔進行像素比對,會在一張空白圖檔中畫上對比結果,對比成功的畫入灰色,對比失敗的畫入紅色;
- 生成測試報告,報告中含有實際截圖、期望截圖、對比截圖,如下圖;
- 如果是前端正常修改導緻的斷言失敗,通過開發的小工具把實際截圖替換期望截圖。
截圖斷言核心代碼如下:
public class ImageDiff {
public static double getDifferencePercent(String actual, String baseline, String diffOut, int threshold, double tolerance) throws IOException {
// 讀取實際圖檔和base圖檔的圖像
InputStream actualInputStream = new FileInputStream(actual);
InputStream baseInputStream = new FileInputStream(baseline);
try {
BufferedImage img1 = ImageIO.read(actualInputStream);
BufferedImage img2 = ImageIO.read(baseInputStream);
// 擷取圖檔的高和寬
int width = img1.getWidth();
int height = img1.getHeight();
int width2 = img2.getWidth();
int height2 = img2.getHeight();
long diffCount = 0;
// 周遊螢幕截圖像素點資料
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
//使用getRGB(w, h)擷取該點的顔色值是ARGB
//而在實際應用中使用的是RGB,是以需要将ARGB轉化成RGB,即bufImg.getRGB(w, h) & 0xFFFFFF。
int i = pixelYIQDiff(img1.getRGB(x, y), img2.getRGB(x, y));
if (i > threshold) {
diffCount++;
img1.setRGB(x, y, 0xffff0000);
} else {
img1.setRGB(x, y, greyPixel(img1.getRGB(x, y)));
}
}
}
long maxDiffCount = width * height;
double diffPct = 100.0 * diffCount / maxDiffCount;
// 不一緻的圖檔大于精度,則使用圖檔标出
if (diffPct > tolerance) {
ImageIO.write(img1, "png", new File(diffOut));
}
return diffPct;
} catch (FileNotFoundException e) {
Log.warn(baseline + "檔案不存在");
DistributeController.uploadFile(baseline, actualInputStream);
// FileUtils.copyFile(actualFile, baselineFile);
e.printStackTrace();
}
return 0;
}
// 分别擷取出rgb中的r、g、b的值,并得到不一緻數
private static int pixelYIQDiff(int rgb1, int rgb2) {
// 對整數直接進行計算得到rgb值
int r1 = (rgb1 >> 16) & 0xff;
int g1 = (rgb1 >> 8) & 0xff;
int b1 = rgb1 & 0xff;
int r2 = (rgb2 >> 16) & 0xff;
int g2 = (rgb2 >> 8) & 0xff;
int b2 = rgb2 & 0xff;
double yDiff = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
double iDiff = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
double qDiff = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
return (int) (0.5053 * yDiff * yDiff + 0.299 * iDiff * iDiff + 0.1957 * qDiff * qDiff);
}
private static double rgb2y(int r, int g, int b) {
return r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
}
private static double rgb2i(int r, int g, int b) {
return r * 0.59597799 - g * 0.27417610 - b * 0.32180189;
}
private static double rgb2q(int r, int g, int b) {
return r * 0.21147017 - g * 0.52261711 + b * 0.31114694;
}
// 像素比對上繪制灰色
private static int greyPixel(int rgb1) {
// 對整數直接進行計算得到rgb值
int r1 = (rgb1 >> 16) & 0xff;
int g1 = (rgb1 >> 8) & 0xff;
int b1 = rgb1 & 0xff;
int y = (int) rgb2y(r1, g1, b1);
int gray = (int) (255 + (y - 255) * 0.2);
return gray << 16 & 0x00ff0000 | gray << 8 & 0x0000ff00 | gray & 0x000000ff | 0xff000000;
}
}
4.6 其他常用操作
- 下拉選擇框:selectOpion
- 檔案上傳:setInputFiles、單個檔案、多個檔案、拖放上傳
- 滑鼠點選:mouse().click、mouse().dblclick
- 滑鼠拖動:mouse().down、mouse().up
- 滑鼠移動:mouse().move
- 鍵盤按鍵:press
- 截屏、錄屏:screenshot、video
- 浏覽器滾動條滾動到元素位置:querySelector(element).scrollIntoViewIfNeeded()
5
代碼實戰
/**
* @description: 測試權限控制功能對訂閱計劃中的内容是否生效
* @date: 2021-12-29 18:33
**/
public class SubscriptionReadTest extends UiAbstract {
String description = "訂閱-檢視";
// 測試類前執行個體化page對象
@BeforeTest
public void beforeTest(){
// Open new page
page = context.newPage();
}
@Test(dataProvider = "datapro")
public void subscriptionReadTest(String role, String loginId) {
page.navigate("/auth/index");
// 輸入域名、賬号、密碼點選登入
page.click("[placeholder=\"公司域\"]");
page.fill("[placeholder=\"公司域\"]", "rbac");
page.click("[placeholder=\"郵箱位址\"]");
page.fill("[placeholder=\"郵箱位址\"]", loginId);
page.click("[placeholder=\"密碼\"]");
page.fill("[placeholder=\"密碼\"]", "******");
page.waitForNavigation(() -> {
page.click("#loginBtn");
});
// 點選九宮格按鈕
page.click(":nth-match(i, 2)");
// 點選訂閱計劃,根據元素截圖
page.click("text=訂閱計劃");
page.waitForSelector("text=1");
ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"卡片訂閱");
// 點選合并訂閱,根據元素截圖
page.click("text=合并訂閱");
page.waitForSelector("text=暫無相關資料");
ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"合并訂閱");
// 點選頁面訂閱,根據元素截圖
page.click("text=頁面訂閱");
page.waitForSelector("text=1");
ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"頁面訂閱");
// 點選資料集訂閱,根據元素截圖
page.click("text=資料集訂閱");
page.waitForSelector("text=暫無相關資料");
ScreenshotUtil.screenshotBySelector(page, "div[class='_1HjXw1zf']", description+'-'+role+"資料集訂閱");
// 對上述的測試截圖進行斷言
ScreenshotUtil.ScreenshotAssert(description+'-'+role+"卡片訂閱");
ScreenshotUtil.ScreenshotAssert(description+'-'+role+"合并訂閱");
ScreenshotUtil.ScreenshotAssert(description+'-'+role+"頁面訂閱");
ScreenshotUtil.ScreenshotAssert(description+'-'+role+"資料集訂閱");
}
// 測試資料
@DataProvider
public Object[][] datapro() {
return new Object[][]{
{"普通使用者","[email protected]"},
{"隻讀使用者","[email protected]"}
};
}
// 測試完成後關閉page
@AfterTest
public void after(){
// Close page
page.close();
}
}
官網文檔:https://playwright.dev/java/docs/intro
GitHub:https://github.com/microsoft/playwright-java
作者:李鳔鳔,觀遠資深測試開發工程師。在業務測試、大資料測試、自動化測試、性能測試等領域都擁有豐富的工作經驗。
來源-微信公衆号:觀遠資料技術團隊
出處:https://mp.weixin.qq.com/s/zmx5ZkEruGJ4EDdfAIzSkA