使用 RSPEC 进行测试时,如何重用 FactorBot 工厂来构建请求属性?

How to re-use FactorBot factories to build request attributes when testing with RSPEC?

提问人:user1185081 提问时间:10/31/2023 更新时间:11/1/2023 访问量:38

问:

我正在重构一个基于 Rails 5.2.4 的应用程序,使用 rspec 3.12。我专注于尚不存在的请求测试。

在为 Playgrounds 控制器定义请求测试时,我希望重用有助于验证 Playground 模型的 Factories。我在途中遇到了几个错误,我现在偶然发现了以前使用的登录功能。以下是第一个请求:

 RSpec.describe "/playgrounds", type: :request do
  
  # Playground. As you add validations to Playground, be sure to
  # adjust the attributes here as well.

  let(:valid_pg) {FactoryBot.create(:playground)}
  let(:invalid_pg) {FactoryBot.create(:playground, status_id: nil)} # Invalid as status is mandatory
  let(:valid_attributes) { valid_pg.attributes.except("id") } # Exclude id as it is generated
  let(:invalid_attributes) { invalid_pg.attributes.except("id") }

# Login as used in the features validation
before do
    get "/users/sign_in"
    test_user = FactoryBot.create(:user, is_admin: true)
    login_as test_user, scope: :user
end

  describe "GET /index" do
    it "1-renders a successful response" do
      valid_attributes["code"].next!

      puts valid_attributes
      puts invalid_attributes
            puts playgrounds_url
      Playground.create! valid_attributes
      get playgrounds_url
      expect(response).to be_successful
    end
  end

  describe "GET /show" do
    it "2-renders a successful response" do
      valid_attributes["code"].next!

      playground = Playground.create! valid_attributes
      get playground_url(playground)
      expect(response).to be_successful
    end
  end

对于每个测试,都会引发以下错误:

 Failure/Error: login_as test_user, scope: :user

 NoMethodError:
   undefined method `login_as' for #<RSpec::ExampleGroups::Playgrounds::GETIndex:0x000001808eda7130>

然后我有 2 个问题:

  1. 在这里重用工厂听起来是避免 DRY 的好主意。是吗?
  2. 如何在请求的上下文中使用 login_as 方法?

感谢您的帮助!

Ruby-on-Rails rspec

评论

0赞 max 10/31/2023
顺便说一句,我想说的是,如果你使用工厂来测试你的验证,你就做错了。验证测试应该只使用,并且你应该只通过你在该特定测试中测试的任何属性。您正在使用工厂这一事实使我确信您正在使用“地毯式轰炸”方法(),这种方法毫无用处,因为它很容易出现误报。ModelName.newexpect(model.valid?).to be true/false
0赞 user1185081 11/1/2023
井。。。我从一些教程中汲取了我的方法:定义一个具有最小必需属性的工厂,并使用它来测试模型的详细要求,例如“it { should validate_uniqueness_of(:code).case_insensitive }”等......然后在功能测试中重用 Factory,并希望在请求测试中重用。
0赞 max 11/1/2023
看起来您正在使用 should've-matchers?好东西。不过,您实际上不必使用工厂,因为它会在测试前自动设置值。

答:

4赞 max 10/31/2023 #1

你真的在很多方面都错过了这个目标。

你真正想要的更像是:

RSpec.describe "Playgrounds", type: :request do
  # this should be done in your test setup
  include FactoryBot::Syntax::Methods 


  # This should be handled with a trait
  let(:user) { create(:user, is_admin: true) }
  let(:playground) { FactoryBot.create(:playground) }

  before do
    # get "/users/sign_in" - don't think you actually need this
    login_as user, scope: :user
  end 

  # Describe the route and not the controller
  describe "GET /playgrounds" do
    let!(:playgrounds) { create_list(:playground, 3) }
    # Don't do that wonky numbering stuff - you don't need it 
    # and it will just confuse you when the examples run in random order
    it "renders a successful response" do
      get playgrounds_url
      # this is ok as a litmus test but doesn't tell you if the controller
      # is actually providing a meaningful response
      expect(response).to be_successful
    end
    # @todo write a test that it actually responds with the playgrounds
  end

  describe "GET /playgrounds/:id" do
    it "renders a successful response" do
      get playground_url(playground)
      # this is ok as a litmus test but doesn't tell you if the controller
      # is actually providing a meaningful response
      expect(response).to be_successful
    end
    # @todo write a test that it actually responds with the playground
  end

  describe "POST /playgrounds" do
    context "with valid attributes" do
      # Use per context let instead 
      let(:attributes) { attributes_for(:playground) }
      it "creates a playground" do
        expect {
          post '/playgrounds', params: {
            playground: attributes
          }
        }.to change(Playground, :count).by(1)
        expect(response).to be_successful
      end
    end 

    context "with invalid attributes" do
      # Use per context let instead 
      let(:attributes) do 
        # I would just define this manually
        {
          foo: ""
        }
      end
      it "doesn't create a playground" do
        expect {
          post '/playgrounds', params: {
            playground: attributes
          }
        }.to_not change(Playground, :count)
        expect(response).to have_http_status :unprocessable_entity
      end
    end 
  end
end

FactoryBot.create并且应该用于填充数据库作为测试设置。您不应该使用它来尝试创建无效记录,因为这会引发错误并且没有意义。索引和显示功能的规范实际上应该只依赖于测试设置来创建前提条件。create_list

如果要在测试创建/更新操作时重用工厂定义,请使用 获取属性的哈希值或仅获取模型的未保存实例。FactoryBot.attributes_forFactoryBot.build

在这里重用工厂听起来是避免 DRY 的好主意。是吗?

如果您订阅 DRY 方法,那么这不是您想要避免的事情。但是,是的,使用工厂可以避免重复,但它们也会降低规范的可读性,因为您必须查看工厂定义才能确切地看到传递的内容。

如何在请求上下文中使用 login_as 方法?

这完全取决于您使用的身份验证系统。如果您使用的是 Devise,则可以使用 Warden 提供的测试助手,这会使身份验证系统短路。这比让每个测试发送额外的 HTTP 请求来登录用户要高效得多。

实际上,登录步骤应该由专门针对身份验证系统的单独测试(功能/系统规范)来涵盖。

如果您自己拼凑了一些东西,则可以存根提供用户的方法,也可以完成直接发布到登录用户的方法的步骤。集成测试使用一种模拟浏览器中 cookie 处理的机制。

我不知道应该做什么,但为什么这是你的测试应该关注的事情?这似乎是内部悬空的,而且很臭。valid_attributes["code"].next!

评论

0赞 user1185081 11/1/2023
感谢您的详细解释!以及关于典狱长测试助手的提示!当我在文件顶部添加“include Warden::Test::Helpers”时,一切都开始工作了:登录操作。现在我可以利用你们的观察继续前进了。谢谢!