这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

指南和建议

Selenium项目的一些测试指南和建议.

关于"最佳实践"的注解:我们有意在本文档中避免使用"最佳实践"的说辞. 没有一种方法可以适用于所有情况. 我们更喜欢"指南和建议"的想法. 我们鼓励您通读这些内容, 并仔细地确定哪种方法适用于您的特定环境.

由于许多原因, 功能测试很难正确完成. 即便应用程序的状态, 复杂性, 依赖还不够让测试变得 足够复杂, 操作浏览器(特别是跨浏览器的兼容性测试)就已经使得写一个好的测试变成一种挑战.

Selenium提供了一些工具使得功能测试用户更简单的操作浏览器, 但是这些工具并不能帮助你来写一个好的 架构的测试套件. 这章我们会针对怎么来做web页面的功能测试的自动化给出一些忠告, 指南和建议.

这章记录了很多历年来成功的使用Selenium的用户的常用的软件设计模式.

1 - 关于测试自动化

首先,问问自己是否真的需要使用浏览器。 在某些情况下,如果您正在开发一个复杂的 web 应用程序, 您需要打开一个浏览器并进行实际测试,这种可能性是很大的。

然而,诸如 Selenium 之类的功能性最终用户测试运行起来很昂贵。 此外,它们通常需要大量的基础设施才能有效运行。 经常问问自己,您想要测试的东西是否可以使用更轻量级的测试方法(如单元测试)完成, 还是使用较低级的方法完成,这是一个很好的规则。

一旦确定您正在进行Web浏览器测试业务, 并且您的 Selenium 环境已经准备好开始编写测试, 您通常会执行以下三个步骤的组合:

  • 设置数据
  • 执行一组离散的操作
  • 评估结果

您需要尽可能缩短这些步骤; 一到两个操作在大多数时间内应该足够了。 浏览器自动化具有“脆弱”的美誉, 但实际上那是因为用户经常对它要求过高。 在后面的章节中,我们将回到您可以使用的技术, 为了缓解测试中明显的间歇性问题, 特别是如何克服 浏览器 和 WebDriver 之间的竞争条件

通过保持测试简短并仅在您完全没有替代方案时使用Web浏览器,您可以用最小的代码片段来完成很多测试。

Selenium测试的一个显著优势是,它能够从用户的角度测试应用程序的所有组件(从后端到前端)。 因此,换句话说,虽然功能测试运行起来可能很昂贵,但它们同时也包含了大量关键业务部分。

测试要求

如前所述,Selenium 测试运行起来可能很昂贵。 在多大程度上取决于您正在运行测试的浏览器, 但历史上浏览器的行为变化太大,以至于通常是针对多个浏览器进行交叉测试的既定目标。

Selenium 允许您在多个操作系统上的多个浏览器上运行相同的指令, 但是对所有可能的浏览器、它们的不同版本以及它们所运行的许多操作系统的枚举将很快成为一项繁重的工作。

让我们从一个例子开始

Larry 写了一个网站,允许用户订购他们自己定制的独角兽。

一般的工作流程(我们称之为“幸福之路”)是这样的:

  • 创建一个账户
  • 配置他们的独角兽
  • 添加到购物车
  • 检验并付款
  • 给出关于他们独角兽的反馈

编写一个宏大的 Selenium 脚本来执行所有这些操作是很诱人的 — 很多人都会尝试这样做。 抵制诱惑! 这样做会导致测试: a) 需要很长时间; b) 会受到一些与页面呈现时间问题有关的常见问题的影响; c) 如果失败,它不会给出一个简洁的、“可检查”的方法来诊断出了什么问题。

测试此场景的首选策略是将其分解为一系列独立的、快速的测试,每个测试都有一个存在的“理由”。

假设您想测试第二步: 配置您的独角兽。 它将执行以下操作:

  • 创建一个帐户
  • 配置一个独角兽

请注意,我们跳过了这些步骤的其余部分, 在完成这一步之后,我们将在其他小的、离散的测试用例中测试工作流的其余部分。

首先,您需要创建一个帐户。在这里您可以做出一些选择:

  • 您想使用现有帐户吗?
  • 您想创建一个新帐户吗?
  • 在配置开始之前,是否需要考虑有任何特殊属性的用户需要吗?

不管您如何回答这个问题, 解决方案是让它成为测试中“设置数据”部分的一部分 - 如果 Larry 公开了一个 API, 使您(或任何人)能够创建和更新用户帐户, 一定要用它来回答这个问题 请确保使用这个 API 来回答这个问题 — 如果可能的话, 您希望只有在您拥有一个用户之后才启动浏览器,您可以使用该用户的凭证进行登录。

如果每个工作流的每个测试都是从创建用户帐户开始的,那么每个测试的执行都会增加许多秒。 调用 API 并与数据库进行通信是快速、“无头”的操作, 不需要打开浏览器、导航到正确页面、点击并等待表单提交等昂贵的过程。

理想情况下,您可以在一行代码中处理这个设置阶段,这些代码将在任何浏览器启动之前执行:

// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
User user = UserFactory.createCommonUser(); //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
AccountPage accountPage = loginAs(user.getEmail(), user.getPassword());
  
# Create a user who has read-only permissions--they can configure a unicorn,
# but they do not have payment information set up, nor do they have
# administrative privileges. At the time the user is created, its email
# address and password are randomly generated--you don't even need to
# know them.
user = user_factory.create_common_user() #This method is defined elsewhere.

# Log in as this user.
# Logging in on this site takes you to your personal "My Account" page, so the
# AccountPage object is returned by the loginAs method, allowing you to then
# perform actions from the AccountPage.
account_page = login_as(user.get_email(), user.get_password())
  
// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
User user = UserFactory.CreateCommonUser(); //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
AccountPage accountPage = LoginAs(user.Email, user.Password);
  
# Create a user who has read-only permissions--they can configure a unicorn,
# but they do not have payment information set up, nor do they have
# administrative privileges. At the time the user is created, its email
# address and password are randomly generated--you don't even need to
# know them.
user = UserFactory.create_common_user #This method is defined elsewhere.

# Log in as this user.
# Logging in on this site takes you to your personal "My Account" page, so the
# AccountPage object is returned by the loginAs method, allowing you to then
# perform actions from the AccountPage.
account_page = login_as(user.email, user.password)
  
// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
var user = userFactory.createCommonUser(); //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
var accountPage = loginAs(user.email, user.password);
  
// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
val user = UserFactory.createCommonUser() //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
val accountPage = loginAs(user.getEmail(), user.getPassword())
  

您可以想象,UserFactory可以扩展为提供诸如createAdminUser()createUserWithPayment()的方法。 关键是,这两行代码不会分散您对此测试的最终目的的注意力: 配置独角兽。

页面对象模型 的复杂性将在后面的章节中讨论,但我们将在这里介绍这个概念:

您的测试应该由操作组成,从用户的角度出发,在站点的页面上下文中执行。 这些页面被存储为对象, 其中包含关于 web 页面如何组成以及如何执行操作的特定信息 — 作为测试人员,您应该很少关注这些信息。

您想要什么样的独角兽? 您可能想要粉红色,但不一定。 紫色最近很流行。 她需要太阳镜吗? 明星纹身? 这些选择虽然困难,但是作为测试人员, 您的主要关注点是 — 您需要确保您的订单履行中心将正确的独角兽发送给正确的人,而这就要从这些选择开始。

请注意,我们在该段落中没有讨论按钮,字段,下拉菜单,单选按钮或 Web 表单。 您的测试也不应该! 您希望像尝试解决问题的用户一样编写代码。 这是一种方法(从前面的例子继续):

// The Unicorn is a top-level Object--it has attributes, which are set here.
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
Unicorn sparkles = new Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS);

// Since we're already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.
AddUnicornPage addUnicornPage = accountPage.addUnicorn();

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
UnicornConfirmationPage unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles);
  
# The Unicorn is a top-level Object--it has attributes, which are set here.
# This only stores the values; it does not fill out any web forms or interact
# with the browser in any way.
sparkles = Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

# Since we're already "on" the account page, we have to use it to get to the
# actual place where you configure unicorns. Calling the "Add Unicorn" method
# takes us there.
add_unicorn_page = account_page.add_unicorn()

# Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
# its createUnicorn() method. This method will take Sparkles' attributes,
# fill out the form, and click submit.
unicorn_confirmation_page = add_unicorn_page.create_unicorn(sparkles)
  
// The Unicorn is a top-level Object--it has attributes, which are set here. 
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
Unicorn sparkles = new Unicorn("Sparkles", UnicornColors.Purple, UnicornAccessories.Sunglasses, UnicornAdornments.StarTattoos);

// Since we are already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.
AddUnicornPage addUnicornPage = accountPage.AddUnicorn();

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
UnicornConfirmationPage unicornConfirmationPage = addUnicornPage.CreateUnicorn(sparkles);
  
# The Unicorn is a top-level Object--it has attributes, which are set here.
# This only stores the values; it does not fill out any web forms or interact
# with the browser in any way.
sparkles = Unicorn.new('Sparkles', UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

# Since we're already "on" the account page, we have to use it to get to the
# actual place where you configure unicorns. Calling the "Add Unicorn" method
# takes us there.
add_unicorn_page = account_page.add_unicorn

# Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
# its createUnicorn() method. This method will take Sparkles' attributes,
# fill out the form, and click submit.
unicorn_confirmation_page = add_unicorn_page.create_unicorn(sparkles)
  
// The Unicorn is a top-level Object--it has attributes, which are set here.
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
var sparkles = new Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS);

// Since we are already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.

var addUnicornPage = accountPage.addUnicorn();

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
var unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles);

  
// The Unicorn is a top-level Object--it has attributes, which are set here. 
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
val sparkles = Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

// Since we are already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.
val addUnicornPage = accountPage.addUnicorn()

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles)

  

既然您已经配置好了独角兽, 您需要进入第三步:确保它确实有效。

// The exists() method from UnicornConfirmationPage will take the Sparkles
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
Assert.assertTrue("Sparkles should have been created, with all attributes intact", unicornConfirmationPage.exists(sparkles));
  
# The exists() method from UnicornConfirmationPage will take the Sparkles
# object--a specification of the attributes you want to see, and compare
# them with the fields on the page.
assert unicorn_confirmation_page.exists(sparkles), "Sparkles should have been created, with all attributes intact"
  
// The exists() method from UnicornConfirmationPage will take the Sparkles 
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
Assert.True(unicornConfirmationPage.Exists(sparkles), "Sparkles should have been created, with all attributes intact");
  
# The exists() method from UnicornConfirmationPage will take the Sparkles
# object--a specification of the attributes you want to see, and compare
# them with the fields on the page.
expect(unicorn_confirmation_page.exists?(sparkles)).to be, 'Sparkles should have been created, with all attributes intact'
  
// The exists() method from UnicornConfirmationPage will take the Sparkles
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
assert(unicornConfirmationPage.exists(sparkles), "Sparkles should have been created, with all attributes intact");

  
// The exists() method from UnicornConfirmationPage will take the Sparkles 
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
assertTrue("Sparkles should have been created, with all attributes intact", unicornConfirmationPage.exists(sparkles))
  

请注意,测试人员在这段代码中除了谈论独角兽之外还没有做任何事情 — 没有按钮、定位器和浏览器控件。 这种对应用程序建模的方法允许您保持这些测试级别的命令不变, 即使 Larry 下周决定不再喜欢 Ruby-on-Rails, 并决定用最新的带有 Fortran 前端的 Haskell 绑定重新实现整个站点。

为了符合站点的重新设计,您的页面对象需要进行一些小的维护,但是这些测试将保持不变。 采用这一基本设计,您将希望继续使用尽可能少的面向浏览器的步骤来完成您的工作流。 您的下一个工作流程将包括在购物车中添加独角兽。 您可能需要多次迭代此测试,以确保购物车正确地保持其状态: 在开始之前,购物车中是否有多个独角兽? 购物车能装多少? 如果您创建多个具有相同名称或特性,它会崩溃吗? 它将只保留现有的一个还是添加另一个?

每次通过工作流时,您都希望尽量避免创建帐户、以用户身份登录和配置独角兽。 理想情况下,您将能够创建一个帐户,并通过 API 或数据库预先配置独角兽。 然后,您只需作为用户登录,找到 Sparkles,并将它添加到购物车中。

是否自动化?

自动化总是有优势吗? 什么时候应该决定去自动化测试用例?

自动化测试用例并不总是有利的. 有时候手动测试可能更合适. 例如,如果应用程序的用户界面,在不久的将来会发生很大变化,那么任何自动化都可能需要重写. 此外,有时根本没有足够的时间来构建自动化测试. 从短期来看,手动测试可能更有效. 如果应用程序的截止日期非常紧迫,当前没有可用的自动化测试,并且必须在特定时间范围内完成,那么手动测试是最好的解决方案.

2 - 测试的类型

验收测试

进行这种类型的测试以确定功能或系统是否满足客户的期望和要求. 这种测试通常涉及客户的合作或反馈, 是一种验证活动, 可以用于回答以下问题:

我们是否在制造 正确的 产品?

对于Web应用程序, 可以通过模拟用户期望的行为 直接使用Selenium来完成此测试的自动化. 可以通过记录/回放, 或通过本文档中介绍的各种支持的语言来完成此类模拟. 注意:有些人可能还会提到, 验收测试是 功能测试 的子类型.

功能测试

进行这种类型的测试是为了确定功能或系统是否正常运行而没有问题. 它会在不同级别检查系统, 以确保涵盖所有方案并且系统能够执行预期的 工作 . 这是一个验证活动, 它回答了以下问题:

我们是否在 正确地 制造产品?

这通常包括: 测试没有错误 (404, 异常…) , 以可用的方式 (正确的重定向) 正常运行, 以可访问的方式并匹配其规格 (请参见前述的 验收测试 ) .

对于Web应用程序, 可以通过模拟预期的结果, 直接使用Selenium来完成此测试的自动化. 可以通过记录/回放或通过本文档中说明的各种支持的语言来完成此模拟.

性能测试

顾名思义, 进行性能测试是为了衡量应用程序的性能.

性能测试主要有两种类型:

负载测试

进行了负载测试, 以验证应用程序在各种特定的负载 (通常是同时连接一定数量的用户) 下的运行状况

压力测试

进行压力测试, 以验证应用程序在压力 (或高于最大支持负载) 下的运行状况.

通常, 性能测试是通过执行一些Selenium书写的测试来完成的, 这些测试模拟了不同的用户 使用Web应用程序的特定功能 并检索了一些有意义的指标.

通常, 这是由其他检索指标的工具完成的.
JMeter 就是这样一种工具.

对于Web应用程序, 要测量的详细信息包括 吞吐量、 延迟、数据丢失、单个组件加载时间…

注意1:所有浏览器的开发人员工具 均具有“性能”标签 (可通过按F12进行访问)

注2:这属于 非功能测试 的类型, 因为它通常是按系统而不是按功能/特征进行测量.

回归测试

此测试通常在修改, 修复或添加功能之后进行.

为了确保所做的更改没有破坏任何现有功能, 将再次执行一些已经执行过的测试.

重新执行的测试集可以是全部或部分, 并且可以包括几种不同的类型, 具体取决于具体的应用程序和开发团队.

测试驱动开发 (TDD)

TDD本身不是一种测试类型, 而是一种迭代开发方法, 用于测试驱动功能的设计.

每个周期都从创建功能应通过的一组单元测试开始 (这将使首次执行失败) .

此后, 进行开发以使测试通过. 在另一个周期开始再次执行测试, 此过程一直持续到所有测试通过为止.

如此做的目的是基于以下情况, 既缺陷发现的时间越早成本越低, 从而加快了应用程序的开发.

行为驱动开发 (BDD)

BDD还是基于上述 (TDD) 的迭代开发方法, 其目的是让各方参与到应用程序的开发中.

每个周期都从创建一些规范开始 (应该失败) . 然后创建会失败的单元测试 (也应该失败) , 之后着手开发.

重复此循环, 直到所有类型的测试通过.

为此, 使用了规范语言. 因为简单、标准和明确, 各方都应该能够理解. 大多数工具都使用 Gherkin 作为这种语言.

目标是为了解决潜在的验收问题, 从而能够检测出比TDD还要多的错误, 并使各方之间的沟通更加顺畅.

当前有一组工具可用于编写规范 并将其与编码功能 (例如 CucumberSpecFlow ) 匹配.

在Selenium之上构建了一系列工具, 可通过将BDD规范直接转换为可执行代码, 使得上述过程更为迅速. 例如 JBehave, Capybara 和 Robot Framework.

3 - PO设计模式

PO(page object)设计模式是在自动化中已经流行起来的一种易于维护和减少代码的设计模式. 在自动化测试中, PO对象作为一个与页面交互的接口. 测试中需要与页面的UI进行交互时, 便调用PO的方法. 这样做的好处是, 如果页面的UI发生了更改,那么测试用例本身不需要更改, 只需更改PO中的代码即可.

PO设计模式具有以下优点:

  • 测试代码与页面的定位代码(如定位器或者其他的映射)相分离.
  • 该页面提供的方法或元素在一个独立的类中, 而不是将这些方法或元素分散在整个测试中.

这允许在一个地方修改由于UI变化所带来的所有修改. 随着这种"测试设计模式"的广泛使用, 可以在众多博客中找到有关此技术的有用信息. 我们鼓励希望了解更多信息的读者在互联网上搜索有关此主题的博客. 许多人已经写过这种设计模式, 并且可以提供超出本用户指南范围的有用提示. 不过, 为了让您入门, 我们将通过一个简单的示例来说明页面对象.

首先, 思考一个不使用PO模式的自动化测试的典型案例:

/***
 * Tests login feature
 */
public class Login {

  public void testLogin() {
    // 在登录页面上填写登录数据
    driver.findElement(By.name("user_name")).sendKeys("testUser");
    driver.findElement(By.name("password")).sendKeys("my supersecret password");
    driver.findElement(By.name("sign-in")).click();

    // 登录后验证h1标签是否为Hello userName
    driver.findElement(By.tagName("h1")).isDisplayed();
    assertThat(driver.findElement(By.tagName("h1")).getText(), is("Hello userName"));
  }
}

这种方法有两个问题.

  • 测试方法与定位器 (在此实例中为By.name)耦合过于严重. 如果测试的用户界面更改了其定位器或登录名的输入和处理方式, 则测试本身必须进行更改.
  • 在对登录页面的所有测试中, 同一个定位器会散布在其中.

可以在以下登录页面的示例中应用PO设计模式重写此示例.

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Sign-in page.
 */
public class SignInPage {
  protected WebDriver driver;

  // <input name="user_name" type="text" value="">
  private By usernameBy = By.name("user_name");
  // <input name="password" type="password" value="">
  private By passwordBy = By.name("password");
  // <input name="sign_in" type="submit" value="SignIn">
  private By signinBy = By.name("sign_in");

  public SignInPage(WebDriver driver){
    this.driver = driver;
  }

  /**
    * Login as valid user
    *
    * @param userName
    * @param password
    * @return HomePage object
    */
  public HomePage loginValidUser(String userName, String password) {
    driver.findElement(usernameBy).sendKeys(userName);
    driver.findElement(passwordBy).sendKeys(password);
    driver.findElement(signinBy).click();
    return new HomePage(driver);
  }
}

Home page的PO如下所示.

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Home Page
 */
public class HomePage {
  protected WebDriver driver;

  // <h1>Hello userName</h1>
  private By messageBy = By.tagName("h1");

  public HomePage(WebDriver driver){
    this.driver = driver;
    if (!driver.getTitle().equals("Home Page of logged in user")) {
      throw new IllegalStateException("This is not Home Page of logged in user," +
            " current page is: " + driver.getCurrentUrl());
    }
  }

  /**
    * Get message (h1 tag)
    *
    * @return String message text
    */
  public String getMessageText() {
    return driver.findElement(messageBy).getText();
  }

  public HomePage manageProfile() {
    // Page encapsulation to manage profile functionality
    return new HomePage(driver);
  }
  /* 提供登录用户主页所代表的服务的更多方法. 这些方法可能会返回更多页面对象. 
  例如, 单击"撰写邮件"按钮可以返回ComposeMail类对象 */
}

那么, 接下来的登录测试用例将使用这两个页面对象.

/***
 * Tests login feature
 */
public class TestLogin {

  @Test
  public void testLogin() {
    SignInPage signInPage = new SignInPage(driver);
    HomePage homePage = signInPage.loginValidUser("userName", "password");
    assertThat(homePage.getMessageText(), is("Hello userName"));
  }

}

PO的设计方式具有很大的灵活性, 但是有一些基本规则可以使测试代码具有理想的可维护性.

PO本身绝不应进行判断或断言. 判断和断言是测试的一部分, 应始终在测试的代码内, 而不是在PO中. PO用来包含页面的表示形式, 以及页面通过方法提供的服务, 但是与PO无关的测试代码不应包含在其中.

实例化PO时, 应进行一次验证, 即验证页面以及页面上可能的关键元素是否已正确加载. 在上面的示例中, SignInPage和HomePage的构造函数均检查预期的页面是否可用并准备接受测试请求.

PO不一定需要代表整个页面. PO设计模式可用于表示页面上的组件. 如果自动化测试中的页面包含多个组件, 则每个组件都有单独的页面对象, 则可以提高可维护性.

还有其他设计模式也可以在测试中使用. 一些使用页面工厂实例化其页面对象. 讨论所有这些都不在本用户指南的范围之内. 在这里, 我们只想介绍一些概念, 以使读者了解可以完成的一些事情. 如前所述, 许多人都在此主题上写博客, 我们鼓励读者搜索有关这些主题的博客.

4 - 领域特定语言

领域特定语言 (DSL) 是一种为用户提供解决问题的表达方式的系统. 它使用户可以按照自己的术语与系统进行交互, 而不仅仅是通过程序员的语言.

您的用户通常并不关心您网站的外观. 他们不在乎装饰, 动画或图形. 他们希望借助于您的系统, 以最小的难度使新员工融入整个流程; 他们想预订去阿拉斯加的旅行; 他们想以折扣价配置和购买独角兽. 您作为测试人员的工作应尽可能接近"捕捉”这种思维定势. 考虑到这一点, 我们开始着手"建模”您正在工作的应用程序, 以使测试脚本 (发布前用户仅有的代理) “说话”并代表用户.

在Selenium中, DSL通常由方法表示, 其编写方式使API简单易读-它们使开发人员和干系人 (用户, 产品负责人, 商业智能专家等) 之间能够产生汇报.

好处

  • 可读: 业务关系人可以理解.
  • 可写: 易于编写, 避免不必要的重复.
  • 可扩展: 可以 (合理地) 添加功能而无需打破约定以及现有功能.
  • 可维护: 通过将实现细节排除在测试用例之外, 您可以很好地隔离 AUT* 的修改.

Java

以下是Java中合理的DSL方法的示例. 为简便起见, 假定 driver 对象是预定义的并且可用于该方法.

/**
 * Takes a username and password, fills out the fields, and clicks "login".
 * @return An instance of the AccountPage
 */
public AccountPage loginAsUser(String username, String password) {
  WebElement loginField = driver.findElement(By.id("loginField"));
  loginField.clear();
  loginField.sendKeys(username);

  // Fill out the password field. The locator we're using is "By.id", and we should
  // have it defined elsewhere in the class.
  WebElement passwordField = driver.findElement(By.id("password"));
  passwordField.clear();
  passwordField.sendKeys(password);

  // Click the login button, which happens to have the id "submit".
  driver.findElement(By.id("submit")).click();

  // Create and return a new instance of the AccountPage (via the built-in Selenium
  // PageFactory).
  return PageFactory.newInstance(AccountPage.class);
}

此方法完全从测试代码中抽象出输入字段, 按钮, 单击甚至页面的概念. 使用这种方法, 测试人员要做的就是调用此方法. 这给您带来了维护方面的优势: 如果登录字段曾经更改过, 则只需更改此方法-而非您的测试.

public void loginTest() {
    loginAsUser("cbrown", "cl0wn3");

    // Now that we're logged in, do some other stuff--since we used a DSL to support
    // our testers, it's as easy as choosing from available methods.
    do.something();
    do.somethingElse();
    Assert.assertTrue("Something should have been done!", something.wasDone());

    // Note that we still haven't referred to a button or web control anywhere in this
    // script...
}

郑重强调: 您的主要目标之一应该是编写一个API, 该API允许您的测试解决 当前的问题, 而不是UI的问题. 用户界面是用户的次要问题–用户并不关心用户界面, 他们只是想完成工作. 您的测试脚本应该像用户希望做的事情以及他们想知道的事情的完整清单那样易于阅读. 测试不应该考虑UI如何要求您去做.

*AUT: 待测系统

5 - 生成应用程序状态

Selenium不应用于准备测试用例. 测试用例中所有重复性动作和准备工作, 都应通过其他方法来完成.
例如, 大多数Web UI都具有身份验证 (诸如一个登录表单) . 在每次测试之前通过Web浏览器进行登录的消除, 将提高测试的速度和稳定性. 应该创建一种方法来获取对 AUT* 的访问权限 (例如, 使用API登录并设置Cookie) . 此外, 不应使用Selenium创建预加载数据来进行测试的方法.
如前所述, 应利用现有的API为 AUT* 创建数据. ***AUT**: 待测系统

6 - 模拟外部服务

消除对外部服务的依赖性将大大提高测试的速度和稳定性.

7 - 改善报告

Selenium并非旨在报告测试用例的运行状态. 利用单元测试框架的内置报告功能是一个好的开始. 大多数单元测试框架都有可以生成xUnit或HTML格式的报告. xUnit报表很受欢迎, 可以将其结果导入到持续集成(CI)服务器, 例如Jenkins、Travis、Bamboo等. 以下是一些链接, 可获取关于几种语言报表输出的更多信息.

NUnit 3 Console Runner

NUnit 3 Console Command Line

xUnit getting test results in TeamCity

xUnit getting test results in CruiseControl.NET

xUnit getting test results in Azure DevOps

8 - 避免共享状态

尽管在多个地方都提到过, 但这点仍值得被再次提及. 确保测试相互隔离.

  • 不要共享测试数据. 想象一下有几个测试, 每个测试都会在选择操作执行之前查询数据库中的有效订单. 如果两个测试采用相同的顺序, 则很可能会出现意外行为.

  • 清理应用程序中过时的数据, 这些数据可能会被其他测试. 例如无效的订单记录.

  • 每次测试都创建一个新的WebDriver实例. 这在确保测试隔离的同时可以保障并行化更为简单.

9 - 测试的独立性

将每个测试编写为独立的单元. 以不依赖于其他测试完成的方式编写测试:

例如有一个内容管理系统, 您可以借助其创建一些自定义内容, 这些内容在发布后作为模块显示在您的网站上, 并且CMS和应用程序之间的同步可能需要一些时间.

测试模块的一种错误方法是在测试中创建并发布内容, 然后在另一测试中检查该模块. 这是不可取的, 因为发布后内容可能无法立即用于其他测试.

与之相反的事, 您可以创建在受影响的测试中打开和关闭的打桩内容, 并将其用于验证模块. 而且, 对于内容的创建, 您仍然可以进行单独的测试.

10 - 考虑使用Fluent API

Martin Fowler创造了术语 “Fluent API”. Selenium已经在其 FluentWait 类中实现了类似的东西, 这是对标准 Wait 类的替代. 您可以在页面对象中启用Fluent API设计模式, 然后使用如下代码段查询Google搜索页面:

driver.get( "http://www.google.com/webhp?hl=en&amp;tab=ww" );
GoogleSearchPage gsp = new GoogleSearchPage();
gsp.withFluent().setSearchString().clickSearchButton();

Google页面对象类具有这种流畅行为后可能看起来像这样:

public class GoogleSearchPage extends LoadableComponent<GoogleSearchPage> {
  private final WebDriver driver;
  private GSPFluentInterface gspfi;

  public class GSPFluentInterface {
    private GoogleSearchPage gsp;

    public GSPFluentInterface(GoogleSearchPage googleSearchPage) {
        gsp = googleSearchPage;
    }

    public GSPFluentInterface clickSearchButton() {
        gsp.searchButton.click();
        return this;
    }

    public GSPFluentInterface setSearchString( String sstr ) {
        clearAndType( gsp.searchField, sstr );
        return this;
    }
  }

  @FindBy(id = "gbqfq") private WebElement searchField;
  @FindBy(id = "gbqfb") private WebElement searchButton;
  public GoogleSearchPage(WebDriver driver) {
    gspfi = new GSPFluentInterface( this );
    this.get(); // If load() fails, calls isLoaded() until page is finished loading
    PageFactory.initElements(driver, this); // Initialize WebElements on page
  }

  public GSPFluentInterface withFluent() {
    return gspfi;
  }

  public void clickSearchButton() {
    searchButton.click();
  }

  public void setSearchString( String sstr ) {
    clearAndType( searchField, sstr );
  }

  @Override
  protected void isLoaded() throws Error {
    Assert.assertTrue("Google search page is not yet loaded.", isSearchFieldVisible() );
  }

  @Override
  protected void load() {
    if ( isSFieldPresent ) {
      Wait<WebDriver> wait = new WebDriverWait( driver, Duration.ofSeconds(3) );
      wait.until( visibilityOfElementLocated( By.id("gbqfq") ) ).click();
    }
  }
}

11 - 每次测试都刷新浏览器

每次测试都从一个干净的已知状态开始. 理想情况下, 为每次测试打开一个新的虚拟机. 如果打开新虚拟机不切实际, 则至少应为每次测试启动一个新的WebDriver. 对于Firefox, 请使用您已知的配置文件去启动WebDriver.

FirefoxProfile profile = new FirefoxProfile(new File("pathToFirefoxProfile"));
WebDriver driver = new FirefoxDriver(profile);