【问题标题】:Shoulda + FactoryGirl: Can I make my tests faster?Shoulda + FactoryGirl:我可以让我的测试更快吗?
【发布时间】:2009-11-03 22:57:51
【问题描述】:

我正在寻找一种方法来加快我的 Shoulda + FactoryGirl 测试。

我正在尝试测试的模型 (StudentExam) 与其他模型有关联。在创建StudentExam 之前,这些关联对象必须存在。因此,它们是在setup 中创建的。

但是,我们的一个模型 (School) 需要大量时间来创建。因为setup 在每个should 语句之前被调用,所以整个测试用例需要很长时间才能执行——它为每个执行的应该语句创建一个新的@school@student@topic@exam

我正在寻找一种方法来创建这些对象一次并且只创建一次。是否有类似startup for before_all 方法的东西可以让我创建将在整个测试用例的其余部分持续存在的记录?

基本上,我正在寻找与 RSpec 的 before(:all) 完全相同的东西。我不关心依赖问题,因为这些测试永远不会修改那些昂贵的对象。

这是一个示例测试用例。为长代码道歉(我还创建了一个gist):

# A StudentExam represents an Exam taken by a Student.
# It records the start/stop time, room number, etc.
class StudentExamTest < ActiveSupport::TestCase

  should_belong_to :student
  should_belong_to :exam

  setup do
    # These objects need to be created before we can create a StudentExam.  Tests will NOT modify these objects.
    # @school is a very time-expensive model to create (associations, external API calls, etc).
    # We need a way to create the @school *ONCE* -- there's no need to recreate it for every single test.
    @school = Factory(:school)
    @student = Factory(:student, :school => @school)
    @topic = Factory(:topic, :school => @school)
    @exam = Factory(:exam, :topic => @topic)
  end

  context "A StudentExam" do

    setup do
      @student_exam = Factory(:student_exam, :exam => @exam, :student => @student, :room_number => "WB 302")
    end

    should "take place at 'Some School'" do
      assert_equal @student_exam, 'Some School'
    end

    should "be in_progress? when created" do
      assert @student_exam.in_progress?
    end

    should "not be in_progress? when finish! is called" do
      @student_exam.finish!
      assert !@student_exam.in_progress
    end

  end

end

【问题讨论】:

    标签: ruby-on-rails testing shoulda factory-bot


    【解决方案1】:

    如果问题是只创建一次这些记录,您可以使用类变量。 这不是一个干净的方法,但至少它应该有效。

    # A StudentExam represents an Exam taken by a Student.
    # It records the start/stop time, room number, etc.
    class StudentExamTest < ActiveSupport::TestCase
    
      should_belong_to :student
      should_belong_to :exam
    
      # These objects need to be created before we can create a StudentExam.  Tests will NOT modify these objects.
      # @school is a very time-expensive model to create (associations, external API calls, etc).
      # We need a way to create the @school *ONCE* -- there's no need to recreate it for every single test.
      @@school = Factory(:school)
      @@student = Factory(:student, :school => @@school)
      @@topic = Factory(:topic, :school => @@school)
      @@exam = Factory(:exam, :topic => @@topic)
    
    
      context "A StudentExam" do
    
        setup do
          @student_exam = Factory(:student_exam, :exam => @@exam, :student => @@student, :room_number => "WB 302")
        end
    
        should "take place at 'Some School'" do
          assert_equal @student_exam, 'Some School'
        end
    
        should "be in_progress? when created" do
          assert @student_exam.in_progress?
        end
    
        should "not be in_progress? when finish! is called" do
          @@student_exam.finish!
          assert !@student_exam.in_progress
        end
    
      end
    
    end
    

    编辑:要修复超级丑陋的解决方法,请使用实例方法推迟评估。

    # A StudentExam represents an Exam taken by a Student.
    # It records the start/stop time, room number, etc.
    class StudentExamTest < ActiveSupport::TestCase
    
      ...
    
      private
    
        def school
          @@school ||= Factory(:school)
        end
    
        # use school instead of @@school
        def student
          @@school ||= Factory(:student, :school => school)
        end
    
    end
    

    【讨论】:

    • 我最喜欢这种方法,但它似乎不能正常工作。 @@school = Factory(:school) 引发验证错误,即 name 已被占用(validates_uniqueness_of)。我试过使用@@school ||= Factory(:school),如果测试数据库是干净的,它就会工作。所以我结束了超级丑陋的@@school ||= School.first || Factory(:school)
    • 要修复超级丑陋的解决方法,请使用实例方法推迟评估。 (见我的编辑)
    【解决方案2】:

    您要编写什么样的测试?如果您确实想确保所有这些对象都适当地协调,那么您正在编写一个集成测试并且速度不是您主要关心的问题。但是,如果您尝试对模型进行单元测试,则可以通过积极地存根来获得更好的结果。

    例如,如果您在调用exam.location(或您所称的任何名称)时尝试检查考试是否使用其学校协会的名称,则不需要整个学校对象。您只需要确保考试在学校调用正确的方法。要对此进行测试,您可以执行以下操作(使用 Test::Unit 和 Mocha,因为这是我所熟悉的):

    test "exam gets location from school name" do
      school = stub_everything
      school.expects(:name).returns(:a_school_name)
      exam = Factory(:exam, :school => school)
    
      assert_equal :a_school_name, exam.location
    end
    

    基本上,如果您因为构建对象的成本太高而需要加快单元测试,那么您并不是真正的单元测试。上面所有的测试用例都感觉应该是单元测试级别的,所以 stub stub stub!

    【讨论】:

    • 也许是因为我对这种做法并不太熟悉,但由于某种原因,我并不热衷于使用存根的想法。我想针对实际模型实例运行这些测试。此外,稍后的一些测试确保学生没有超过“n”次考试,这将需要数据库查询——这些情况存根可以很好地处理吗?
    • 基本上,我想我想传达的是不同的测试服务于不同的目的。如果您正在编写单元测试,那么您正在测试单个“单元”代码——您的模型本身。在这种情况下,您所关心的只是它在界面级别上的表现是否良好,因此您应该假设另一个对象返回了不错的数据(例如通过模拟)并确保您的模型在完美世界中运行。当您实际上想要测试系统的多个“单元”(一次几个类)时,您应该有一个较慢的集成测试来实际创建所有对象。
    【解决方案3】:

    http://m.onkey.org/2009/9/20/make-your-shoulda-tests-faster-with-fast_context 是一篇关于如何使用名为 fast_context 的 gem 让你的 shoulda/factory-girl 测试更快的优秀帖子。如果它不是您需要的,请告诉我。

    【讨论】:

    • 我看到了 fast_context,但我认为它根本没有帮助。我可以看到它仍在创建@school 记录每个测试。那篇文章中的 cmets 也启发了我尝试这样的事情,但没有奏效:gist.github.com/221668
    【解决方案4】:

    有一个名为 fast_context (github link) 的插件将 should 语句组合到一个上下文中,从而加快测试速度。

    我用来加速测试的另一件事是预先填充夹具数据。 FactoryGirl 很慢,因为它会在每次设置块运行时创建这些记录。

    我编写了一个名为 Fixie 的插件,它使用 ActiveRecord 预填充测试数据库,因此您的测试所需的记录已经创建。如果您想在运行时创建新记录,也可以将 Fixie 与 FactoryGirl 一起使用。

    【讨论】:

    • (请参阅我上面的评论:fast_context)。我不想预先填充测试数据库——这就是我首先使用 FactoryGirl 的原因(而不是固定装置)。针对预先填充的数据集编写测试非常脆弱。我更喜欢在测试用例中创建测试数据(我只是不希望它重新创建每个断言,基本上)。我将继续寻找一种方法,让每个测试用例只初始化一次。
    • 我不同意,但每个人都有自己的看法。如果创建对象的成本很高(尤其是 API 调用),为什么不将部分或全部内容存根?
    猜你喜欢
    • 2017-11-06
    • 1970-01-01
    • 2013-10-30
    • 2011-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多