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.
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.
Cat model has a feature behind a feature flag called
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
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
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.
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 🐱.