Rails 与子域的集成测试,RSpec 和 capybara-webkit(对于 JavaScript)

Rails integration tests with subdomains, RSpec and capybara-webkit (for JavaScript)

我在使用 capybara-webkit 进行集成测试时遇到问题。该问题与使用子域直接相关。

我所有的正常(非 JS)集成测试都在工作,但我不太清楚如何让它为多租户应用程序工作。 我将公寓 gem 用于多租户,它使用 PostgreSQL 模式来分割用户数据。用户可以注册,选择子域,然后访问该应用程序。

申请设置如下:

我的 accounts_controller 上有一个 before_action 调用私有方法 load_schema 切换到当前子域或重定向到 root_url.

def load_schema
  Apartment::Tenant.switch!('public')
  return unless request.subdomain.present?

  if current_account
    Apartment::Tenant.switch!(current_account.subdomain)
  else
    redirect_to root_url(subdomain: false)
  end
end

def current_account
  @current_account ||= Account.find_by(subdomain: request.subdomain)
end
helper_method :current_account

在我的所有 RSpec 功能规范中(目前正在通过但未使用 JavaScript),我 运行 一个 sign_user_in 方法来让用户登录:

def sign_user_in(user, opts={})
  if opts[:subdomain]
    visit new_user_session_url(subdomain: opts[:subdomain])
  else
    visit new_user_session_path
  end

  fill_in 'user[email]', with: user.email
  fill_in 'user[password]', with: (opts[:password] || user.password)
  click_button 'Log in'
end

我的所有规格都通过了这种方法。

然而,当我 运行 开启 :js 的规范时,它永远无法找到 'user[email]'。该视图使用 bootstrap 模式弹出创建或编辑操作,然后 AJAX(通过远程:true)在页面上添加或更新新创建的资源。我目前试图通过的一个简单的功能规范是:

require 'rails_helper'

feature 'user creates product' do
  let(:user) { build(:user) }
  let(:account) { create(:account_with_schema, owner: user) }

  scenario 'successfully', :js do
    product = build_stubbed(:product, name: 'Test Product', amazon_sku: 'test_sku', price: 9.99)
    sign_user_in(user, subdomain: account.subdomain)
    click_on 'Products'
    click_on 'New Product'
    fill_in 'product[name]', with: product.name
    fill_in 'product[amazon_sku]', with: product.amazon_sku
    fill_in 'product[price]', with: product.price
    click_on 'Create Product'

    expect(page).to have_content('Test Product')
  end
end

在我的日志中,它显示它正在尝试连接到 subdomain1.example。com/users/sign_in:

Received "Visit(http://subdomain1.example.com/users/sign_in)" 
Started "Visit(http://subdomain1.example.com/users/sign_in)" 
Load started 
"Visit(http://subdomain1.example.com/users/sign_in)" started page load 
Started request to "http://subdomain1.example.com/users/sign_in" 
Finished "Visit(http://subdomain1.example.com/users/sign_in)" with response "Success()" 
Received 200 from "http://subdomain1.example.com/users/sign_in" 

这个子域 1 是基于我的帐户工厂的序列,例如:

factory :account do
    sequence(:subdomain) { |n| "subdomain#{n}" }
...

当然这会失败,因为它无法连接到 subdomain1.example.com:

Failures:

  1) user creates product successfully
     Failure/Error: fill_in 'user[email]', with: user.email

     Capybara::ElementNotFound:
       Unable to find field "user[email]"

当我在本地测试时,我使用 lvh.me 在本地测试子域(因为您不能在本地主机上执行子域)。

因为我看到了这些错误,所以我将以下内容添加到我的 rails_helper.rb:

Capybara::Webkit.configure do |config|
  config.debug = true
  config.allow_unknown_urls
  config.allow_url("lvh.me")
  config.allow_url("*.lvh.me")
end

但这仍然导致相同的结果。经过一番谷歌搜索后,我发现了一个问题,提到使用 "path" 与 "url"。例如,在我的 sign_user_in 助手

而不是:

  if opts[:subdomain]
    visit new_user_session_url(subdomain: opts[:subdomain])
  else

使用:

  if opts[:subdomain]
    visit new_user_session_path(subdomain: opts[:subdomain])
  else

当我这样做时,我似乎更接近了,因为日志现在显示如下:

Received "AllowUrl(*.lvh.me)" 
Started "AllowUrl(*.lvh.me)" 
Finished "AllowUrl(*.lvh.me)" with response "Success()" 
Wrote response true "" 
Received "Visit(http://lvh.me:3000/users/sign_in)" 
Started "Visit(http://lvh.me:3000/users/sign_in)" 

太好了,进步了。虽然它不包括子域,即使它正在传入。此外,这会导致其他 5 个测试全部失败并出现 RoutingError:

例如:

  5) user authentication does not allow user from one subdomain to sign in on another subdomain
     Failure/Error: visit new_user_session_path(subdomain: opts[:subdomain])

     ActionController::RoutingError:
       No route matches [GET] "/users/sign_in"

即使传入子域,它也会被忽略,除非有子域,否则该路由不可用。

在谷歌搜索并弄清楚如何为 lvh.me 设置 config.allow_url 部分后,我还发现我应该将其添加到我的 development.rb 文件中,以便知道端口:

Capybara.always_include_port = 真

这是有效的,因为上面的日志输出显示它正在使用端口 3000。

接下来,我将 sign_user_in 方法更改为再次使用 visit new_user_session_url(subdomain: opts[:subdomain]) (这样我之前的规范仍然可以通过)。按照这个 SO 线程的建议:Capybara with subdomains - default_host 我对我的规范做了一个调整:

before(:each) do
  set_host "lvh.me:3000"
end

def set_host (host)
  default_url_options[:host] = host
  Capybara.app_host = "http://" + host
end

现在,当我 运行 规范时,我在日志中看到以下内容:

Received "AllowUrl(*.lvh.me)" 
Started "AllowUrl(*.lvh.me)" 
Finished "AllowUrl(*.lvh.me)" with response "Success()" 
Wrote response true "" 
Received "Visit(http://subdomain1.lvh.me:3000/users/sign_in)" 
Started "Visit(http://subdomain1.lvh.me:3000/users/sign_in)" 
Load started 
"Visit(http://subdomain1.lvh.me:3000/users/sign_in)" started page load 
Started request to "http://subdomain1.lvh.me:3000/users/sign_in" 
Finished "Visit(http://subdomain1.lvh.me:3000/users/sign_in)" with response "Success()" 
Started request to "http://lvh.me:3000/" 
Received 302 from "http://subdomain1.lvh.me:3000/users/sign_in" 
Started request to "http://lvh.me:3000/assets/application.self-e7adbbd6d89b36b8d2524d4a3bbcb85ee152c7a2641271423c86da07df306565.css?body=1" 
Started request to "http://lvh.me:3000/assets/jquery.self-660adc51e0224b731d29f575a6f1ec167ba08ad06ed5deca4f1e8654c135bf4c.js?body=1" 
Started request to "http://lvh.me:3000/assets/bootstrap/transition.self-6ad2488465135ab731a045a8ebbe3ea2fc501aed286042496eda1664fdd07ba9.js?body=1"

更多进步!它现在包括子域、端口并指向端口 3000。不过我仍然遇到相同的错误:

  1) user creates product successfully
     Failure/Error: fill_in 'user[email]', with: user.email

     Capybara::ElementNotFound:
       Unable to find field "user[email]"

那个 SO 线程的评论之一说:

"this works perfectly. Also, if you're using a public domain like lvh.me you can set the port automatically using Capybara.server_port = 31234, and then set_host "lvh.me:31234

所以在我的 rails_helper 中,我在设置 app_host 的下方添加了 server_port:

Capybara.app_host = 'http://lvh.me/'
Capybara.server_port = 31234

并将 before(:each) 更改为使用端口 31234。结果相同:

"Visit(http://subdomain6.lvh.me:31234/users/sign_in)" started page load 
Started request to "http://subdomain6.lvh.me:31234/users/sign_in" 
Finished "Visit(http://subdomain6.lvh.me:31234/users/sign_in)" with response "Success()" 
Started request to "http://lvh.me:31234/" 
Received 302 from "http://subdomain6.lvh.me:31234/users/sign_in" 
Started request to "http://lvh.me:31234/assets/application-2f17abe5cd0f04e7f5455c4ae0a6e536b5d84dd05e600178874c6a5938ac0804.css" 
Started request to "http://lvh.me:31234/assets/application-8e1c2330cf761b5bfefcaa648b8994224c7c6a87b2f76475831c76474ddca9d1.js" 
Received 200 from "http://lvh.me:31234/" 
Received 200 from "http://lvh.me:31234/assets/application-8e1c2330cf761b5bfefcaa648b8994224c7c6a87b2f76475831c76474ddca9d1.js" 
Received 200 from "http://lvh.me:31234/assets/application-2f17abe5cd0f04e7f5455c4ae0a6e536b5d84dd05e600178874c6a5938ac0804.css" 
...
Also see this repeated about 100 times (in this and in all previous examples):
Wrote response true "" 
Received "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[normalize-space(string(.)) = 'user[email]']/@for)] | .//label[normalize-space(string(.)) = 'user[email]']//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" 
Started "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[normalize-space(string(.)) = 'user[email]']/@for)] | .//label[normalize-space(string(.)) = 'user[email]']//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" 
Finished "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[normalize-space(string(.)) = 'user[email]']/@for)] | .//label[normalize-space(string(.)) = 'user[email]']//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" with response "Success()" 
Wrote response true "" 
Received "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[contains(normalize-space(string(.)), 'user[email]')]/@for)] | .//label[contains(normalize-space(string(.)), 'user[email]')]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" 
Started "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[contains(normalize-space(string(.)), 'user[email]')]/@for)] | .//label[contains(normalize-space(string(.)), 'user[email]')]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" 
Finished "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[contains(normalize-space(string(.)), 'user[email]')]/@for)] | .//label[contains(normalize-space(string(.)), 'user[email]')]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" with response "Success()" 
...

但是,唉,得到相同的结果:

Failures:

  1) user creates product successfully
     Failure/Error: fill_in 'user[email]', with: user.email

     Capybara::ElementNotFound:
       Unable to find field "user[email]"

看起来它应该可以工作,但一定有我遗漏的东西。任何帮助将不胜感激。

从你的日志来看,你的请求似乎是“http://subdomain6.lvh.me:31234/users/sign_in" is redirecting to "http://lvh.me:31234/" which would happen if the account didn't exist. I'm guessing you haven't disabled transactional testing which would mean the app can't actually see the records created in your test thread. See - https://github.com/jnicklas/capybara#transactions-and-database-setup and https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example