如果我调用 Factory.build 以便快速进行控制器测试,如何才能让 Factory Girl 永远不会访问数据库?

发布于 2024-11-09 16:58:10 字数 6993 浏览 3 评论 0原文

我正在寻求让我的 Rails 测试更快。我只有 520 个测试,但在 bash 中运行需要 62 秒,在 Rubymine 中运行需要 82 秒。

作为典型控制器测试的示例,我使用此代码作为 @user 登录,并在 CommentsController 中为我的 RSpec 控制器测试创建基本 @comment:

before(:each) do
  @user = Factory.create(:user)
  sign_in @user

  @comment = Factory.create(:comment)
end

正如您可能意识到的那样......这很慢。它构建了一个@user,同时也构建了该用户的关联。 @comment 也是如此。

所以我认为调用 Factory.build(:user) 可以解决这个问题......但我得到了奇怪的错误。例如,current_user 返回nil

所以...我决定使用 Factory.build() 并在父控制器中删除所有之前的过滤器。然而,当我事后检查 RSPec 日志时,我的 rspec 日志仍然显示大量插入正在访问数据库(我们正在谈论仅 3 次测试的数百行代码!)

  before(:each) do
    @user = Factory.build(:user)
    #sign_in @user

    controller.stub(:authenticate_user!) #before_filter
    controller.stub(:add_secure_model_data) #before_filter
    controller.stub(:current_user).and_return(@user)

    @comment = Factory.build(:comment)
  end

可悲的事实是,上面的 before(:每个) 块对测试性能的影响为零。正如我发现的,调用 Factory.build() 仍会在子关联上内部调用 Factory.create()

这是一个 before(:each) 块,它可以有效地删除 RSpec 日志中产生的垃圾。它使我的测试性能提升了 35-40%,

  before(:each) do
    @user = Factory.build(:user, :role => Factory.build(:role))
    #sign_in @user

    controller.stub(:authenticate_user!)
    controller.stub(:add_secure_model_data)
    controller.stub(:current_user).and_return(@user)

    # both of these are still super slow. WTF?!
    @site_update = Factory.build(:site_update, :id => 5, :author => Factory.build(:user, :role => Factory.build(:role)))

    @comment = Factory.build(:comment,
                             :author => Factory.build(:user, :role => Factory.build(:role)),
                             :commentable => @site_update)
  end

这使得测试运行得更快,但它也很丑陋。我们不能认真地为每个测试编写这个......是吗?那太疯狂了。我不做。

我还想指出,这些 Factory.build() 行中的任何一行仍然需要大约 0.15 秒,即使它们没有访问数据库!

仅运行 3 个测试仍然会导致factory_girl PER 测试占用约 0.3 到 0.35 秒的时间!我认为这是完全不能接受的。如果删除 Factory.build() 行,测试将在 0.00001 秒内运行。

我认为评审团的意见是:factory_girl 是一个非常慢的库。唯一的解决办法是不使用它吗?

这是我的factories.rb:

Factory.define :role do |f|
  f.name "Admin"
end

Factory.define :user do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "Banoo.Smith#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :admin do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "admin#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :course_provider do |f|
  f.first_name "Josh"
  f.last_name "Bolson"
  f.sequence(:email) { |n| "josh.bolson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :director do |f|
  f.first_name "Director"
  f.last_name "Dude"
  f.sequence(:email) { |n| "director#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :instructor do |f|
  f.first_name "Instructor"
  f.last_name "Dude"
  f.sequence(:email) { |n| "instructor#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :trainee do |f|
  f.first_name "Trainee"
  f.last_name "Dude"
  f.sequence(:email) { |n| "trainee#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :private_message do |f|
  f.subject "Subject"
  f.content "content"
  f.is_deleted_by_sender false
  f.association :sender, :factory => :user
end

Factory.define :recipient do |f|
  f.is_read false
  f.is_deleted false
  f.association :receiver, :factory => :user
  f.association :private_message
end

Factory.define :course_template do |f|
  f.name "name"
  f.description "description"
  f.association :course_provider
end

Factory.define :site_update do |f|
  f.subject "Subject"
  f.intro "intro"
  f.content "content"
  f.association :author, :factory => :user
end

Factory.define :comment do |f|
  f.content "content"
  f.association :author, :factory => :user
  f.association :commentable, :factory => :site_update
end

Factory.define :country do |f|
  f.name "Liberty"
end

Factory.define :province do |f|
  f.name "Freedom"
  f.association :country
end

Factory.define :payment_plan do |f|
  f.name "name"
  f.monthly_amount 79
  f.audience "Enterprises"
  f.active_courses "500-2000"
end

Factory.define :company do |f|
  f.name "name"
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N7G-5F4"
  f.association :province
  f.association :payment_plan
end

Factory.define :company_user do |f|
  f.first_name "Dan"
  f.last_name "Grayson"
  f.sequence(:email) { |n| "dan.grayson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
  f.association :company
end

Factory.define :course do |f|
  f.notes "notes"
  f.difficulty 100
  f.association :course_template
  f.association :instructor, :factory => :company_user
end

Factory.define :study_group do |f|
  f.name "name"
end

Factory.define :help_category do |f|
  f.name "name"
end

Factory.define :help_document do |f|
  f.question "question"
  f.content "content"
  f.association :category, :factory => :help_category
end

Factory.define :tag do |f|
  f.name "name"
end

Factory.define :partial_mapping do |f|
  f.from_suffix "ing"
  f.to_suffix "ing"
end

Factory.define :newsletter do |f|
  f.subject "subject"
  f.content "content"
end

Factory.define :press_contact do |f|
  f.full_name "Banoo Smith"
  f.email '[email protected]'
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N9B-3W5"
  f.association :province
end

Factory.define :press_release do |f|
  f.headline "Headline"
  f.origin "origin"
  f.intro "intro"
  f.body "body"
  f.association :contact, :factory => :press_contact
end

Factory.define :theme do |f|

end

和有趣的基准。调用 Factory.create(:user) 平均需要 0.1 到 0.14 秒:

$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.create(:user) } } }' 
      user     system      total        real
  9.940000   0.080000  10.020000 ( 14.872736)

即使是 Factory.build(:user) 也需要很长时间。这是 :default_strategy => 。 :build 打开!

$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.build(:user) } } }'
      user     system      total        real
  9.350000   0.030000   9.380000 ( 11.798339)

显然,这证明了factory_girl 出了问题。解决方案是删除它或确保它使用 Factory.build。这就是答案。

既然我自己的问题已经基本解决了,我就想知道为什么Factory_girl这么受欢迎,为什么是“常识”?人们可以客观地得出结论,无论使用 Factory Girl 可以获得什么好处(而且它有很多好处),都不值得付出性能代价。我确信可以开发出更好的工厂宝石,性能更高......但不幸的是,遗憾的是,factory_girl 不是这样。

我的下面的解决方案使用基本的对象实例化和存根,并且测试继续通过。我认为,如果您想避免固定装置并在运行测试时获得高性能,那么在每次测试的基础上使用基本的 Ruby、存根和手动填充对象值是“正确”的做法。

I am on a quest to make my Rails tests faster. I only have 520 tests, but they take 62 seconds to run in bash, and 82 seconds to run in Rubymine.

As an example of a typical controller test, I was using this code to sign_in as a @user and create the basic @comment in a CommentsController for my RSpec controller tests:

before(:each) do
  @user = Factory.create(:user)
  sign_in @user

  @comment = Factory.create(:comment)
end

As you might realize... this is slow. It builds a @user, but also builds the associations for that user. Same for the @comment.

So I thought calling Factory.build(:user) would solve it... but I get weird errors. For example, current_user returns nil.

So... I decided to use Factory.build() and stub out all the before filters in my parent controller. However, my rspec log still says a TON of inserts are hitting the database when I inspect the RSPec log afterwards (we are talking hundreds of lines of code for just 3 tests!)

  before(:each) do
    @user = Factory.build(:user)
    #sign_in @user

    controller.stub(:authenticate_user!) #before_filter
    controller.stub(:add_secure_model_data) #before_filter
    controller.stub(:current_user).and_return(@user)

    @comment = Factory.build(:comment)
  end

The sad fact is, the above before(:each) block has ZERO effect on test performance. As I discovered, calling Factory.build() will still internally call Factory.create() on the child associations.

Here is a before(:each) block that effectively removes the junk produced in the RSpec log. It gave me a 35-40% test performance boost

  before(:each) do
    @user = Factory.build(:user, :role => Factory.build(:role))
    #sign_in @user

    controller.stub(:authenticate_user!)
    controller.stub(:add_secure_model_data)
    controller.stub(:current_user).and_return(@user)

    # both of these are still super slow. WTF?!
    @site_update = Factory.build(:site_update, :id => 5, :author => Factory.build(:user, :role => Factory.build(:role)))

    @comment = Factory.build(:comment,
                             :author => Factory.build(:user, :role => Factory.build(:role)),
                             :commentable => @site_update)
  end

This makes the tests run faster, but it's also ugly as sin. We can't seriously write this for every test... do we? That's nuts. I'm not doing it.

I also want to point out that any one of these Factory.build() lines still takes about .15 seconds even though they are NOT hitting the database!

Running only 3 tests still results in about .3 to .35 seconds of time taken up by factory_girl PER test! I think that is totally unacceptable. If you remove the Factory.build() lines, the tests run in 0.00001 seconds.

I think the jury is in: factory_girl is one really slow library. Is the only solution to not use it?

Here is my factories.rb:

Factory.define :role do |f|
  f.name "Admin"
end

Factory.define :user do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "Banoo.Smith#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :admin do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "admin#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :course_provider do |f|
  f.first_name "Josh"
  f.last_name "Bolson"
  f.sequence(:email) { |n| "josh.bolson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :director do |f|
  f.first_name "Director"
  f.last_name "Dude"
  f.sequence(:email) { |n| "director#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :instructor do |f|
  f.first_name "Instructor"
  f.last_name "Dude"
  f.sequence(:email) { |n| "instructor#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :trainee do |f|
  f.first_name "Trainee"
  f.last_name "Dude"
  f.sequence(:email) { |n| "trainee#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :private_message do |f|
  f.subject "Subject"
  f.content "content"
  f.is_deleted_by_sender false
  f.association :sender, :factory => :user
end

Factory.define :recipient do |f|
  f.is_read false
  f.is_deleted false
  f.association :receiver, :factory => :user
  f.association :private_message
end

Factory.define :course_template do |f|
  f.name "name"
  f.description "description"
  f.association :course_provider
end

Factory.define :site_update do |f|
  f.subject "Subject"
  f.intro "intro"
  f.content "content"
  f.association :author, :factory => :user
end

Factory.define :comment do |f|
  f.content "content"
  f.association :author, :factory => :user
  f.association :commentable, :factory => :site_update
end

Factory.define :country do |f|
  f.name "Liberty"
end

Factory.define :province do |f|
  f.name "Freedom"
  f.association :country
end

Factory.define :payment_plan do |f|
  f.name "name"
  f.monthly_amount 79
  f.audience "Enterprises"
  f.active_courses "500-2000"
end

Factory.define :company do |f|
  f.name "name"
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N7G-5F4"
  f.association :province
  f.association :payment_plan
end

Factory.define :company_user do |f|
  f.first_name "Dan"
  f.last_name "Grayson"
  f.sequence(:email) { |n| "dan.grayson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
  f.association :company
end

Factory.define :course do |f|
  f.notes "notes"
  f.difficulty 100
  f.association :course_template
  f.association :instructor, :factory => :company_user
end

Factory.define :study_group do |f|
  f.name "name"
end

Factory.define :help_category do |f|
  f.name "name"
end

Factory.define :help_document do |f|
  f.question "question"
  f.content "content"
  f.association :category, :factory => :help_category
end

Factory.define :tag do |f|
  f.name "name"
end

Factory.define :partial_mapping do |f|
  f.from_suffix "ing"
  f.to_suffix "ing"
end

Factory.define :newsletter do |f|
  f.subject "subject"
  f.content "content"
end

Factory.define :press_contact do |f|
  f.full_name "Banoo Smith"
  f.email '[email protected]'
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N9B-3W5"
  f.association :province
end

Factory.define :press_release do |f|
  f.headline "Headline"
  f.origin "origin"
  f.intro "intro"
  f.body "body"
  f.association :contact, :factory => :press_contact
end

Factory.define :theme do |f|

end

And interesting benchmark. It takes .1 to .14 seconds on average to make a call to Factory.create(:user):

$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.create(:user) } } }' 
      user     system      total        real
  9.940000   0.080000  10.020000 ( 14.872736)

Even a Factory.build(:user) takes forever... and this is with :default_strategy => :build turned on!

$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.build(:user) } } }'
      user     system      total        real
  9.350000   0.030000   9.380000 ( 11.798339)

Clearly this is evidence that something is wrong with factory_girl. The solution is to get rid of it or make sure it's using Factory.build. That is the answer.

Since I have basically solved my own problem, I am wondering why Factory_girl is so popular, and why is it "common wisdom"? One can objectively conclude that whatever benefits may be gained by using Factory Girl - and there's a lot of nice things about it - it is not worth the performance cost. I am sure a better factory gem could be developed that is much more performant... but factory_girl is unfortunately and regretfully not it.

My solution below uses basic object instantiation and stubs, and the tests continue to pass. I think using basic Ruby, stubs and filling in the object values manually on a per-test basis is the 'right' thing to do if you want to avoid fixtures and also get high performance when running tests.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

别念他 2024-11-16 16:58:10

好吧,我想我会回答我自己的问题。我认为这是正确的答案,也许其他人可以从中学习,因为我不得不花几个小时来学习它。

以下是我如何获得 2000%(或 20 倍)速度提升的方法:

before(:each) do
  @user = User.new
  controller.stub(:authenticate_user!)
  controller.stub(:current_user).and_return(@user)
  controller.stub(:add_secure_model_data)

  @site_update = SiteUpdate.new
  @comment = Comment.new
end

解决方案就是不使用任何类型的工厂进行控制器测试(或许还有其他类型的测试)。我建议只有当其他方法实在太痛苦时才使用 Factory 的。

所有 3 个测试现在只需 0.07 秒即可运行!之前运行所有 3 个测试需要 1.4 秒。

Factory_girl 是一个非常慢的库。我不知道它到底在做什么,但它的分析不正确。

是的,我知道它所做的不仅仅是简单的 MyClass.new 语句...但即使对于像 Ruby 这样速度较慢的脚本语言,其性能也比基本类实例化慢许多数量级。它需要进行一些大规模的优化,以便 Factory.build(:my_class) 更加符合 MyClass.new

我建议 Factory_girl 的实现者尝试并获取它,这样它的开销不会比基本的 MyClass.new 调用慢很多(不包括数据库开销......这是无法避免的)。它应该提供一种构建对象的好方法,并且您不必付出 20 倍的性能损失即可获得此好处。这不是一个可以接受的权衡。

这真的太糟糕了,因为当您在控制器规范中打开 render_views 时,Factory.build 在控制器中会很好。应该有很大的动力来纠正这个问题。

同时,只需使用基本的 Ruby/Rails 类。我想你会惊讶于它们实际上有多快......

Well, I guess I will answer my own question. I think it's the right answer, and maybe others can learn from it as I had to spend a few hours to learn it.

Here's how I got a 2000% (or 20x) speed improvement:

before(:each) do
  @user = User.new
  controller.stub(:authenticate_user!)
  controller.stub(:current_user).and_return(@user)
  controller.stub(:add_secure_model_data)

  @site_update = SiteUpdate.new
  @comment = Comment.new
end

The solution is simply not to use Factories of any kind for controller tests (and perhaps other kinds of tests). I suggest only use Factory's when it is too much of a pain in the ass to do otherwise.

All 3 tests now run in 0.07 seconds! Before it was 1.4 seconds to run all 3 tests.

Factory_girl is simply a terribly slow library. I don't know what the heck it is doing, but it is not profiled properly.

Yes, I know it's doing a lot more than simple MyClass.new statements... but even for a slower scripting language like Ruby, the performance is many orders of magnitude slower than basic class instantiation. It needs to undergo some massive optimization so that Factory.build(:my_class) is brought more in line with MyClass.new

I would suggest to the implementers of Factory_girl to try and get it so that it's overhead is not much slower than a basic MyClass.new call (excluding database overhead... that can't be avoided). It should provide a nice way to build objects and you shouldn't have to pay a 20x performance penalty to get this benefit. That's not an acceptable trade-off.

This is all really too bad, because Factory.build would be nice in controllers when you have render_views turned on inside of your controller specs. There should be significant motivation to correct this.

In the meantime, just use basic Ruby/Rails classes. I think you'll be amazed how fast they actually are....

谁对谁错谁最难过 2024-11-16 16:58:10

我遇到了与 @FireEmblem 相同的问题,并最终将问题范围缩小到 FactoryGirl.buildFactoryGirl.stub 并没有让事情变得更好。

我终于意识到这是因为我的一个模型具有验证逻辑,当存在某个字段时会发出 HTTP 请求。工厂在该字段中设置了一个值,因此从表面上看,FactoryGirl 似乎减慢了我的测试速度。事实上,确实如此,但只是因为它触发了 HTTP 请求。从我的一个工厂中删除一行就消除了 HTTP 请求,从而使性能提高了 60 倍。

I had the same problem as @FireEmblem and eventually narrowed the issue down to FactoryGirl.build. FactoryGirl.stub didn't make things any better.

I finally realized it's because one of my models had validation logic that made an HTTP request when a certain field was present. The factory put a value in that field, so on the outside, it looked like FactoryGirl was slowing down my tests. In reality, it was, but only because it triggered the HTTP request. Removing one line from one of my factories eliminated the HTTP request, causing a 60x performance improvement.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文