How to stub Feature Flags with RSpec

Stefanni Brasil and Elias Rodrigues

If you are using feature flags with Flipper or a similar gem on your Rails project, and storing them in Redis, you may run into the issue of leaking feature flag states between test cases.

That increases the chances of making your test suite flaky, and an easy strategy to counter that is to stub all of the feature flags on your specs.

Creating Feature Flag Helper Methods With RSpec

Inspired by the strategy that GitLab follows to stub all feature flags by default, we adapted it to our case, which was a much simpler scenario.

The first step is to create a helper method in your RSpec support folder:

# spec/support/helpers/stub_feature_flags.rb

module StubFeatureFlags
  # Enable/disable Flipper globally
  # Disabled by default in spec/spec_helper.rb
  def stub_all_feature_flags(value)
    allow(Flipper).to receive(:enabled?).and_return(value)
  end

  # Unstub any global stubs
  # aka undo `stub_all_feature_flags`
  # More details at the end of the post
  def unstub_all_feature_flags
    RSpec::Mocks.space.proxy_for(Flipper).reset
  end

  # Enable/disable Flipper for a specific feature for any actors
  # e.g.: stub_feature_flag(:allow_cat_to_have_snacks, true)
  def stub_feature_flag(feature_name, value)
    allow(Flipper).to receive(:enabled?).with(feature_name, anything).and_return(value)
    allow(Flipper).to receive(:enabled?).with(feature_name).and_return(value)
  end

  # Enable/disable Flipper for a specific feature and actor
  # e.g.: stub_feature_flag_for_actor(:allow_cat_to_have_snacks, cat, true)
  def stub_feature_flag_for_actor(feature_name, actor, value)
    allow(Flipper).to receive(:enabled?).with(feature_name, actor).and_return(value)
  end
end

The next step is to make those stub methods available to the whole RSpec test suite:

# spec/spec_helper.rb

require "support/helpers/stub_feature_flags.rb"

RSpec.configure do |config|
  config.include StubFeatureFlags
  # ...

  # Disable all feature flags by default
  # Use StubFeatureFlags methods to stub feature flags
  config.before(:each) do
    stub_all_feature_flags(false)
  end
  # ...
end

Now, to test behaviors related to feature flags, we need to stub them explicitly on our test cases. Let’s see how.

How to use Feature Flag Helper Methods in RSpec test cases

Our Cat model has a feature behind a feature flag called allow_cat_to_have_snacks.

In our Cat Shop, only the cats with the feature flag enabled can have snacks, so we need to test the business logic around their snack allowance.

Here are some test cases to show how to use the helper methods for our cats:

# spec/models/cat_spec.rb

describe Cat do
  describe "#can_have_snacks?" do
    context "when feature flag allow_cat_to_have_snacks is enabled for all cats" do
      it "returns true" do
        cat = create(:cat)
        stub_feature_flag(:allow_cat_to_have_snacks, true)

        expect(cat.can_have_snacks?).to be(true)
      end
    end

    context "when feature flag allow_cat_to_have_snacks is enabled only for specific cats" do
      it "returns true only for the allowed cats" do
        allowed_cat = create(:cat)
        not_allowed_cat = create(:cat)

        stub_feature_flag_for_actor(:allow_cat_to_have_snacks, allowed_cat, true)
        stub_feature_flag_for_actor(:allow_cat_to_have_snacks, not_allowed_cat, false)

        expect(allowed_cat.can_have_snacks?).to be(true)
        expect(not_allowed_cat.can_have_snacks?).to be(false)
      end
    end
  end
end

Unstubbing Feature Flags For Flipper groups

There might be cases where resetting the default stubbing in the spec/spec_helper.rb is necessary.

In our app, we had a Flipper group defined in the gem’s initializer:

# config/initializers/flipper.rb

Flipper.register(:cat_owners) do |actor|
  actor.respond_to?(:cats) && actor.cats.any?
end

A Flipper Group checks if a flag is enabled? for its actors behind the scenes. Because our spec helper sets Flipper’s enabled? to always return false, we needed to undo that default stub.

Otherwise, the following test would fail because enabled? was always returning false for any actors:


# spec/models/user_spec.rb

describe User do
  describe "#can_pet_cat?" do
    it "returns true only for cat owners" do
      unstub_all_feature_flags # <-- resets Flipper stubs
      Flipper.enable_group(:allow_pet_cat, :cat_owners)

      user = create(:user)
      user_cat_owner = create(:user, cat_owner: true)

      expect(user.can_pet_cat?).to be(false)
      expect(user_cat_owner.can_pet_cat?).to be(true)
    end
  end
end

Calling RSpec::Mocks.space.proxy_for(Flipper).reset on our StubFeatureFlags will remove the stubs, therefore allowing the above test to succeed. Handy for undoing any stubs on your specs.

Bye, Flaky Feature Flags Tests!

And that’s it!

Now, because we are required to explicitly stub feature flags only on test cases that need them, our test suite became more reliable and easier to maintain.

This approach also made it easier to test both scenarios: when the feature flag is enabled and when it isn’t.

All of those benefits while not having flaky tests anymore!

Now our Cat Shop’s employees can nap peacefully knowing the cat’s snack allowance is properly tested 🐱.