GrowthBook Testing: Feature Flags and A/B Experiments in Jest and Vitest

GrowthBook Testing: Feature Flags and A/B Experiments in Jest and Vitest

GrowthBook is an open-source feature flagging and A/B testing platform. Its JavaScript SDK is designed to be testable: it accepts forced variations and feature overrides, making unit and integration tests straightforward.

SDK Setup

npm install @growthbook/growthbook
# React
npm install @growthbook/growthbook-react

Basic Usage

// lib/growthbook.js
import { GrowthBook } from '@growthbook/growthbook'

export function createGrowthBook(attributes = {}) {
  const gb = new GrowthBook({
    apiHost: process.env.GROWTHBOOK_API_HOST,
    clientKey: process.env.GROWTHBOOK_CLIENT_KEY,
    attributes,
    trackingCallback: (experiment, result) => {
      // Send to analytics
      analytics.track('Experiment Viewed', {
        experimentId: experiment.key,
        variationId: result.variationId,
      })
    }
  })
  return gb
}

Testing with Forced Variations

GrowthBook provides forcedVariations (for experiments) and forcedFeatureValues (for feature flags) to make tests deterministic.

// test helpers
import { GrowthBook } from '@growthbook/growthbook'

export function createTestGrowthBook({
  forcedVariations = {},
  forcedFeatureValues = {},
  attributes = {}
} = {}) {
  const gb = new GrowthBook({
    forcedVariations,
    forcedFeatureValues,
    attributes,
    trackingCallback: vi.fn(),
  })
  return gb
}

Feature Flag Tests

// pricing.js
export function getPricingTier(gb) {
  if (gb.isOn('new-pricing')) {
    return { monthly: 49, annual: 39 }
  }
  return { monthly: 29, annual: 24 }
}

// pricing.test.js
import { getPricingTier } from './pricing'
import { createTestGrowthBook } from './test-helpers'

test('returns new pricing when flag is on', () => {
  const gb = createTestGrowthBook({ forcedFeatureValues: { 'new-pricing': true } })
  const pricing = getPricingTier(gb)
  
  expect(pricing.monthly).toBe(49)
  expect(pricing.annual).toBe(39)
})

test('returns old pricing when flag is off', () => {
  const gb = createTestGrowthBook({ forcedFeatureValues: { 'new-pricing': false } })
  const pricing = getPricingTier(gb)
  
  expect(pricing.monthly).toBe(29)
  expect(pricing.annual).toBe(24)
})

A/B Experiment Tests

// checkout.js
export function getCheckoutVariant(gb) {
  const result = gb.run({
    key: 'checkout-flow',
    variations: ['single-page', 'multi-step'],
  })
  return result.value
}

// checkout.test.js
test('returns single-page variant (index 0)', () => {
  const gb = createTestGrowthBook({ forcedVariations: { 'checkout-flow': 0 } })
  expect(getCheckoutVariant(gb)).toBe('single-page')
})

test('returns multi-step variant (index 1)', () => {
  const gb = createTestGrowthBook({ forcedVariations: { 'checkout-flow': 1 } })
  expect(getCheckoutVariant(gb)).toBe('multi-step')
})

React Component Testing

// CheckoutButton.jsx
import { useFeatureIsOn, useExperiment } from '@growthbook/growthbook-react'

export function CheckoutButton({ onCheckout }) {
  const newDesign = useFeatureIsOn('new-button-design')
  const { value: variant } = useExperiment({ key: 'cta-text', variations: ['Buy Now', 'Get Started'] })
  
  return (
    <button
      data-testid="checkout-btn"
      className={newDesign ? 'btn-primary-v2' : 'btn-primary'}
      onClick={onCheckout}
    >
      {variant}
    </button>
  )
}
// CheckoutButton.test.jsx
import { render, screen } from '@testing-library/react'
import { GrowthBookProvider, GrowthBook } from '@growthbook/growthbook-react'
import { CheckoutButton } from './CheckoutButton'

function renderWithGB(overrides = {}) {
  const gb = new GrowthBook({
    forcedVariations: overrides.forcedVariations || {},
    forcedFeatureValues: overrides.forcedFeatureValues || {},
  })
  
  return render(
    <GrowthBookProvider growthbook={gb}>
      <CheckoutButton onCheckout={vi.fn()} />
    </GrowthBookProvider>
  )
}

test('shows old button style when new-button-design is off', () => {
  renderWithGB({ forcedFeatureValues: { 'new-button-design': false } })
  expect(screen.getByTestId('checkout-btn')).toHaveClass('btn-primary')
  expect(screen.getByTestId('checkout-btn')).not.toHaveClass('btn-primary-v2')
})

test('shows new button style when new-button-design is on', () => {
  renderWithGB({ forcedFeatureValues: { 'new-button-design': true } })
  expect(screen.getByTestId('checkout-btn')).toHaveClass('btn-primary-v2')
})

test('shows "Buy Now" for control variant', () => {
  renderWithGB({ forcedVariations: { 'cta-text': 0 } })
  expect(screen.getByTestId('checkout-btn')).toHaveTextContent('Buy Now')
})

test('shows "Get Started" for treatment variant', () => {
  renderWithGB({ forcedVariations: { 'cta-text': 1 } })
  expect(screen.getByTestId('checkout-btn')).toHaveTextContent('Get Started')
})

Testing Tracking Callbacks

test('calls tracking callback when experiment is evaluated', () => {
  const trackMock = vi.fn()
  const gb = new GrowthBook({
    forcedVariations: { 'checkout-flow': 1 },
    trackingCallback: trackMock,
  })
  
  gb.run({ key: 'checkout-flow', variations: ['single-page', 'multi-step'] })
  
  expect(trackMock).toHaveBeenCalledOnce()
  expect(trackMock).toHaveBeenCalledWith(
    expect.objectContaining({ key: 'checkout-flow' }),
    expect.objectContaining({ value: 'multi-step', variationId: 1 })
  )
})

Testing Targeting Conditions

GrowthBook supports targeting users based on attributes (country, plan, etc.). Test that targeting works correctly:

test('experiment only runs for premium users', () => {
  const gb = new GrowthBook({
    attributes: { plan: 'premium' },
    features: {
      'premium-feature-test': {
        rules: [{
          condition: { plan: 'premium' },
          variations: ['control', 'new-ui'],
          weights: [0.5, 0.5],
        }]
      }
    }
  })
  
  const result = gb.run({
    key: 'premium-feature-test',
    variations: ['control', 'new-ui'],
  })
  
  expect(result.inExperiment).toBe(true)
})

test('experiment excluded for free users', () => {
  const gb = new GrowthBook({
    attributes: { plan: 'free' },
    features: {
      'premium-feature-test': {
        rules: [{
          condition: { plan: 'premium' },
          variations: ['control', 'new-ui'],
        }]
      }
    }
  })
  
  const result = gb.run({
    key: 'premium-feature-test',
    variations: ['control', 'new-ui'],
  })
  
  expect(result.inExperiment).toBe(false)
})

Loading Features from API in Tests

In most test setups, skip the API fetch and load features directly:

beforeAll(async () => {
  gb.setFeatures(require('./fixtures/growthbook-features.json'))
  // or
  await gb.init({ streaming: false })
})

Keep a fixtures/growthbook-features.json snapshot of your production features. Update it periodically or generate it as part of CI.

Summary

GrowthBook testing is straightforward because the SDK explicitly supports forced variations and feature values. The pattern is: create a GrowthBook instance with forcedVariations / forcedFeatureValues set to the variant you're testing, wrap your component or function under test, and assert on behavior. Every variant of every experiment should have corresponding test coverage.

Read more