Smartest
Smartest is a small Ruby test runner with a keyword-fixture-first design.
It lets you write tests like this:
test("factorial") do
expect(1 * 2 * 3).to eq(6)
end
and fixture-driven tests like this:
test("GET /me") do |logged_in_client:|
response = logged_in_client.get("/me")
expect(response.status).to eq(200)
end
Smartest is designed around three ideas:
- Tests should be readable at the top level.
- Fixture dependencies should be explicit.
- Teardown should be written only when it is needed.
Installation
Add this line to your application's Gemfile:
gem "smartest"
Then run:
bundle install
Or install it directly:
gem install smartest
Quick start
Initialize a test scaffold:
bundle exec smartest --init
This creates:
smartest/test_helper.rb
smartest/fixtures/
smartest/example_test.rb
The generated example looks like this:
# smartest/example_test.rb
require "test_helper"
test("example") do
expect(1 + 1).to eq(2)
end
Run the suite:
bundle exec smartest
By default, Smartest loads smartest/**/*_test.rb, so a separate test/
directory can remain available for Minitest.
You can also pass explicit paths:
bundle exec smartest smartest/**/*_test.rb
CLI help and version output are available with:
bundle exec smartest --help
bundle exec smartest --version
Expected output:
Running 1 test
✓ example
1 test, 1 passed, 0 failed
Defining tests
Use test at the top level:
test("adds numbers") do
expect(1 + 2).to eq(3)
end
A test can request fixtures using required keyword arguments:
test("uses a user") do |user:|
expect(user.name).to eq("Alice")
end
Smartest intentionally favors keyword arguments for fixture injection:
test("GET /me") do |logged_in_client:|
# ...
end
This makes fixture usage explicit and avoids relying on positional argument order.
Expectations
Smartest uses an expectation style:
expect(actual).to eq(expected)
expect(actual).not_to eq(expected)
Examples:
test("string") do
expect("hello").to eq("hello")
end
test("array") do
expect([1, 2, 3]).to include(2)
end
Supported matchers include:
eq(expected)
include(expected)
be_nil
raise_error(ErrorClass)
Fixtures
Fixtures are defined in classes.
class AppFixture < Smartest::Fixture
fixture :user do
User.create!(
name: "Alice",
email: "alice@example.com"
)
end
end
use_fixture AppFixture
test("user") do |user:|
expect(user.name).to eq("Alice")
end
A fixture is requested by name from a test block keyword argument.
test("user") do |user:|
# Smartest resolves the `user` fixture
end
Fixture dependencies
Fixtures can depend on other fixtures using required keyword arguments.
class AppFixture < Smartest::Fixture
suite_fixture :server do
TestServer.start
end
fixture :client do |server:|
Client.new(base_url: server.url)
end
end
The dependency is explicit:
fixture :client do |server:|
Client.new(base_url: server.url)
end
When a test requests client, Smartest resolves server first.
test("GET /health") do |client:|
response = client.get("/health")
expect(response.status).to eq(200)
end
Fixture scopes
Regular fixture definitions are test-scoped. Smartest creates a fresh value
for each test that requests the fixture.
Use suite_fixture for expensive resources that should be created once and
released after the full suite finishes:
class BrowserFixture < Smartest::Fixture
suite_fixture :browser do
browser = Browser.launch
cleanup { browser.close }
browser
end
fixture :page do |browser:|
browser.new_page
end
end
Suite fixtures are lazy: setup runs the first time a test requests the fixture, and cleanup runs once after all tests finish. Test-scoped fixtures can depend on suite fixtures, but suite fixtures cannot depend on test-scoped fixtures.
Fixtures with teardown
Not every fixture needs teardown. For fixtures that do, use cleanup.
class WebFixture < Smartest::Fixture
fixture :server do
server = TestServer.start
cleanup { server.stop }
server.wait_until_ready!
server
end
fixture :client do |server:|
Client.new(base_url: server.url)
end
end
cleanup blocks run after the fixture's scope finishes. For regular fixtures
that means after the test. For suite_fixture, cleanup runs after the full
suite.
They are executed in reverse order of registration.
fixture :temp_dir do
dir = Dir.mktmpdir
cleanup { FileUtils.rm_rf(dir) }
dir
end
Recommended pattern:
fixture :server do
server = TestServer.start
cleanup { server.stop }
server.wait_until_ready!
server
end
Register cleanup immediately after acquiring the resource, before later setup steps that may fail.
Logged-in client example
class WebFixture < Smartest::Fixture
fixture :server do
server = TestServer.start
cleanup { server.stop }
server.wait_until_ready!
server
end
fixture :client do |server:|
Client.new(base_url: server.url)
end
fixture :user do
User.create!(
name: "Alice",
email: "alice@example.com"
)
end
fixture :logged_in_client do |client:, user:|
client.login(user)
client
end
end
use_fixture WebFixture
test("GET /me") do |logged_in_client:|
response = logged_in_client.get("/me")
expect(response.status).to eq(200)
end
Dependency graph:
logged_in_client
├── client
│ └── server
└── user
Execution flow:
server setup
client setup
user setup
logged_in_client setup
test body
server cleanup
Registering fixture classes
Use use_fixture:
use_fixture AppFixture
Multiple fixture classes can be registered:
use_fixture UserFixture
use_fixture WebFixture
use_fixture ApiFixture
Fixture names must be unique across registered fixture classes.
If two fixture classes define the same fixture name, Smartest raises an error.
Hooks
Smartest may support simple hooks:
before do
DatabaseCleaner.start
end
after do
DatabaseCleaner.clean
end
Hooks are separate from fixture cleanup.
Use fixture cleanup for resource-specific teardown:
fixture :server do
server = TestServer.start
cleanup { server.stop }
server
end
Use hooks for broad test-level behavior:
before do
reset_global_state
end
Recommended file structure
smartest/
test_helper.rb
fixtures/
app_fixture.rb
web_fixture.rb
example_test.rb
# smartest/test_helper.rb
require "smartest/autorun"
Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
require fixture_file
end
The generated helper loads Ruby files under smartest/fixtures/ in sorted order.
Test files still register the fixture classes they need with use_fixture.
Example:
# smartest/fixtures/web_fixture.rb
class WebFixture < Smartest::Fixture
fixture :server do
server = TestServer.start
cleanup { server.stop }
server
end
fixture :client do |server:|
Client.new(base_url: server.url)
end
end
# smartest/example_test.rb
require "test_helper"
use_fixture WebFixture
test("GET /health") do |client:|
expect(client.get("/health").status).to eq(200)
end
Design choices
Smartest intentionally does not use this style as the primary API:
test("GET /me") do |logged_in_client|
end
Instead, Smartest prefers:
test("GET /me") do |logged_in_client:|
end
Keyword arguments make fixture injection explicit.
Smartest also avoids this fixture dependency style:
fixture :client, with: [:server] do |server|
Client.new(base_url: server.url)
end
Instead, it prefers:
fixture :client do |server:|
Client.new(base_url: server.url)
end
The dependency declaration and usage are in one place.
Status
Smartest is currently a design-stage test runner.
The intended MVP includes:
- top-level
test - class-based fixtures
- keyword-argument fixture injection
- fixture dependencies through keyword arguments
- fixture cleanup
expect(...).to eq(...)- console reporter
- CLI runner
- circular fixture dependency detection
- duplicate fixture detection