天天看點

技術實踐幹貨 | 觀遠資料 web 自動化架構 Playwright 詳解

作者:閃念基因

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自動化編寫效率。主要流程:

  1. 編寫用例時,把需要斷言的頁面進行截圖,圖檔上傳到 Samba 伺服器相應的路徑下;
  2. 測試類中引用截圖斷言方法(可以通過元素定位截圖,也可以全螢幕進行截圖),并設定像素相似度(0-1 之間),生成的截圖結果會放到 Samba 伺服器對應路徑下;
  3. 截圖斷言方法會針對兩張圖檔進行像素比對,會在一張空白圖檔中畫上對比結果,對比成功的畫入灰色,對比失敗的畫入紅色;
  4. 生成測試報告,報告中含有實際截圖、期望截圖、對比截圖,如下圖;
  5. 如果是前端正常修改導緻的斷言失敗,通過開發的小工具把實際截圖替換期望截圖。
技術實踐幹貨 | 觀遠資料 web 自動化架構 Playwright 詳解

截圖斷言核心代碼如下:

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