Java Selenium WebDriver 实用手册(五)

原文:zh.annas-archive.org/md5/35b7bb6327cca70dfdbf1a17bd553748

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:第三方集成

本章介绍了不同的第三方技术(如库或框架),我们可以与 Selenium WebDriver 结合使用。当 Selenium WebDriver API 不足以执行特定任务时,我们需要使用这些技术,比如文件下载,我们需要使用第三方工具来等待文件正确下载,或者使用 HTTP 客户端来控制下载。我们还可以使用第三方代理来捕获 HTTP 流量。

另一个场景是我们需要使用 Selenium WebDriver 与外部工具结合来实现非功能性测试,比如性能、安全性、可访问性或 A/B 测试。我们还可以使用第三方库开发 Selenium WebDriver 测试,使用流畅的 API,生成虚拟测试数据,或者改进测试报告。最后,我们可以集成相关框架如 Cucumber 用于行为驱动开发(BDD),或者 Spring Framework(用于开发 Web 应用)。本章节将详细介绍所有这些用途。

小贴士

要使用本章介绍的第三方工具,你必须首先在项目中包含所需的依赖项。你可以在 附录 C 中找到使用 Maven 和 Gradle 解决每个依赖项的详细信息。

文件下载

Selenium WebDriver 对文件下载的支持有限,因为其 API 不公开下载进度。换句话说,我们可以使用 Selenium WebDriver 下载来自 Web 应用的文件,但无法控制将这些文件复制到本地文件系统所需的时间。因此,我们可以使用第三方库来增强使用 Selenium WebDriver 进行 Web 下载的体验。有不同的替代方案来实现这个目标。下面的小节将详细解释如何做到这一点。

使用浏览器特定的能力

我们可以使用特定于浏览器的能力(就像我们在 第五章 中所做的那样)来配置文件下载的各种参数,例如目标文件夹。这种方法很方便,因为这些功能在 Selenium WebDriver API 中是开箱即用的,但它也有几个缺点。首先,它与不同的浏览器类型(如 Chrome、Firefox 等)不兼容。换句话说,每个浏览器的所需能力是不同的。其次,更重要的是,我们无法控制跟踪下载进度。为了解决这个问题,我们需要使用第三方库。在本书中,我建议使用开源库 Awaitility

Awaitility 是一个流行的库,提供处理异步操作的功能。它通过提供流畅的 API 来管理线程、超时和并发问题。在使用 Selenium WebDriver 下载文件的情况下,我们使用 Awaitility API 等待直到文件存储在文件系统中。示例 9-1 展示了使用 Chrome 和 Awaitility 的示例。示例 9-2 展示了使用 Firefox 时等效的测试设置。

Example 9-1. 使用 Chrome 和 Awaitility 测试下载文件
class DownloadChromeJupiterTest {

    WebDriver driver;

    File targetFolder;

    @BeforeEach
    void setup() {
        targetFolder = new File(System.getProperty("user.home"), "Downloads"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
        Map<String, Object> prefs = new HashMap<>();
        prefs.put("download.default_directory", targetFolder.toString()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        ChromeOptions options = new ChromeOptions();
        options.setExperimentalOption("prefs", prefs);

        driver = WebDriverManager.chromedriver().capabilities(options).create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testDownloadChrome() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/download.html"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

        driver.findElement(By.xpath("(//a)[2]")).click(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
        driver.findElement(By.xpath("(//a)[3]")).click();

        ConditionFactory await = Awaitility.await()
                .atMost(Duration.ofSeconds(5)); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
        File wdmLogo = new File(targetFolder, "webdrivermanager.png");
        await.until(() -> wdmLogo.exists()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>

        File wdmDoc = new File(targetFolder, "webdrivermanager.pdf");
        await.until(() -> wdmDoc.exists()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO1-1

我们指定一个文件夹保存下载的文件。但是需要注意,Chrome 只允许特定的目录用于下载。例如,它允许下载目录(及其子文件夹),但禁止使用其他路径,如桌面文件夹或主目录。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO1-2

我们使用 Chrome 首选项指定目标文件夹。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO1-3

我们使用练习网站上提供的网页通过点击按钮下载不同的文件(见图 9-1)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO1-4

我们点击页面上的两个按钮。结果,浏览器开始下载两个文件:一个 PNG 图片和一个 PDF 文档。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO1-5

我们使用 Awaitility 配置了五秒的等待超时。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO1-6

我们等待直到第一个文件在文件系统中。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO1-7

我们还等待第二个文件下载完成。

示例 9-2. 使用 Firefox 下载文件的测试设置
@BeforeEach
void setup() {
    FirefoxOptions options = new FirefoxOptions();
    targetFolder = new File("."); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    options.addPreference("browser.download.dir",
            targetFolder.getAbsolutePath()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    options.addPreference("browser.download.folderList", 2); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    options.addPreference("browser.helperApps.neverAsk.saveToDisk",
            "image/png, application/pdf"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    options.addPreference("pdfjs.disabled", true); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO2-1

Firefox 允许指定任何文件夹来下载文件。在这种情况下,我们使用本地项目文件夹。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO2-2

我们使用 Firefox 首选项指定自定义下载目录。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO2-3

我们需要将首选项 browser.download.folderList 设置为 2 以选择自定义下载文件夹。其他可能的值为 0(将文件下载到用户桌面)和 1(使用下载文件夹,即默认值)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO2-4

我们指定 Firefox 不会要求保存在本地文件系统中的内容类型。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO2-5

我们禁用 PDF 文件的预览。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0901.png

图 9-1. 用于下载文件的练习网页

使用 HTTP 客户端

使用 Selenium WebDriver 下载文件的另一种机制是使用 HTTP 客户端库。我建议使用Apache HttpClient,因为 WebDriverManager 内部使用了这个库,因此你可以在项目中作为传递依赖使用它。示例 9-3 展示了使用 Apache HttpClient 从实践站点下载多个文件的完整测试用例。注意,在这种情况下,不需要显式等待文件下载完成,因为 Apache HttpClient 同步处理 HTTP 响应。

示例 9-3. 使用 HTTP 客户端下载文件的测试
class DownloadHttpClientJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testDownloadHttpClient() throws IOException {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/download.html"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

        WebElement pngLink = driver.findElement(By.xpath("(//a)[2]")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        File pngFile = new File(".", "webdrivermanager.png");
        download(pngLink.getAttribute("href"), pngFile); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
        assertThat(pngFile).exists();

        WebElement pdfLink = driver.findElement(By.xpath("(//a)[3]")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
        File pdfFile = new File(".", "webdrivermanager.pdf");
        download(pdfLink.getAttribute("href"), pdfFile);
        assertThat(pdfFile).exists();
    }

    void download(String link, File destination) throws IOException {
        try (CloseableHttpClient client = HttpClientBuilder.create().build()) { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
            HttpUriRequestBase request = new HttpGet(link);
            try (CloseableHttpResponse response = client.execute(request)) { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>
                FileUtils.copyInputStreamToFile(
                        response.getEntity().getContent(), destination); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>
            }
        }
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO3-1

我们再次使用实践网页下载文件。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO3-2

我们点击一个按钮来下载一个文件。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO3-3

我们重构了类方法download中下载文件的通用逻辑。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO3-4

我们为第二个要下载的文件重复操作。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO3-5

我们在 try-with-resources 中创建了一个 Apache HTTPClient 实例。这个客户端在语句作用域结束时会自动关闭。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO3-6

我们使用另一个 try-with-resources 语句向提供的 URL 发送 HTTP 请求,并获得 HTTP 响应。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO3-7

我们将生成的文件复制到本地文件系统中。

捕获网络流量

“网络监控” 和 “网络拦截器” 解释了如何使用特定于浏览器的功能来捕获 Selenium WebDriver 和测试中的 Web 应用程序之间的 HTTP 流量。这种机制的缺点是只有支持 CDP 的浏览器才能使用。然而,我们可以为其他浏览器使用第三方代理。在本书中,我建议您为此目的使用BrowserMob代理。

BrowserMob 是一个开源代理,允许使用 Java 库操纵 HTTP 流量。示例 9-4 展示了在 Selenium WebDriver 测试中使用此代理的完整测试。在此示例中,我们使用 BrowserMob 代理拦截测试和目标网站之间的 HTTP 流量,并跟踪这些流量(请求-响应)作为日志跟踪。

示例 9-4. 通过 BrowserMob 代理捕获网络流量的测试
class CaptureNetworkTrafficFirefoxJupiterTest {

    static final Logger log = getLogger(lookup().lookupClass());

    WebDriver driver;

    BrowserMobProxy proxy;

    @BeforeEach
    void setup() {
        proxy = new BrowserMobProxyServer(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
        proxy.start(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        proxy.newHar(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
        proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT,
                CaptureType.RESPONSE_CONTENT); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>

        Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
        FirefoxOptions options = new FirefoxOptions();
        options.setProxy(seleniumProxy); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>
        options.setAcceptInsecureCerts(true); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>

        driver = WebDriverManager.firefoxdriver().capabilities(options)
                .create();
    }

    @AfterEach
    void teardown() {
        proxy.stop(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png>
        driver.quit();
    }

    @Test
    void testCaptureNetworkTrafficFirefox() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");

        List<HarEntry> logEntries = proxy.getHar().getLog().getEntries();
        logEntries.forEach(logEntry -> { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/9.png>
            log.debug("Request: {} - Response: {}",
                    logEntry.getRequest().getUrl(),
                    logEntry.getResponse().getStatus());
        });
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-1

我们创建一个 BrowserMob 的实例。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-2

我们启动这个代理。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-3

我们使用 HAR(HTTP 存档)来捕获 HTTP 流量,这是一种基于 JSON 的文件格式,用于捕获和导出这些流量。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-4

我们启用捕获交换的 HTTP 请求和响应。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-5

我们将 BrowserMob 服务器转换为 Selenium WebDriver 代理。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-6

我们将这个代理设置为浏览器选项(在这种情况下,用于 Firefox)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-7

我们需要允许不安全的证书,因为与代理的通信是使用 HTTP(而不是 HTTPS)完成的。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-8

我们在测试后停止代理。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO4-9

我们使用代理实例来收集 HTTP 流量(请求和响应)。在这个基本示例中,我们使用记录器将这些信息写入标准输出。

非功能测试

如第一章所述,Selenium WebDriver 主要用于评估 Web 应用程序的功能需求。换句话说,测试人员使用 Selenium WebDriver API 来验证测试中的 Web 应用程序是否按预期行为。然而,我们可以利用这个 API 来测试非功能需求,即性能、安全性、可访问性等质量属性。实现这一目标的常见策略是与特定的第三方实用程序集成。以下各小节解释了用于非功能测试的 Selenium WebDriver 的不同集成。

性能

性能测试评估了特定工作负载下系统在响应速度和稳定性方面的表现。与 Selenium WebDriver 不同,测试人员通常采用专门的工具,如Apache JMeter进行性能测试。 Apache JMeter 是一个开源工具,允许向给定的 URL 端点发送多个 HTTP 请求,同时测量响应时间和其他指标。尽管 Selenium WebDriver 与 Apache JMeter 之间的直接集成并不容易,但我们可以将现有的 Selenium WebDriver 测试用作 JMeter 测试计划(即 JMeter 执行的一系列步骤)。这种方法的好处在于,生成的 JMeter 测试计划将模仿 Selenium WebDriver 测试中使用的相同用户工作流程,重用浏览器进行的相同 HTTP 流量(例如,用于 JavaScript 库、CSS 等)。为此,我提出以下程序:

  1. 使用 BrowserMob 代理(在前一节中介绍)将 Selenium WebDriver 中交换的网络流量捕获为 HAR 文件。

  2. 将生成的 HAR 文件转换为 JMeter 测试计划。 JMeter 中的测试计划存储为扩展名为 JMX 的基于 XML 的文件。

  3. 在 JMeter 中加载 JMX 测试计划并进行调整以模拟并发用户并包括结果监听器

  4. 运行测试计划并评估结果。

示例 9-5 展示了实现第一步的完整测试案例。正如您所见,需要登录才能开始并创建 HAR 文件的步骤在每次测试前后都已完成。您可以使用此方法将现有的功能测试(即 @Test 方法中的逻辑)作为性能测试来执行(在 JMeter 中执行)。

示例 9-5. 创建 HAR 文件的测试。
class HarCreatorJupiterTest {

    WebDriver driver;

    BrowserMobProxy proxy;

    @BeforeEach
    void setup() {
        proxy = new BrowserMobProxyServer(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
        proxy.start();
        proxy.newHar();
        proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT,
                CaptureType.RESPONSE_CONTENT);

        Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);
        ChromeOptions options = new ChromeOptions();
        options.setProxy(seleniumProxy);
        options.setAcceptInsecureCerts(true);

        driver = WebDriverManager.chromedriver().capabilities(options).create();
    }

    @AfterEach
    void teardown() throws IOException {
        Har har = proxy.getHar(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        File harFile = new File("login.har");
        har.writeTo(harFile);

        proxy.stop();
        driver.quit();
    }

    @Test
    void testHarCreator() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/login-form.html");

        driver.findElement(By.id("username")).sendKeys("user");
        driver.findElement(By.id("password")).sendKeys("user");
        driver.findElement(By.cssSelector("button")).click();
        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText).contains("Login successful");
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO5-1

我们在测试前启动 BrowserMob 并在 WebDriver 会话中配置它。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO5-2

在测试后,我们获取 HAR 文件并将其写入本地文件。

运行前面的测试后,我们获得一个名为 login.har 的 HAR 文件。现在,我们需要将其转换为 JMeter 测试计划。有多种替代方案可供选择。您可以找到多个程序(例如 Ruby 或 Java)在网上免费提供此服务。此外,您还可以使用在线转换服务,如 BlazeMeter JMX Converter。在本示例中,我使用了这个在线服务,并在 JMeter 中打开生成的 JMX 测试计划。此时,您可以根据需要调整 JMeter 配置(您可以在官方的 用户手册 中找到有关 JMeter 的更多信息)。例如,图 9-2 显示了加载生成的 JMX 测试计划后 JMeter GUI 的更改情况:

  • 将并发用户数增加到一百(在“Thread Group”选项卡中)

  • 包括一些结果监听器,例如“聚合图”和“图形结果”。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0902.png

图 9-2. JMeter GUI 加载生成的测试计划。

现在,我们可以使用 JMeter 运行测试计划(例如,在 JMeter GUI 中单击绿色三角形按钮)。结果是,根据最初开发的互动,生成了一百个并发用户的负载,作为 Selenium WebDriver 测试的一部分(示例 9-5)。图 9-3 显示了之前添加的监听器的结果。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0903.png

图 9-3. JMeter 结果。

使用浏览器生成负载

对于许多 Web 应用性能测试场景,使用像 JMeter 这样的工具非常方便。然而,在需要实际浏览器重现完整用户工作流程(例如视频会议 Web 应用程序)时,此方法不适合。在这种情况下,一种可能的解决方案是与 Docker 一起使用 WebDriverManager。示例 9-6 展示了这种用法。正如您在本测试中所见,WebDriverManager 允许通过在 create() 方法中指定大小参数来简单地创建 WebDriver 实例列表。然后,例如,我们可以使用标准的 Java 使用线程池并行执行测试的 Web 应用程序。

示例 9-6. 使用 WebDriverManager 和 Docker 进行负载测试。
class LoadJupiterTest {

    static final int NUM_BROWSERS = 5;

    final Logger log = getLogger(lookup().lookupClass());

    List<WebDriver> driverList;

    WebDriverManager wdm = WebDriverManager.chromedriver().browserInDocker(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

    @BeforeEach
    void setupTest() {
        assumeThat(isDockerAvailable()).isTrue(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        driverList = wdm.create(NUM_BROWSERS); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    }

    @AfterEach
    void teardown() {
        wdm.quit();
    }

    @Test
    void testLoad() throws InterruptedException {
        ExecutorService executorService = newFixedThreadPool(NUM_BROWSERS); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
        CountDownLatch latch = new CountDownLatch(NUM_BROWSERS);

        driverList.forEach((driver) -> { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
            executorService.submit(() -> {
                try {
                    checkHomePage(driver);
                } finally {
                    latch.countDown();
                }
            });
        });

        latch.await(60, SECONDS); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>
        executorService.shutdown();
    }

    void checkHomePage(WebDriver driver) {
        log.debug("Session id {}", ((RemoteWebDriver) driver).getSessionId());
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO6-1

我们创建一个 Chrome 管理器实例,使用 Docker 将浏览器作为容器执行。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO6-2

我们假设运行此测试的机器上已安装 Docker。否则,将跳过此测试。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO6-3

我们创建一个 WebDriver 列表(本示例中包含五个实例)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO6-4

我们使用与 WebDriver 列表相同大小的线程池。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO6-5

我们使用线程池来并行执行 SUT 评估。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO6-6

我们等待每个并行评估完成。我们使用基于计数器门闩的同步方法来实现。

安全性

软件安全领域的一个相关组织是 OWASP(开放网络应用安全项目),这是一个促进开放解决方案以提高软件安全性的非营利基金会。其中最流行的 OWASP 项目之一是 Zed Attack Proxy(ZAP)。OWASP ZAP 是一个开源的 Web 应用安全性扫描工具,用于实施漏洞评估(即寻找安全问题)或渗透测试(即模拟的网络攻击)以发现可利用的 Web 应用漏洞。

我们可以将 OWASP ZAP 作为独立的桌面应用程序使用。Figure 9-4 显示了其 GUI 的截图。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0904.png

图 9-4. OWASP ZAP GUI

此 GUI 提供不同的功能以自动化扫描,以检测 Web 应用可能面临的安全威胁,如 SQL 注入、跨站点脚本(XSS)或跨站点请求伪造(CSRF)等。

除了独立应用程序外,我们还可以将 Selenium WebDriver 测试与 ZAP 集成。示例 9-7 提供了一个说明此集成的测试用例。执行此测试所需的步骤包括:

  1. 在本地主机上启动 OWASP ZAP。默认情况下,OWASP 启动一个代理,监听端口 8080。您可以使用 OWASP GUI 中的菜单选项 Tools → Options → Local Proxies 更改此端口。

  2. 禁用 API 密钥(或在 Selenium WebDriver 测试中复制其值)。您可以在菜单选项 Tools → Options → API 中更改此值。

  3. 实现一个使用 OWASP ZAP 作为代理的 Selenium WebDriver 测试(类似于 示例 9-7)。

  4. 执行 Selenium WebDriver 测试。此时,您应该在 ZAP GUI 中看到生成的漏洞报告。

示例 9-7. 使用 OWASP ZAP 作为安全扫描器的测试
class SecurityJupiterTest {

    static final Logger log = getLogger(lookup().lookupClass());

    static final String ZAP_PROXY_ADDRESS = "localhost"; <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    static final int ZAP_PROXY_PORT = 8080;
    static final String ZAP_API_KEY = "<put-api-key-here-or-disable-it>"; <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>

    WebDriver driver;

    ClientApi api;

    @BeforeEach
    void setup() {
        String proxyStr = ZAP_PROXY_ADDRESS + ":" + ZAP_PROXY_PORT;
        assumeThat(isOnline("http://" + proxyStr)).isTrue();

        Proxy proxy = new Proxy(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
        proxy.setHttpProxy(proxyStr);
        proxy.setSslProxy(proxyStr);

        ChromeOptions options = new ChromeOptions();
        options.setAcceptInsecureCerts(true);
        options.setProxy(proxy);

        driver = WebDriverManager.chromedriver().capabilities(options).create();

        api = new ClientApi(ZAP_PROXY_ADDRESS, ZAP_PROXY_PORT, ZAP_API_KEY); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    }

    @AfterEach
    void teardown() throws ClientApiException {
        if (api != null) {
            String title = "My ZAP report";
            String template = "traditional-html";
            String description = "This is a sample report";
            String reportfilename = "zap-report.html";
            String targetFolder = new File("").getAbsolutePath();
            ApiResponse response = api.reports.generate(title, template, null,
                    description, null, null, null, null, null, reportfilename,
                    null, targetFolder, null); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
            log.debug("ZAP report generated at {}", response.toString());
        }
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void testSecurity() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO7-1

我们配置 ZAP 本地代理监听的地址和端口。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO7-2

如果未禁用 ZAP API 密钥,则需要在这里设置其值。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO7-3

我们将 ZAP 配置为 Selenium WebDriver 代理。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO7-4

我们使用其 API 与 ZAP 进行交互。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO7-5

在测试结束后,我们生成一个 HTML 报告,报告中包含在执行 Selenium WebDriver 测试期间发现的漏洞。图 9-5 显示了此报告的屏幕截图。

注意

我们还可以作为独立的 GUI 使用 OWASP ZAP,如前所介绍。与 Selenium WebDriver 集成的潜在好处可能是重用现有的功能测试以评估安全性或自动化安全评估(例如,由 CI 服务器执行的回归测试套件)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0905.png

图 9-5. ZAP 在执行 Selenium WebDriver 测试后生成的报告

可访问性

数字可访问性指的是残障用户有效使用软件系统(如网站、移动应用等)的能力。在这一领域的一个重要参考是 Web 内容可访问性指南(WCAG),这是由 W3C Web 可访问性倡议(WAI)制定的一套标准建议,解释如何使网页内容对残障人士更易访问。

有几种方法可以测试 Web 应用的可访问性。最常见的方法是检查 WCAG 建议。为此,我们可以使用自动化可访问性扫描器,如 Axe,这是一个开源引擎,用于按照 WCAG 规则自动测试 Web 应用的可访问性。Axe 通过 Selenium WebDriver 的 Java 绑定与一个 辅助库 实现无缝集成。示例 9-8 展示了使用此库进行测试。

示例 9-8. 使用 Axe 生成可访问性报告的测试
@Test
void testAccessibility() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    assertThat(driver.getTitle()).contains("Selenium WebDriver");

    Results result = new AxeBuilder().analyze(driver); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    List<Rule> violations = result.getViolations(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    violations.forEach(rule -> {
        log.debug("{}", rule.toString()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    });
    AxeReporter.writeResultsToJsonFile("testAccessibility", result); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO8-1

我们使用 Axe 分析当前的 WebDriver 会话。这样,浏览器中加载的所有页面都将由 Axe 扫描。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO8-2

我们获得了可访问性违规的报告。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO8-3

我们将每个违规记录在标准输出中。在此示例中,发现的问题包括:

色彩对比

元素必须具有足够的颜色对比度。

标题顺序

标题级别应仅增加一级。

图片替代文本

图像必须具有替代文本。

链接名称

链接必须具有可识别的文本。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO8-4

我们将结果写入本地的 JSON 文件中。

A/B 测试

A/B 测试是一种评估可用性的形式,它比较同一应用程序的不同变体,以发现哪一个对最终用户更有效。不同的商业产品为 Selenium WebDriver 测试提供了 A/B 测试的高级功能。例如,Applitools Eyes 提供了多个网页变体的自动视觉比较。另一个选择是 Optimizely,这是一家提供定制和实验 A/B 测试工具的公司。

另一种进行 A/B 测试的方法是使用原始的 Selenium WebDriver API 和自定义条件来处理网页的不同变体。示例 9-9 展示了一个基于手动方法实现多变体网页的基本测试。请注意,这个测试展示了一种基于评估不同页面变体的 A/B 测试的简单方法。

示例 9-9. 使用 Selenium WebDriver 的基本 A/B 测试
@Test
void testABTesting() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/ab-testing.html"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    WebElement header = wait.until(
            ExpectedConditions.presenceOfElementLocated(By.tagName("h6")));

    if (header.getText().contains("variation A")) { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        assertBodyContains(driver, "Lorem ipsum");
    } else if (header.getText().contains("variation B")) {
        assertBodyContains(driver, "Nibh netus");
    } else {
        fail("Unknown variation");
    }
}

void assertBodyContains(WebDriver driver, String text) {
    String bodyText = driver.findElement(By.tagName("body")).getText();
    assertThat(bodyText).contains(text);
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO9-1

我们打开一个多变体练习网页。该页面的内容每次随机加载的概率为 50%。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO9-2

我们检查页面的变体是否符合预期。

流畅 API

如在 第一章 中介绍的,Selenium 是其他框架和库的基础技术。例如,我们可以找到几个封装了 Selenium WebDriver 的库,以便为 Web 应用程序创建端到端测试的流畅 API。这类库的一个例子是 Selenide,这是一个开源(MIT 许可证)的库,它在 Selenium WebDriver 之上定义了一个简洁的流畅 API。Selenide 提供多种好处,例如自动等待 Web 元素或支持 AJAX 应用程序。

与 Selenium WebDriver 相比,Selenide 的一个显著区别在于它在内部处理 WebDriver 对象。为此,它使用 WebDriverManager 来解析所需的驱动程序(例如 chromedriver、geckodriver 等),将 WebDriver 实例保持在一个单独的线程中,在测试结束时关闭。因此,不需要创建和终止 WebDriver 对象所需的测试样板代码。示例 9-10 通过展示一个基本的 Selenide 测试演示了这个特性。

示例 9-10. 使用 Selenide 进行测试
class SelenideJupiterTest {

    @Test
    void testSelenide() {
        open("https://bonigarcia.dev/selenium-webdriver-java/login-form.html"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

        $(By.id("username")).val("user"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        $(By.id("password")).val("user");
        $("button").pressEnter(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
        $(By.id("success")).shouldBe(visible)
                .shouldHave(text("Login successful")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO10-1

我们使用 Selenide 提供的 open 静态方法来访问给定的 URL。默认情况下,Selenide 使用本地的 Chrome 浏览器,尽管可以通过配置类(例如 Configuration.browser = "firefox";)或使用 Java 系统属性(例如 -Dselenide.browser=firefox)来更改浏览器。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO10-2

Selenide 的 $ 方法允许您通过 CSS 选择器或使用 Selenium WebDriver 的 By 定位器定位 Web 元素。此行代码使用后者向输入字段输入文本。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO10-3

我们通过 CSS 选择器定位另一个网页元素,并单击它。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO10-4

我们验证登录成功的网页元素是否存在,并包含预期的文本。

测试数据

任何测试用例的相关部分都是测试数据,即用于执行 SUT 的输入数据。选择适当的测试数据对于实施有效的测试至关重要。经典测试理论中用于测试数据选择的不同技术包括:

等价分区

通过将所有可能的输入测试数据分成我们假定以相同方式处理的值集的过程来进行测试。

边界测试

在输入值的极端端点或分区之间进行测试的过程。这种方法的基本思想是在输入域中选择代表性极限值(例如,最小值以下、最小值、略高于最小值、标称值、略低于最大值、最大值和超过最大值的值)。

这些方法在端到端测试中可能并不实用,因为执行这些策略所需的测试数目(以及随之而来的执行时间)可能是巨大的。相反,我们通常手动选择一些代表性测试数据来验证快乐路径(即正向测试),并可选择一些用于意外情况的测试数据(负向测试)。

选择测试数据的另一种选择是使用数据,即不同域的随机数据,如个人姓名、姓氏、地址、国家、电子邮件、电话号码等。实现这一目标的一个简单替代方法是使用Java Faker,这是流行的Ruby faker库的 Java 移植版。示例 9-11 展示了使用该库进行测试的示例。此测试使用假数据提交了一个练习站点上可用的 Web 表单。图 9-6 展示了在使用该假数据提交表单后的网页。

示例 9-11. 使用 Java Faker 生成不同类型的假数据进行测试
@Test
void testFakeData() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/data-types.html");

    Faker faker = new Faker(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

    driver.findElement(By.name("first-name")) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
            .sendKeys(faker.name().firstName());
    driver.findElement(By.name("last-name"))
            .sendKeys(faker.name().lastName());
    driver.findElement(By.name("address"))
            .sendKeys(faker.address().fullAddress());
    driver.findElement(By.name("zip-code"))
            .sendKeys(faker.address().zipCode());
    driver.findElement(By.name("city")).sendKeys(faker.address().city());
    driver.findElement(By.name("country"))
            .sendKeys(faker.address().country());
    driver.findElement(By.name("e-mail"))
            .sendKeys(faker.internet().emailAddress());
    driver.findElement(By.name("phone"))
            .sendKeys(faker.phoneNumber().phoneNumber());
    driver.findElement(By.name("job-position"))
            .sendKeys(faker.job().position());
    driver.findElement(By.name("company")).sendKeys(faker.company().name());

    driver.findElement(By.tagName("form")).submit();

    List<WebElement> successElement = driver
            .findElements(By.className("alert-success"));
    assertThat(successElement).hasSize(10); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

    List<WebElement> errorElement = driver
            .findElements(By.className("alert-danger"));
    assertThat(errorElement).isEmpty(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO11-1

我们创建一个 Java Faker 实例。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO11-2

我们发送不同类型的随机数据(姓名、地址、国家等)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO11-3

我们验证数据是否被正确提交。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO11-4

我们检查页面上是否没有错误。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0906.png

图 9-6. 使用假数据的实践页面

报告

测试报告是在执行测试套件后总结结果的文档。这份文档通常包括执行的测试数量及其判定结果(通过、失败、跳过)和执行时间。在我们的 Java 项目中,有不同的方法可以获得测试报告。例如,在使用 Maven 时,我们可以使用以下命令创建基本的测试报告:

mvn test <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
mvn surefire-report:report-only <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
mvn site -DgenerateReports=false <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO12-1

我们使用 Maven 执行测试。因此,Maven Surefire 插件在target文件夹中生成了一组 XML 文件。这些文件包含了测试执行的结果。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO12-2

我们将 XML 报告转换为 HTML 报告。您可以在项目文件夹target/site中找到此 HTML 报告。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO12-3

我们强制复制 CSS 和 HTML 报告中所需的图像。图 9-7 显示了这份报告的屏幕截图。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0907.png

图 9-7。使用 Maven 生成的测试报告

我们还可以使用 Gradle 生成等效的报告。在使用此构建工具执行测试套件后,Gradle 会自动在文件夹build/reports中生成一个 HTML 报告。图 9-8 展示了执行一组测试(使用 shell 命令gradle test --tests Hello*)时生成的测试报告示例。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0908.png

图 9-8。使用 Gradle 生成的测试报告

除了 Maven 和 Gradle,我们还可以使用现有的报告库来创建更丰富的报告。一个可能的替代方案是Extent Reports,一个用于创建交互式测试报告的库。Extent Reports 提供专业(商业)和社区(开源)版本。示例 9-12 展示了使用后者的测试。

示例 9-12。使用 Extent Reports 生成 HTML 报告的测试
class ReportingJupiterTest {

    WebDriver driver;

    static ExtentReports reports;

    @BeforeAll
    static void setupClass() {
        reports = new ExtentReports(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
        ExtentSparkReporter htmlReporter = new ExtentSparkReporter(
                "extentReport.html");
        reports.attachReporter(htmlReporter); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    }

    @BeforeEach
    void setup(TestInfo testInfo) {
        reports.createTest(testInfo.getDisplayName()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @AfterAll
    static void teardownClass() {
        reports.flush();
    }

    // Test methods <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO13-1

我们创建了一个测试报告生成器的实例。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO13-2

我们对其进行配置,以生成一个 HTML 报告。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO13-3

在每次测试后,我们使用测试名称作为标识符在测试报告中创建一个条目。在 JUnit 5 中,我们使用TestInfo,这是一个内置的参数解析器,允许检索关于当前测试的信息。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO13-4

和往常一样,您可以在示例仓库中找到完整的源代码。特别是,这个类有两个测试方法。图 9-9 展示了当执行这个测试类时生成的测试报告的结果。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0909.png

图 9-9。使用 Extent Reports 生成的测试报告

Extent Reports 的一个不便之处是我们需要显式地将每个测试添加到报告器中。解决此问题的一个可能方法是使用自定义测试监听器(如“测试监听器”中所述)来组织报告的公共逻辑。

另一个生成丰富测试报告的可能库是Allure,一个开源报告框架,用于生成 Java、Python 和 JavaScript 等不同编程语言的测试报告。Allure 和 Extent Reports 之间一个显著的区别在于 Allure 使用了在构建工具 Maven 或 Gradle 中配置的测试监听器(详见附录 C 了解有关此配置的详细信息)。因此,我们无需更改测试套件即可生成 Allure 报告。表格 9-1 总结了使用 Maven 和 Gradle 创建 Allure 报告所需的命令。

表格 9-1. 使用 Allure 生成测试报告的 Maven 和 Gradle 命令

MavenGradle描述

|

mvn test
mvn allure:report
mvn allure:serve

|

gradle test
gradle allureReport
gradle allureServe

| 运行测试用例,生成目标文件夹中的报告

使用本地 Web 服务器打开 HTML 报告(如图 9-10 所示) |

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0910.png

图 9-10. 使用 Allure 生成并本地展示的测试报告

行为驱动开发

如第一章介绍的,行为驱动开发(BDD)是一种软件方法论,促进使用高级用户场景开发和测试软件系统。不同的工具实现了 BDD 方法论。其中最流行的之一是Cucumber。Cucumber 根据用Gherkin语言编写的用户故事执行测试,这种语言是一种基于自然语言(如英语和其他语言)的人类可读标记。Gherkin 旨在供非程序员(例如客户或最终用户)使用,其主要关键字如下(详见Gherkin 用户手册获取更多信息):

特性

被测试的软件功能的高级描述。

场景

描述业务规则的具体测试。场景描述了 Gherkin 行话中称为步骤的不同信息,例如:

给定

前提条件和初始状态

用户动作

并且

附加用户操作

然后

预期结果

示例 9-13 展示了一个包含两个场景(登录成功和失败)的 Gherkin 特性。

示例 9-13. 用于登录实践站点的 Gherkin 场景
Feature: Login in practice site <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

  Scenario: Successful login <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    Given I use "Chrome" <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    When I navigate to
        "https://bonigarcia.dev/selenium-webdriver-java/login-form.html" <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    And I log in with the username "user" and password "user" <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
    And I click Submit
    Then I should see the message "Login successful" <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>

  Scenario: Failure login <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>
    Given I use "Chrome"
    When I navigate to
        "https://bonigarcia.dev/selenium-webdriver-java/login-form.html"
    And I log in with the username "bad-user" and password "bad-password"
    And I click Submit
    Then I should see the message "Invalid credentials"

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO14-1

功能描述

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO14-2

第一个场景(登录成功)

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO14-3

要使用的浏览器

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO14-4

网页 URL

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO14-5

一组动作(输入凭据并点击提交按钮)

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO14-6

预期的消息

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO14-7

第二个场景(登录失败)

要将 Gherkin 场景作为测试用例运行,我们首先必须创建相应的步骤定义。步骤定义是使用在场景中指定信息的glue code来执行 SUT。在 Java 中,我们使用注解(如@Given@Then@When@And)来装饰实现每个步骤的方法。这些注解包含一个字符串值,用于映射每个步骤定义和参数。Example 9-14 展示了用于 Gherkin 场景的步骤定义,该场景在 Example 9-13 中定义。我们使用 Selenium WebDriver API 来执行导航、网页元素交互等所需的操作。

Example 9-14. 使用 Cucumber 登录练习站点的步骤
public class LoginSteps {

    private WebDriver driver;

    @Given("I use {string}") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    public void iUse(String browser) {
        driver = WebDriverManager.getInstance(browser).create();
    }

    @When("I navigate to {string}") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    public void iNavigateTo(String url) {
        driver.get(url);
    }

    @And("I log in with the username {string} and password {string}") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    public void iLogin(String username, String password) {
        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);

    }

    @And("I click Submit") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    public void iPressEnter() {
        driver.findElement(By.cssSelector("button")).click();
    }

    @Then("I should see the message {string}") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
    public void iShouldSee(String result) {
        try {
            String bodyText = driver.findElement(By.tagName("body")).getText();
            assertThat(bodyText).contains(result);
        } finally {
            driver.quit();
        }
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO15-1

我们使用第一步创建一个WebDriver实例。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO15-2

我们打开 URL。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO15-3

我们输入凭据。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO15-4

我们点击提交按钮。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO15-5

我们验证页面上是否有预期的消息。

最后,我们需要将步骤定义作为测试用例运行。通常情况下,我们使用单元测试框架创建该测试。这个测试在与 Cucumber 集成时依赖于单元测试框架,换句话说,不同的是在 JUnit 4 中(见 Example 9-15),JUnit 5 中(见 Example 9-16)和 TestNG 中(见 Example 9-17)。生成的测试以通常的方式执行(即使用 Shell 或 IDE)。

注意

Selenium-Jupiter 在与 Cucumber 集成时并未提供任何额外功能,因此在示例存储库中的 Selenium-Jupiter 项目中,使用的是默认的 JUnit 5 过程。

Example 9-15. 使用 JUnit 4 进行 Cucumber 测试
@RunWith(Cucumber.class) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
@CucumberOptions(features = "classpath:io/github/bonigarcia", glue = {
        "io.github.bonigarcia" }) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
public class CucumberTest {

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO16-1

我们使用 Cucumber 运行器来执行步骤定义作为测试用例。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO16-2

我们指定了 Gherkin 场景的位置。在这种情况下,特性位于项目类路径中的文件夹io/github/bonigarcia(具体而言,在src/test/resources文件夹中)。此注解还指定了要搜索 glue code(即步骤定义)的初始包。

Example 9-16. 使用 JUnit 5 进行 Cucumber 测试
@Suite <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
@IncludeEngines("cucumber") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
@SelectClasspathResource("io/github/bonigarcia") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "io.github.bonigarcia") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
public class CucumberTest {

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO17-1

我们需要使用 JUnit 5 套件模块来运行 Cucumber 测试。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO17-2

我们在 JUnit 平台中包含了 Cucumber 引擎。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO17-3

我们指定了项目类路径中特性文件的路径。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO17-4

我们将初始包设定为搜索粘合代码。

例 9-17。使用 TestNG 进行 Cucumber 测试
@CucumberOptions(features = "classpath:io/github/bonigarcia", glue = {
        "io.github.bonigarcia" }) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
public class CucumberTest extends AbstractTestNGCucumberTests { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO18-1

我们指定 Gherkin 场景的位置和搜索粘合代码的包的位置。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO18-2

我们扩展 TestNG 父类以运行 Cucumber 测试。

Web 框架

Web 框架是旨在支持 Web 应用程序和服务开发的软件框架。Java 语言中最流行的框架之一是 Spring Framework。Spring 是一个用于构建 Java 应用程序(包括企业 Web 应用程序和服务)的开源框架。Spring 的核心技术被称为控制反转(IoC),它是一种在使用这些对象的类之外创建实例的过程。Spring 术语中称这些对象为beancomponent,它们稍后按需作为依赖项由 Spring IoC 容器注入。

以下示例展示了用 Spring-Boot 创建的本地 Web 应用程序的基本测试,Spring-Boot 是 Spring 系列的一个子项目,通过惯例优于配置和自动发现功能简化了基于 Spring 的应用程序的开发。此外,Spring-Boot 提供了一个嵌入式 Web 服务器,以便轻松开发 Web 应用程序。在这种项目中与 Selenium WebDriver 的集成有助于通过在每个测试案例中自动部署在嵌入式 Web 服务器中来简化 Spring 基础 Web 应用程序的测试过程。

用于集成 Spring-Boot 与 Selenium WebDriver 和本书中使用的单元测试框架的代码是不同的。示例 9-18 显示了一个集成 Spring-Boot 和 JUnit 4 的测试。示例 9-19 展示了使用 TestNG 时的差异,示例 9-20 说明了如何使用 JUnit 5,最后,示例 9-21 展示了一个基于 JUnit 5 和 Selenium-Jupiter 的 Spring 测试。

例 9-18。使用 Spring-Boot 和 JUnit 4 进行测试
@RunWith(SpringRunner.class) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
public class SpringBootJUnit4Test {

    private WebDriver driver;

    @LocalServerPort <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    protected int serverPort;

    @Before
    public void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @After
    public void teardown() {
        driver.quit();
    }

    @Test
    public void testSpringBoot() {
        driver.get("http://localhost:" + serverPort);
        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText)
                .contains("This is a local site served by Spring-Boot");
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO19-1

我们在 JUnit 4 中使用 Spring 运行器。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO19-2

我们使用 Spring-Boot 的测试注解来定义 Spring-Boot 类名。此外,我们指定 Web 应用程序使用随机可用端口部署。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO19-3

我们将 Web 应用程序端口注入为类属性。

例 9-19。使用 Spring-Boot 和 TestNG 进行测试
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
public class SpringBootNGTest extends AbstractTestNGSpringContextTests { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>

    // Same logic as the previous test 
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO20-1

我们在 JUnit 4 中也同样使用 @SpringBootTest 注解。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO20-2

我们扩展 TestNG 父类以使用 Spring 上下文运行此测试。

例 9-20。使用 Spring-Boot 和 JUnit 5 进行测试
@ExtendWith(SpringExtension.class) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
class SpringBootJupiterTest {

    // Same logic as the previous test 
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO21-1

我们使用 JUnit 5 的 Spring 扩展将 Spring 上下文集成到 Jupiter 测试中。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO21-2

我们使用 Spring-Boot 来启动我们的 Spring 应用程序上下文,就像之前的例子中一样。

示例 9-21. 使用 Spring-Boot 和 JUnit 5 加上 Selenium-Jupiter 进行测试
@ExtendWith({ SeleniumJupiter.class, SpringExtension.class }) <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SpringBootSelJupTest {

    @LocalServerPort
    protected int serverPort;

    @Test
    void testSpringBoot(ChromeDriver driver) { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        driver.get("http://localhost:" + serverPort);
        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText)
                .contains("This is a local site served by Spring-Boot");
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO22-1

在 Selenium-Jupiter 的情况下,我们使用了两个 JUnit 5 扩展(用于 Spring 和 Selenium WebDriver)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_third_party_integrations_CO22-2

如同在 Selenium-Jupiter 中一贯的做法,我们使用了 JUnit 5 的参数解析机制来声明在本测试中使用的 WebDriver 实例的类型。

总结与展望

本章为使用 Selenium WebDriver 进行端到端测试的不同技术(如工具、库和框架)集成提供了实际概述。首先,我们使用 Awaitility(处理异步操作的库)等待 Selenium WebDriver 下载文件。执行相同用例(即下载文件)的另一种库是 Apache HttpClient。然后,我们使用 BrowserMob 代理拦截 Selenium WebDriver 测试交换的 HTTP 流量。接下来的技术组集中于使用 Selenium WebDriver 进行非功能测试:BrowserMob(为性能测试创建 JMeter 测试计划)、OWASP ZAP(安全测试)和 Axe(可访问性测试)。然后,我们使用 Selenide 提供的流畅 API 和 Java Faker 为 Selenium WebDriver 测试创建虚拟测试数据,并使用 Extent Reports 和 Allure 生成丰富的测试报告。最后,我们了解了如何将 Cucumber(BDD 框架)和 Spring(Java 和 Web 框架)与 Selenium WebDriver 集成。

下一章通过介绍 Selenium WebDriver 的补充框架来总结本书,即用于测试 REST 服务的 REST Assured 和用于测试移动应用程序的 Appium。最后,本章介绍了几个流行的 Selenium WebDriver 浏览器自动化空间的替代品:Cypress、WebDriverIO、TestCafe、Puppeteer 和 Playwright。

第十章:超越 Selenium

本章通过介绍几种与 Selenium 互补的技术来结束本书。首先,我们分析了移动应用程序的基础并介绍了 Appium,这是一个流行的用于移动测试的测试框架。然后,您将学习如何使用名为 REST Assured 的开源 Java 库测试 REST(表现状态转移)服务。最后,您将介绍用于实施 Web 应用程序端到端测试的 Selenium WebDriver 的替代工具,即:Cypress、WebDriverIO、TestCafe、Puppeteer 和 Playwright。

移动应用程序

移动应用程序(通常称为移动应用程序或简称为应用程序)是专为运行在移动设备上设计的软件应用程序,例如智能手机、平板电脑或可穿戴设备。移动设备的两个主要操作系统为:

安卓

基于修改版 Linux 的开源(Apache 2.0 许可)移动操作系统。最初由名为 Android 的初创公司开发,于 2005 年被 Google 收购。

iOS

由苹果专门为其硬件(例如 iPhone、iPad 或 Watch)创建的专有移动操作系统。

对移动应用程序进行分类的一种常见方式如下:

本地应用程序

针对特定移动操作系统(例如 Android 或 iOS)开发的移动应用程序。

基于 Web 的应用程序

在移动浏览器(例如 Chrome、Safari 或 Firefox Mobile)中渲染的 Web 应用程序。这些应用程序通常设计为 响应式(即可适应不同的屏幕大小和视口)。

混合应用程序

使用客户端 Web 标准(即 HTML、CSS 和 JavaScript)开发的移动应用程序,并使用称为 webview 的本地容器部署到移动设备上。支持混合应用程序开发的框架示例包括 IonicReact NativeFlutter

渐进式 Web 应用程序(PWA)

使用现代 Web 标准 API 构建的 Web 应用程序(用于可安装性、响应性等),旨在在多个平台上运行,包括桌面和移动设备。

移动测试

测试是移动应用程序开发中的重要流程。移动测试涉及诸如硬件兼容性、网络连接性或操作系统特定性等不同挑战。执行移动测试的不同方法包括:

使用桌面浏览器进行移动仿真

我们可以使用 Selenium WebDriver 进行此类移动测试。为此,您可以使用特定于浏览器的功能(如“设备仿真”中所述),或者使用基于 Chromium 的浏览器中的 CDP(如“设备仿真”中所述)。

使用虚拟设备

有两种类型的虚拟移动设备:

模拟器

桌面应用程序,虚拟化移动设备的所有方面,包括硬件和操作系统。

模拟器

模仿移动操作系统某些功能的桌面应用程序。它们主要用于 iOS,因为 Android 设备很容易模拟。

使用真实设备

使用实际设备及其在真实条件下的本机 Android 或 iOS API。

Appium

Appium 是一个用于移动应用的开源测试自动化框架。Appium 提供了一个跨平台的 API,允许在虚拟或真实设备上测试 iOS 和 Android 的本地、混合和移动 Web 应用程序。此外,Appium 还能够在 Windows 和 macOS 上对桌面应用程序进行自动化测试。

Appium 的故事始于 2011 年,当时 Dan Cuellar 创建了一个名为 iOSAuto 的用于 C# 开发的 iOS 应用程序自动化工具。他在 2012 年的伦敦 SeleniumConf 上遇到了 Selenium 的共同创始人 Jason Huggins。Jason 通过添加一个 Web 服务器,并使用基于 HTTP 的 WebDriver wire 协议,使 iOSAuto 兼容任何 Selenium WebDriver 客户端。他们将项目更名为 Appium(适用于应用程序的 Selenium)。2013 年 1 月,Sauce Labs 决定支持 Appium 并提供更多开发者力量。新团队使用 Node.js 重写了 Appium,因为它是服务器端的知名高效框架。

如 图 10-1 所示,Appium 遵循客户端-服务器架构。Appium 是一个 Web 服务器,暴露了一个 REST API,用于在移动或桌面应用上执行自动化会话。因此,Appium 服务器接收来自客户端的传入请求,在目标设备/应用上执行这些命令,并以 HTTP 响应形式返回代表命令执行结果的信息。Appium 客户端库使用移动 JSON Wire Protocol(作为原始 WebDriver 协议的官方扩展草案)。Appium 服务器及其客户端还使用 W3C WebDriver 规范。Appium 项目和社区维护了不同的 Appium 客户端库。表 10-1 总结了这些官方维护的和社区维护的库。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1001.png

图 10-1. Appium 架构

表格 10-1. Appium 客户端库

名称语言许可证维护者网站
Appium java-clientJavaApache 2.0Appium 团队https://github.com/appium/java-client
Appium ruby_libRubyApache 2.0Appium 团队https://github.com/appium/ruby_lib
Appium Python 客户端PythonApache 2.0Appium 团队https://github.com/appium/python-client
appium-dotnet-driverC#Apache 2.0Appium 团队https://github.com/appium/appium-dotnet-driver
WebdriverIOJavaScript(Node.js)MITWebdriverIO 团队https://webdriver.io
web2driverJavaScript(浏览器)Apache 2.0HeadSpinhttps://github.com/projectxyzio/web2driver
RobotFramework 的 Appium 库PythonApache 2.0Serhat Bolsuhttps://github.com/serhatbolsu/robotframework-appiumlibrary

在 Appium 中,对特定平台的自动化支持是由 Appium 行话中称为 driver 的组件提供的。这些驱动程序在版本 1 中与 Appium 服务器紧密耦合。然而,在本文撰写时的最新版本 Appium 2 中,这些驱动程序与 Appium 服务器分开(参见 图 10-1)并单独安装。

Appium 驱动程序表格 10-2

名称目标描述存储库
XCUITest 驱动程序iOS 和 tvOS 应用程序利用 Apple 的 XCUITest 库实现自动化https://github.com/appium/appium-xcuitest-driver
Espresso 驱动程序Android 应用程序通过 Espresso(Android 的测试框架)实现自动化https://github.com/appium/appium-espresso-driver
UiAutomator2 驱动程序Android 应用程序利用 Google UiAutomator2 技术在 Android 设备或模拟器上实现自动化https://github.com/appium/appium-uiautomator2-driver
Windows 驱动程序Windows 桌面应用程序使用 WinAppDriver,Windows 桌面应用程序的 WebDriver 服务器https://github.com/appium/appium-windows-driver
Mac 驱动程序macOS 桌面应用程序使用 Apple 的 XCTest 框架自动化 macOS 应用程序https://github.com/appium/appium-mac2-driver

一个基本的 Appium 测试

本节介绍了使用 Appium 服务器 2 和 Appium Java 客户端的基本测试用例。为了简单起见,我使用了 UiAutomator2 驱动程序和一个模拟的 Android 设备。SUT 将是一个 Web 应用程序,具体来说,是本书中始终使用的练习站点。对 Appium Java 客户端的调用嵌入在其他示例中使用的不同单元测试框架中(即 JUnit 4 和 5、TestNG 和 Selenium-Jupiter)。如往常一样,您可以在示例存储库中找到完整的源代码。运行此测试的要求如下:

  1. 安装 Appium 服务器 2。

  2. 安装 UiAutomator2 驱动程序。

  3. 安装 Android SDK(即 Android 的官方软件开发工具包)。您可以通过在计算机上安装Android Studio来轻松安装此 SDK。

  4. 使用 Android Studio 中的 AVD 管理器创建 Android 虚拟设备(AVD)。图 10-2 显示了打开此工具的菜单选项,图 10-3 显示了测试中使用的虚拟设备(使用 Android API 级别 30 的 Nexus 5 手机)。

  5. 启动虚拟设备和 Appium 服务器。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1002.png

图 10-2. Android Studio

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1003.png

图 10-3. AVD 管理器

如前所述,Appium 服务器是一个 Node.js 应用程序。因此,您需要在系统中安装 Node.js 才能运行 Appium。以下命令概述了如何安装 Appium 服务器 2 和 UiAutomator2 驱动程序,以及如何启动 Appium 服务器:

npm install -g appium@next <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
appium driver install uiautomator2 <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
appium --allow-insecure chromedriver_autodownload <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO1-1

我们使用 npm(Node.js 的默认软件包管理器)安装 Appium 2。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO1-2

我们使用 Appium 安装 UiAutomator2 驱动程序。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO1-3

我们启动 Appium 服务器(默认情况下,它监听端口 4723)。我们包含一个标志,让 Appium 管理所需的浏览器驱动程序(例如,chromedriver)以自动化 Web 应用程序(就像在 Selenium WebDriver 中一样)。

示例 10-1 展示了使用 Appium Java 客户端进行完整测试的示例。正如你所见,这个测试与本书中解释的常规 Selenium WebDriver 测试非常相似。在这种情况下的主要区别是,我们使用了 AppiumDriver 的一个实例,这是 Appium Java 客户端提供的一个类。该类扩展了 Selenium WebDriver API 的 RemoteWebDriver 类。因此,我们可以利用 Selenium WebDriver API 来测试移动设备上的 Web 应用程序。图 10-4 展示了在此测试期间的模拟移动设备(Nexus 5)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1004.png

图 10-4. Android 设备
示例 10-1. 使用 Appium Java 客户端进行测试
class AppiumJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() throws MalformedURLException {
        URL appiumServerUrl = new URL("http://localhost:4723"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
        assumeThat(isOnline(new URL(appiumServerUrl, "/status"))).isTrue(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>

        ChromeOptions options = new ChromeOptions(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
        options.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
        options.setCapability(MobileCapabilityType.DEVICE_NAME,
                "Nexus 5 API 30"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
        options.setCapability(MobileCapabilityType.AUTOMATION_NAME,
                "UiAutomator2"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>

        driver = new AppiumDriver(appiumServerUrl, options); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>
    }

    @AfterEach
    void teardown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void testAppium() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png>
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-1

我们指定了 Appium 服务器 URL。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-2

我们假设使用 Appium 服务器 URL 的端点 /status。如果此 URL 不在线,则跳过测试。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-3

我们使用 Chrome 选项来指定功能。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-4

使用 Appium 时的第一个强制性功能是平台名称(在本例中为 Android)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-5

以下是设备名称。此名称必须与 AVD 管理器中定义的名称匹配(参见 图 10-3)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-6

最后一个强制性功能是驱动程序名称(在本例中为 UiAutomator2)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-7

我们使用 Appium 服务器 URL 和浏览器选项创建了一个 AppiumDriver 的实例。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO2-8

我们像往常一样使用 driver 对象来执行 SUT。

REST 服务

REST(表现状态转移)是一种用于设计分布式服务的架构风格。Roy Fielding 在他的 2000 年博士论文中创造了这个术语。REST 是在 HTTP 协议之上创建 Web 服务的流行方式。

REST 遵循客户端-服务器架构。服务器处理一组资源,监听客户端发出的传入请求。这些资源是 REST 服务的构建块,并定义了传输的信息类型。每个资源都有唯一的标识。在 HTTP 中,我们使用 URL(也称为端点)来访问单个资源。每个资源都有一个表示形式,即资源当前状态的机器可读说明。我们使用数据交换格式来定义表示,如 JSON、YAML 或 XML。REST 服务公开一组对资源执行的操作,例如 CRUD(创建、检索、更新和删除)。我们可以使用 HTTP 方法(也称为动词)来映射 REST 操作。表 10-3 总结了用于创建 REST 服务的 HTTP 方法。最后,我们可以使用 HTTP 状态码来识别与 REST 操作相关联的响应。表 10-4 总结了在 REST 中使用的典型 HTTP 状态码。图 10-5 显示了一个示例 REST 服务的请求和响应序列,该服务使用了不同的 HTTP 方法和响应代码。

表 10-3. 用于创建 REST 服务的 HTTP 方法

HTTP 方法描述
GET读取资源
POST将新资源发送到服务器
PUT更新资源
DELETE删除资源
PATCH部分更新资源
HEAD询问给定资源是否存在,而不返回其任何表示形式
OPTIONS检索给定资源的可用动词

表 10-4. 用于创建 REST 服务的 HTTP 状态码

状态码描述
200 OK请求成功,返回请求的内容(例如在 GET 请求中)。
201 Created已创建资源(例如在 POSTPUT 请求中)。
204 No content操作成功,但未返回内容。此状态码在不需要响应主体的操作中很有用(例如在 DELETE 请求中)。
301 Moved permanently资源已移动到另一个位置。
400 Bad request请求存在问题(例如,缺少参数)。
401 Unauthorized请求的资源对于发出请求的用户不可访问。
403 Forbidden资源不可访问,但与 401 不同,身份验证不会影响响应。
404 Not found提供的端点未标识任何资源。
405 Method not allowed不允许使用的动词(例如在只读资源中使用 PUT)。
500 Internal server error服务器端发生一般性意外情况。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1005.png

Figure 10-5. REST 服务示例

REST Assured

REST API 是无处不在的。通常情况下,强烈建议为验证这些服务实现自动化测试,例如使用 REST Assured。REST Assured 是一个流行的开源(Apache 2.0 许可)Java 库,用于测试 REST 服务。它提供了一个流畅的 API 用于测试和验证 REST 服务。使用 REST Assured 创建可读断言的便捷方法是生成 POJOs(Plain Old Java Objects),将 REST 响应(例如 JSON 格式)映射为 Java 类。然后,我们可以使用像 AssertJ 这样的库,通过这些 POJOs 的访问器(即 getter 方法)来验证预期条件。示例 Example 10-2 展示了使用这种方法的测试用例。示例 Example 10-3 包含了此测试中使用的 POJO。

Example 10-2. 使用 REST Assured 进行测试
class RestJupiterTest {

    @Test
    void testRest() {
        HttpBinGet get = RestAssured.get("https://httpbin.org/get").then()
                .assertThat().statusCode(200).extract().as(HttpBinGet.class); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

        assertThat(get.getHeaders()).containsKey("Accept-Encoding"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        assertThat(get.getOrigin()).isNotBlank(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO3-1

我们使用 REST Assured 使用 GET HTTP 方法请求在线公共 REST 服务。此行还验证了预期的状态码(200),并将响应有效负载(以 JSON 形式)转换为 Java 类(如示例 Example 10-3 所示)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO3-2

我们断言头部列表(使用相应的访问器方法)包含给定的键值对。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO3-3

我们断言原点不为空。

Example 10-3. 用于测试 REST 服务的 POJO 类
public class HttpBinGet {

    public Map<String, String> args; <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    public Map<String, String> headers;
    public String origin;
    public String url;

    // Getters and setters <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO4-1

此 POJO 定义了一组属性,用于将 JSON 响应有效载荷映射到 Java 中。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO4-2

我们为每个类属性定义访问器(getter)和修改器(setter)。现代 IDE 允许从类属性自动生成这些方法。

Selenium 的替代方案

Selenium 目前是实现端到端测试的领先技术。尽管如此,它并非唯一的可用选择。本节提供了其他框架和库的概述,这些框架和库同样允许为 Web 应用程序实现端到端测试。此外,以下小节还审视了每个这些替代方案的主要优缺点。在我看来,Selenium 仍然是端到端测试的参考解决方案,因为它是为促进 Web 标准(即 W3C WebDriver 和 WebDriver BiDi)而构建,以支持自动化过程,从而确保跨浏览器兼容性。

Cypress

Cypress 是一个 JavaScript 端到端自动化测试框架。如 Figure 10-6 所示,Cypress 架构包括一个 Node.js 进程以及在浏览器中执行的 Test Runner 工具。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1006.png

Figure 10-6. Cypress 架构

测试运行器是一个交互式的 Web 应用,它包含基于 Mocha(一个 JavaScript 单元测试框架)的测试和被测试的 Web 应用作为两个 iframe。测试代码和应用程序代码在同一个浏览器标签页中运行(即在同一个 JavaScript 循环中)。Node.js 进程使用 WebSocket 与测试运行器进行通信。最后,Node.js 进程是测试运行器和被测试的 Web 应用之间 HTTP 流量的代理。

Cypress 测试运行器是开源的,根据 MIT 许可证授权。Cypress 团队还提供商业支持以获取高级功能。其中之一是 Cypress Dashboard,一个云管理的 Web 应用,用于跟踪在测试运行器中执行的测试。表格 10-5 总结了 Cypress 的一些最相关的优缺点。

表格 10-5. Cypress 的优缺点

优点缺点

|

  • 自动等待和快速执行,因为测试和应用程序在同一个浏览器中运行。

  • 实时重载(测试运行器自动跟踪测试中的变化)

|

  • 仅支持一些浏览器:Firefox 和基于 Chromium 的浏览器(包括 Chrome、Edge 和 Electron),但不支持其他浏览器如 Safari 或 Opera。

  • 由于应用程序在浏览器 iframe 中执行,某些操作是不允许的(例如驱动不同的浏览器或多个标签页)。

|

以下命令展示如何在本地安装 Cypress 并执行它。执行完这些命令后,你会看到 Cypress 的 GUI(类似于 图 10-7)。你可以使用这个 GUI 来执行 Cypress 的测试。

npm install cypress <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
npx cypress open <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO5-1

我们可以使用 npm(Node.js 中的默认包管理器)来安装 Cypress。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO5-2

我们可以使用 npx(一个 npm 包运行器)来运行 Cypress 进程。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1007.png

图 10-7. Cypress GUI

在 Cypress GUI 中,默认情况下可以在 1-getting-started2-advanced-examples 文件夹中找到入门测试示例。此外,我们可以使用 New Spec File 按钮创建新的测试。例如,示例 10-4 展示了使用 Cypress 的全新基础测试(即 Cypress 中的 hello world)。这个测试被称为 hello-world-cypress.spec.js(默认在 Mocha 测试中使用 .spec.js 扩展名),并存储在 Cypress 安装路径的 cypress/integration 中。图 10-8 展示了在执行此测试时的 Cypress 测试运行器的截图。

示例 10-4. 使用 Cypress 进行的 Hello world 测试
describe('Hello World with Cypress', () => {
   it('Login in the practice site', () => {
      cy.visit('https://bonigarcia.dev/selenium-webdriver-java/login-form.html') <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

      cy.get('#username').type('user') <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
      cy.get('#password').type('user')
      cy.contains('Submit').click() <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
      cy.contains('Login successful') <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>

      cy.screenshot("hello-world-cypress") <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
  })
})

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO6-1

我们在实践站点中打开登录页面。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO6-2

我们输入正确的凭据(用户名和密码)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO6-3

我们点击提交按钮。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO6-4

我们验证结果页面包含成功登录的消息。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO6-5

我们进行浏览器截图。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1008.png

图 10-8. Cypress 测试运行器

WebDriverIO

WebDriverIO是用于 Web 和移动应用程序的自动化测试框架。它完全开源(MIT 许可证),基于诸如 W3C WebDriver 协议之类的 Web 标准。图 10-9 展示了它的架构。WebDriverIO 是用 JavaScript 编写的,运行在 Node.js 上。它使用几个服务来支持自动化:chromedriver(用于本地 Chrome 浏览器)、Selenium Server(用于其他浏览器)、Appium Server(用于移动设备)、Chrome DevTools(用于使用 CDP 的基于 Chromium 的本地浏览器)以及云提供商(如 Sauce Labs、BrowserStack 或 TestingBot)。这些服务操作相应的浏览器和移动设备。表 10-6 总结了 WebDriverIO 的一些优缺点。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1009.png

图 10-9. WebDriverIO 架构

表格 10-6. WebDriverIO 的优缺点

优点缺点

|

  • 支持多个浏览器和移动设备

  • 适用于不同的测试和报告框架

  • 基于 Web 标准

|

  • 仅支持 JavaScript

|

下面的npm命令在本地安装 WebDriverIO。此安装程序显示一个命令行向导,询问几个选项,例如服务(chomedriver、Selenium Server、Appium Server、CDP 或云提供商)、测试框架(Mocha、Jasmine 或 Cucumber)或报告工具(JUnit 或 Allure 等):

npm init wdio .

当前命令完成后,我们可以创建自定义测试。例如,示例 10-5 展示了一个基本的使用 Mocha 的 WebDriverIO 测试。我们将此测试放在项目脚手架的test文件夹下,并通过以下命令运行它:

npx wdio run ./wdio.conf.js
示例 10-5. 使用 WebDriverIO 的 Hello world 测试
describe('Hello World with WebDriverIO', () => {
   it('Login in the practice site', async () => {
      await browser.url(
            `https://bonigarcia.dev/selenium-webdriver-java/login-form.html`);

      await $('#username').setValue('user');
      await $('#password').setValue('user');
      await $('button[type="submit"]').click();
      await expect($('#success')).toHaveTextContaining('Login successful');
      await browser.saveScreenshot('hello-world-webdriverio.png');
    });
});

TestCafe

TestCafe是一个开源的跨浏览器自动化测试工具(MIT 许可证)。TestCafe 的核心思想是避免使用外部驱动程序来支持自动化过程,而是使用混合的客户端-服务器架构模拟用户操作(参见图 10-10)。服务器端采用 Node.js 实现,并包含一个代理,用于拦截与测试中的 Web 应用程序的 HTTP 流量。TestCafe 测试也是作为 Node.js 脚本编写并在服务器端执行的。在浏览器中测试页上运行模拟用户活动的自动化脚本在客户端上运行。表 10-7 总结了 TestCafe 的一些优势和局限性。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1010.png

图 10-10. TestCafe 架构

表格 10-7. TestCafe 优缺点

优点缺点

|

  • 完全跨浏览器支持(由于 TestCafe 仅启动浏览器,因此可以自动化任何浏览器)

|

  • 仅支持 JavaScript 和 TypeScript

  • 由于 JavaScript 的限制,某些操作无法自动化

|

我们可以使用 npm 轻松安装 TestCafe。然后,我们可以使用 TestCafe CLI 工具从命令行运行 TestCafe 脚本。以下代码段说明了如何操作:

npm install -g testcafe <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
testcafe chrome helloworld-testcafe.js <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO7-1

我们全局安装 TestCafe。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO7-2

我们启动一个使用 Chrome 作为浏览器的 TestCafe 基本脚本(示例 10-6)。

示例 10-6. 使用 TestCafe 的 Hello world 测试
import { Selector } from 'testcafe';

fixture`Hello World with TestCafe`
   .page`https://bonigarcia.dev/selenium-webdriver-java/login-form.html`;
test('Login in the practice site', async t => {
   await t
      .typeText('#username', 'user')
      .typeText('#password', 'user')
      .click('button[type="submit"]')
      .expect(Selector('#success').innerText).eql('Login successful')
      .takeScreenshot();
});

Puppeteer

Puppeteer 是一个开源(MIT 许可)的 Node.js 库,提供了一个高级 API 来通过 DevTools 协议控制基于 Chromium 的浏览器。Puppeteer 由 Google 的 Chrome DevTools 团队维护。图 10-11 展示了 Puppeteer 的架构。表 10-8 展示了 Puppeteer 的主要优缺点。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1011.png

图 10-11. Puppeteer 架构

表格 10-8. Puppeteer 的优缺点

优点缺点

|

  • 快速执行和全面的自动化能力(由于使用 CDP 与浏览器直接通信)

|

  • 仅支持有限的跨浏览器(仅基于 Chromium 的浏览器,尽管在撰写时有试验性的 Firefox 支持)

  • 仅支持 JavaScript 和 TypeScript

|

我们可以使用 npm 安装 Puppeteer。然后,我们需要使用 Node.js 运行 Puppeteer 测试(例如,示例 10-7)。以下代码段显示了这些命令:

npm install puppeteer
node helloword-puppeteer.js
示例 10-7. 使用 Puppeteer 的 Hello world 测试
const puppeteer = require('puppeteer');

(async () => {
   const browser = await puppeteer.launch(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
   const page = await browser.newPage();

   await page.goto('https://bonigarcia.dev/selenium-webdriver-java/login-form.html');
   await page.type('#username', 'user');
   await page.type('#password', 'user');
   await page.click('button[type="submit"]');
   await page.waitForXPath('//*[contains(text(), "Login successful")]');
   await page.screenshot({ path: 'helloword-puppeteer.png' });

   await browser.close();
})();

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_beyond_selenium_CO8-1

Puppeteer 默认以无头模式运行浏览器。可以通过将此语句更改为以下内容来配置为使用非无头浏览器:

const browser = await puppeteer.launch({ headless: false });

Playwright

Playwright 是由微软支持的开源(Apache 2.0 许可)的浏览器自动化库。Playwright 最初是一个 Node.js 库。除了 JavaScript,它现在还支持其他编程语言,包括 Python、Java 和 .NET C#。

Playwright 支持三种类型的 Web 引擎:Chromium、Firefox 和 WebKit(即 Safari 使用的 Web 浏览器引擎)。支持这些引擎的想法是它们涵盖了大部分的浏览器市场。因此,Playwright 团队维护了这些浏览器的补丁版本,以公开必要的功能来实现自动化。这些补丁版本提供了一个事件驱动的架构,以访问不同的内部浏览器进程(例如,渲染、网络、浏览器或服务工作进程)。图 10-12 说明了这种架构。表 10-9 包含了 Playwright 的一些最重要的优缺点。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_1012.png

图 10-12. Playwright 架构

表格 10-9. Playwright 的优缺点

优点缺点

|

  • 自动等待元素准备就绪

  • 多语言 API

  • 通过记录浏览器中的用户操作来提供测试生成器

  • 允许浏览器会话记录

  • 拦截网络流量以进行存根和模拟

|

  • 使用修补过的浏览器版本而不是实际发布版本

|

要使用 Playwright,我们首先需要安装修补过的浏览器二进制文件。我们可以使用 npm 来实现这一目标。以下命令将下载适用于运行此命令的操作系统(支持 Windows、Linux 和 macOS)的 Chromium、Firefox 和 WebKit 的正确浏览器二进制文件:

npm install -D playwright

然后,我们可以使用一个支持的 API 实现 Playwright 脚本。例如,当使用 JavaScript API 时,我们可以使用第三方测试运行器(例如 Jest、Jasmine、Mocha 等),或者使用 Playwright 测试(即由 Playwright 团队提供的测试运行器)。要使用后者,我们需要按照以下方式安装它:

npm install -D @playwright/test

示例 10-8 包含一个基本的 Playwright JavaScript 测试,可以使用 Playwright 运行器执行。这个命令假设这个测试(称为 helloworld-playwright.spec.mjs)位于 tests 目录下。我们可以像下面的片段所示调用 Playwright 运行器来运行这个测试。这个命令默认以无头模式运行 Playwright 测试。要在非无头模式下运行浏览器,您需要在命令的末尾包含 --headed 标志:

npx playwright test
示例 10-8. 使用 Playwright 进行的 Hello world 测试
const { test, expect } = require('@playwright/test');

test('Hello World with Playwright', async ({ page }) => {
   await page.goto('https://bonigarcia.dev/selenium-webdriver-java/login-form.html');

   await page.type('#username', 'user');
   await page.type('#password', 'user');
   await page.click('button[type="submit"]');
   await expect(page.locator('#success')).toHaveText('Login successful');

   await page.screenshot({ path: 'helloworld-playwright.png' });
});

摘要和最终说明

Web 开发是一个涉及许多不同技术的异构学科,例如客户端、服务器端或者与外部服务集成等。因此,本章介绍了两种对 Selenium 有帮助的补充技术,用于测试 Web 应用程序:Appium(用于移动应用程序的开源测试自动化框架)和 REST Assured(用于测试 REST 服务的开源 Java 库)。您还学习了用于实现 Web 应用程序端到端测试的替代工具的基础知识,包括 Cypress、WebDriverIO、TestCafe、Puppeteer 和 Playwright。虽然这些替代方案与 Selenium 相比具有显著的优势(例如自动等待),但在我看来,由于 Selenium 是基于 Web 标准构建的(例如 W3C WebDriver 和 WebDriver BiDi),因此提供了更全面的自动化模型。此外,Selenium 项目还积极参与这些规范的开发。

本章节总结了你通过使用 Selenium 开发端到端测试的旅程。下一步是将本书中提供的所有知识付诸实践到你的项目中。这样,你可以为你的团队、项目、公司等构建定制的自动化框架。你需要做出许多决策,比如项目设置(如 Maven、Gradle)、单元测试框架(如 JUnit、TestNG)、浏览器基础设施(如 Docker、云服务提供商)以及与第三方工具的集成。为了应对所有这些复杂性,作为最后一句话,我建议你在本书中提供的示例中进行实践。换句话说:克隆仓库,运行测试,并编辑代码以满足你的需求。书籍发布后,我会维护 GitHub 仓库。还要记住:这是一个开源软件项目,所以如果你想做出贡献,随时可以提交 pull request 来改进它。

附录 A. Selenium 4 新功能简介

本附录总结了 Selenium 4 中可用的新功能。内容的目的有两个:首先,它列举了 Selenium 套件核心组件(即 WebDriver、Driver 和 IDE)中的新功能,并提供了解释每个方面的书籍章节的链接。此外,本附录还描述了 Selenium 4 中随 Selenium 3 到 4 迁移时变化的其他方面,例如文档和治理。第二个目标是识别从 Selenium 3 到 4 迁移时废弃部分及相应的新功能。

Selenium WebDriver

Selenium WebDriver 4.0.0 的第一个稳定版本于 2021 年 10 月 13 日发布。表 A-1 总结了此版本与上一个稳定版本(即 Selenium WebDriver 3.141.59)相比最重要的新功能。

表 A-1. Selenium WebDriver 4 中的新功能

功能描述章节部分
完全采纳 W3C WebDriverSelenium WebDriver API 和驱动程序之间的标准通信协议第一章“Selenium WebDriver”
相对定位器基于其他网页元素的接近程度的定位策略第三章“相对定位器”
固定脚本将一段 JavaScript 附加到 WebDriver 会话第四章“固定脚本”
元素截图捕获网页元素的截图(而不是整个页面)第四章“WebElement 截图”
影子 DOM无缝访问影子树第四章“影子 DOM”
打开新窗口和标签页改进的导航到不同窗口和标签页的方式第四章“标签页和窗口”
装饰器用于实现事件监听器的 WebDriver 对象包装器第四章“事件监听器”
Chrome DevTools 协议在基于 Chromium 的浏览器中(如 Chrome 和 Edge)原生访问 DevTools第五章“Chrome DevTools 协议”
网络拦截针对后端请求进行存根处理并拦截网络流量第五章“网络拦截器”
基本身份验证简化的基本和摘要身份验证 API第五章“基本和摘要身份验证”
全页面截图捕获网页内容的完整截图第五章“全页面截图”
位置上下文模拟地理位置坐标第五章“位置上下文”
将网页打印为 PDF将网页保存为 PDF 文档第五章“打印页面”
WebDriver BiDi驱动程序和浏览器之间的双向通信第五章“WebDriver BiDi”

迁移指南

本节总结了将使用 Selenium WebDriver 3 的现有代码库迁移到版本 4 所需的更改。

定位器

用于查找元素的实用方法(FindsBy 接口)在 Selenium WebDriver 4 中已移除。表 A-2 比较了 Selenium WebDriver 中查找 Web 元素的旧 API 和新 API。有关此功能的更多详细信息,请参见 “定位 Web 元素”。

表 A-2. Selenium WebDriver 4 中网页元素位置的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

driver.findElementByTagName("tagName");
driver.findElementByLinkText("link");
driver
    .findElementByPartialLinkText("partLink");
driver.findElementByName("name");
driver.findElementById("id");
driver.findElementByClassName("class");
driver.findElementByCssSelector("css");
driver.findElementByXPath("xPath");

|

driver.findElement(By.tagName("tagName"));
driver.findElement(By.linkText("link"));
driver.findElement(By
    .partialLinkText("partLink"));
driver.findElement(By.name("name"));
driver.findElement(By.id("id"));
driver.findElement(By.className("class"));
driver.findElement(By.cssSelector("css"));
driver.findElement(By.xpath("xPath"));

|

用户手势

Actions 允许模拟复杂的用户手势(如拖放、悬停、鼠标移动等)。正如表 A-3 中展示的,这个类暴露的 API 在 Selenium WebDriver 4 中已经简化。关于这个类的更多信息和示例,请参见 “用户手势”。

表 A-3. Selenium WebDriver 4 中 Actions 类的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

actions.moveToElement(webElement).click();
actions.moveToElement(webElement).doubleClick();
actions.moveToElement(webElement).contextClick();
actions.moveToElement(webElement).clickAndHold();
actions.moveToElement(webElement).release();

|

actions.click(webElement);
actions.clickAndHold(webElement);
actions.contextClick(webElement);
actions.doubleClick(webElement);
actions.release(webElement);

|

等待和超时

指定超时的参数从 TimeUnit 切换到 Duration。表 A-4 中描述了这一变化。您可以在 “等待策略” 和 “超时” 中找到更多细节。

表 A-4. Selenium WebDriver 4 中等待和超时的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

new WebDriverWait(driver, 3).
    until(ExpectedConditions.
    elementToBeClickable(By.id("id")));

Wait<WebDriver> wait =
    new FluentWait<WebDriver>(driver)
    .withTimeout(30, TimeUnit.SECONDS)
    .pollingEvery(5, TimeUnit.SECONDS)
    .ignoring(NoSuchElementException.class);

driver.manage().timeouts()
    .implicitlyWait(10, TimeUnit.SECONDS);
driver.manage().timeouts()
    .setScriptTimeout(3, TimeUnit.MINUTES);
driver.manage().timeouts()
    .pageLoadTimeout(30, TimeUnit.SECONDS);

|

new WebDriverWait(driver,
    Duration.ofSeconds(3)).until(
    ExpectedConditions.
    elementToBeClickable(By.id("id")));

Wait<WebDriver> wait =
     new FluentWait<WebDriver>(driver)
    .withTimeout(Duration.ofSeconds(30))
    .pollingEvery(Duration.ofSeconds(5))
    .ignoring(NoSuchElementException.class);

driver.manage().timeouts()
    .implicitlyWait(Duration.ofSeconds(10));
driver.manage().timeouts()
    .scriptTimeout(Duration.ofMinutes(3));
driver.manage().timeouts()
    .pageLoadTimeout(Duration.ofSeconds(30));

|

事件监听器

在 Selenium WebDriver 3 中,我们使用 EventFiringWebDriver 类来创建事件监听器。在 Selenium WebDriver 4 中,这个类已被弃用,建议改用 EventFiringDecorator 类。表 A-5 概述了这一变化。关于此功能的完整示例,请参见 “事件监听器”。

表 A-5. Selenium WebDriver 4 中事件监听器的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

EventFiringWebDriver newDriver =
    new EventFiringWebDriver(originalDriver);
wrapper.register(myListener);

|

WebDriver newDriver =
    new EventFiringDecorator(myListener)
    .decorate(originalDriver);

|

功能

选择不同浏览器类型的静态方法在 Selenium WebDriver 4 中已移除。相反,我们应该使用特定于浏览器的选项。表 A-6 概述了这一变化。关于这个特性的更多细节,请参见 “浏览器能力”。

表 A-6. Selenium WebDriver 4 中期望能力的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

DesiredCapabilities caps =
    DesiredCapabilities.chrome();
DesiredCapabilities caps =
    DesiredCapabilities.edge();
DesiredCapabilities caps =
    DesiredCapabilities.firefox();
DesiredCapabilities caps =
    DesiredCapabilities.internetExplorer();
DesiredCapabilities caps =
    DesiredCapabilities.safari();
DesiredCapabilities caps =
    DesiredCapabilities.chrome();

|

ChromeOptions options =
    new ChromeOptions();
EdgeOptions options =
    new EdgeOptions();
FirefoxOptions options =
    new FirefoxOptions();
InternetExplorerOptions options =
    new InternetExplorerOptions();
SafariOptions options =
    new SafariOptions();
ChromeOptions options =
    new ChromeOptions();

|

然后,基于能力的 WebDriver 构造函数已弃用,而非特定于浏览器的选项。表 A-7 展示了此过程的示例。您可以在 “浏览器能力” 中找到使用此构造函数的示例。

表 A-7. Selenium WebDriver 4 中使用能力进行 WebDriver 实例化的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

WebDriver driver = new ChromeDriver(caps);

|

WebDriver driver = new ChromeDriver(options);

|

此外,Selenium WebDriver 4 中的能力合并方式发生了变化。在 Selenium WebDriver 3 中,可以通过改变调用对象来合并能力。而在 Selenium WebDriver 4 中,需要分配合并操作。表 A-8 提供了这一变化的示例。

表 A-8. Selenium WebDriver 4 中合并能力的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

MutableCapabilities caps =
    new MutableCapabilities();
caps.setCapability("platformName",
    "Linux");
ChromeOptions options =
    new ChromeOptions();
options.setHeadless(true);
options.merge(caps);

|

MutableCapabilities caps =
    new MutableCapabilities();
caps.setCapability("platformName",
    "Linux");
ChromeOptions options =
    new ChromeOptions();
options.setHeadless(true);
options = options.merge(caps);

|

最后,BrowserType 接口已弃用,转而采用新的 Browser 接口。表 A-9 显示了指定能力时这些接口之间的区别。有关此方面的更多细节,请参阅 “创建 RemoteWebDriver 对象”。

表 A-9. Selenium WebDriver 4 中能力的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

caps.setCapability("browserName",
    BrowserType.CHROME);

|

caps.setCapability("browserName",
    Browser.CHROME);

|

Selenium Grid

Selenium Grid 4 已完全重写。新代码库基于 Selenium Grid 3 的所有经验教训,提高了其源代码的可维护性。Selenium Grid 4 支持经典模式,即独立和中心节点架构,同时提供了完全分布式架构以改善其整体性能和可扩展性。最后,Selenium Grid 4 使用 Docker、Kubernetes 或使用 OpenTelemetry 进行分布式跟踪等现代基础设施技术。

“Selenium Grid” 介绍了 Selenium Grid。然后,“Selenium Grid” 解释了 Selenium Grid 不同模式(即独立、中心节点和完全分布式)的详细信息,以及如何在 Selenium WebDriver 测试中使用它。

迁移指南

当作为 Java 依赖项使用 Selenium Grid 并升级到版本 4 时,除了升级版本外,还需知道 Selenium Grid 4 中项目坐标的变化。以前的 artifactIdselenium-server,在 Selenium Grid 4 中变为 selenium-grid。表 A-10 包含了 Maven 和 Gradle 中 Selenium Grid 4 的新坐标。

表 A-10. 将 Selenium Grid 4 作为 Maven 和 Gradle 依赖项的迁移

Selenium WebDriver 3Selenium WebDriver 4

|

<dependency>
 <groupId>org.seleniumhq.selenium</groupId>
 <artifactId>selenium-server</artifactId>
 <version>3.141.59</version>
</dependency>

|

<dependency>
 <groupId>org.seleniumhq.selenium</groupId>
 <artifactId>selenium-grid</artifactId>
 <version>4.0.0</version>
</dependency>

|

|

testImplementation("org.seleniumhq.selenium:
 selenium-server:3.141.59")

|

testImplementation("org.seleniumhq.selenium:
 selenium-grid:4.0.0")

|

Selenium IDE

Selenium IDE 在 “Selenium IDE” 中引入。截至 Selenium 4,一些新功能包括:

备份元素选择器

Selenium IDE 为每个元素记录多个定位器(例如按 id、XPath 或 CSS 选择器),因此,如果测试执行未能使用第一个定位器找到元素,它会依次尝试后续的定位器直到找到为止。

控制流

Selenium IDE 通过条件语句(例如ifelseelse ifend)和循环(例如dowhiletimesforEach)增强了脚本执行。

代码导出

Selenium IDE 允许将录制结果导出为多种 Selenium WebDriver 绑定语言(即 C#、Java、JavaScript、Python 和 Ruby)和单元测试框架(即 NUnit、xUnit、JUnit、Mocha、pytest 和 RSpec)。

插件

我们可以通过自定义插件扩展 Selenium IDE(例如引入新的命令或第三方集成)。

其他新功能

Selenium 4 大大改进了官方的 Selenium 文档。新网站位于https://www.selenium.dev,内容涵盖了 Selenium 的子项目(WebDriver、IDE 和 Grid)、用户指南、博客、支持和其他项目信息。

最后,自 2009 年以来一直担任 Selenium 项目联合创始人和项目负责人的 Simon Stewart,于 2021 年 10 月 27 日辞去了 Selenium 项目的领导职务。你可以在Selenium 网站上找到当前的项目结构(包括项目领导委员会、技术领导委员会和 Selenium 贡献者及触发器)和治理(即模型、哲学、项目角色、决策过程等)。

附录 B. 驱动程序管理

正如在第一章讨论的那样,驱动程序管理涉及三个步骤:下载、设置和维护。手动驱动程序管理在努力方面昂贵,并且在可维护性方面可能存在问题。因此,在本书的所有示例中,我使用 WebDriverManager 来以自动化和自动维护的方式执行这个过程。为了完整起见,本附录还描述了手动驱动程序管理中涉及的步骤(下载、设置和维护)。

WebDriverManager:自动化驱动程序管理

WebDriverManager 是一个开源的 Java 库,用于自动管理 Selenium WebDriver 所需的驱动程序(例如 chromedriver、geckodriver、msedgedriver 等)。WebDriverManager 为不同的浏览器提供一组管理器,包括 Chrome、Firefox、Edge、Opera、Chromium 和 Internet Explorer。

WebDriverManager 在内部执行一个解析算法来管理每个浏览器所需的驱动程序。该算法旨在自动发现、下载、设置和维护这些驱动程序。

图 B-1 在 WebDriverManager 实现的方法论背景下代表了这个算法。对于每个管理器(例如,chromedriver()firefoxdriver()等),解析算法的工作原理如下:

  1. WebDriverManager 尝试查找安装在本地计算机上的浏览器版本(例如 Chrome)。为此,它使用一个名为命令数据库的内部知识数据库。该数据库包含一组 Shell 命令(在不同操作系统上),允许发现浏览器版本(例如,在 Linux 上使用google-chrome --version)。

  2. 使用找到的主要浏览器版本(例如,Chrome 89),WebDriverManager 确定正确的驱动程序版本(例如,chromedriver 89.0.4389.23)。我称这个过程为版本解析。为了简化这个过程,几个驱动程序维护者(例如 chromedriver 和 msedgedriver)在其在线存储库中发布特定的驱动程序版本信息,使用简单的文本文件(例如,https://chromedriver.storage.googleapis.com/LATEST_RELEASE_89)。不幸的是,对于 geckodriver 或 operadriver 等其他驱动程序,这些信息是不可用的。因此,WebDriverManager 使用另一个内部知识数据库(称为版本数据库)来保持浏览器版本和驱动程序之间的关联。这两个版本和命令数据库通过存储在 GitHub 上的在线主参考进行值的同步。

  3. WebDriverManager 下载特定的驱动程序,适用于本地操作系统(Windows、Linux 或 macOS),并将其存储在本地文件系统中的驱动程序缓存中(默认情况下在路径~/.cache/selenium)。

  4. 最后,WebDriverManager 使用适当的 Java 系统属性(例如,webdriver.chrome.driver)导出已下载的驱动程序路径。

出于性能和可维护性考虑,WebDriverManager 在内部使用解析缓存。该缓存(默认情况下存储为属性文件)保持了解决驱动程序版本之间的关系。此关系遵循存活时间(TTL)方法有效。默认情况下,驱动程序的 TTL 为一天(例如 chromedriver 89.0.4389.23),浏览器的 TTL 为一小时(例如 Chrome 89)。解析算法在后续调用中使用缓存文件来解决驱动程序(通常在 Selenium WebDriver 测试套件中发生)。然后,当 TTL 过期时,解析算法尝试解析新的驱动程序发布。最后,当检测到不同的浏览器版本时,WebDriverManager 会下载新的驱动程序(如果需要)。通过此过程,即使对于常青树浏览器,也可以确保浏览器和驱动程序的版本兼容性。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_ab01.png

图 B-1. WebDriverManager 方法论

通用管理器

除了特定于浏览器的管理器(例如chromedriver()firefoxdriver()等),WebDriverManager 还提供了通用管理器,即可以参数化为充当特定管理器(例如 Chrome、Firefox 等)的管理器。通过 WebDriverManager API 的getInstance()方法可以使用此功能。有多种调用此方法的选项:

getInstance(Class<? extends WebDriver> webDriverClass)

其中webDriverClassWebDriver层次结构的类,如ChromeDriver.classFirefoxDriver.class等。

getInstance(DriverManagerType driverManagerType)

其中driverManagerType是由 WebDriverManager 提供的枚举,用于识别可用的管理器。该枚举的可能值为CHROMEFIREFOXEDGEOPERACHROMIUMIEXPLORERSAFARI

getInstance(String browserName)

其中browserName是不区分大小写的浏览器名称字符串。可能的值包括ChromeFirefoxEdgeOperaChromiumIExplorerSafari

getInstance()

当未指定参数时,将使用配置键wdm.defaultBrowser来选择管理器(默认为 Chrome)。

高级配置

WebDriverManager 提供了不同的配置方式。首先,您可以通过每个管理器使用其 Java API。此 API 允许连接多个方法以指定自定义选项或首选项。您可以在其文档中找到 WebDriverManager API 的完整描述。例如,以下命令显示如何为网络连接设置代理:

WebDriverManager.chromedriver().proxy("server:port").setup();

配置 WebDriverManager 的第二种方式是使用 Java 系统属性。每个 WebDriverManager API 方法都有一个相应的配置键。例如,API 方法 cachePath()(用于指定驱动程序缓存文件夹)的工作方式与配置键 wdm.cachePath 相同。例如,可以通过命令行传递这些类型的配置键:

mvn test -Dwdm.cachePath=/custom/path/to/driver/cache

最后,您还可以使用环境变量来配置 WebDriverManager。变量名称派生自每个配置键(例如,wdm.cachePath),将其转换为大写,并用下划线替换符号 .(例如,WDM_CACHEPATH)。此机制可以方便地在操作系统级别配置全局参数。

其他用途

除了作为 Java 依赖项外,WebDriverManager 还可以以其他方式使用,即:

作为命令行界面(CLI)工具

此模式允许您解析驱动程序(例如,chromedriver、geckodriver)。此外,此模式还允许您在 Docker 容器中执行浏览器,并通过远程桌面会话与其交互。

作为服务器

WebDriverManager 服务器基于 HTTP,并提供两种类型的服务。首先,它公开了一个简单的类似 REST 的 API 来解析驱动程序。其次,它作为常规 Selenium 服务器运行,因此您可以与不同于 Java 的语言绑定一起使用它。

作为 Java 代理

在这种情况下,并使用 JVM 插装 API,WebDriverManager 使用 Java 插装 API 检查在 JVM 中创建的对象。当实例化 WebDriver 对象(ChromeDriverFirefoxDriver 等)时,将使用所需的管理器来解析其驱动程序(chromedrivergeckodriver 等)。由于此方法,您可以从测试中移除 WebDriverManager 的调用。

手动驱动管理

本节描述了如何手动实现驱动程序管理过程(下载、设置和维护)。

下载

驱动管理的第一步是下载适当的驱动程序。表 B-1 显示了获取主要浏览器驱动程序的网站。您需要为您计划使用的浏览器找到正确的驱动程序版本和平台(Windows、Linux、macOS)。关于版本,Chrome 和 Edge(但不幸的是,不包括 Firefox)的维护者遵循相同的驱动程序和浏览器版本号方案,以简化此过程。例如,如果您使用 Chrome 或 Edge 91.x,您还需要使用 chromedriver 和 msedgedriver 91.x。您可以在网站提供的文档中找到具体的驱动程序版本。例如,要使用 Chrome 91,您需要下载 ChromeDriver 91.0.4472.19。

表 B-1. 设置驱动程序的 Java 系统属性

浏览器驱动程序下载网站
Chrome/Chromiumchromedriverhttps://chromedriver.chromium.org/downloads
Edgemsedgedriverhttps://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver
Firefoxgeckodriverhttps://github.com/mozilla/geckodriver/releases

设置

当您为 WebDriver 脚本准备好所需的驱动程序后,需要正确设置它们。有两种方法可以进行此过程。第一种方法是将驱动程序位置(完整路径或包含驱动程序的父文件夹)添加到您的PATH环境变量中(env)。PATH环境变量在类 Unix 系统(如 Linux 和 macOS)和 Windows 操作系统中是标准的。该环境变量允许指定操作系统定位可执行程序的一组文件夹。我们配置PATH(以及其他环境变量)的方式取决于具体的操作系统。例如,在 Windows 系统中,可以通过其 GUI 进行配置(控制面板 → 系统 → 高级 → 环境变量)。在类 Unix 系统中,我们可以使用命令行来执行此过程,例如使用以下命令(或等效命令):

export PATH=$PATH:/path/to/drivers >> ~/.profile

设置驱动程序的第二种方法是使用Java 系统属性,这些属性是以名称/值形式传递给 JVM 的配置属性。表 B-2 总结了 Selenium WebDriver 中主要驱动程序的名称。这些属性的值是给定驱动程序的完整路径(例如/path/to/drivers/chromedriver)。

表 B-2. 设置驱动程序的 Java 系统属性

BrowserDriverJava 系统属性名称
Chrome/Chromiumchromedriverwebdriver.chrome.driver
Edgemsedgedriverwebdriver.edge.driver
Firefoxgeckodriverwebdriver.gecko.driver

配置这些属性有两种方法:命令行(使用-Dname=value语法传递系统属性)或 Java 代码。例如,示例 B-1 展示了使用 Maven 和 Gradle 命令来执行给定项目的所有测试,并传递属性以设置 Chrome、Edge 和 Firefox 的驱动程序的方法。然后,示例 B-2 展示了如何进行相同的配置,但这次使用 Java 语言。

示例 B-1. Maven 和 Gradle 命令以在命令行中配置系统属性
mvn test -Dwebdriver.chrome.driver=/path/to/drivers/chromedriver
mvn test -Dwebdriver.edge.driver=/path/to/drivers/msedgedriver
mvn test -Dwebdriver.gecko.driver=/path/to/drivers/geckodriver

gradle test -Dwebdriver.chrome.driver=/path/to/drivers/chromedriver
gradle test -Dwebdriver.edge.driver=/path/to/drivers/msedgedriver
gradle test -Dwebdriver.gecko.driver=/path/to/drivers/geckodriver
示例 B-2. Java 命令以配置系统属性
System.setProperty("webdriver.chrome.driver", "/path/to/drivers/chromedriver");
System.setProperty("webdriver.edge.driver", "/path/to/drivers/msedgedriver");
System.setProperty("webdriver.gecko.driver", "/path/to/drivers/geckodriver");

维护

最后但同样重要的是,驱动程序管理的最后一步是维护这些驱动程序。由于 Chrome、Edge 或 Firefox 等持续更新的浏览器会自动进行升级,因此这种维护是必要的。尽管从用户角度来看自动升级很吸引人,但对于手动管理驱动程序的 Selenium WebDriver 脚本来说,这种自动化升级会带来问题。在这种情况下,驱动程序和浏览器的兼容性长期无法保证。

特定的驱动程序(例如,chromedriver 版本 84.0.4147.30)通常与特定的浏览器版本兼容(例如,Chrome 84)。但是,由于自动升级,这种兼容性并不保证。因此,基于这种驱动程序的 Selenium WebDriver 脚本可能会停止工作(即测试被称为失败)。在实践中,当由于驱动程序与浏览器不兼容而导致测试失败时,Selenium WebDriver 开发人员经常会遇到这个问题。例如,在使用 Chrome 作为浏览器时,由于驱动程序不兼容而导致的测试失败会报告以下错误消息:“this version of chromedriver only supports Chrome version N”(其中 N 是特定版本 chromedriver 支持的最新 Chrome 版本)。为了说明这个问题,图 B-2 显示了这个错误消息在 2019 年和 2020 年期间在 Google 上的全球搜索兴趣,以及该时期不同 Chrome 版本的发布日期。正如您所见,关于这个错误消息的兴趣随时间变化与某些 Chrome 版本的发布相关联。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_ab02.png

图 B-2. “this version of chromedriver only supports chrome version” 在 Google 趋势中的全球相对搜索兴趣随时间变化的图表,以及 2019 年和 2020 年 Chrome 的发布日期

摘要

Selenium WebDriver 是一个允许你以编程方式控制 web 浏览器的库。自动化基于每个浏览器的本地能力。因此,我们需要在使用 Selenium WebDriver API 的脚本/测试和浏览器之间放置一个依赖于平台的二进制文件,称为驱动程序。一些驱动程序的例子包括 chromedriver(用于 Chrome)、geckodriver(用于 Firefox)和 msedgedriver(用于 Edge)。本附录介绍了驱动程序管理过程。此过程包括三个步骤(下载、设置和维护),可以手动或自动完成。默认情况下,我建议您使用自动化驱动程序管理方法。为此,Java 中的参考工具是 WebDriverManager。

附录 C. 示例仓库设置

示例仓库 是本书的重要组成部分,因为它包含了所有涵盖的示例以及 Maven 和 Gradle 的完整配置。此外,此仓库使用 GitHub 提供的多项服务,如:

GitHub Pages

一个允许从 GitHub 仓库直接配置公共网站的服务。我使用一个简单的网站链接到示例仓库,展示用作 Selenium WebDriver 测试示例中 SUT 的网页:https://bonigarcia.dev/selenium-webdriver-java。正如您所见,它包含了使用 Bootstrap 作为 CSS 框架的不同 HTML 页面。

GitHub Actions

用于 GitHub 仓库的 CI/CD 构建服务器。我使用此服务在每次新提交时构建和测试整个仓库。您可以在本节末尾看到有关工作流配置的详细信息。

Dependabot

一个自动更新项目依赖的机器人。当此机器人检测到任何 Maven 和 Gradle 依赖的新版本(有关更多详情,请参见下一小节)时,它将创建相应的拉取请求。

在本附录的其余部分,您将找到示例仓库的配置详细信息。此配置包括 Maven 和 Gradle 依赖声明以及其他方面,并且对于使用 Selenium WebDriver 的标准项目应该是足够的。此外,本附录的最后部分解释了如何配置日志库、Dependabot 和 GitHub Actions(以 CI 方法构建和测试项目)。

项目布局

图 C-1 显示了示例仓库布局的示意图。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_ac01.png

图 C-1. 示例仓库布局(托管在 GitHub 上)

由于我为每个示例提供了四种风格(JUnit 4、JUnit 5、JUnit 5 加 Selenium-Jupiter 和 TestNG),因此 Maven 和 Gradle 中的配置基于 多项目。这样,示例仓库就有四个模块,每个测试框架一个:selenium-webdriver-junit4selenium-webdriver-junit5selenium-webdriver-junit5-seljupselenium-webdriver-testng。在 Maven 中,多项目设置位于根文件夹中的 pom.xml 中,在 Gradle 中则位于 settings.gradle 文件中。

如你所见,在图 C-1 中,每个模块都有相同的结构。你可以在src/test/java文件夹中找到测试源代码。我使用 Java 包将示例按章节分开(例如,io.github.bonigarcia.webdriver.jupiter.ch02.helloworld)。接下来,每个项目都需要自己的 Logback 配置文件。我使用通用配置文件(即logback.xml),放置在src/main/resources文件夹下。我遵循这种约定,因为通常也会将日志记录用于应用程序,并且如果你计划重用这个项目结构,这是标准做法。最后,在每个子项目的根目录下,你可以找到用于 Maven(pom.xml)和 Gradle(build.gradle)的特定配置文件。你可以在这些文件中找到依赖项的声明,如下一节所述。

Maven

Maven 中的核心概念之一是构建生命周期,这是指构建和分发特定项目的过程。在 Maven 中有三个标准的构建生命周期:default(用于项目部署),clean(用于项目清理)和site(用于文档)。这些构建生命周期包含一系列构建阶段,每个阶段代表生命周期中的一个阶段。default生命周期的主要阶段包括:

validate

确保项目正确并且所有必要信息都可用。

compile

编译源代码。

test

使用单元测试框架执行测试。

package

将编译后的代码打包成可分发的格式,如 Java ARchive(JAR)文件。

verify

执行进一步的测试(通常是集成或其他高级测试)。

install

将包安装到本地仓库。

deploy

将包安装到远程仓库或服务器。

我们可以使用 Shell 来调用 Maven,使用命令mvn。例如,以下命令调用clean生命周期(即清理target文件夹及其所有内容),然后按顺序调用default生命周期的所有阶段直到package(即validatecompiletest,最后是package):

mvn clean package

Maven 中的另一个核心元素是插件的概念。插件是一个内置的工件,用于执行上述阶段。在本书中,我们特别关注测试。因此,我们着重介绍testverify阶段及其对应的插件:maven-surefire-pluginmaven-failsafe-plugin。表 C-1 总结了这两个插件之间的主要差异。

表 C-1. Surefire 和 Failsafe Maven 插件之间的差异

maven-surefire-pluginmaven-failsafe-plugin
描述在打包前执行测试的 Maven 插件在打包后执行测试的 Maven 插件
经典用法单元测试集成(和其他高级)测试
基本命令mvn testmvn verify
类型默认插件(即,我们可以在 pom.xml 中不声明而使用)非默认插件(即,我们需要在 pom.xml 中声明才能使用)
使用的版本Maven 内部定义的版本最新可用版本

| 测试名称模式 | **/Test*.java **/*Test.java

**/*Tests.java

**/*TestCase.java | **/IT*.java **/*IT.java

**/*ITCase.java |

为简单起见,我在示例库中仅使用 maven-surefire-plugin 执行测试。尽管这些测试不是单元测试(事实上,它们是端到端测试),但使用 maven-surefire-plugin 运行它们不成问题(即,在编译后和打包前)。表 C-2 总结了使用此插件从 shell 中运行测试的基本命令。

表 C-2. 使用 maven-surefire-plugin 运行测试的基本命令

命令描述
mvn test运行项目中的所有测试
mvn test -Dtest=MyClass运行单个类中的所有测试
mvn test -Dtest=MyClass#myMethod运行单个类中的单个测试

但是,如果您想要使用 maven-failsafe-plugin 执行测试,您需要在 pom.xml 文件中使用 示例 C-1 中展示的设置。最后,您可以使用命令 mvn verify 运行测试(即,在打包后运行测试)。

示例 C-1. 使用 maven-failsafe-plugin 执行测试所需的 Maven 配置
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

公共设置

示例 C-2 包含示例库中 Maven 配置的公共部分。

示例 C-2. 示例库中的常见 Maven 依赖项
<properties>
    <java.version>1.8</java.version> <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <maven.compiler.source>${java.version}</maven.compiler.source>
</properties>

<dependencies> <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
    </dependency>

    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>${selenium.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>${assertj.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.github.bonigarcia</groupId>
        <artifactId>webdrivermanager</artifactId>
        <version>${wdm.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin> <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
        </plugin>
    </plugins>
</build>

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO1-1

在本项目中我们使用 Java 8。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO1-2

我们指定了常见的依赖项。一方面,我们声明 Selenium WebDriver、AssertJ 和 WebDriverManager,并将其范围设置为 test。这样,我们只能从测试逻辑中(即位于 src/test/java 文件夹下的 Java 类)使用这些依赖项。另一方面,缺少 Simple Logging Facade for Java (SLF4J) 和 Logback 的范围,默认情况下 Maven 使用 compile。这意味着我们可以从应用程序和测试逻辑中使用这些依赖项。最后,请注意我们使用 Maven 属性来声明依赖项的版本(例如 ${selenium.version})。您可以在在线库中找到确切的版本。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO1-3

我们需要声明特定版本的 maven-surefire-plugin。如 表 C-1 所述,此插件的版本由 Maven 内部定义。但为了充分利用此插件,我们需要指定一个更新的版本。

JUnit 4

在一个使用 JUnit 4 作为单元测试框架的 Maven 项目中,我们还需要声明以下依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit4.version}</version>
    <scope>test</scope>
</dependency>

JUnit 5

虽然 JUnit 5 是一个模块化框架,但我们可以在 Maven 项目中声明单个依赖项来使用 Jupiter 编程模型。如下片段所示,这个构件被称为junit-jupiter,并且它传递地拉取以下 JUnit 5 的构件:

junit-jupiter-api

用于开发测试

junit-jupiter-engine

用于在 JUnit 平台中执行测试

junit-jupiter-params

用于开发参数化测试(参见第八章)

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>${junit5.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Selenium-Jupiter

当与 Selenium-Jupiter 一起使用 Jupiter 时,除了以前的构件(junit-jupitermaven-surefire-plugin),我们还需要包含 Selenium-Jupiter 的坐标(请参见下一个代码示例)。在这种情况下,我们可以移除 WebDriverManager 的坐标,因为 Selenium-Jupiter 会传递地拉取它。

<dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>selenium-jupiter</artifactId>
    <version>${selenium-jupiter.version}</version>
    <scope>test</scope>
</dependency>

TestNG

最后,在我们的pom.xml中包含 TestNG 所需的坐标是:

<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>${testng.version}</version>
    <scope>test</scope>
</dependency>

虽然在示例仓库中没有使用,但 TestNG 测试也可以在 JUnit 平台上执行。如果要启用此模式,需要在项目设置中添加 JUnit 平台的 TestNG 引擎。有关此信息,请参阅TestNG 引擎页面

其他依赖项

本书解释了与 Selenium WebDriver 结合使用的其他依赖项。表 C-3 总结了这些依赖项及其所在的章节。

表 C-3. 示例仓库中用于第三方集成的依赖项

DependencyChaptergroupIdartifactId
HtmlUnitDriver第一章org.seleniumhq.seleniumhtmlunit-driver
Selenium Grid第六章org.seleniumhq.seleniumselenium-grid
rerunner-jupiter第八章io.github.artsokrerunner-jupiter
JUnit 平台启动器第八章org.junit.platformjunit-platform-launcher
Awaitility第九章org.awaitilityawaitility
BrowserMob第九章net.lightbody.bmpbrowsermob-core
OWASP ZAP 客户端 API第九章org.zaproxyzap-clientapi
Axe Selenium 集成第九章com.deque.html.axe-coreselenium
Selenide第九章com.codeborneselenide
JavaFaker第九章com.github.javafakerjavafaker
Extent Reports第九章com.aventstackextentreports

| Allure | 第九章 | io.qameta.allure | io.qameta.allure allure-junit5

allure-testng |

Cucumber Java第九章io.cucumbercucumber-java

| Cucumber JUnit 4, 5 或 TestNG | 第九章 | io.cucumber | cucumber-junit cucumber-junit-platform-engine

cucumber-testng |

Spring-Boot Web第九章org.springframework.bootspring-boot-starter-web
Spring-Boot 测试第九章org.springframework.bootspring-boot-starter-test
Appium Java 客户端第十章io.appiumjava-client
REST Assured第十章io.rest-assuredrest-assured

此外,使用一些这些第三方依赖项需要插件声明进行一些额外设置。以下代码片段显示了这个新的设置。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <!-- The following setup is required only when using Allure --> <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
            <configuration>
                <properties>
                    <property>
                        <name>listener</name>
                        <value>io.qameta.allure.junit4.AllureJunit4</value> <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
                    </property>
                </properties>
            </configuration>
            <!-- /Allure -->
        </plugin>
        <plugin> <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-maven</artifactId>
            <version>${allure-maven.version}</version>
        </plugin>
        <plugin> <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>${spring-boot.version}</version>
        </plugin>
    </plugins>
</build>

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO2-1

如果您计划使用 Allure 生成测试报告,则需要从这一行到 <!-- /Allure --> 进行设置。如果不使用它,可以安全地从项目中删除它。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO2-2

不同单元测试框架的监听器类有所不同:

  • io.qameta.allure.junit4.AllureJunit4 适用于 JUnit 4

  • io.qameta.allure.junit5.AllureJunit5 适用于 JUnit 5(以及 JUnit 5 加 Selenium-Jupiter)

  • TestNG 不需要监听器

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO2-3

除了监听器外,使用此报告工具还需要 Allure 插件。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO2-4

在使用 Spring-Boot 时建议使用 Spring-Boot 插件。

Gradle

每个 Gradle 项目由多个 任务 组成。每个任务代表构建中的一个原子工作单元。在 Java 项目中,任务的典型示例包括:

compileJava

编译应用程序逻辑(即位于文件夹 src/main/java 的 Java 类)。

processResources

复制应用程序资源(即位于文件夹 src/main/resources 的文件)到输出文件夹(build)。

compileTestJava

编译测试逻辑(即位于文件夹 src/test/java 的 Java 类)。

processTestResources

复制测试资源(即位于文件夹 src/test/resources 的文件)到输出文件夹中。

test

使用 JUnit 或 TestNG 运行测试。表 C-4 总结了在 shell 中运行 Gradle 测试的常见命令。

clean

删除项目输出文件夹及其内容。

表 C-4. 使用 Gradle 运行测试的基本命令

命令描述
gradle test运行项目中的所有测试
gradle test --rerun-tasks运行项目中的所有测试(即使一切都是最新的)
gradle test --tests MyClass运行单个类中的所有测试
gradle test --tests MyClass.MyMethod运行单个类中的单个测试

示例 C-3 包含示例存储库所有子项目的通用配置。接下来我将解释这个片段的相关部分。

示例 C-3. Gradle 项目的通用设置
plugins {
    id "java" <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
}

compileTestJava { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    options.compilerArgs += "-parameters"
}

test {
    testLogging { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
        events "passed", "skipped", "failed"
        showStandardStreams = true
    }

    systemProperties System.properties <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>

    if (project.hasProperty("excludeTests")) { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
        "$excludeTests".split(",").each { excludeTests ->
            exclude excludeTests
        }
    }

    if (project.hasProperty("parallel")) { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>
        maxParallelForks = Runtime.runtime.availableProcessors()
    }

    ext.failedTests = [] <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>

    tasks.withType(Test) {
        afterTest { TestDescriptor descriptor, TestResult result ->
            if(result.resultType ==
                  org.gradle.api.tasks.testing.TestResult.ResultType.FAILURE) {
                failedTests << ["${descriptor.className}::${descriptor.name}"]
            }
        }
    }

    gradle.buildFinished {
        if(!failedTests.empty){
            println "Failed test(s) for ${project.name}:"
            failedTests.each { failedTest ->
                println failedTest
            }
        }
    }
}

repositories {
    mavenCentral() <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png>
}

dependencies { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/9.png>
    implementation("org.slf4j:slf4j-api:${slf4jVersion}")
    implementation("ch.qos.logback:logback-classic:${logbackVersion}")

    testImplementation("org.seleniumhq.selenium:selenium-java:${seleniumVersion}")
    testImplementation("org.assertj:assertj-core:${assertjVersion}")
    testImplementation("io.github.bonigarcia:webdrivermanager:${wdmVersion}")
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-1

由于我们正在实现一个 Java 项目,因此需要声明 java 插件

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-2

用于编译测试时,我们使用 Java 8。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-3

虽然不是强制的,但我们强制将测试日志写入标准输出。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-4

这允许在命令行中传递 Java 系统属性(如 示例 B-1 中所述)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-5

此子句允许在命令行中使用属性 excludeTests 来排除一些测试。例如,以下命令排除以 Docker 开头的测试:gradle test -PexcludeTests=**/Docker*

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-6

这些行允许使用命令 gradle test -Pparallel 来并行运行测试。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-7

下列子句将失败的测试汇总到 failedTests 属性,并在测试套件执行结束时将此信息显示在标准输出中。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-8

我们使用 Maven 中央仓库 来获取依赖项。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO3-9

常用依赖项包括 Selenium WebDriver、AssertJ、WebDriverManager(用于测试)、SLF4J 和 Logback(整个项目)。

JUnit 4

JUnit 4 的具体设置如下:

test {
    useJUnit() { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
        if (project.hasProperty("groups")) {
            includeCategories "$groups"
        }
        if (project.hasProperty("excludedGroups")) {
            excludeCategories "$excludedGroups"
        }
    }
}

dependencies { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    testImplementation("junit:junit:${junit4Version}")
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO4-1

我们使用额外的配置来允许使用类名进行测试筛选(参见 “分类和筛选测试”)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO4-2

我们包含了 JUnit 4 依赖。

JUnit 5

在使用 JUnit 5 时,我们需要指定 junit-jupiter 构件(就像 Maven 中依赖于 junit-jupiter-api junit-jupiter-enginejunit-jupiter-params)。此外,我们需要使用 test 任务设置中的 useJUnitPlatform() 子句来选择使用 JUnit 平台执行。

test {
    useJUnitPlatform() {
        if (project.hasProperty("groups")) {
            includeTags "$groups"
        }
        if (project.hasProperty("excludedGroups")) {
            excludeTags "$excludedGroups"
        }
    }
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:${junit5Version}")
}

Selenium-Jupiter

如果使用 Selenium-Jupiter,除了之前配置 JUnit 5 的设置外,还需要包含以下依赖项。在这种情况下,我们可以移除 WebDriverManager,因为它被 Selenium-Jupiter 传递性地引入。

dependencies {
    testImplementation("io.github.bonigarcia:selenium-jupiter:${selJupVersion}")
}

TestNG

最后,为了将 TestNG 作为单元测试框架使用,我们需要包含以下设置:

test {
    useTestNG() {
        if (project.hasProperty("groups")) { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
            includeGroups "$groups"
        }
        if (project.hasProperty("excludedGroups")) {
            excludeGroups "$excludedGroups"
        }
    }

    scanForTestClasses = false <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
}

dependencies { <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    testImplementation("org.testng:testng:${testNgVersion}")
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO5-1

我们包含这些语句以允许按类名进行筛选。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO5-2

这个属性需要设置为 false,以匹配筛选过程中的包含和排除模式。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO5-3

我们包含了 TestNG 依赖。

其他依赖项

我们需要在 Gradle 设置中添加额外的依赖项,以使用第三方库。表 C-3(在前一节中)总结了这些依赖项的坐标及其所述章节。此外,还需要在 Gradle 设置中使用一些额外的插件(用于使用 Allure 和 Spring-Boot,分别)。要使用 Allure,还需要定义一个额外的仓库,如下所示:

plugins {
    id "io.qameta.allure"
    id "org.springframework.boot"
}

repositories {
    maven {
       url "https://plugins.gradle.org/m2/"
    }
}

日志记录

我在示例库中使用了两个日志库:

Logback

这是实际的日志记录框架(也称为记录器)。Logback 被许多重要的 Java 项目使用,例如 Spring Framework 和 Groovy 等。

Simple Logging Facade for Java (SLF4J)

这是一个基于外观设计模式的流行实用工具,用于解耦底层记录器。它支持主要的日志框架(例如 Logback、Log4j 或 SimpleLogger 等)。正如在表 C-5 中总结的那样,SLF4J 根据消息的严重性定义了五个日志级别。

表 C-5. SLF4J 中的日志级别

日志级别描述
ERROR用于报告应用程序中的缺陷。
WARN发生了一些意外情况,但不影响预期的应用行为。
INFO提供信息的消息,例如应用程序进入了特定状态等。
DEBUG用于诊断和故障排除的信息。
TRACE最精细的信息。我们仅在需要完全了解应用程序发生情况的特殊情况下使用此级别。

通常情况下,要使用这些库,我们需要解决相应的依赖关系(详见 Maven 和 Gradle 的下一节)。然后,我们需要正确配置 Logback。为此,我们需要在项目类路径中包含一个 XML 配置文件。如果我们正在为整个项目配置日志记录(即应用程序加测试逻辑),则此文件的名称应为 logback.xml。在这种情况下,它应该在应用程序资源中可用,通常位于 src/main/resources 文件夹下(有关项目布局的更多信息,请参见下一节)。如果我们仅用于测试日志记录,配置文件的名称为 logback-test.xml,存储在测试资源中(例如位于 src/test/resources 文件夹下)。

在这两种情况下的语法(logback.xmllogback-test.xml)是相同的。示例 C-4 展示了配置文件的示例。这个 XML 文件设置了每条日志行的模式,由时间戳、线程名称、跟踪级别、源(包、类名和代码行)和消息组成。在这个示例中,INFO是默认的日志级别。这样,每个这个级别或更严重的追踪(即WARNERRORFATAL)都会显示,但不包括以下级别(即DEBUGTRACE)。此外,来自包io.github.bonigarcia的追踪(用于测试示例、WebDriverManager 和 Selenium-Jupiter)为DEBUG

C-4. Logback 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}.%M\(%line\)
                  - %msg%n</pattern>
      </encoder>
   </appender>

   <logger name="io.github.bonigarcia" level="DEBUG" />

   <root level="INFO">
      <appender-ref ref="STDOUT" />
   </root>

</configuration>

最后一步是在我们的 Java 类中使用变量记录日志。为此,我们可以使用示例 C-5 中的代码。这段代码通过lookup()方法提供了一种方便的方式来获取当前类的反射信息。然后,我们声明记录日志的变量(在这个示例中称为log),并使用 SLF4J 的getLogger()方法。最后,我们可以在这个类的任何方法中使用变量log记录不同级别的消息。

C-5. 记录消息的示例
static final Logger log = getLogger(lookup().lookupClass());

log.info("This is an informative message");
log.debug("This is a debugging message");

GitHub Actions

我使用 GitHub Actions 作为示例仓库的 CI 服务器。这样,每次我向仓库提交新更改时,GitHub Actions 都会构建项目并执行所有测试。示例 C-6 展示了执行此过程的配置。

C-6. GitHub Actions 工作流配置
name: build

on: <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

env: <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
  DISPLAY: :99
  WDM_GITHUBTOKEN: ${{ secrets.WDM_GITHUBTOKEN }}

jobs:
  tests: <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ ubuntu-latest, windows-latest, macos-latest ]
        java: [ 8 ]

    steps: <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    - name: Checkout GitHub repo
      uses: actions/checkout@v2
    - name: Set up Java
      uses: actions/setup-java@v2
      with:
        distribution: 'temurin'
        java-version: ${{ matrix.java }}
    - name: Start Xvfb
      run: Xvfb :99 &
    - name: Test with Maven
      run: mvn -B test
    - name: Test with Gradle
      run: ./gradlew test

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO6-1

触发工作流的事件是push(仓库中的新提交)和pull_request(其他开发人员提出的提交)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO6-2

需要两个环境变量:

DISPLAY

默认情况下,由 Selenium WebDriver 控制的浏览器需要在带有图形用户界面的操作系统中执行。另一方面,GitHub Actions 中提供的 Linux 发行版是无头的(即没有图形用户界面)。因此,我们使用 Xvfb(X 虚拟帧缓冲)在这些 Linux 发行版上运行 WebDriver 测试。Xvfb 是 Unix-like 系统的内存中显示服务器,需要在 Linux(X11)中声明带有屏幕号的环境变量DISPLAY

WDM_GITHUBTOKEN

GitHub 托管 Selenium WebDriver 需要的一些驱动程序(如 geckodriver 或 operadriver)。当外部客户端(如 WebDriverManager)连续向 GitHub 发送多个请求时,GitHub 最终会返回 HTTP 错误响应(403,禁止),原因是其速率限制。WebDriverManager 可以使用个人访问令牌进行身份验证请求,以避免此问题。图 C-2 展示了授予此令牌的权限在示例仓库中。总之,此环境变量导出了此令牌的值。我将此令牌的实际值保存为 GitHub 仓库的机密。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO6-3

为了完整起见,我在三种不同的操作系统中执行工作流程:Ubuntu(即 Linux)、Windows 和 macOS,所有操作系统均使用 Java 8。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO6-4

工作流程包括五个步骤:

  1. 检出仓库。

  2. 使用Eclipse Adoptium设置 Java 8。

  3. 启动 X 虚拟帧缓冲区。

  4. 使用 Maven 运行所有测试。

  5. 使用 Gradle 运行所有测试。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_ac02.png

图 C-2. GitHub 仓库示例中使用的个人访问令牌的权限

Dependabot

要配置 Dependabot,我们需要在仓库的 .github 文件夹中包含名为 dependabot.yml 的文件。示例 C-7 展示了示例仓库中的内容。

示例 C-7. Dependabot 配置
version: 2
updates:
- package-ecosystem: maven <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
  directory: "/"
  schedule:
    interval: daily
    time: '06:00'
  open-pull-requests-limit: 99

- package-ecosystem: gradle <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
  directory: "/"
  schedule:
    interval: daily
    time: '06:00'
  open-pull-requests-limit: 99

- package-ecosystem: github-actions <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
  directory: "/"
  schedule:
    interval: daily
    time: '06:00'
  open-pull-requests-limit: 99

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO7-1

我们每天检查 Maven 依赖项的更新情况。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO7-2

我们每天检查 Gradle 依赖项的更新情况。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_examples_repository_setup_CO7-3

我们每天检查 GitHub Actions 设置的更新情况。

摘要

本书中所有示例都可在公共GitHub 仓库中找到。本附录展示了构建工具(Maven 和 Gradle)、依赖项(Selenium WebDriver、JUnit、TestNG、Selenium-Jupiter、WebDriverManager 等)、日志记录(Logback 和 SLF4J)以及其他服务(GitHub Actions、GitHub Pages 和 Dependabot)的详细配置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值