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
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
What kind of tests are you trying to write? If you actually want to make sure that all of these objects are coordinating appropriately, you're writing an integration test and speed is not your primary concern. However, if you're trying to unit test the model, you could achieve better results by stubbing aggressively.
For example, if you're trying to check that an exam uses the name of its school association when you call exam.location (or whatever you're calling it), you don't need a whole school object. You just need to make sure that exam is calling the right method on school. To test that, you could do something like the following (using Test::Unit and Mocha because that's what I'm familiar with):
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
Basically, if you need to speed up your unit tests because objects are too expensive to construct, you're not really unit testing. All of the test cases above feel like they should be at the unit test level, so stub stub stub!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With