Skip to content

Testing

Testing Your Settings

Basic Test Setup

# spec/models/settings_spec.rb
require 'rails_helper'

RSpec.describe Settings do
  describe 'configuration' do
    it 'loads the settings file' do
      expect(Settings.app_name).not_to be_nil
    end

    it 'uses the correct environment' do
      expect(Settings.namespace).to eq(Rails.env)
    end

    it 'has required settings' do
      expect(Settings.database.host).not_to be_nil
      expect(Settings.database.port).to be_a(Integer)
    end
  end
end

Testing with Different Settings

RSpec.describe MyService do
  context 'with caching enabled' do
    before do
      allow(Settings.features).to receive(:caching).and_return(true)
    end

    it 'uses cache' do
      expect(Rails.cache).to receive(:fetch)
      MyService.new.perform
    end
  end

  context 'with caching disabled' do
    before do
      allow(Settings.features).to receive(:caching).and_return(false)
    end

    it 'skips cache' do
      expect(Rails.cache).not_to receive(:fetch)
      MyService.new.perform
    end
  end
end

Test Helpers

Settings Test Helper

Create a helper for managing settings in tests:

# spec/support/settings_helper.rb
module SettingsHelper
  def with_modified_settings
    original_settings = Settings.to_h.deep_dup
    yield
  ensure
    Settings.reload!
  end

  def stub_settings(overrides)
    overrides.each do |keys, value|
      keys = keys.to_s.split('.') if keys.is_a?(Symbol)
      keys = [keys] unless keys.is_a?(Array)

      stub_nested_setting(Settings, keys, value)
    end
  end

  private

  def stub_nested_setting(obj, keys, value)
    if keys.length == 1
      allow(obj).to receive(keys.first).and_return(value)
    else
      nested = double
      allow(obj).to receive(keys.first).and_return(nested)
      stub_nested_setting(nested, keys[1..-1], value)
    end
  end
end

RSpec.configure do |config|
  config.include SettingsHelper
end

Using the Helper

RSpec.describe OrderService do
  it 'sends email when enabled' do
    stub_settings('email.enabled' => true, 'email.from' => 'test@example.com')

    expect(OrderMailer).to receive(:confirmation)
    OrderService.new.process_order
  end

  it 'skips email when disabled' do
    stub_settings('email.enabled' => false)

    expect(OrderMailer).not_to receive(:confirmation)
    OrderService.new.process_order
  end
end

Testing settingslogic Itself

Unit Tests

# spec/unit/settingslogic_spec.rb
require 'spec_helper'
require 'settingslogic'

RSpec.describe Settingslogic do
  let(:settings_class) do
    Class.new(Settingslogic) do
      source({ 
        'name' => 'Test App',
        'nested' => { 'value' => 42 }
      })
    end
  end

  describe '#[]' do
    it 'accesses values by string key' do
      expect(settings_class['name']).to eq('Test App')
    end

    it 'accesses values by symbol key' do
      expect(settings_class[:name]).to eq('Test App')
    end

    it 'accesses nested values' do
      expect(settings_class['nested']['value']).to eq(42)
    end
  end

  describe '#method_missing' do
    it 'provides method access to settings' do
      expect(settings_class.name).to eq('Test App')
    end

    it 'provides method access to nested settings' do
      expect(settings_class.nested.value).to eq(42)
    end

    it 'returns nil for missing settings' do
      expect(settings_class.missing).to be_nil
    end
  end
end

Testing YAML Loading

RSpec.describe 'YAML loading' do
  let(:yaml_file) { Rails.root.join('spec', 'fixtures', 'test_settings.yml') }

  before do
    File.write(yaml_file, <<~YAML)
      test:
        app_name: Test Application
        features:
          api: true
          cache: false
    YAML
  end

  after do
    File.delete(yaml_file) if File.exist?(yaml_file)
  end

  it 'loads YAML file correctly' do
    settings = Class.new(Settingslogic) do
      source yaml_file.to_s
      namespace 'test'
    end

    expect(settings.app_name).to eq('Test Application')
    expect(settings.features.api).to be true
    expect(settings.features.cache).to be false
  end
end

Testing Security Features

RSpec.describe 'Security features' do
  context 'YAML.safe_load' do
    it 'rejects unsafe YAML by default' do
      unsafe_yaml = "exploit: !ruby/object:Kernel {}"

      expect {
        YAML.safe_load(unsafe_yaml, permitted_classes: Settingslogic.yaml_permitted_classes)
      }.to raise_error(Psych::DisallowedClass)
    end

    it 'allows permitted classes' do
      safe_yaml = "date: 2024-01-01\ntime: 2024-01-01 10:00:00"

      result = YAML.safe_load(safe_yaml, permitted_classes: Settingslogic.yaml_permitted_classes)
      expect(result['date']).to be_a(Date)
      expect(result['time']).to be_a(Time)
    end
  end

  context 'permitted_classes configuration' do
    it 'can add additional permitted classes' do
      original = Settingslogic.yaml_permitted_classes.dup

      Settingslogic.yaml_permitted_classes += [Regexp]
      expect(Settingslogic.yaml_permitted_classes).to include(Regexp)

      Settingslogic.yaml_permitted_classes = original
    end
  end
end

Integration Tests

Rails Integration Test

# spec/integration/rails_settings_spec.rb
require 'rails_helper'

RSpec.describe 'Rails settings integration', type: :request do
  it 'uses settings in controllers' do
    get '/'
    expect(response.body).to include(Settings.app_name)
  end

  it 'settings persist across requests' do
    get '/'
    first_response = response.body

    get '/'
    second_response = response.body

    expect(first_response).to eq(second_response)
  end
end

Testing Environment-Specific Settings

RSpec.describe 'Environment settings' do
  it 'loads development settings in development' do
    allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))

    settings = Class.new(Settingslogic) do
      source Rails.root.join('config', 'application.yml')
      namespace Rails.env
      reload!
    end

    expect(settings.debug).to be true
  end

  it 'loads production settings in production' do
    allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))

    settings = Class.new(Settingslogic) do
      source Rails.root.join('config', 'application.yml')
      namespace Rails.env
      reload!
    end

    expect(settings.debug).to be false
  end
end

Testing Best Practices

1. Isolate Settings in Tests

RSpec.describe MyService do
  let(:service) { described_class.new }

  # Good - isolates the setting being tested
  it 'respects timeout setting' do
    allow(Settings).to receive(:timeout).and_return(5)

    expect(service.timeout).to eq(5)
  end

  # Avoid - changes global state
  it 'respects timeout setting' do
    Settings.timeout = 5  # Don't do this!

    expect(service.timeout).to eq(5)
  end
end

2. Test Settings Validation

RSpec.describe Settings do
  describe '.validate!' do
    it 'raises error for missing required settings' do
      allow(Settings).to receive(:database).and_return(nil)

      expect { Settings.validate! }.to raise_error(/database required/)
    end

    it 'passes for valid settings' do
      expect { Settings.validate! }.not_to raise_error
    end
  end
end

3. Test ERB Processing

RSpec.describe 'ERB in settings' do
  it 'processes ERB tags' do
    yaml_content = <<~YAML
      test:
        year: <%= Date.today.year %>
        calculated: <%= 5 + 5 %>
    YAML

    Tempfile.create(['settings', '.yml']) do |file|
      file.write(yaml_content)
      file.flush

      settings = Class.new(Settingslogic) do
        source file.path
        namespace 'test'
      end

      expect(settings.year).to eq(Date.today.year)
      expect(settings.calculated).to eq(10)
    end
  end
end

4. Test Error Handling

RSpec.describe 'Error handling' do
  it 'raises error for missing file without suppress_errors' do
    expect {
      Class.new(Settingslogic) do
        source '/nonexistent/file.yml'
      end.load!
    }.to raise_error(Errno::ENOENT)
  end

  it 'returns empty settings with suppress_errors' do
    settings = Class.new(Settingslogic) do
      source '/nonexistent/file.yml'
      suppress_errors true
    end

    expect(settings.to_h).to eq({})
  end
end

Continuous Integration

GitHub Actions Example

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: '3.2'
        bundler-cache: true

    - name: Run tests
      run: |
        bundle exec rspec

    - name: Test settings loading
      run: |
        bundle exec rspec spec/models/settings_spec.rb