Stubs
Smartest provides small stub helpers for replacing Ruby methods during a test. They are useful when a test depends on stubbed behavior and you want that dependency to be visible in the test signature:
test("shows the suspended account state") do |suspended_user_logged_in:|
expect(AccountStatus.call).to eq(:suspended)
end
The fixture name makes it clear that this test depends on an authenticated suspended user.
If you are familiar with RSpec, this is similar to putting a stub in before:
let(:suspended_user) { create(:user, :suspended) }
before do
allow_any_instance_of(ApplicationController)
.to receive(:current_user)
.and_return(suspended_user)
end
In Smartest, the same idea is expressed as a fixture:
class EdgeCaseFixture < Smartest::Fixture
fixture :suspended_user do
create(:user, :suspended)
end
fixture :suspended_user_logged_in do |suspended_user:|
simple_stub_any_instance_of(ApplicationController, :current_user) { suspended_user }
suspended_user
end
end
Register the fixture class from around_suite before tests request the fixture:
around_suite do |suite|
use_fixture EdgeCaseFixture
suite.run
end
use_fixture is available inside around_suite or around_test blocks, not as
a top-level method in a test file.
The stub is automatically reset when the fixture is torn down. Because the stub
fixture depends on suspended_user:, the user record and authenticated state
stay tied together in one fixture dependency graph.
Instance Method Stubs
Use simple_stub_any_instance_of for instance methods:
simple_stub_any_instance_of(ApplicationController, :current_user) { user }
The stub affects existing instances and new instances of the target class until teardown resets it. Method stubs are shared across Fibers and Threads, so a stub applied by test setup is also visible to a Rails test server running in another thread.
You can also use method stubs for external services that should not run during tests, such as push notifications or payment processing:
class ApplicationTestFixture < Smartest::Fixture
fixture :payment_gateway_stub do
simple_stub_any_instance_of(PaymentGateway, :charge) { :approved }
end
end
Class Method Stubs
Use simple_stub for singleton methods, including class methods:
class TimeFixture < Smartest::Fixture
fixture :fixed_time do
frozen_time = Time.utc(2026, 1, 1, 0, 0, 0)
simple_stub(Time, :now) { frozen_time }
frozen_time
end
end
test("uses fixed time") do |fixed_time:|
expect(Time.now).to eq(fixed_time)
end
Constant Stubs
Use with_stub_const with a block for constants. It is available in test
bodies, around_test, and around_suite, not in fixture blocks.
test("uses fake payment provider") do
with_stub_const("AppConfig::PAYMENT_PROVIDER", "fake") do
expect(Checkout.call).to eq(:paid)
end
end
If you are coming from RSpec, use Smartest's around_test where you would often
think of an around example hook:
around_test do |test|
with_stub_const("AppConfig::PAYMENT_PROVIDER", "fake") do
test.run
end
end
Use around_suite when the constant should stay replaced for the whole suite
run:
around_suite do |suite|
with_stub_const("AppConfig::PAYMENT_PROVIDER", "fake") do
suite.run
end
end
Constant stubs are process-global until the block exits.
How Method Stub Teardown Works
You do not need to call reset manually when using method stub helpers inside
fixtures. The helper internally:
- creates the stub state
- applies the replacement
- registers teardown to reset it
Conceptually, this:
simple_stub_any_instance_of(ApplicationController, :current_user) { user }
behaves like:
stub = Smartest::SimpleStub.new(ApplicationController, :current_user) { user }
stub.apply
on_teardown { stub.reset }
Teardown is tied to the fixture lifecycle:
fixturemethod stubs reset after each test.suite_fixturemethod stubs reset after the suite fixture scope ends.
When method stubs for the same class and method overlap, the most recently applied stub wins. Resetting that stub restores the previous stub instead of resetting the whole stack. This lets a test-scoped stub temporarily override a suite-scoped stub.
In most cases, prefer the fixture helpers so stub lifetime is automatically tied to the fixture lifecycle.
How Constant Stub Blocks Work
with_stub_const records the previous constant value, replaces it,
yields to the block, and restores or removes the constant with ensure.
Conceptually, this:
with_stub_const("AppConfig::PAYMENT_PROVIDER", "fake") do
call_api
end
behaves like:
old_value = AppConfig.const_get(:PAYMENT_PROVIDER, false)
AppConfig.__send__(:remove_const, :PAYMENT_PROVIDER)
AppConfig.const_set(:PAYMENT_PROVIDER, "fake")
begin
call_api
ensure
AppConfig.__send__(:remove_const, :PAYMENT_PROVIDER)
AppConfig.const_set(:PAYMENT_PROVIDER, old_value)
end
The real implementation uses remove_const and const_set so it can also
restore constants that did not exist before the block.
API
simple_stub_any_instance_of(klass, method_name) { ... } stubs an instance
method on klass.
simple_stub_any_instance_of(ApplicationController, :current_user) { user }
simple_stub(object, method_name) { ... } stubs a singleton method on object.
For class methods, pass the class object:
simple_stub(Time, :now) { fixed_time }
with_stub_const(constant_path, value) { ... } stubs a constant for the
duration of the block. The path may be a String or Symbol:
with_stub_const("AppConfig::PAYMENT_PROVIDER", "fake") do
call_api
end
simple_stub_any_instance_of and simple_stub return the
Smartest::SimpleStub object. with_stub_const returns the block result.
simple_stub_any_instance_of and simple_stub are available inside
Smartest::Fixture fixture blocks, including fixture and suite_fixture,
because they need on_teardown to keep the stub lifetime tied to the fixture scope.
with_stub_const is available in test bodies, around_test, and
around_suite.
What Stubs Are Not
Smartest stubs are not a mock framework. They do not:
- verify calls
- record arguments
- provide expectations
Use Smartest expectations for assertions.
Low-Level API
Use Smartest::SimpleStub directly when you need to manage reset manually:
stub = Smartest::SimpleStub.new(PaymentGateway, :charge) { :approved }
stub.apply
stub.reset
The first argument must be a Class, and the second argument must be a
Symbol. For singleton methods, pass the object's singleton class:
stub = Smartest::SimpleStub.new(Time.singleton_class, :now) { fixed_time }
stub.apply
stub.reset
apply raises Smartest::SimpleStub::AlreadyAppliedError when the same stub
object is already applied. reset raises
Smartest::SimpleStub::NotAppliedError when that stub object is not applied.
Reset must be called on the stub object returned by the original setup.
Scope and Concurrency
Smartest stubs are process-wide state. They are intended for serial test execution and for cases like a Rails test server thread serving the current test. They do not provide isolation for multi-threaded parallel test execution: one test can observe or reset another test's method or constant stub.
Smartest::SimpleStub installs a process-wide dispatcher method and stores
active method stubs in a process-wide registry. Applying a stub changes behavior
in all Fibers and Threads until that stub is reset:
stub = Smartest::SimpleStub.new(User, :name) { "Stubbed" }
stub.apply
User.new.name
# => "Stubbed"
Thread.new do
User.new.name
# => "Stubbed"
end.join
stub.reset
Method stubs for the same class and method are stacked. The newest applied stub is used, and resetting it restores the previous stub. This stack is for deliberately nested stub lifetimes, such as a test-scoped fixture temporarily overriding a suite-scoped fixture.
Constant stubs are also process-global: with_stub_const replaces the constant
on the owner module until the block exits.