不可靠/不稳定的 Capybara/AngularJS 集成测试与计时问题

ori*_*ion 5 integration-testing rspec capybara phantomjs poltergeist

如何使这些测试可靠地通过?

目前这些测试很不稳定。
有时他们会过去。有时他们会失败。
下面是演示此问题的设置、代码和输出。
克服这个问题的建议将不胜感激,我相信也会帮助许多其他人,所以请发表评论!

测试代码环境

  1. 轨道3.2
  2. RSpec 2.x
  3. 水豚
  4. 恶作剧鬼
  5. PhantomJS
  6. AngularJS
  7. 谷歌浏览器版本 47.0.2526.106(64 位)

从 Gemfile.lock 测试 Gem

capybara (2.1.0)
database_cleaner (0.7.1)
debug_inspector (0.0.2)
guard-bundler (0.1.3)
guard-livereload (1.2.0)
guard-rspec (2.1.2)
jasminerice (0.0.10)
pg (0.17.1)
phantomjs (2.1.1.0)
poltergeist (1.4.1)
protractor-rails (0.0.17)
pry (0.9.12)
rack (1.4.7)
rack-test (0.6.3)
rails (3.2.21)
rails-assets-angular (1.3.20)
rspec-rails (2.11.4)
simplecov (0.8.2)
sprockets (2.2.3)
zeus (0.13.3)
zeus-parallel_tests (0.2.1)
Run Code Online (Sandbox Code Playgroud)

我尝试过的事情

  1. 确保我使用 Capybara 的等待 DSL 匹配器
  2. 确保我的数据库清理器设置正确
  3. 测试每个页面项目,假设它可能不在页面上并且仍然可以加载
  4. 缩小不一致的测试范围
  5. 单独运行不一致的测试
  6. 识别触发不一致测试结果的 Capybara DSL 代码。

    • 即创建一条新记录并假设页面已重定向并且该记录位于页面 click_on 上

    或者

    • .click 不一致“工作”
  7. 将 Capybara 升级到最新版本(在单独的分支中)
  8. 将 Poltergeist 和 RSpec 升级到最新版本(在单独的分支中,仍在处理此问题)

我使用的资源

[1] Capybara DSL
[2] Capybara、PhantomJs、Poltergeist 和 Rspec 技巧
还有更多...

测试是如何进行的

rspec spec/integration/costings/show_costing_spec.rb --format documentation

测试代码

显示_costing_spec.rb
require "spec_helper"

RSpec.describe "Show a new costing in the listing," do

  before :each do
    admin_sign_in
    create_costing("test1")
  end

  it "shows the costing after creation" do
    within "#costings_table" do
      expect(page).to have_css("#name", text: "test1")
    end
  end

  it "shows the details of the new costing after creation" do
    expect(page).to have_content("Costings")
    within "#costings_table" do
      expect(page).to have_content("test1")
      all("#show").last.click
    end

    expect(page).to have_content("Costing Details")
    expect(page).to have_css("#name", text: "test1")
  end
end  
Run Code Online (Sandbox Code Playgroud) 规范助手.rb
# This file is copied to spec/ when you run 'rails generate r spec:install'  
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
# Add library functions here so we can test them.
require File.expand_path(File.dirname(__FILE__) + "/../lib/general")
require 'rspec/rails'
require 'rspec/autorun'

# Integration Testing
require 'capybara/poltergeist'
Capybara.register_driver :poltergeist_debug do |app|
 Capybara::Poltergeist::Driver.new(app, :inspector => true)  
end
Capybara.javascript_driver = :poltergeist_debug
Capybara.default_driver = :poltergeist_debug

# Capybara Integration Test Helpers
def admin_sign_in
  visit "/login"
  #Create staff member in database
  Staff.make!(:admin)
  #Log In
  fill_in "staff_username", with: "adminstaff"
  fill_in "staff_password", with: "password"
  click_button "login"
end

def create_costing(item)
  visit "/api#/costings"
  click_on "new_btn"
  within "#form_costing" do
    find("#name", match: :first).set("#{item}")
    find("#description", match: :first).set("test description")    
    find("#from_date", match: :first).set("15/02/2016")
    find("#cost_hourly_cents", match: :first).set("1.00")
    click_on "create_btn"
  end
end

RSpec.configure do |config|
  config.before(:suite) do
    # Requires supporting ruby files with custom matchers and macros, etc,
    # in spec/support/ and its subdirectories.
    require File.expand_path(File.dirname(__FILE__) + "/support/blueprints")
    Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
  end

  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # Allow a 'focus' tag so that we can run just a few tests which we are currently working on
  config.treat_symbols_as_metadata_keys_with_true_values = true
  config.filter_run focus: true
  config.run_all_when_everything_filtered = true
  config.filter_run_excluding :slow unless ENV["SLOW_SPECS"]

  # Defer Garbage Collection
  config.before(:all) { DeferredGarbageCollection.start }
  config.after(:all)  { DeferredGarbageCollection.reconsider }

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = false
  # config.infer_spec_type_from_file_location!

  # Configure Database Cleaner
  config.include Capybara::DSL
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end
Run Code Online (Sandbox Code Playgroud)

检测结果

Test Run 1: Failing

运行选项: include {:focus=>true} except {:slow=>true}

所有的例子都被过滤掉了;忽略 {:focus=>true}

在列表中显示新成本核算,显示创建后的成本核算 显示创建后新成本核算的详细信息(失败 - 1)

失败:

1) 在列表中显示新的成本核算,
显示创建后新成本核算的详细信息
失败/错误:expect(page).to have_content("test1")
预期 #has_content?("test1") 返回 true,得到 false
# ./spec/integration/costings/show_costing_spec.rb:20:in 块(3 个级别)中
# ./spec/integration/costings/show_costing_spec.rb:19:in 块(2 个级别)中

在 5.46 秒内完成 2 个示例,1 个失败

Test Run 2: Passing

运行选项: include {:focus=>true} except {:slow=>true}

所有的例子都被过滤掉了;忽略 {:focus=>true}

在列表中显示新的成本核算,
显示创建后的成本核算
显示创建后的新成本核算的详细信息

3.57 秒完成 2 个示例,0 次失败

更新1

将测试 gem 升级到以下版本:
capybara (2.6.2) from (2.1.0)
database_cleaner (1.5.1) from (0.7.1)
debug_inspector (0.0.2)
Guard-bundler (0.1.3)
Guard-livereload ( 1.2.0)
guard-spec(2.1.2)
jasminerice(0.0.10)
pg(0.17.1)
phantomjs(2.1.1.0)
poltergeist(1.9.0)来自(1.4.1)
protractor-rails(0.0.17)
从(0.9.12)
机架(1.4.7)
机架测试(0.6.3)
轨道(3.2.21)
轨道资产角(1.4.9)从(1.3.20)
rspec-撬(0.10.3)来自 (2.11.4)
simplecov (0.8.2)
链轮 (2.2.3)
zeus (0.13.3)
zeus-parallel_tests (0.2.1) 的导轨 (3.4.2)

Result 1

不幸的是,升级这些宝石似乎并没有产生什么影响,而且我的测试仍然不稳定。

更新2

我实施了汤姆·沃波尔的建议。确保我的 admin_sign_in 等待 Sign_in 完成。

还按照汤姆的建议更新了我的database_cleaner设置。

Result 2

对于我的堆栈来说,这些更改似乎没有效果。

注意:如果不使用 AngularJS,我觉得这些更改会产生影响。谢谢汤姆的建议。

更新3

我需要获得有关测试运行期间发生的情况的更多信息。网上有一些建议来记录、使用屏幕截图保存宝石等,但我不觉得这些是最省时的。我想指定测试在哪里暂停并查看变量和表单字段的内容。在浏览器中是理想的选择。

我使用的
我使用“save_and_open_page”和“print page.html”进行调试。

我的迁移内容
当我运行 RSpec 3.4.2 时,我添加了一个调试帮助器方法:

Rails_helper.rb

def debugit
  puts current_url
  require 'pry'
  binding.pry
end
Run Code Online (Sandbox Code Playgroud)

Result 3

URL 将打印在控制台中,并且测试将暂停。在此阶段,我将能够导航到 URL、登录、导航到测试页面并查看 Capybara 测试已完成的操作。

这使我能够确定问题的根源是在使用水豚的 fill_in DSL 进行测试时出现的。在某些测试运行中,字段将被正确填充并且表单将被提交。在另一种情况下,表单可以正确填写,但提交按钮会被点击得太快。这里的结果是创建了一条记录,但名称和描述的输入字段未保留。

更新4

我发现因为我使用的是 AngularJS 输入表单和表格,所以 AngularJS 需要一点时间来绑定到输入字段。如果这次不允许,则不会保存输入数据。

Capybara提供了“within”、“find”等等待方法。我使用了这些,但它们对解决 AngularJS 绑定时间问题没有帮助。我发现 ng-if 可以用来创建一个 if 语句来等待特定项目,该项目将表示 AngularJS 与表单字段的绑定完成。

所以我使用 Capybara 等待方法来等待我想要填充的字段,并使用 AngularJS 的 ng-if 在字段准备好之前不显示字段。

实施
index.html.erb

<div  ng-if="tableParams.data">
  <table id="costings_table ng-table="tableParams" class="table">
    <td id="field1">{{table.field1}}</td>
    <td id="field2">{{table.field2}}</td>
  </table>
</div>
Run Code Online (Sandbox Code Playgroud)

Result 4

测试终于通过了!然而,我拥有所有这些带有 xpath 的查找方法,确保等待特定且难以定位的项目...

更新5

尽管在我的 gemfile 中我运行的是 gem phantomJS 版本 2.1.1,但我的命令行版本仅为 1.X。事实证明这意义重大。

我将命令行 phantomJS 版本更新为 2.1.1。同时,我确保所有输入框、按钮、表格、标题都有唯一的 ID。然后我能够删除所有 find(:xpath) 出现而不会破坏测试。

Result 5

这套测试现在一直可靠地通过!正是我想要的!是的!

ori*_*ion 1

问题

当测试使用 capybara 的 fill_in DSL 时出现了问题。在某些测试运行中,字段将被正确填充并且表单将被提交。在另一种情况下,表单可以正确填写,但提交按钮会被点击得太快。这里的结果是创建了一条记录,但名称和描述的输入字段未保留。

1.填写表单时,确保AngularJS绑定完整,并使用水豚等待方法

AngularJS 的 ng-if 语句需要在表单字段准备好之前不显示。
这需要结合使用 Capybara 等待方法来完成,以确保仅在表单加载完成后提交 fill_in 字段。

index.html.erb 或同等内容:

<div  ng-if="tableParams.data">
  <table id="costings_table ng-table="tableParams" class="table">
    <td id="field1">{{table.field1}}</td>
    <td id="field2">{{table.field2}}</td>
   </table>
</div>
Run Code Online (Sandbox Code Playgroud)

2.更新PhantomJS命令行版本至最新(2.1.1)

这似乎使得测试能够在没有那么多水豚等待方法的情况下运行,从而实现可靠的测试。

更新的测试代码
show_costing_spec.rb

require "rails_helper"

RSpec.describe "Show a new costing in the listing,", :type => :feature do

  before :each do
    admin_sign_in
    create_costing("test1")
  end

  it "shows the costing after creation" do
    within "#costings_table" do
      expect(page.find("#code2")).to have_content("2")
      expect(page.find("#name2")).to have_content("test1")
    end
  end

  it "shows the details of the new costing after creation" do
    within "#costings_table" do
      click_on "show2"
    end

    expect(page.find("#page_title")).to have_content("Costing Details")
    expect(page.find("#code")).to have_content("2")
    expect(page.find("#name")).to have_content("test1") 
    expect(page.find("#description")).to have_content("test description")
  end
end
Run Code Online (Sandbox Code Playgroud)

Rails_helper.rb

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)

# Add library functions here so we can test them.
require File.expand_path(File.dirname(__FILE__) + "/../lib/general")

require 'rspec/rails'
require 'devise'

RSpec.configure do |config|
  config.before(:suite) do
    # Requires supporting ruby files with custom matchers and macros, etc,
    # in spec/support/ and its subdirectories.
    require File.expand_path(File.dirname(__FILE__) + "/support/blueprints")
    Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

    # Setup Devise before it is used in rails_helper
    config.include Devise::TestHelpers, :type => :controller
    Devise.stretches = 1 # Improves speed.
end

config.include Capybara::DSL, :type => :feature
  config.mock_with :rspec

# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"

# Allow a 'focus' tag so that we can run just a few tests which we are currently working on
config.filter_run focus: true
config.run_all_when_everything_filtered = true
config.filter_run_excluding :slow unless ENV["SLOW_SPECS"]

# Defer Garbage Collection
config.before(:all) { DeferredGarbageCollection.start }
config.after(:all)  { DeferredGarbageCollection.reconsider }

# Integration Testing
require 'capybara/rspec'
require 'capybara/poltergeist'

Capybara.register_driver :poltergeist_debug do |app|
  Capybara::Poltergeist::Driver.new(app, {:inspector => true, js_errors: false })  
end

Capybara.javascript_driver = :poltergeist_debug
Capybara.default_driver = :poltergeist_debug

# Debugging tools
def debugit
  puts current_url
  require 'pry'
  binding.pry
end

# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = false

#Show Deprications As Errors with full backtracing
config.raise_errors_for_deprecations!

#rest of the file....
# Final part of Configure Database Cleaner

Capybara.default_max_wait_time = 5
config.use_transactional_fixtures = false

config.before(:suite) do
  if config.use_transactional_fixtures?
    raise(<<-MSG)
      Delete line `config.use_transactional_fixtures = true` from
      rails_helper.rb (or set it to false) to prevent uncommitted
      transactions being used in JavaScript-dependent specs. During 
      testing, the app-under-test that the browser driver connects to 
      uses a different database connection to the database connection 
      used by the spec. The app's database connection would not be 
      able to access uncommitted transaction data setup over the 
      spec's database connection.
     MSG
  end
  DatabaseCleaner.clean_with(:truncation)
end  

config.before(:each) do
  DatabaseCleaner.strategy = :transaction
end

config.before(:each, type: :feature) do
  # :rack_test driver's Rack app under test shares database connection
  # with the specs, so continue to use transaction strategy for speed.
  driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

    if !driver_shares_db_connection_with_specs
      # Driver is probably for an external browser with an app
      # under test that does *not* share a database connection with the
      # specs, so use truncation strategy.
      DatabaseCleaner.strategy = :truncation
    end
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.append_after(:each) do
    DatabaseCleaner.clean
  end
end


def admin_sign_in
  visit "/login"

  #Create staff member in database
  Staff.make!(:admin)

  #Log In
  fill_in "staff_username", with: "adminstaff"
  fill_in "staff_password", with: "password"
  click_button "login"

  expect(page).to have_text('Logout')
end

def create_costing(item)
  @item = item
  visit "/api#/costings"

  expect(page).to have_selector("#new_btn")
  click_on "new_btn"

  expect(page).to have_text("New Costing")
  within "#form_costing" do
    fill_in "name", with: "#{@item}"
    fill_in "description", with: "test description"
    fill_in "from_date1", with: "15/02/2015" 
    fill_in "cost_hourly_cents1", with: "12.00"

    expect(page).to have_selector("#create_btn")
    click_on "create_btn"
  end
  expect(page.find("#page_title")).to have_content("Costings")
end
Run Code Online (Sandbox Code Playgroud)