Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ RSpec.describe Book do
end
```

`fixture` returns a `FixtureSet` and exposes records as methods (for example, `fixture.owner`).

### 3. Configure RSpec

```ruby
Expand All @@ -128,14 +130,25 @@ FixtureKit.configure do |config|

# Whether to regenerate caches on every run (default: true)
config.autogenerate = true

# Optional: customize how pregeneration is wrapped.
# Default is FixtureKit::TestCase::Generator.
# config.generator = FixtureKit::TestCase::Generator
end
```

Custom generators should subclass `FixtureKit::Generator` and implement `#run`.
`#run` receives the pregeneration block and should execute it in whatever lifecycle you need.

### Autogenerate

When `autogenerate` is `true` (the default), FixtureKit clears all caches at the start of each test run, then regenerates them on first use. Subsequent tests that use the same fixture reuse the cache from earlier in the run. This ensures your test data always matches your fixture definitions.

When `autogenerate` is `false`, FixtureKit pre-generates all fixture caches at suite start. This happens in rolled-back transactions so no data persists to the database. Any fixtures that already have caches are skipped. This mode is useful for CI where you want consistent, predictable cache generation.
When `autogenerate` is `false`, FixtureKit pre-generates all fixture caches at suite start. This runs through the configured `generator`, and still rolls back database changes.

By default, FixtureKit uses `FixtureKit::TestCase::Generator`, which runs pregeneration inside an internal `ActiveSupport::TestCase` so setup/teardown hooks and transactional fixture behavior run as expected. The internal test case is removed from Minitest runnables, so it does not count toward suite totals.

When using `fixture_kit/rspec`, FixtureKit sets `FixtureKit::RSpec::Generator` as the generator. This runs pregeneration inside an internal RSpec example so your normal `before`/`around`/`after` hooks apply. The internal example uses a null reporter, so it does not count toward suite example totals.

### Preserving Cache Locally

Expand Down
4 changes: 3 additions & 1 deletion lib/fixture_kit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Error < StandardError; end
class DuplicateFixtureError < Error; end
class DuplicateNameError < Error; end
class CacheMissingError < Error; end
class PregenerationError < Error; end
class ExposedRecordNotFound < Error; end

autoload :VERSION, File.expand_path("fixture_kit/version", __dir__)
Expand All @@ -17,7 +18,8 @@ class ExposedRecordNotFound < Error; end
autoload :SqlCapture, File.expand_path("fixture_kit/sql_capture", __dir__)
autoload :FixtureCache, File.expand_path("fixture_kit/fixture_cache", __dir__)
autoload :FixtureRunner, File.expand_path("fixture_kit/fixture_runner", __dir__)
autoload :TransactionalHarness, File.expand_path("fixture_kit/transactional_harness", __dir__)
autoload :Generator, File.expand_path("fixture_kit/generator", __dir__)
autoload :TestCase, File.expand_path("fixture_kit/test_case", __dir__)

extend Singleton
end
2 changes: 2 additions & 0 deletions lib/fixture_kit/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ module FixtureKit
class Configuration
attr_writer :fixture_path
attr_writer :cache_path
attr_accessor :generator
attr_accessor :autogenerate

def initialize
@fixture_path = nil
@cache_path = nil
@generator = FixtureKit::TestCase::Generator
@autogenerate = true
end

Expand Down
7 changes: 4 additions & 3 deletions lib/fixture_kit/fixture_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ def run
end

# Generate cache only (used for pregeneration in before(:suite))
# Wraps execution in ActiveRecord::TestFixtures transactional lifecycle,
# so no data persists across configured writing pools.
# Wraps execution in the configured generator lifecycle.
# The default generator uses ActiveRecord::TestFixtures transactions.
# Entry points (like `fixture_kit/rspec`) can install richer generators.
# Always regenerates the cache, even if one exists
def generate_cache_only
# Clear any existing cache for this fixture
FixtureCache.clear(@fixture_name.to_s)

FixtureKit::TransactionalHarness.run do
FixtureKit.configuration.generator.run do
execute_and_cache
end

Expand Down
14 changes: 14 additions & 0 deletions lib/fixture_kit/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module FixtureKit
# Base class for fixture cache generators.
class Generator
def self.run(&block)
new.run(&block)
end

def run
raise NotImplementedError, "#{self.class} must implement #run"
end
end
end
4 changes: 4 additions & 0 deletions lib/fixture_kit/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "fixture_kit"
require_relative "rspec/declaration"
require_relative "rspec/generator"

module FixtureKit
module RSpec
Expand Down Expand Up @@ -45,6 +46,9 @@ def fixture
end
end

# Install the RSpec generator by default for this entrypoint.
FixtureKit.configuration.generator = FixtureKit::RSpec::Generator

# Configure RSpec integration
RSpec.configure do |config|
config.extend FixtureKit::RSpec::ClassMethods
Expand Down
45 changes: 45 additions & 0 deletions lib/fixture_kit/rspec/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module FixtureKit
module RSpec
class Generator < FixtureKit::Generator
def run(&block)
previous_example = ::RSpec.current_example
previous_scope = ::RSpec.current_scope
example_group = build_example_group
example = build_example(example_group, &block)
instance = example_group.new(example.inspect_output)
succeeded =
begin
example.run(instance, ::RSpec::Core::NullReporter)
ensure
example_group.remove_example(example)
::RSpec.current_example = previous_example
::RSpec.current_scope = previous_scope
end

unless succeeded
raise example.exception if example.exception
raise FixtureKit::PregenerationError, "FixtureKit pregeneration failed"
end
end

private

def build_example(example_group, &block)
example_group.example(
"FixtureKit cache pregeneration"
) { block.call }
end

def build_example_group
::RSpec::Core::ExampleGroup.subclass(
::RSpec::Core::ExampleGroup,
"FixtureKit::RSpec::Generator",
[],
[]
)
end
end
end
end
3 changes: 1 addition & 2 deletions lib/fixture_kit/singleton.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
module FixtureKit
module Singleton
def configure
@configuration = Configuration.new
yield(@configuration) if block_given?
yield(configuration) if block_given?
self
end

Expand Down
7 changes: 7 additions & 0 deletions lib/fixture_kit/test_case.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module FixtureKit
module TestCase
autoload :Generator, File.expand_path("test_case/generator", __dir__)
end
end
37 changes: 37 additions & 0 deletions lib/fixture_kit/test_case/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require "active_support/test_case"
require "active_record/fixtures"

module FixtureKit
module TestCase
class Generator < FixtureKit::Generator
TEST_METHOD_NAME = "test_fixture_kit_cache_pregeneration"

def run(&block)
result = build_test_class(&block).run
return if result.passed?

failure = result.failures.first
raise failure.error if failure.respond_to?(:error)
raise failure if failure

raise FixtureKit::PregenerationError, "FixtureKit pregeneration failed"
end

private

def build_test_class(&block)
Class.new(ActiveSupport::TestCase) do
::Minitest::Runnable.runnables.delete(self)
include(::ActiveRecord::TestFixtures)

define_method(TEST_METHOD_NAME) do
block.call
pass
end
end.new(TEST_METHOD_NAME)
end
end
end
end
29 changes: 0 additions & 29 deletions lib/fixture_kit/transactional_harness.rb

This file was deleted.

17 changes: 13 additions & 4 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,19 @@
# Use transactional fixtures - each test runs in a transaction that rolls back
config.use_transactional_fixtures = true

# Reset FixtureKit configuration before each test
config.before(:each) do
FixtureKit.configuration.cache_path = Rails.root.join("tmp/cache/fixture_kit").to_s
FixtureKit.configuration.fixture_path = Rails.root.join("spec/fixture_kit").to_s
# Keep fixture_kit configuration isolated per example.
config.around(:each) do |example|
previous_cache_path = FixtureKit.configuration.cache_path
previous_fixture_path = FixtureKit.configuration.fixture_path
previous_autogenerate = FixtureKit.configuration.autogenerate
previous_generator = FixtureKit.configuration.generator

example.run
ensure
FixtureKit.configuration.cache_path = previous_cache_path
FixtureKit.configuration.fixture_path = previous_fixture_path
FixtureKit.configuration.autogenerate = previous_autogenerate
FixtureKit.configuration.generator = previous_generator
end

# Clean up cache after suite
Expand Down
1 change: 1 addition & 0 deletions spec/support/dummy_rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ def setup_databases
FixtureKit.configure do |config|
config.fixture_path = Rails.root.join("spec/fixture_kit").to_s
config.cache_path = Rails.root.join("tmp/cache/fixture_kit").to_s
config.generator = FixtureKit::RSpec::Generator if defined?(FixtureKit::RSpec::Generator)
end
19 changes: 19 additions & 0 deletions spec/unit/configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe FixtureKit::Configuration do
describe "#generator" do
it "defaults to FixtureKit::TestCase::Generator" do
expect(described_class.new.generator).to eq(FixtureKit::TestCase::Generator)
end

it "returns an explicitly configured class" do
custom_generator_class = Class.new(FixtureKit::Generator)
configuration = described_class.new
configuration.generator = custom_generator_class

expect(configuration.generator).to eq(custom_generator_class)
end
end
end
1 change: 1 addition & 0 deletions spec/unit/fixture_cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
FixtureKit.configure do |config|
config.cache_path = cache_path
config.fixture_path = fixture_path
config.generator = FixtureKit::RSpec::Generator
end

# Clear both disk and memory cache before each test
Expand Down
23 changes: 23 additions & 0 deletions spec/unit/fixture_runner_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe FixtureKit::FixtureRunner do
describe "#generate_cache_only" do
it "uses the configured generator" do
generator = class_double("CustomGenerator")
runner = described_class.new("project_management")

previous_generator = FixtureKit.configuration.generator
FixtureKit.configuration.generator = generator

expect(FixtureKit::FixtureCache).to receive(:clear).with("project_management")
expect(generator).to receive(:run).and_yield
expect(runner).to receive(:execute_and_cache)

expect(runner.generate_cache_only).to be(true)
ensure
FixtureKit.configuration.generator = previous_generator
end
end
end
Loading