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 🐱.