Password manager-backed settings stores for Hanami
2.7 kB · 91 lines
Markdown
at main

Testing #

This guide covers how to write and organize tests in hanami-settings-stores. Tests live in the spec/ directory, mirroring the structure of lib/.

The goal is to test meaningful behavior without over-testing trivial code. When in doubt, focus on logic that could break — transformations, edge cases, and error handling.

Running Tests #

mise run spec                        # Run all tests with coverage
bundle exec rspec --tag focus        # Run only focused tests
bundle exec rspec spec/path/to_spec  # Run a specific file

What to Test #

Test:

  • Methods with logic (conditionals, iteration, transformations)
  • Error handling and edge cases
  • Fallback behavior and delegation
  • Public API contracts

Skip:

  • Simple attribute readers
  • Trivial delegations with no logic
  • Private methods directly (test them through public methods)

Before writing a test, ask: "Does this test verify actual logic, or just that Ruby's attribute access works?"

Test Structure #

Tests are organized using RSpec's describe and context blocks:

# frozen_string_literal: true

RSpec.describe Hanami::Settings::CompositeStore do
  subject(:store) { described_class.new(*stores) }

  let(:stores) { [primary, fallback] }
  let(:primary) { { database_url: "postgres://localhost/myapp" } }
  let(:fallback) { { session_secret: "supersecret" } }

  describe "#fetch" do
    it "returns the value from the first store that has the key" do
      expect(store.fetch(:database_url)).to eq("postgres://localhost/myapp")
    end

    it "falls back to subsequent stores when the key is missing" do
      expect(store.fetch(:session_secret)).to eq("supersecret")
    end
  end
end

Conventions #

Naming: Test descriptions use the pattern it "<does something>". Use describe "#method_name" for instance methods and describe ".method_name" for class methods.

Ordering: Describe methods in the order they appear in the source class. Within a describe block, order context blocks from the happy path to edge cases.

Test body structure: Use subject and let for setup. Separate setup from assertions with a blank line when inline setup is needed:

it "handles the edge case" do
  store = described_class.new(empty_store)

  expect { store.fetch(:missing) }.to raise_error(KeyError)
end

Matchers: Prefer specific matchers over generic ones:

# Preferred
expect(result).to eq("value")
expect { action }.to raise_error(KeyError, /missing/)

# Avoid
expect(result == "value").to be true

Shared examples: Use shared examples for behavior common across multiple classes, but don't over-abstract. If a shared example only applies to two specs, inline the tests instead.