Ruby Rails 黄瓜故障排除
Ruby Rails Cucumber Troubleshooting
我第一次尝试使用 Cucumber 构建应用程序,使用 Rails 5.2.6、Rspec、Capybara 和 Factory bot。
我成功完成了我的第一个功能,其中包含使用设计进行身份验证的场景。
更新
通过下面详述的一系列故障排除步骤,问题是控制器集合 @cocktails 不知何故仅在黄瓜测试中没有传递给视图。
使用rails服务器,没问题。
我检查了一下,@cocktails 只出现在控制器和视图上。所以它不会被覆盖或删除,至少不会直接被覆盖。
它在单元 RSpec 测试和 rails 服务器中工作。
黄瓜测试怎么没通过?谁能看出为什么它不能从控制器传递到测试?
但是我在我的第二个特征文件中遇到了 CRUD 功能的障碍。给定的第一个步骤使用 FactoryBot 创建 2 个项目,登录用户并直接转到这两个项目的索引。但是该页面显示的是视图,而不是使用确认的 2 个创建的项目:
放page.body
在黄瓜文件中
当我在实际应用程序上创建它们时,它运行正常,直接进入索引并显示它们。
所以我想弄清楚如何解决这个问题。我的第一个想法是找到一种方法来确认 FactoryBot 创建了这 2 个项目。我的第二个想法是确认它们实际上是为用户设置的。我尝试过使用puts来显示两个创建的对象或用户,但我还没有想出如何在cucumber中调用它们。
这是步骤文件:
Given('I have populated a cocktail list for this User') do
FactoryBot.create(:cocktail,
:user => @registered_user,
:name => "Frank Wallbanger",
:ingredients => "Lots of Booze, a pinch of lime")
FactoryBot.create(:cocktail,
:user => @registered_user,
:name => "Fuzzy Naval Orange",
:ingredients => "Lots of Tequila, a pinch of orange")
end
When('I visit the website and log in') do
expect(page).to have_content("Signed in successfully")
end
Then('I will see the cocktail list') do
puts page.body
expect(page).to have_content("Frank Wallbanger")
expect(page).to have_content("Fuzzy Naval Orange")
end
这是我的RSpec单元测试文件及其绿色
require "rails_helper"
RSpec.describe CocktailsController do
let(:user) { instance_double(User) }
before { log_in(user) }
describe "GET #index" do
let(:cocktails) { [
instance_double(Cocktail),
instance_double(Cocktail)
] }
before do
allow(user).to receive(:cocktails).and_return(cocktails)
get :index
end
it "looks up all cocktails that belong to the current user" do
expect(assigns(:cocktails)).to eq(cocktails)
end
end
end
这是rails_helper.rb
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# 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 = true
# You can uncomment this line to turn off ActiveRecord support entirely.
# config.use_active_record = false
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, type: :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
# https://relishapp.com/rspec/rspec-rails/docs
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :active_record
with.library :active_model
with.library :action_controller
with.library :rails
end
end
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
require "support/controller_helpers"
RSpec.configure do |config|
config.include Warden::Test::Helpers
config.include Devise::Test::ControllerHelpers, :type => :controller
config.include ControllerHelpers, :type => :controller
end
这是我正在测试的控制器:
class CocktailsController < ApplicationController
before_action :set_cocktail, only: %i[ show edit update destroy ]
# GET /cocktails or /cocktails.json
def index
@cocktails = current_user.cocktails
end
# GET /cocktails/1 or /cocktails/1.json
def show
end
# GET /cocktails/new
def new
@cocktail = Cocktail.new
render :new
end
# GET /cocktails/1/edit
def edit
end
# POST /cocktails or /cocktails.json
def create
@cocktail = Cocktail.new cocktail_params.merge(user: current_user)
if @cocktail.save
redirect_to cocktails_path
else
render :new
end
end
# PATCH/PUT /cocktails/1 or /cocktails/1.json
def update
respond_to do |format|
if @cocktail.update(cocktail_params)
format.html { redirect_to @cocktail, notice: "Cocktail was successfully updated." }
format.json { render :show, status: :ok, location: @cocktail }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @cocktail.errors, status: :unprocessable_entity }
end
end
end
# DELETE /cocktails/1 or /cocktails/1.json
def destroy
@cocktail.destroy
respond_to do |format|
format.html { redirect_to cocktails_url, notice: "Cocktail was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_cocktail
@cocktail = Cocktail.find(params[:id])
end
# Only allow a list of trusted parameters through.
def cocktail_params
params.require(:cocktail).permit(:name, :ingredients, :user)
end
end
我对此完全陌生,所以如果您需要查看其他文件,请告诉我,或者如果我在错误区域放置了一个块。
至少,我只是在寻找如何显示这些值。用户登录 运行s 使用前面的步骤文件,其中创建了 @registered_user 并且它似乎坚持确认“已成功登录”的第二步。因此,我最有可能怀疑的是未创建或未分配给用户的项目。
谢谢
这是 index.html.erb 视图
<h1>Cocktails</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Ingredients</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @cocktails.each do |cocktail| %>
<tr>
<td><%= cocktail.name %></td>
<td><%= cocktail.ingredients %></td>
<td><%= cocktail.user_id %></td>
<td><%= link_to 'Show', cocktail %></td>
<td><%= link_to 'Edit', edit_cocktail_path(cocktail) %></td>
<td><%= link_to 'Destroy', cocktail, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Cocktail', new_cocktail_path %>
<%= button_to 'Log out', destroy_user_session_path, :method => :delete %>
所以,根据Lam下面的建议,我尝试了这个尝试来验证。
我进入 rails 控制台,并尝试 运行 第一个 FactoryBot.create 从步骤文件。这产生了用户@registered_user 不存在的错误。这在控制台中是有意义的。所以我无法验证。但是,如果我从现有用户那里创建了@registered_user,那么该片段会成功地对 Frank Wallbanger 鸡尾酒做出反应。所以 FactoryBot 似乎可以正常工作。那么问题可能出在@registered_user上吗?
所以我在步骤文件中添加了“puts @registered_user”,它确实在黄瓜输出中打印了一条活动记录。
当我 运行 rails 服务器并显示该视图时,我看到列出了鸡尾酒。但它在步骤文件中“puts page.body”行的黄瓜输出区域仍然空白。
更新
好的,我想我已经找到原因了。但不是修复。
在控制器索引
@cocktails = current_user.cocktails
正在 rails 服务器上工作,但它未填充在 cucumber 测试中。
我将此代码添加到视图
<% if @cocktails.any? %>
<%= @cocktails.first.name %>
<% else %>
<p> none </p>
<% end %>
那为什么它没有通过呢?我尝试将控制器更改为:
@cocktails = Cocktail.all
这也行不通。
好的,我终于明白了。
我需要在步骤文件的 Then 步骤中添加访问 root_path。终于一切都好了。
Then('I will see the cocktail list') do
visit root_path
expect(page).to have_content("Frank Wallbanger")
expect(page).to have_content("Fuzzy Naval Orange")
end
我要啤酒.....
你应该避免在 cuking 时使用 Factory Bot。下面是与 Rails 一起使用的更好模式。
让控制器的创建和更新操作调用服务而不是直接调用 Active Record。
您的服务采用新模型对象并保存或更新它们。他们 return 一个包含以下键的结果哈希 [success, model]
如果你的服务成功了{success: true, model: model}
如果您的服务失败{success: false, model: model(with errors),...}
您的服务为您创建了一个地方来处理创建事件时发生的业务逻辑,例如发送电子邮件、审核事件、后台长 运行 进程等
现在您可以通过
在您的 Cuking 中快速创建内容
a) 使用参数初始化合适的模型对象
b) 调用服务并传入模型
理想情况下,您可以在步骤定义中调用的辅助方法中执行此操作。
这取代了 FactoryBot,同时确保您创建的所有对象都使用您的应用程序使用的相同业务逻辑正确创建。它非常快(比工厂机器人快一点,比使用 UI 快得多)。它通过配置 FactoryBot 来匹配您的应用程序业务逻辑,从而避免您必须复制业务逻辑。最后,与仅依赖 ActiveModel 回调相比,它在应用业务逻辑方面提供了更大的灵活性。
这为您提供了 CRUD 模式。当您开始新事物时,您将拥有
Scenario: Create a foo
Given ...
When I create a new foo
Then I should have a new foo
这会带动model,controller,controller#new,view#new,form,controller#create,controller#show view#show的开发
一旦你有了这些并遵循了上面的模式,你就可以在你的给定中使用 foo 例如
Given there is a foo
When I bar with my foo
...
并且您可以在没有用户交互且不需要工厂的情况下创建 foo。
例如
module FooSH
def create_foo(foo: )
Result = CreateFooService.new(foo: foo).call
Result.model
end
def create_default_foo
Result = CreateFooService.new(foo: Foo.new(default_foo_params).call
Result.model
end
def default_foo_params
{
}
end
end
World FooSH
然后是你的步骤
Given 'there is a foo' do
@foo = create_default_foo
end
您可以将自己的样式和模式应用到此,并以多种方式扩展对象创建。你可以在这个相当古老的仓库中看到一个例子 https://github.com/diabolo/cuke_up
我第一次尝试使用 Cucumber 构建应用程序,使用 Rails 5.2.6、Rspec、Capybara 和 Factory bot。
我成功完成了我的第一个功能,其中包含使用设计进行身份验证的场景。
更新
通过下面详述的一系列故障排除步骤,问题是控制器集合 @cocktails 不知何故仅在黄瓜测试中没有传递给视图。
使用rails服务器,没问题。
我检查了一下,@cocktails 只出现在控制器和视图上。所以它不会被覆盖或删除,至少不会直接被覆盖。
它在单元 RSpec 测试和 rails 服务器中工作。
黄瓜测试怎么没通过?谁能看出为什么它不能从控制器传递到测试?
但是我在我的第二个特征文件中遇到了 CRUD 功能的障碍。给定的第一个步骤使用 FactoryBot 创建 2 个项目,登录用户并直接转到这两个项目的索引。但是该页面显示的是视图,而不是使用确认的 2 个创建的项目:
放page.body
在黄瓜文件中
当我在实际应用程序上创建它们时,它运行正常,直接进入索引并显示它们。
所以我想弄清楚如何解决这个问题。我的第一个想法是找到一种方法来确认 FactoryBot 创建了这 2 个项目。我的第二个想法是确认它们实际上是为用户设置的。我尝试过使用puts来显示两个创建的对象或用户,但我还没有想出如何在cucumber中调用它们。
这是步骤文件:
Given('I have populated a cocktail list for this User') do
FactoryBot.create(:cocktail,
:user => @registered_user,
:name => "Frank Wallbanger",
:ingredients => "Lots of Booze, a pinch of lime")
FactoryBot.create(:cocktail,
:user => @registered_user,
:name => "Fuzzy Naval Orange",
:ingredients => "Lots of Tequila, a pinch of orange")
end
When('I visit the website and log in') do
expect(page).to have_content("Signed in successfully")
end
Then('I will see the cocktail list') do
puts page.body
expect(page).to have_content("Frank Wallbanger")
expect(page).to have_content("Fuzzy Naval Orange")
end
这是我的RSpec单元测试文件及其绿色
require "rails_helper"
RSpec.describe CocktailsController do
let(:user) { instance_double(User) }
before { log_in(user) }
describe "GET #index" do
let(:cocktails) { [
instance_double(Cocktail),
instance_double(Cocktail)
] }
before do
allow(user).to receive(:cocktails).and_return(cocktails)
get :index
end
it "looks up all cocktails that belong to the current user" do
expect(assigns(:cocktails)).to eq(cocktails)
end
end
end
这是rails_helper.rb
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# 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 = true
# You can uncomment this line to turn off ActiveRecord support entirely.
# config.use_active_record = false
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, type: :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
# https://relishapp.com/rspec/rspec-rails/docs
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :active_record
with.library :active_model
with.library :action_controller
with.library :rails
end
end
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
require "support/controller_helpers"
RSpec.configure do |config|
config.include Warden::Test::Helpers
config.include Devise::Test::ControllerHelpers, :type => :controller
config.include ControllerHelpers, :type => :controller
end
这是我正在测试的控制器:
class CocktailsController < ApplicationController
before_action :set_cocktail, only: %i[ show edit update destroy ]
# GET /cocktails or /cocktails.json
def index
@cocktails = current_user.cocktails
end
# GET /cocktails/1 or /cocktails/1.json
def show
end
# GET /cocktails/new
def new
@cocktail = Cocktail.new
render :new
end
# GET /cocktails/1/edit
def edit
end
# POST /cocktails or /cocktails.json
def create
@cocktail = Cocktail.new cocktail_params.merge(user: current_user)
if @cocktail.save
redirect_to cocktails_path
else
render :new
end
end
# PATCH/PUT /cocktails/1 or /cocktails/1.json
def update
respond_to do |format|
if @cocktail.update(cocktail_params)
format.html { redirect_to @cocktail, notice: "Cocktail was successfully updated." }
format.json { render :show, status: :ok, location: @cocktail }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @cocktail.errors, status: :unprocessable_entity }
end
end
end
# DELETE /cocktails/1 or /cocktails/1.json
def destroy
@cocktail.destroy
respond_to do |format|
format.html { redirect_to cocktails_url, notice: "Cocktail was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_cocktail
@cocktail = Cocktail.find(params[:id])
end
# Only allow a list of trusted parameters through.
def cocktail_params
params.require(:cocktail).permit(:name, :ingredients, :user)
end
end
我对此完全陌生,所以如果您需要查看其他文件,请告诉我,或者如果我在错误区域放置了一个块。
至少,我只是在寻找如何显示这些值。用户登录 运行s 使用前面的步骤文件,其中创建了 @registered_user 并且它似乎坚持确认“已成功登录”的第二步。因此,我最有可能怀疑的是未创建或未分配给用户的项目。
谢谢
这是 index.html.erb 视图
<h1>Cocktails</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Ingredients</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @cocktails.each do |cocktail| %>
<tr>
<td><%= cocktail.name %></td>
<td><%= cocktail.ingredients %></td>
<td><%= cocktail.user_id %></td>
<td><%= link_to 'Show', cocktail %></td>
<td><%= link_to 'Edit', edit_cocktail_path(cocktail) %></td>
<td><%= link_to 'Destroy', cocktail, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Cocktail', new_cocktail_path %>
<%= button_to 'Log out', destroy_user_session_path, :method => :delete %>
所以,根据Lam下面的建议,我尝试了这个尝试来验证。
我进入 rails 控制台,并尝试 运行 第一个 FactoryBot.create 从步骤文件。这产生了用户@registered_user 不存在的错误。这在控制台中是有意义的。所以我无法验证。但是,如果我从现有用户那里创建了@registered_user,那么该片段会成功地对 Frank Wallbanger 鸡尾酒做出反应。所以 FactoryBot 似乎可以正常工作。那么问题可能出在@registered_user上吗?
所以我在步骤文件中添加了“puts @registered_user”,它确实在黄瓜输出中打印了一条活动记录。
当我 运行 rails 服务器并显示该视图时,我看到列出了鸡尾酒。但它在步骤文件中“puts page.body”行的黄瓜输出区域仍然空白。
更新
好的,我想我已经找到原因了。但不是修复。 在控制器索引
@cocktails = current_user.cocktails
正在 rails 服务器上工作,但它未填充在 cucumber 测试中。 我将此代码添加到视图
<% if @cocktails.any? %>
<%= @cocktails.first.name %>
<% else %>
<p> none </p>
<% end %>
那为什么它没有通过呢?我尝试将控制器更改为:
@cocktails = Cocktail.all
这也行不通。
好的,我终于明白了。
我需要在步骤文件的 Then 步骤中添加访问 root_path。终于一切都好了。
Then('I will see the cocktail list') do
visit root_path
expect(page).to have_content("Frank Wallbanger")
expect(page).to have_content("Fuzzy Naval Orange")
end
我要啤酒.....
你应该避免在 cuking 时使用 Factory Bot。下面是与 Rails 一起使用的更好模式。
让控制器的创建和更新操作调用服务而不是直接调用 Active Record。
您的服务采用新模型对象并保存或更新它们。他们 return 一个包含以下键的结果哈希 [success, model]
如果你的服务成功了{success: true, model: model}
如果您的服务失败{success: false, model: model(with errors),...}
您的服务为您创建了一个地方来处理创建事件时发生的业务逻辑,例如发送电子邮件、审核事件、后台长 运行 进程等
现在您可以通过
在您的 Cuking 中快速创建内容a) 使用参数初始化合适的模型对象 b) 调用服务并传入模型
理想情况下,您可以在步骤定义中调用的辅助方法中执行此操作。
这取代了 FactoryBot,同时确保您创建的所有对象都使用您的应用程序使用的相同业务逻辑正确创建。它非常快(比工厂机器人快一点,比使用 UI 快得多)。它通过配置 FactoryBot 来匹配您的应用程序业务逻辑,从而避免您必须复制业务逻辑。最后,与仅依赖 ActiveModel 回调相比,它在应用业务逻辑方面提供了更大的灵活性。
这为您提供了 CRUD 模式。当您开始新事物时,您将拥有
Scenario: Create a foo
Given ...
When I create a new foo
Then I should have a new foo
这会带动model,controller,controller#new,view#new,form,controller#create,controller#show view#show的开发
一旦你有了这些并遵循了上面的模式,你就可以在你的给定中使用 foo 例如
Given there is a foo
When I bar with my foo
...
并且您可以在没有用户交互且不需要工厂的情况下创建 foo。
例如
module FooSH
def create_foo(foo: )
Result = CreateFooService.new(foo: foo).call
Result.model
end
def create_default_foo
Result = CreateFooService.new(foo: Foo.new(default_foo_params).call
Result.model
end
def default_foo_params
{
}
end
end
World FooSH
然后是你的步骤
Given 'there is a foo' do
@foo = create_default_foo
end
您可以将自己的样式和模式应用到此,并以多种方式扩展对象创建。你可以在这个相当古老的仓库中看到一个例子 https://github.com/diabolo/cuke_up