Testing
The helpers temporarily replace definitions on the already configured global client and restore them when the block exits, including exceptions. Always pass a block. Do not run different overrides concurrently against the same client; prefer process-isolated test workers.
The Rails view/request/system examples below assume the named application routes, templates and fixtures exist. Helper tests exercise your enabled/disabled branches; overriding a flag does not prove its targeting rules.
RSpec Integration
Setup
First select the offline test initializer so a global client exists before any helper runs.
# rails_helper normally loads Rails before this point.
require 'rspec'
require 'rspec/expectations' # Define matchers before loading SDK helpers.
require 'toggly/rails/testing'
RSpec.configure do |config|
config.include Toggly::Rails::Testing::RSpecHelpers
end
Stubbing Features
Single Feature
describe 'Dashboard' do
it 'shows new UI when feature enabled' do
with_feature(:new_dashboard, enabled: true) do
visit dashboard_path
expect(page).to have_content('New Dashboard')
end
end
it 'shows old UI when feature disabled' do
with_feature(:new_dashboard, enabled: false) do
visit dashboard_path
expect(page).not_to have_content('New Dashboard')
end
end
end
Multiple Features
it 'shows full experience when all features enabled' do
with_features(
new_dashboard: true,
dark_mode: true,
premium_features: true
) do
visit dashboard_path
expect(page).to have_css('.new-dashboard')
expect(page).to have_css('.dark-mode')
expect(page).to have_css('.premium')
end
end
Convenience Methods
it 'shows promo when enabled' do
enable_feature(:promo_banner) do
visit home_path
expect(page).to have_css('.promo')
end
end
it 'hides promo when disabled' do
disable_feature(:promo_banner) do
visit home_path
expect(page).not_to have_css('.promo')
end
end
Custom Matchers
RSpec.describe 'Feature flags' do
it 'scopes and restores the definition' do
original = Toggly.enabled?(:my_feature)
with_feature(:my_feature, enabled: true) do
expect(:my_feature).to be_feature_enabled
end
expect(Toggly.enabled?(:my_feature)).to eq(original)
with_feature(:my_feature, enabled: false) do
expect(:my_feature).not_to be_feature_enabled
end
end
end
To scope a whole RSpec example, use around { |example| with_feature(:my_feature, enabled: true) { example.run } }, not a blockless before hook.
Minitest Integration
Setup
require 'toggly/rails/testing'
class ActiveSupport::TestCase
include Toggly::Rails::Testing::MinitestHelpers
end
Usage
class DashboardTest < ActionDispatch::IntegrationTest
test 'shows new UI when feature enabled' do
with_feature(:new_dashboard, enabled: true) do
get dashboard_path
assert_includes response.body, 'New Dashboard'
end
end
test 'hides new UI when feature disabled' do
with_feature(:new_dashboard, enabled: false) do
get dashboard_path
refute_includes response.body, 'New Dashboard'
end
end
end
Assertions
test 'feature is enabled' do
with_feature(:my_feature, enabled: true) do
assert_feature_enabled(:my_feature)
end
end
test 'feature is disabled' do
with_feature(:my_feature, enabled: false) do
assert_feature_disabled(:my_feature)
end
end
Test Configuration
Disable Background Refresh
Disabling polling alone still allows the initial HTTP fetch. Use an explicit test branch in your single initializer instead:
Use Defaults in Tests
Toggly::Rails.configure do |config|
config.app_key = Rails.env.test? ? nil : Rails.application.credentials.dig(:toggly, :app_key)
config.environment = Rails.env.production? ? 'Production' : 'Staging'
config.disable_background_refresh = Rails.env.test?
# Nonempty defaults select offline mode when app_key is nil.
config.defaults = { 'my_feature' => false, 'new_dashboard' => false }
end
This omits the key in tests and prevents definitions/telemetry networking. The Railtie owns cleanup. Do not create a second global client in the helper file.
Mock API Responses
For an HTTP integration test, add webmock to the test Gemfile group. Install the stub before constructing the client, since its constructor performs the initial fetch. If stubbing Rails startup, install the stub before requiring config/environment.
require 'rspec'
require 'webmock/rspec'
require 'json'
require 'toggly'
RSpec.describe 'Definitions loading' do
it 'loads the initial response' do
endpoint = 'https://definitions.toggly.io/definitions/test-app/Production'
stub_request(:get, endpoint).to_return(
status: 200,
body: { features: [
{ featureKey: 'default-feature', enabled: true }
] }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
client = Toggly::Client.new(
app_key: 'test-app', environment: 'Production',
disable_background_refresh: true,
disable_entity_context_registration: true,
enable_usage_tracking: false, enable_metrics: false
)
begin
expect(client.enabled?('default-feature')).to be true
expect(a_request(:get, endpoint)).to have_been_made.once
ensure
client.close
end
end
end
Testing Patterns
Testing Feature Variations
describe ProductsController do
describe 'GET #index' do
context 'with new product list feature' do
it 'renders v2 template' do
with_feature(:new_product_list, enabled: true) do
get :index
expect(response).to render_template(:index_v2)
end
end
end
context 'without new product list feature' do
it 'renders default template' do
with_feature(:new_product_list, enabled: false) do
get :index
expect(response).to render_template(:index)
end
end
end
end
end
Testing with Context
This proves the application passes a context while the flag is stubbed on. To verify filter behavior, load real filter definitions through a deterministic HTTP fixture instead of with_feature overrides.
describe 'Premium features' do
let(:premium_user) do
Toggly::Context.new(
identity: 'user-123',
groups: ['premium'],
traits: { plan: 'enterprise' }
)
end
it 'is enabled for premium users' do
with_feature(:premium_feature, enabled: true) do
result = Toggly.enabled?(:premium_feature, context: premium_user)
expect(result).to be true
end
end
end
Testing View Helpers
describe 'Feature helper', type: :view do
it 'shows content when enabled' do
with_feature(:promo, enabled: true) do
render partial: 'shared/promo'
expect(rendered).to have_css('.promo-banner')
end
end
it 'hides content when disabled' do
with_feature(:promo, enabled: false) do
render partial: 'shared/promo'
expect(rendered).not_to have_css('.promo-banner')
end
end
end
System/Integration Tests
require 'rails_helper'
RSpec.describe 'Checkout', type: :system do
it 'uses new checkout when enabled' do
with_feature(:new_checkout, enabled: true) do
visit products_path
click_button 'Add to Cart'
click_link 'Checkout'
expect(page).to have_css('.new-checkout-form')
end
end
end
Request Specs
require 'rails_helper'
RSpec.describe 'Features API', type: :request do
describe 'GET /api/features/:id' do
it 'returns feature status' do
with_feature(:my_feature, enabled: true) do
get '/api/features/my_feature'
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to include(
'feature' => 'my_feature',
'enabled' => true
)
end
end
end
end
Debugging in Tests
# Print all feature states
it 'debug features' do
puts "Features: #{Toggly.client.feature_keys}"
puts "Enabled: #{Toggly.enabled?(:my_feature)}"
end
# Check evaluation details
it 'check evaluation' do
context = Toggly::Context.with_identity('user-123')
result = Toggly.client.evaluate(:my_feature, context: context)
puts "Result: #{result.to_h}"
end