Hola, Hanamigos! ![]()
In May we’ll be releasing the first version of Hanami Minitest for Hanami v2. I’ve been working on this PR to put it together: Get started by timriley · Pull Request #3 · hanami/hanami-minitest · GitHub
I’m not a regular Minitest user myself, so I need your help to get this right.
I’ve put together an example app that uses the files that will be generated by Hanami Minitest: GitHub - timriley/minitest_example: Example Hanami app using Minitest
Setup and support files
These setup and support files are broadly a mirror of how we set up RSpec, just converted into what I hope is reasonable minitest idioms.
Of note:
- We open up
Minitest::Testto allow for custom behaviour to be provided to all tests. You can see an example of us doing this ourselves fromspec/support/operations.rb. Most ordinary tests will therefore inherit fromMinitest::Test. - We have (top-level)
RequestTestandFeatureTestclasses to inherit from for those particular categories of test. I went with this approach because it felt like it would look and feel better compared to nested superclasses.- In general we try not to take top-level names away from our users, but I did struggle to find a decent namespace to keep these within. Putting them inside
Hanami::Minitestmay work, e.g.Hanami::Minitest::FeatureTest, but that limits our ability in the future to put actual classes with that name in the gem in the future. Another option would be to take just a single top-level name and put these test superclasses within it, e.g.Test::FeatureTest. But when I played that out, the naming for the basic test class never felt right:Test::Test? Gross.Test::Case? But then that’s inconsistent withTest::FeatureTest. See what I mean? This is why I chose those basic top-level classes to start with.
- In general we try not to take top-level names away from our users, but I did struggle to find a decent namespace to keep these within. Putting them inside
- Where we have any support code that actually needs to exist as concrete classes, we put them inside a
TestSupportnamespace. (Naming this one was easier!). - The general approach we’ve taken for both Hanami RSpec and now Hanami Minitest has been to generate all necessary setup code into the user’s app, rather than trying to keep it within the gem code. This gives the user maximum flexibility to tweak that setup to suit their app. Over time, we may look to provide some more of this setup “out of the box”, but for now that’s out of scope for these initial
test/test_helper.rb:
# frozen_string_literal: true
require "pathname"
TEST_ROOT = Pathname(__dir__).realpath.freeze
ENV["HANAMI_ENV"] ||= "test"
require "hanami/prepare"
require_relative "support/minitest"
TEST_ROOT.glob("support/**/*.rb").each { |f| require f }
test/support/minitest.rb:
# frozen_string_literal: true
require "minitest/autorun"
module Minitest
class Test
# Add helper methods to be used by all tests here.
end
end
test/support/operations.rb:
# frozen_string_literal: true
require "dry/monads"
module Minitest
class Test
# Provide `Success` and `Failure` for testing operation results.
include Dry::Monads[:result]
end
end
test/support/requests.rb:
# frozen_string_literal: true
require "rack/test"
class RequestTest < Minitest::Test
include Rack::Test::Methods
# Defines the app for Rack::Test requests.
def app
Hanami.app
end
end
test/support/features.rb:
# frozen_string_literal: true
require "capybara/minitest"
class FeatureTest < Minitest::Test
include Capybara::DSL
include Capybara::Minitest::Assertions
def teardown
super
Capybara.reset_sessions!
Capybara.use_default_driver
end
end
Capybara.app = Hanami.app
test/support/db.rb:
# frozen_string_literal: true
require_relative "features"
require_relative "db/cleaning"
module TestSupport
module DB
def self.included(mod)
mod.include DB::Cleaning
end
# Add helper methods to be used by DB tests here.
end
end
class FeatureTest
include TestSupport::DB
end
test/support/db/cleaning.rb:
# frozen_string_literal: true
require "database_cleaner/sequel"
module TestSupport
module DB
module Cleaning
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def db_cleaning_with_truncation!
@db_cleaning_with_truncation = true
end
def js! = db_cleaning_with_truncation!
end
def setup
# Clean all databases before the first test
Cleaning.once do
Cleaning.all_databases.each do |db|
DatabaseCleaner[:sequel, db: db].clean_with :truncation, except: ["schema_migrations"]
end
end
use_truncation = self.class.instance_variable_get(:@db_cleaning_with_truncation)
strategy = use_truncation ? :truncation : :transaction
Cleaning.all_databases.each do |db|
DatabaseCleaner[:sequel, db: db].strategy = strategy
DatabaseCleaner[:sequel, db: db].start
end
super
end
def teardown
Cleaning.all_databases.each do |db|
DatabaseCleaner[:sequel, db: db].clean
end
super
end
class << self
def once
@cleaned_once ||= false
return if @cleaned_once
yield
@cleaned_once = true
end
def all_databases
@all_databases ||= begin
slices = [Hanami.app] + Hanami.app.slices.with_nested
slices.each_with_object([]) { |slice, dbs|
next unless slice.key?("db.rom")
dbs.concat slice["db.rom"].gateways.values.map(&:connection)
}.uniq
end
end
end
end
end
end
Some actual tests
Now you’ve seen all the setup, here’s how real tests look.
A request test (generated as part of the basic hanami-minitest install), test/requests/root_test.rb:
# frozen_string_literal: true
require "test_helper"
class RootTest < RequestTest
def test_not_found
get "/"
# Generate new action via:
# `bundle exec hanami generate action home.index --url=/`
assert_equal 404, last_response.status
end
end
An action test (generated as part of hanami g action), test/actions/posts/index_test.rb:
# frozen_string_literal: true
require "test_helper"
class MinitestExample::Actions::Posts::IndexTest < Minitest::Test
def test_works
params = {}
response = subject.call(params)
assert_predicate response, :successful?
end
private
def subject
MinitestExample::Actions::Posts::Index.new
end
end
A plain old regular test, this one demonstrating that Result is available to use in every test, test/posts/operations/create_test.rb:
# frozen_string_literal: true
require "test_helper"
module Posts
module Operations
class CreateTest < Minitest::Test
def test_returns_success
result = subject.call
assert_equal Success("Created!"), result
end
private
def subject
MinitestExample::Posts::Operations::Create.new
end
end
end
end
And lastly, a Capybara-driven feature test, this one demonstrating the database cleaning working in between individual test scenarios, test/features/posts_test.rb:
# frozen_string_literal: true
require "test_helper"
class PostsTest < FeatureTest
def test_displays_posts_index_page
relation = Hanami.app["relations.posts"]
relation.insert(title: "Together breakfast", published_at: Time.local(2026,2,20,16,15))
visit "/posts"
assert_equal 200, page.status_code
assert_selector "h1", text: "MinitestExample::Views::Posts::Index"
assert_selector "div.post", text: "Together breakfast"
end
# Add another test to demonstrate database cleaning
def test_displays_posts_index_page_2
relation = Hanami.app["relations.posts"]
relation.insert(title: "Tiger Millionaire", published_at: Time.local(2026,2,20,16,15))
visit "/posts"
assert_equal 200, page.status_code
assert_selector "h1", text: "MinitestExample::Views::Posts::Index"
assert_selector "div.post", text: "Tiger Millionaire"
end
end
How to run your tests
These tests all run via bundle exec rake test:
❯ bundle exec rake test
Run options: --seed 51773
# Running:
.......
Finished in 0.089953s, 77.8184 runs/s, 144.5199 assertions/s.
7 runs, 13 assertions, 0 failures, 0 errors, 0 skips
Run an individual test file by passing TEST=
❯ bundle exec rake test TEST=test/features/posts_test.rb
Run options: --seed 24183
# Running:
..
Finished in 0.052808s, 37.8730 runs/s, 113.6191 assertions/s.
2 runs, 6 assertions, 0 failures, 0 errors, 0 skips
Or by running that test file directly using bundle exec ruby -Itest:
❯ bundle exec ruby -Itest test/features/posts_test.rb
Run options: --seed 24272
# Running:
..
Finished in 0.061096s, 32.7354 runs/s, 98.2061 assertions/s.
2 runs, 6 assertions, 0 failures, 0 errors, 0 skips
Your feedback, please!
Minitest users, how does this all feel to you? Is there anything you’d like to see us change, in order for this to be a more conventional, natural-feeling Minitest experience?
One thing I want specific feedback about is what to name our test classes.
If you review the test examples above, you’ll see a variety of approaches:
class RootTest < RequestTest
class MinitestExample::Actions::Posts::IndexTest < Minitest::Test
module Posts
module Operations
class CreateTest < Minitest::Test
class PostsTest < FeatureTest
Obviously, we can’t ship tests like this. We need a single consistent naming approach that can apply to all test types. What should it be?
If you’d like to try this out yourself directly, please pull down GitHub - timriley/minitest_example: Example Hanami app using Minitest and have a play!