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 一起使用的更好模式。

  1. 让控制器的创建和更新操作调用服务而不是直接调用 Active Record。

  2. 您的服务采用新模型对象并保存或更新它们。他们 return 一个包含以下键的结果哈希 [success, model]

  3. 如果你的服务成功了{success: true, model: model}

  4. 如果您的服务失败{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