Search code examples
ruby-on-railstestingshouldafactory-bot

Shoulda + FactoryGirl: Can I make my tests faster?


I'm looking for a way to speed up my Shoulda + FactoryGirl tests.

The model I'm trying to test (StudentExam) has associations to other models. These associated objects must exist before I can create a StudentExam. For that reason, they are created in setup.

However, one of our models (School) takes significant time to create. Because setup gets called before every should statement, the entire test case takes eons to execute -- it creates a new @school, @student, @topic and @exam for every should statement executed.

I'm looking for a way to create these objects once and only once. Is there something like a startup for before_all method that would allow me to create records which will persist throughout the rest of the test case?

Basically I'm looking for something exactly like RSpec's before(:all). I'm not concerned about the issue of dependencies since these tests will never modify those expensive objects.

Here's an example test case. Apologies for the long code (I've also created a 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

Solution

  • If the problem is creating these records only once, you can use a class variable. It's not a clean approach but at least it should work.

    # 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
    

    EDIT: To fix the super-ugly workaround postpone the evaluation with an instance method.

    # 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