Skip to content

Allow let! without a block

Created by: danielfrey

We make extensive use of rspec tests in our projects. The biggest has over 5200 tests at the moment. Most of the tests need a small or normal setup, but some need a really heavy setup. With heavy setups we usually build up a tree of dependent objects. A big part of the setup-tree is built in the most outer context, whereas specific object are only built in the inner contexts. Since those tests are often integration tests, these objects are activerecord objects created with factory-girl.

Before "let" was introduced, we did all setups in the before-hooks. With the introduction of "let" things got clearer as we all know. Typically not every test needs the full setup, but for understanding the object dependencies, it's better to define most of it in the most outer context and then activate the objects by calling the variable lazy loading the block. But, and here comes the problem, if you need in some tests a "let!" to e.g. have a saved version of an object, then you have two options to do this, both not really satisfying. Either you define this object with "let!" in the topmost context an therefore instatiate it in every test and not only in the needed ones. Or you define it with "let", but then you have to call the variable explicitly in the test or in a before-hook, if you need it in this context.

As a possible and in my eyes clean solution for this problem would be, of you could define an object with "let" and, in a second statement, e.g in the inner context just declare it a second time with "let!", but without declaring the block.

With this solution you can build the heavy setup logically in the most outer context (or wherever needed) and in the inner context you just call "let!" without a block. This let's your tests become much more readable.

You could of course argue that you should build smaller setups, eg with more complex factories. But in really big applications large setups are reality. And the proposed let! would be a really elegant solution.

As a (admittedly very constructed) sample look at this:

describe Family do
  context "with big setup" do
    let!(:father)   {FactoryGirl.create :person, gender: :male}
    let!(:mother)    {FactoryGirl.create :persongender: :female}
    let(:parents)   {FactoryGirl.create :parents, father: father, mother: mother}
    let(:son)       {FactoryGirl.create :person, gender: :male, parents: parents}
    let(:daughter)  {FactoryGirl.create :person, gender: :female, parents: parents}
    let(:family)    {FactoryGirl.create :family, with: [parents, son, daughter]}

    context "without children" do
      it "test1" do
        Person.where{gender == 'male'}.should eq(1)
      end
    end

    # the "old" way #1
    context "with children" do

      it "test2" do
        son
        daughter
        Person.count.should eq(4)
      end
    end

    # the "old" way #2
    context "with children" do

      before do
        son
        daughter
      end

      it "test2" do
        Person.count.should eq(4)
      end
    end

    # the "proposed new" way
    context "with children" do
      let!(:son)      #only instantiated in this context
      let!(:daughter) #only instantiated in this context

      it "test2" do
        Person.count.should eq(4)
      end
    end


  end
end

What do you think?

Merge request reports