编写 UI 测试

已完成

在本部分中,你将帮助 Andy 和 Amita 编写 Selenium 测试,以验证 Amita 描述的 UI 行为。

Amita 通常会在 Chrome、Firefox 和 Microsoft Edge 上运行测试。 现在将执行相同的操作。 你将使用的 Microsoft 托管代理已预先配置好,可与所有这些浏览器一起使用。

从 GitHub 中提取分支

在本部分中,将从 GitHub 提取 selenium 分支。 然后,签出或切换到该分支。 该分支的内容将帮助你跟随 Andy 和 Amita 编写的测试。

该分支包含在之前的模块中使用过的 Space Game 项目。 它还包含要从其开始的 Azure Pipelines 配置。

  1. 在 Visual Studio Code 中打开集成终端。

  2. 要从 Microsoft 存储库中下载名为 selenium 的分支,请切换到该分支,并运行以下 git fetchgit checkout 命令:

    git fetch upstream selenium
    git checkout -B selenium upstream/selenium
    

    提示

    如果在上一单元中按照 Amita 的手动测试进行了操作,你可能已经运行了这些命令。 如果已在上一单元中运行过它们,现在仍然可以再次运行。

    回忆一下,upstream 指的是 Microsoft GitHub 存储库。 项目的 Git 配置能够识别 upstream 远程库,因为你已建立了这种关系。 在从 Microsoft 存储库为该项目创建分支并在本地克隆该项目时,你就对其进行了设置。

    稍后,你会将此分支推送到 GitHub 存储库(称作 origin)。

  3. (可选)在 Visual Studio Code 中,打开 azure-pipelines.yml 文件。 熟悉初始配置。

    该配置类似于你在此学习路径前面的模块中创建的配置。 它只生成应用程序的发布配置。 为简洁起见,它也省略了在之前的模块中设置的触发器、手动审批和测试。

    备注

    可以使用更可靠的配置来指定参与生成过程的分支。 例如,为了帮助验证代码质量,你可以在每次对任何分支推送更改时运行单元测试。 你还可以将应用程序部署到执行更全面测试的环境。 但仅当你有拉取请求、候选发布或将代码合并到主分支时,才能执行此部署。

    有关详细信息,请参阅使用 Git 和 GitHub 在生成管道中实现代码工作流生成管道触发器

编写单元测试代码

Amita 很高兴能够学习编写控制 Web 浏览器的代码。

她和 Andy 将共同编写 Selenium 测试。 Andy 已经设置了一个空的 NUnit 项目。 在整个过程中,他们会参考 Selenium 文档、一些在线教程,以及 Amita 手动测试时所记的笔记。 在本模块末尾,你将找到可帮助你完成整个过程的更多资源。

让我们回顾一下 Andy 和 Amita 编写其测试的过程。 可以通过在 Visual Studio Code 的 Tailspin.SpaceGame.Web.UITests 目录中打开 HomePageTest.cs 来执行此操作。

定义 HomePageTest 类

Andy:首先需要定义测试类。 我们可以选择遵循几种命名约定中的一种。 让我们将我们的类命名为 HomePageTest。 在此类中,我们将放入与主页相关的所有测试。

Andy 将此代码添加到了 HomePageTest.cs:

public class HomePageTest
{
}

Andy:我们需要将此类标记为 public,以便 NUnit 框架可以使用它。

添加 IWebDriver 成员变量

Andy:接下来,需要一个 IWebDriver 成员变量。 IWebDriver 是用于启动 Web 浏览器并与网页内容进行交互的编程接口。

Amita:我听说过编程中的接口。 你能告诉我更多详细信息吗?

Andy:可将接口视为组件行为方式的规范或蓝图。 接口提供了该组件的方法或行为。 但接口不提供任何基础详细信息。 你或其他人会创建一个或多个实现该接口的具体类。 Selenium 提供了我们需要的具体类。

以下关系图显示了 IWebDriver 接口以及实现该接口的一些类:

Diagram of the IWebDriver interface, its methods, and concrete classes.

此关系图显示了 IWebDriver 提供的三种方法:NavigateFindElementClose

此处显示的三个类 ChromeDriverFirefoxDriverEdgeDriver 分别实现 IWebDriver 及其方法。 还有其他类,例如 SafariDriver,也可以实现 IWebDriver。 每个驱动程序类都可以控制其表示的 Web 浏览器。

Andy 将一个名为 driver 的成员变量添加到了 HomePageTest 类中,如以下代码所示:

public class HomePageTest
{
    private IWebDriver driver;
}

定义测试固定例程

Andy:我们想在Chrome、Firefox 和 Microsoft Edge 上运行整套测试。 在 NUnit 中,我们可以使用测试固定例程多次运行整套测试,每次对要测试的一个浏览器运行一套测试。

在 NUnit 中,可以使用 TestFixture 属性定义测试固定例程。 Andy 将这三个测试固定例程添加到了 HomePageTest 类中:

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private IWebDriver driver;
}

Andy:接下来,我们需要为测试类定义构造函数。 当 NUnit 创建此类的实例时,将调用构造函数。 作为其参数,构造函数将使用我们附加到测试固定例程的字符串。 代码如下所示:

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private string browser;
    private IWebDriver driver;

    public HomePageTest(string browser)
    {
        this.browser = browser;
    }
}

Andy:我们添加了 browser 成员变量,我们可以在安装代码中使用当前的浏览器名称。 接下来编写安装代码。

定义 Setup 方法

Andy:接下来,我们需要将 IWebDriver 成员变量分配给一个类实例,该实例将为要测试的浏览器实现此接口。 ChromeDriverFirefoxDriverEdgeDriver 类分别为 Chrome、Firefox 和 Edge 实现此接口。

让我们创建一个名为 Setup 的方法来设置 driver 变量。 我们使用 OneTimeSetUp 属性告诉 NUnit 每个测试固定例程运行一次此方法。

[OneTimeSetUp]
public void Setup()
{
}

Setup 方法中,我们可以使用 switch 语句根据浏览器名称将 driver 成员变量分配给适当的具体实现。 现在,我们来添加这个代码。

// Create the driver for the current browser.
switch(browser)
{
    case "Chrome":
    driver = new ChromeDriver(
        Environment.GetEnvironmentVariable("ChromeWebDriver")
    );
    break;
    case "Firefox":
    driver = new FirefoxDriver(
        Environment.GetEnvironmentVariable("GeckoWebDriver")
    );
    break;
    case "Edge":
    driver = new EdgeDriver(
        Environment.GetEnvironmentVariable("EdgeWebDriver"),
        new EdgeOptions
        {
            UseChromium = true
        }
    );
    break;
    default:
    throw new ArgumentException($"'{browser}': Unknown browser");
}

每个驱动程序类的构造函数都采用 Selenium 控制 Web 浏览器所需的驱动程序软件的可选路径。 稍后,我们将讨论此处显示的环境变量的作用。

在此示例中,EdgeDriver 构造函数还需要其他选项来指定我们想要使用 Edge 的 Chromium 版本。

定义帮助程序方法

Andy:我知道我们需要在整个测试中重复执行两个操作:

  • 在页面上查找元素,例如我们单击的链接和希望显示的模式窗口
  • 单击页面上的元素,例如显示模式窗口的链接和关闭每个模式的按钮

让我们编写两个帮助程序方法,每个操作一个。 我们将首先编写在页面上查找元素的方法。

编写 FindElement 帮助程序方法

当你在页面上查找元素时,通常会对其他事件进行响应,例如页面加载或用户输入信息。 Selenium 提供了 WebDriverWait 类,使你可以等待条件为 true。 如果条件在给定时间段内不是 true,则 WebDriverWait 将引发异常或错误。 我们可以使用 WebDriverWait 类等待显示给定元素,并准备好接收用户输入。

要在页面上查找元素,请使用 By 类。 By 类提供了一些方法,使你能够按元素名称、其 CSS 类名称、其 HTML 标记或其 id 属性(在我们的示例中),来查找元素。

Andy 和 Amita 编写了 FindElement 帮助程序方法的代码。 如下代码所示:

private IWebElement FindElement(By locator, IWebElement parent = null, int timeoutSeconds = 10)
{
    // WebDriverWait enables us to wait for the specified condition to be true
    // within a given time period.
    return new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds))
        .Until(c => {
            IWebElement element = null;
            // If a parent was provided, find its child element.
            if (parent != null)
            {
                element = parent.FindElement(locator);
            }
            // Otherwise, locate the element from the root of the DOM.
            else
            {
                element = driver.FindElement(locator);
            }
            // Return true after the element is displayed and is able to receive user input.
            return (element != null && element.Displayed && element.Enabled) ? element : null;
        });
}

编写 ClickElement 帮助程序方法

Andy:接下来,让我们编写一个可单击链接的帮助程序方法。 Selenium 提供了几种方式来编写该方法。 其中一个是 IJavaScriptExecutor 接口。 利用该接口,可以通过使用 JavaScript 以编程方式单击链接。 此方法非常有效,因为它可以单击链接,而无需先将其滚动到视图中。

ChromeDriverFirefoxDriverEdgeDriver 各自实现 IJavaScriptExecutor。 需要将驱动程序强制转换为此接口,然后调用 ExecuteScript 以对基础 HTML 对象运行 JavaScript click() 方法。

Andy 和 Amita 编写了 ClickElement 帮助程序方法的代码。 如下代码所示:

private void ClickElement(IWebElement element)
{
    // We expect the driver to implement IJavaScriptExecutor.
    // IJavaScriptExecutor enables us to execute JavaScript code during the tests.
    IJavaScriptExecutor js = driver as IJavaScriptExecutor;

    // Through JavaScript, run the click() method on the underlying HTML object.
    js.ExecuteScript("arguments[0].click();", element);
}

Amita:我喜欢添加这些帮助程序方法的想法。 它们似乎足够通用,几乎可以在任何测试中使用。 稍后我们可以根据需要添加更多帮助程序方法。

定义测试方法

Andy:现在我们准备定义测试方法。 根据之前运行的手动测试,让我们将此方法称为 ClickLinkById_ShouldDisplayModalById。 为测试方法提供描述性名称以精确定义测试的完成效果是一种好习惯。 在这里,我们要单击由其 id 属性定义的链接。 然后,我们还希望通过使用其 id 属性来验证是否显示了正确的模式窗口。

Andy 为测试方法添加了起始代码:

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
}

Andy:在添加更多代码之前,让我们定义此测试应执行的操作。

Amita:我可以处理此部分。 我们希望:

  1. 通过链接的 id 属性找到链接,然后单击链接。
  2. 找到生成的模式。
  3. 关闭模式。
  4. 验证是否已成功显示模式。

Andy:很好。 还需要处理一些其他事项。 例如,如果无法加载驱动程序,我们需要忽略测试,仅当已成功显示模式后才需要关闭模式。

重新装满咖啡杯后,Andy 和 Amita 将代码添加到了他们的测试方法中。 他们使用编写的帮助程序方法来查找页面元素,然后单击链接和按钮。 结果如下:

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
    // Skip the test if the driver could not be loaded.
    // This happens when the underlying browser is not installed.
    if (driver == null)
    {
        Assert.Ignore();
        return;
    }

    // Locate the link by its ID and then click the link.
    ClickElement(FindElement(By.Id(linkId)));

    // Locate the resulting modal.
    IWebElement modal = FindElement(By.Id(modalId));

    // Record whether the modal was successfully displayed.
    bool modalWasDisplayed = (modal != null && modal.Displayed);

    // Close the modal if it was displayed.
    if (modalWasDisplayed)
    {
        // Click the close button that's part of the modal.
        ClickElement(FindElement(By.ClassName("close"), modal));

        // Wait for the modal to close and for the main page to again be clickable.
        FindElement(By.TagName("body"));
    }

    // Assert that the modal was displayed successfully.
    // If it wasn't, this test will be recorded as failed.
    Assert.That(modalWasDisplayed, Is.True);
}

Amita:到目前为止,编码看起来很棒。 但如何将此测试连接到先前收集的 id 属性呢?

Andy:很好的问题。 稍后我们将处理。

定义测试用例数据

Andy:在 NUnit 中,可以通过几种方式为测试提供数据。 在这里,我们使用 TestCase 属性。 该属性接受参数,然后在运行时将其传递回测试方法。 我们可以具有多个 TestCase 属性,每个属性都可以测试应用的不同功能。 每个 TestCase 属性都会生成一个测试用例,该测试用例将包含在管道运行结束时显示的报表中。

Andy 将这些 TestCase 属性添加到了测试方法中。 这些属性描述了“下载游戏”按钮、游戏屏幕之一以及排行榜上的顶级玩家。 每个属性指定两个 id 属性:一个用于单击链接,另一个用于相应的模式窗口。

// Download game
[TestCase("download-btn", "pretend-modal")]
// Screen image
[TestCase("screen-01", "screen-modal")]
// // Top player on the leaderboard
[TestCase("profile-1", "profile-modal-1")]
public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{

...

Andy:对于每个 TestCase 属性,第一个参数都是要单击的链接的 id 属性。 第二个参数是预期显示的模式窗口的 id 属性。 你可以看到这些参数如何与测试方法中的两个字符串参数相对应。

Amita:我看到了。 通过一些练习,我认为我可以添加自己的测试。 我们什么时候可以看到这些测试在我们的管道中运行?

Andy:在通过管道推送更改之前,首先要验证代码是否在本地编译并运行。 我们将提交更改并将其推送到 GitHub,并仅在确认一切正常后,才能看到它们在管道中移动。 现在让我们在本地运行测试。