views:

315

answers:

4

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
A: 

http://m.onkey.org/2009/9/20/make-your-shoulda-tests-faster-with-fast_context is an excellent post about how to make your shoulda/factory-girl tests faster, using a gem called fast_context. Let me know if its not what you need.

Vertis
I saw fast_context, but I don't think it helps at all. I can see that it's still creating the `@school` record each and every test. The comments on that post also inspired me to try something like this, but it didn't work: http://gist.github.com/221668
Kyle Fox
does @school ||= Factory(:school) work?
Vertis
A: 

There is a plugin called fast_context (github link) that combines should statements into a single context, speeding up the tests.

The other thing I have been using to speed up my tests is pre-populating the fixture data. FactoryGirl is slow because it's creating those records each time the setup block runs.

I wrote a plugin called Fixie that uses ActiveRecord to pre-populate the test database, so the records you need for your tests are already created. You can use Fixie along with FactoryGirl if you want to create new records at run-time, too.

Luke Francl
(See my comment above re: fast_context).I don't want to prepopulate the test database -- that's why I'm using FactoryGirl in the first place (as opposed to fixtures). Writing your tests against a prepopulated set of data is quite brittle. I prefer creating the test data inside the test case (I just don't want it re-created every single assertion, basically).I'm going to keep searching for a way to have stuff initialized exactly once per test case.
Kyle Fox
I disagree, but to each his own. If the object is expensive to create (especially the API calls), why not stub some or all of of that stuff out?
Luke Francl
+1  A: 

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
Simone Carletti
I like this approach best, but it doesn't seem to be working properly. `@@school = Factory(:school)` raises a validation error, that `name` is already taken (it `validates_uniqueness_of`). I've tried using `@@school ||= Factory(:school)` and it will work if the test database is clean. So I've wound up with the super-ugly `@@school ||= School.first || Factory(:school)`
Kyle Fox
To fix the super-ugly workaround postpone the evaluation with an instance method. (see my edit)
Simone Carletti
A: 

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!

Kyle
Maybe it's because I'm not overly familiar with the practice, but for some reason I'm not keen on the idea of using stubs. I'd like to be running these tests against actual model instances. Also, some of the tests later on ensure that a student hasn't written an exam more than "n" times, which will require database queries -- are these situations stubs can deal with nicely?
Kyle Fox
Basically, I think what I was trying to communicate is that different tests serve different purposes. If you're writing a unit test, you're testing a single "unit" of code -- your model itself. In this case, all you care about is that it plays nicely at the interface level, so you should assume that the other object returns nice data (by mocking, for example) and make sure your model behaves in a perfect world. When you actually want to test multiple "units" of your system (several classes at once), you should have a slower integration test that actually creates all the objects.
Kyle