Testing a Hanami Slice
Test a slice in isolation — verify its behavior without loading the full app.
Quick Reference
- Goal: Test a slice's public interface. Never reach into another slice's internals.
- Isolation: Load only the slice under test. Mock cross-slice dependencies.
- Scope: Action specs, operation specs, repository specs, integration specs.
- Rule: A slice test must NOT depend on another slice booting.
HARD-GATE
Write test → Run test → Verify it FAILS → Implement → Verify it PASSES
DO NOT test private methods or internal slice structure.
DO NOT load another slice in a slice's isolated test. Mock cross-slice calls.
Core Process
Pattern reference: See SLICE_TEST_PATTERNS.md for slice-isolated test helpers, database strategies, action/operation/integration spec patterns, and cross-slice mocking.
- Slice test setup — configure RSpec to load only the target slice:
Validation checkpoint: confirm the slice loads before writing specs:# spec/slices/api/slice_helper.rb require "hanami/rspec" RSpec.configure do |config| config.before(:suite) do Hanami.app.prepare(:api) # load only the :api slice end endbundle exec rspec spec/slices/api/ --dry-run # Expect: 0 errors, all examples collected. Fix any boot errors before proceeding. - Action specs — test the HTTP boundary. Stub injected operations:
RSpec.describe Api::Actions::Users::Create, :slice do let(:create_user) { instance_double(Users::CreateUser) } let(:action) { described_class.new(create_user:) } end - Operation specs — test business logic with stubbed repositories and services:
RSpec.describe Api::Operations::Users::Create, :slice do let(:repo) { instance_double(Api::Repositories::UserRepo) } subject(:operation) { described_class.new(user_repo: repo) } it "creates a user and returns a success result" do allow(repo).to receive(:create).and_return(double(id: 42, email: "a@example.com")) result = operation.call(email: "a@example.com", name: "Alice") expect(result).to be_success end end - Repository specs — test query methods against a test database. No stubs needed. Requires a running test DB and
DatabaseCleaner(or equivalent) configured inspec/support/database.rb:RSpec.describe Api::Repositories::UserRepo, :slice, :db do subject(:repo) { described_class.new } it "finds a user by email" do repo.create(email: "b@example.com", name: "Bob") user = repo.find_by_email("b@example.com") expect(user.email).to eq("b@example.com") end end - Integration specs — test a full slice workflow (action → operation → repository) within the slice's boundary. Boot only the target slice, use real collaborators inside it, and assert on the HTTP response or returned result:
RSpec.describe "Api users flow", :slice, :db do it "creates and retrieves a user" do result = Api::Actions::Users::Create.new.call(params: { email: "c@example.com", name: "Carol" }) expect(result[:status]).to eq(201) end end - Cross-slice testing — test interactions between slices through their public API only. When a slice boundary must be exercised, stub the public interface or use a shared test helper rather than instantiating the other slice's internals:
# Never: Api::Repositories::UserRepo.new.find(id) from another slice # Prefer: stub the public action let(:users_api) { instance_double(Api::Actions::Users::Show) } before { allow(users_api).to receive(:call).and_return({ status: 200, user: { id: 1 } }) } # Or: extract shared test helpers to spec/support/slices/api_helpers.rb # and include them in both slices' specs.
Output Style
- Slice helper — the test setup file for the slice.
- Action spec — test the HTTP boundary with stubbed operations.
- Operation spec — test business logic with stubbed dependencies.
- Repository spec — test queries against test data.
- English only unless user requests otherwise.
Integration
| Skill | When to chain |
|---|---|
| create-slice | Test immediately after creating a new slice |
| plan-tests | Plan the test strategy before writing slice specs |
| slice-lifecycle | Part of the slice development lifecycle agent |