简体   繁体   English

Rails RSpec,DRY规范:共享示例与帮助程序方法与自定义匹配器

[英]Rails RSpec, DRY specs: shared example vs. helper method vs. custom matcher

I have the following test repeated once for each HTTP method/controller action combination within a controller spec: 对于控制器规范中的每个HTTP方法/控制器操作组合,我将以下测试重复一次:

it "requires authentication" do
  get :show, id: project.id
  # Unauthenticated users should be redirected to the login page
  expect(response).to redirect_to new_user_session_path
end

I've found the three following ways to refactor it and eliminate repetition. 我发现了以下三种重构和消除重复的方法。 Which one is the most appropriate? 哪一个最合适?

Shared Example 共享的例子

It seems to me that shared examples are the most appropriate solution. 在我看来,共享示例是最合适的解决方案。 However, having to use a block in order to pass the params to the shared example feels a bit awkward. 但是,必须使用一个块才能将params传递给共享示例感到有点尴尬。

shared_examples "requires authentication" do |http_method, action|
  it "requires authentication" do
    process(action, http_method.to_s, params)
    expect(response).to redirect_to new_user_session_path
  end
end

RSpec.describe ProjectsController, type: :controller do
  describe "GET show", :focus do
    let(:project) { Project.create(name: "Project Rigpa") }

    include_examples "requires authentication", :GET, :show do
      let(:params) { {id: project.id} }
    end
  end
end

Helper Method 辅助方法

This has the advantage of not requiring a block to pass project.id to the helper method. 这具有不需要块将project.id传递给helper方法的优点。

RSpec.describe ProjectsController, type: :controller do
  def require_authentication(http_method, action, params)
    process(action, http_method.to_s, params)
    expect(response).to redirect_to new_user_session_path
  end

  describe "GET show", :focus do
    let(:project) { Project.create(name: "Project Rigpa") }

    it "requires authentication" do
      require_authentication(:GET, :show, id: project.id )
    end
  end
end

Custom Matcher 自定义匹配器

It would be nice to have a single-line test. 进行单行测试会很好。

RSpec::Matchers.define :require_authentication do |http_method, action, params|
  match do
    process(action, http_method.to_s, params)
    expect(response).to redirect_to Rails.application.routes.url_helpers.new_user_session_path
  end
end

RSpec.describe ProjectsController, type: :controller do
  describe "GET show", :focus do
    let(:project) { Project.create(name: "Project Rigpa") }

    it { is_expected.to require_authentication(:GET, :show, {id: project.id}) }
  end
end

Thanks in advance. 提前致谢。

In the case you describe, I would go for RSpec Custom Matchers . 在您描述的情况下,我将选择RSpec Custom Matchers They keep your specs easier to read and closer to the domain of you application. 它们使您的规范更易于阅读,并且更接近您的应用程序领域。 https://relishapp.com/rspec/rspec-expectations/v/2-4/docs/custom-matchers/define-matcher https://relishapp.com/rspec/rspec-expectations/v/2-4/docs/custom-matchers/define-matcher

I would use shared_examples to specify more complex scenarios and call it_behaves_like to check it all at once in different contexts. 我将使用shared_examples指定更复杂的场景,并调用it_behaves_like在不同的上下文中一次检查所有内容。

You should try to avoid helper methods if possible and only use them in a single file if it helps keep your specs clean. 您应该尽可能避免使用辅助方法,并且只有在有助于保持规范的情况下才在单个文件中使用它们。

A suggestion provided by didroe in this Reddit post got me thinking that placing the method/action call ( process ) within shared code is not a good idea as it increases complexity (reduces readability) and does not actually reduce code duplication. didroe在此Reddit帖子中提供的建议使我想到,将方法/操作调用( process )放在共享代码中不是一个好主意,因为它增加了复杂性(降低了可读性),并且实际上并未减少代码重复。

After searching some more, I have found what I believe to be best option in the Everyday Rails Testing with RSpec by Aaron Sumner book (p. 102). 经过更多搜索之后,我发现在Aaron Sumner撰写的《 使用RSpec进行日常Rails测试 》(第102页)中,我认为是最好的选择。

Create the following custom matcher: 创建以下自定义匹配器:

# spec/support/matchers/require_login.rb
RSpec::Matchers.define :require_login do |expected|
  match do |actual|
    expect(actual).to redirect_to \
      Rails.application.routes.url_helpers.new_user_session_path
  end

  failure_message do |actual|
    "expected to require login to access the method"
  end

  failure_message_when_negated do |actual|
    "expected not to require login to access the method"
  end

  description do
    "redirect to the login form"
  end
end

And use a test like the following for each action of each controller: 并对每个控制器的每个动作使用如下测试:

it "requires authentication" do
  get :show, id: project.id
  expect(response).to require_login
end

Compared to repeating expect(response).to redirect_to new_user_session_path in all tests, this approach has the following advantages: 与在所有测试中重复expect(response).to redirect_to new_user_session_path相比,此方法具有以下优点:

  • Improved maintanability. 改善了可维护性。 If we eventually have to change this assertion, we change it in one place instead of having to change dozens or hundreds of tests. 如果最终不得不更改此断言,则可以在一处更改它,而不必更改数十或数百个测试。
  • Better failure messages. 更好的失败消息。

What do you think? 你怎么看?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM