Is there a way to use Rspec let! with a scope so it can be used across multiple examples?

66 Views Asked by At

I'm trying to figure out a good way to handle setting rspec memoized variables that will be cleaned up after a set of tests, but not after an individual test.

I'd like it to achieve something similar to the following, but with let or something similar that doesn't require me to be responsible for reverting the state back afterwards.

RSpec.describe person do
  before(:context) do
    @shared_value = create(:person)
  end
  after(:context) do
    @shared_value.destroy
  end

  it "test 1" do
    expect(@shared_value).to be_a(Person)
  end

  it "test 2" do
    expect(@shared_value).to be_a(Person)
  end
end

Using let! only provides a scope around each example. Using a pattern like this raises an error for rspec

  let(:shared_person)
  before(:all)
    shared_person
  end
  it "test 1" do
    expect(shared_person).to be_a(Person)
  end

The around method only provides a scope around each example

1

There are 1 best solutions below

2
Allison On

It's erroring because you're not defining the object... the syntax works like this:

let(:my_variable_name) {
  # block that returns the object I want to assign to my variable
}

Your code is missing the block portion defining the object, and rspec is seeing everything else you've defined after that and getting confused. Your test should look something like this:

describe Person do
  subject { person }

  let(:person) { create :person }

  it { should be_kind_of(described_class) }
  it { should be_valid }
end

If you aren't using FactoryBot, then your person definition would look something like:

let(:person) { described_class.create(first_name: 'Spaghetti') }

Subject defines the object under test (making the should shorthand syntax work). Unless it's necessary for the object to exist before the example runs, you should lazy load it with let; if it's necessary for the object to exist before the example runs, then use let!. If you need to mix them, you can do something like this:

let(:person) { create :person }

context 'when person is lazy loaded' do
  it 'does not load the unused object' do
    expect(described_class.count).to eq(0)
  end

  it 'instantiates the object when it is referenced' do
    expect(person.class.count).to eq(1)
  end
end

context 'when person needs to exist before example' do
  let!(:person) { super() }

  it 'instantiates the object before running the example' do
    expect(described_class.count).to eq(1)
  end

  it 'does not create additional objects' do
    expect { person }.not_to change(described_class, :count).from(1)
  end
end

Use context blocks to encapsulate the test setup to simulate a specific scenario. Tests for public methods should be grouped in a describe block (i.e., describe '.class_method_name' do or describe '#instance_method_name') with 0-or-more context-specific blocks inside if it.