![最终产品图片](https://i-blog.csdnimg.cn/blog_migrate/351ac609873d8f8dd77ea6c5d9de405a.png)
什么是页面对象?
我先给你简短的发言。 这是一种封装标记和页面交互的设计模式,特别是重构您的功能规格。 它是两种非常常见的重构技术的组合: 提取类和提取方法 —不必同时发生,因为您可以逐步构建并通过新的Page Object提取整个类。
此技术使您可以编写具有很高表现力和DRY的高级功能规范。 在某种程度上,它们是使用应用程序语言的验收测试。 您可能会问,用Capybara编写的规范是否已经具有较高的表达能力? 当然,对于每天编写代码的开发人员而言,水豚规范可以很好地阅读。 它们开箱即用吗? 并非如此-实际上,当然不是!
feature 'M creates a mission' do
scenario 'successfully' do
sign_in_as 'M@mi6.com'
visit missions_path
click_on 'Create Mission'
fill_in 'Mission Name', with: 'Project Moonraker'
click_button 'Submit'
expect(page).to have_css 'li.mission-name', text: 'Project Moonraker'
end
end
feature 'M marks mission as complete' do
scenario 'successfully' do
sign_in_as 'M@mi6.com'
visit missions_path
click_on 'Create Mission'
fill_in 'Mission Name', with: 'Octopussy'
click_button 'Submit'
within "li:contains('Octopussy')" do
click_on 'Mission completed'
end
expect(page).to have_css 'ul.missions li.mission-name.completed', text: 'Octopussy'
end
end
当您查看这些功能规格示例时,在哪里看到使阅读效果更好的机会,以及如何提取信息以避免重复? 此外,此级别是否足以对用户故事进行轻松建模并让非技术利益相关者理解?
在我看来,有两种方法可以改善这种情况并使每个人都满意:开发人员可以避免在应用OOP时不理会与DOM交互的细节,而其他非编码团队成员则可以在用户故事之间轻松切换和这些测试。 当然,最后一点很高兴,但是最重要的好处主要来自使DOM交互规范更可靠。
封装是页面对象的关键概念。 编写功能规格时,您将受益于提取测试流程中行为的策略。 对于高质量的代码,您希望捕获页面上与特定元素集的交互,尤其是如果您偶然发现重复的模式时。 随着应用程序的增长,您希望/需要一种避免在整个规范中散布该逻辑的方法。
”那不是太夸张了吗? 水豚读得很好!” 你说?
问自己:为什么在进行更稳定的测试时,为什么不将所有HTML实现细节都放在一个地方? UI交互测试为什么不应该具有与应用程序代码测试相同的质量? 您真的要停在那儿吗?
由于每天的变化,您的Capybara代码在遍及各处时很容易受到攻击-它会引入可能的断点。 假设设计师想更改按钮上的文字。 没关系,对吗? 但是,您是要在规格中的某个元素的中央包装中适应这种变化,还是希望在整个地方都做到这一点? 我是这么想的!
您的功能规格可能有很多重构,但是Page Objects提供了最简洁的抽象,用于封装页面或更复杂的流程的面向用户的行为。 但是,您不必模拟整个页面-专注于用户流程所必需的基本位。 无需过度操作!
验收测试/功能规格
在深入探讨问题之前,我想退后一步,向刚开始整个测试业务的新手介绍一下,并清除一些在这种情况下很重要的术语。 如果您对TDD更加熟悉,那么他们将不会错过很多。
我们在说什么呢? 验收测试通常在项目的后期进行,以评估您是否正在为用户,产品所有者或其他利益相关者建立有价值的东西。 这些测试通常由客户或您的用户运行。 这是对是否满足要求的一种检查。 各种测试层都有类似金字塔的东西,验收测试几乎是最顶层的。 由于此过程通常包括非技术人员,因此编写这些测试的高级语言是来回交流的宝贵资产。
另一方面,功能规格在测试食物链中要低一些。 比单元测试更高级,单元测试侧重于模型的技术细节和业务逻辑,功能规格描述页面之间和页面之间的流程。
像Capybara这样的工具可帮助您避免手动执行此操作,这意味着您几乎不必打开浏览器即可手动测试内容。 通过这些类型的测试,我们希望尽可能自动地执行这些任务,并在编写针对页面的断言时通过浏览器测试驱动交互。 顺便说一句,您不会像处理请求规范那样使用get
, put
, post
或delete
。
功能规格与验收测试非常相似,有时我觉得差异太模糊了,无法真正关心术语。 您编写测试整个应用程序的测试,该测试通常涉及多个步骤的用户操作。 这些交互测试显示了您的组件在组合在一起时是否和谐地工作。
在Ruby领域,当我们处理Page Objects时,它们是主要角色。 功能规范本身已经具有很高的表达力,但是可以通过将其数据,行为和标记提取到一个或多个单独的类中来对其进行优化和清理。
我希望清除这种模糊的术语可以帮助您看到拥有Page Objects有点像在编写功能规格时进行验收级别测试。
水豚
也许我们也应该很快地解决这个问题。 该库将自己描述为“ Web应用程序的验收测试框架”。 您可以通过功能强大且方便的特定于域的语言来模拟用户与页面的交互。 我个人认为,RSpec与Capybara配对是目前编写功能规格的最佳方法。 它使您可以访问页面,填写表单,单击链接和按钮,并在页面上查找标记,并且可以轻松地将所有这些命令组合在一起,以通过测试与页面进行交互。
您基本上可以避免自己打开浏览器来大部分时间手动进行测试,这不仅不那么优雅,而且耗时且容易出错。 如果没有此工具,“外部测试”过程(即您将代码从高级测试驱动到单元级测试)将变得更加痛苦,因此可能会被忽略。
换句话说,您开始根据您的用户故事编写这些功能测试,然后从那里开始,直到您的单元测试提供了所需的功能规格。 此后,当您的测试当然是绿色的时,游戏将重新开始,然后您返回以继续进行新的功能测试。
怎么样?
让我们看一下功能规格的两个简单示例,这些示例使M创建可以完成的机密任务。
在标记,你的任务列表,并成功完成创建一个额外的类completed
对li
特定的使命。 简单的东西,对不对? 作为第一种方法,我从很小的非常常见的重构开始,这些重构将常见的行为提取到方法中。
spec / features / m_creates_a_mission_spec.rb
require 'rails_helper'
feature 'M creates mission' do
scenario 'successfully' do
sign_in_as 'M@mi6.com'
create_classified_mission_named 'Project Moonraker'
agent_sees_mission 'Project Moonraker'
end
def create_classified_mission_named(mission_name)
visit missions_path
click_on 'Create Mission'
fill_in 'Mission Name', with: mission_name
click_button 'Submit'
end
def agent_sees_mission(mission_name)
expect(page).to have_css 'li.mission-name', text: mission_name
end
def sign_in_as(email)
visit root_path
fill_in 'Email', with: email
click_button 'Submit'
end
end
规格/功能/agent_completes_a_mission_spec.rb
require 'rails_helper'
feature 'M marks mission as complete' do
scenario 'successfully' do
sign_in_as 'M@mi6.com'
create_classified_mission_named 'Project Moonraker'
mark_mission_as_complete 'Project Moonraker'
agent_sees_completed_mission 'Project Moonraker'
end
def create_classified_mission_named(mission_name)
visit missions_path
click_on 'Create Mission'
fill_in 'Mission Name', with: mission_name
click_button 'Submit'
end
def mark_mission_as_complete(mission_name)
within "li:contains('#{mission_name}')" do
click_on 'Mission completed'
end
end
def agent_sees_completed_mission(mission_name)
expect(page).to have_css 'ul.missions li.mission-name.completed', text: mission_name
end
def sign_in_as(email)
visit root_path
fill_in 'Email', with: email
click_button 'Submit'
end
end
当然,尽管还有其他方法可以处理诸如sign_in_as
, create_classified_mission_named
类的东西,但是很容易看出这些东西开始吸收和堆积的速度。
我认为与UI相关的规范通常没有得到他们需要/应得的OO处理。 他们的声誉是提供很少的回报,当然,开发人员对需要大量修改标记的东西并不喜欢。 在我看来,这使得删除这些规范变得更加重要,并通过抛出几个Ruby类使其变得有趣起来。
让我们做一个魔术,这里我暂时隐藏Page Object的实现,只向您显示应用于上述功能规范的最终结果:
spec / features / m_creates_a_mission_spec.rb
require 'rails_helper'
feature 'M creates mission' do
scenario 'successfully' do
sign_in_as 'M@mi6.com'
visit missions_path
mission_page = Pages::Missions.new
mission_page.create_classified_mission_named 'Project Moonraker'
expect(mission_page).to have_mission_named 'Project Moonraker'
end
end
规格/功能/agent_completes_a_mission_spec.rb
require 'rails_helper'
feature 'M marks mission as complete' do
scenario 'successfully' do
sign_in_as 'M@mi6.com'
visit missions_path
mission_page = Pages::Missions.new
mission_page.create_classified_mission_named 'Project Moonraker'
mission_page.mark_mission_as_complete 'Project Moonraker'
expect(mission_page).to have_completed_mission_named 'Project Moonraker'
end
end
读起来还不错吧? 您基本上是在Page对象上创建表达性包装方法,这些方法使您可以处理高级概念-而不是一直在各处摆弄标记的肠子。 您现在提取的方法会做这种肮脏的工作,而shot弹枪手术不再是您的问题。
换句话说,您封装了大多数杂乱无章的DOM交互代码。 不过,我不得不说,有时在功能规格中以智能方式提取的方法就足够了,并且读起来更好一些,因为您可以避免处理Page Object实例。 无论如何,让我们看一下实现:
规格/支持/功能/页面/missions.rb
module Pages
class Missions
include Capybara::DSL
def create_classified_mission_named(mission_name)
click_on 'Create Mission'
fill_in 'Mission name', with: mission_name
click_button 'Submit'
end
def mark_mission_as_complete(mission_name)
within "li:contains('#{mission_name}')" do
click_on 'Mission completed'
end
end
def has_mission_named?(mission_name)
mission_list.has_css? 'li', text: mission_name
end
def has_completed_mission_named?(mission_name)
mission_list.has_css? 'li.mission-name.completed', text: mission_name
end
private
def mission_list
find('ul.missions')
end
end
end
您所看到的是一个普通的旧Ruby对象-页面对象本质上是非常简单的类。 通常,您不使用数据实例化Page Objects(当然,可以在需要时使用数据),而是主要通过API来创建一种语言,该语言可以由用户或团队中的非技术利益相关者使用。 当您考虑命名您的方法时,我认为最好问自己一个问题:用户将如何描述流程或所采取的措施?
我也许应该补充一点,如果不包括Capybara,音乐会很快停止。
include Capybara::DSL
您可能想知道这些自定义匹配器如何工作:
expect(page).to have_mission_named 'Project Moonraker'
expect(page).to have_completed_mission_named 'Project Moonraker'
def has_mission_named?(mission_name)
...
end
def has_completed_mission_named?(mission_name)
...
end
RSpec基于页面对象上的谓词方法生成这些自定义匹配器。 RSpec通过删除?
转换它们?
并且has
have
改变。 景气,从无到有从零开始的匹配器! 有点神奇,我给你,但是我会说是一种很好的巫术。
由于我们将Page对象停放在了specs/support/features/pages/missions.rb
,因此您还需要确保spec/rails_helper.rb
中的以下内容未被注释掉。
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
如果您遇到带有uninitialized constant Pages
的NameError
,您将知道该怎么办。
如果您想知道sign_in_as
方法发生了什么,我将其提取到spec/support/sign_in_helper.rb
的模块中,并告诉RSpec包含该模块。 这与Page Objects直接无关,与通过Page Object相比,以更全局可访问的方式存储测试功能(如sign in
更加有意义。
spec / support / sign_in_helper.rb
module SignInHelper
def sign_in_as(email)
visit root_path
fill_in 'Email', with: email
click_button 'Submit'
end
end
您需要让RSpec知道您要访问此帮助程序模块:
spec / spec_helper.rb
...
require 'support/sign_in_helper'
RSpec.configure do |config|
config.include SignInHelper
...
end
总体而言,很容易看出我们成功隐藏了水豚的详细信息,例如查找元素,单击链接等。我们现在可以专注于功能,而不再关注标记的实际结构,该标记现在封装在Page Object中—当您测试功能规格如此高级的东西时,DOM结构应该是您最不关心的事情。
注意!
诸如工厂数据之类的安装程序属于规范而非页面对象。 另外,断言可能最好放在页面对象之外,以实现关注点分离。
关于该主题有两种不同的观点。 支持将断言放入Page Objects的拥护者说,这样做有助于避免断言的重复。 您可以提供更好的错误消息并获得更好的“告诉,不要问”样式。 另一方面,主张无断言的Page Objects的主张者最好不要混用责任。 提供对页面数据和断言逻辑的访问是两个独立的问题,并且在混合时会导致页面对象膨胀。 页面对象的责任是访问页面状态,并且断言逻辑属于规范。
页面对象类型
组件代表最小的单元,并且更加集中-例如,像表单对象。
页面结合了更多这些组件,并且是整个页面的抽象。
正如您现在所猜测的那样, 体验跨越了可能跨越许多不同页面的整个流程。 他们是更高层次的。 他们专注于用户在与各种页面进行交互时所经历的流程。 考虑几个步骤的结帐流程就是一个很好的例子。
什么时候和为什么?
在项目的生命周期中稍晚些时候应用这种设计模式是个好主意—当您在功能规格中积累了一点点复杂性时,并且当您可以识别重复的模式(例如DOM结构,提取的方法或其他常见的)时,页面上保持一致。
因此,您可能不应该立即开始编写Page Objects。 当您的应用程序/测试的复杂性和规模不断增长时,您将逐步进行这些重构。 随着时间的流逝,需要页面对象更好地进行复制和重构的位置将变得更加容易。
我的建议是从本地功能规范中的提取方法开始。 一旦达到临界质量,它们看起来就很像是进一步提取的候选对象,并且大多数可能会适合Page Objects的配置文件。 从小做起,因为过早的优化会留下讨厌的咬痕!
最后的想法
页面对象为您提供了编写更清晰的规范的机会,这些规范阅读得更好,并且总体上更具表现力,因为它们具有更高的层次。 除此之外,它们为喜欢编写OO代码的每个人提供了一个很好的抽象。 它们隐藏了DOM的详细信息,还使您能够拥有私有方法,这些方法在不公开给公共API的情况下仍能完成肮脏的工作。 功能规格中的提取方法不能提供完全相同的效果。 Page Objects的API不需要共享实质性的Capybara详细信息。
对于设计实现发生变化的所有场景,使用页面对象时无需更改对应用程序工作方式的描述,因为您的功能规格更着重于用户级别的交互,并且不太关心具体细节DOM实现。 由于更改是不可避免的,因此页面对象在应用程序增长时变得至关重要,并且在应用程序的绝对规模意味着复杂性急剧增加时也有助于理解。
翻译自: https://code.tutsplus.com/articles/ruby-page-objects-for-capybara-connoisseurs--cms-25204