Save and restore the state of Ruby modules and classes in tests: class
variables (@@var) and instance variables of the modules and classes
themselves (@var). It keeps tests that touch global state (singletons,
registries, caches, settings stored in modules) from affecting each other.
Not intended for production use. The gem does not create a truly isolated environment: it only puts values back into the same objects.
Русская версия: README-ru.md
Add to your Gemfile:
gem 'is-module-reset', group: :testOr install directly:
gem install is-module-resetrequire 'is-module-reset'
module App
@@requests = 0
@settings = { debug: false }
def self.requests
@@requests
end
def self.hit!
@@requests += 1
end
def self.settings
@settings
end
endRun a block and roll everything back afterwards:
IS::ModuleReset.isolate(App) do
App.hit!
App.settings[:debug] = true
App.requests # => 1
end
App.requests # => 0
App.settings # => { debug: false }Or take a snapshot and restore it manually:
state = IS::ModuleReset.dump(App)
App.hit!
App.settings[:debug] = true
IS::ModuleReset.undump(state)
App.requests # => 0
App.settings # => { debug: false }For every module or class you pass in, and for every module or class
reachable from it through constants (recursively, using only the module's own
constants, i.e. Module#constants(false)), the gem saves:
- the class variables defined by that module itself (inherited ones are not included);
- the instance variables of the module object itself (
@varset at module or class level, including those created byattr_accessorinclass << self).
When restoring, all such variables are removed and the saved ones are set again. This means that variables created after the snapshot disappear, changed ones return to their old values, and deleted ones come back.
Values are copied with deep_dup from is-deep:
Hash and Array are copied recursively (shared and circular references are
preserved), modules and classes are kept as they are, and other objects are
copied with dup (if an object cannot be duplicated, the same object is kept).
Restoring also works on a copy, so the same snapshot can be restored any
number of times.
Takes a snapshot of the given modules and everything reachable from them via
constants. The returned hash is an internal representation; treat it as an
opaque value and pass it to undump.
Restores the state from a snapshot returned by dump. May be called multiple
times with the same snapshot.
Takes a snapshot, runs the block, and restores the snapshot afterwards, even if
the block raised an exception. Returns the value of the block. Raises
ArgumentError if no block is given.
Isolate every example:
RSpec.configure do |config|
config.around do |example|
IS::ModuleReset.isolate(MyApp) { example.run }
end
endaround hooks wrap before and after hooks, so changes made there are
rolled back too.
Or restore the state captured when the suite started:
RSpec.configure do |config|
baseline = nil
config.before(:suite) { baseline = IS::ModuleReset.dump(MyApp) }
config.after(:each) { IS::ModuleReset.undump(baseline) }
endclass Minitest::Test
def setup
super
@module_reset_state = IS::ModuleReset.dump(MyApp)
end
def teardown
IS::ModuleReset.undump(@module_reset_state)
super
end
endIf your test classes define their own setup and teardown, call super.
- Constants are not restored. Neither reassigned, added, or removed
constants, nor the contents of mutable constants (
CONFIG = {}). - Only module-level state is saved. Global variables (
$var),ENV, thread-local variables, instance variables of ordinary objects, and anything outside the namespaces you pass in are not touched. - Values are copies. Objects other than
HashandArrayare copied with a shallowdup: their inner mutable state is shared with the original, and after restoring the module holds the copy rather than the original instance. If you store, say, a singleton object in@instance, it may become a different object after restoring. - Copies are not frozen. A frozen value becomes mutable after restoring.
- Constants are followed blindly. An alias to a module from another
namespace (
Log = ::Logger) is included in the snapshot and rolled back too. Traversal may also trigger loading ofautoloadconstants. - Not thread-safe. The gem changes global state, so do not use it with tests running in parallel threads of the same process. Process-based parallelism is fine.
- Ruby >= 3.4
- is-deep
LGPL-3.0-or-later. See the LICENSE file.