Vial: Fixtures, Reinvented
Vial is a Ruby gem for Rails 8.1+ on Ruby 3.4+ that compiles programmable fixture intent into explicit, deterministic fixture files.
Requirements
- Ruby 3.4+ (Ruby Box optional via
RUBY_BOX=1) - Rails 8.1+
Support Policy
- Ruby 3.4+ only
- Rails 8.1+ only
- No backward compatibility guarantees for older Ruby/Rails
Features
- Vial Compiler: Ruby DSL input → explicit YAML fixtures output
- Fixture Discovery: Automatically finds all YAML fixtures across configured paths
- Model Mapping Analysis: Determines which models fixtures belong to using Rails conventions
- Validation: Identifies orphaned fixtures that don't have corresponding models
- Multiple Detection Methods: Supports all Rails fixture-to-model mapping methods
Installation
Add this gem to your Gemfile in the development/test group:
group :development, :test do
gem 'vial'
endThen run:
bundle installUsage
Compile Vial Sources
Vial reads *.vial.rb files from test/vials and writes YAML fixtures to test/fixtures:
bin/rails vial:compileIf RUBY_BOX=1 is set, Vial evaluates vial sources inside Ruby::Box for isolation.
Compile only specific vials:
bin/rails vial:compile:only[users,posts]Note: vial:compile:only still validates the full dataset to catch global ID collisions; it only limits which files are written.
Dry run (validate and show output targets without writing files):
bin/rails vial:compile:dry_runExample test/vials/users.vial.rb:
vial :users do
base do
active true
country "MA"
end
variant :admin do
role "admin"
end
variant :guest do
role "guest"
active false
end
sequence(:email) { |i| "user#{i}@test.local" }
generate 10, :admin
generate 50, :guest
endOutput: test/fixtures/users.yml with deterministic IDs and explicit records.
Global Validation
vial:compile validates the entire dataset before writing any fixtures. Any collision or duplicate label fails the compile with a precise error.
Composition
vial :base_users, abstract: true do
base do
active true
country "MA"
end
end
vial :admin_users do
include_vial :base_users do
override :role, "admin"
end
generate 2
endinclude_vial composes base attributes (and sequences) only; variants are not imported.
Abstract Vials are compile-time only and do not emit fixture files.
ID Strategy
Derived IDs are deterministic integers based on the identity tuple:
[vial_name, record_type, label, variants[], index]
You can set a namespace range per Vial:
vial :users, id_base: 100_000, id_range: 90_000 do
base do
role "user"
end
generate 3
endExplicit IDs are supported and never rewritten:
vial :users do
base do
id 1001
role "system"
end
generate 1
endExplain a derived ID:
bin/rails vial:explain_id['users.admin.eu[3]']Determinism & Faker
Vial seeds Ruby's RNG with Vial.config.seed (default: 1). As long as you avoid non-deterministic sources
(Time.now, SecureRandom, etc.), recompiles produce identical output.
If you use Faker, seed it explicitly:
Faker::Config.random = Random.new(Vial.config.seed)Troubleshooting
-
Vial: no vial definitions found→ No*.vial.rbfiles were found underVial.config.source_paths. -
Vial: no records generated→ All vials are abstract or missinggeneratecalls. - Fixture ID standardization skipped for ERB → ERB fixtures are not rewritten to avoid altering dynamic content.
- Output path looks wrong →
Vial.config.output_pathdefaults to the firstActiveSupport::TestCase.fixture_pathsentry.
Configuration
Vial.configure do |config|
config.source_paths = [Rails.root.join("test/vials")]
config.output_path = Rails.root.join("test/fixtures")
config.seed = 1
endExample Vial sources live in examples/:
examples/users.vial.rbexamples/company__users.vial.rbexamples/base_users.vial.rbexamples/admin_users.vial.rb
Use erb("...") inside a Vial file to emit raw ERB (for associations or special IDs).
Vial provides several rake tasks for fixture analysis:
Count Fixtures
Get an overview of all YAML fixtures in your project:
bin/rails vial:count_fixturesThis shows:
- Total number of fixture files
- Distribution by directory
- Configured fixture paths
Note: Vial.config.output_path defaults to the first configured fixture_paths when available.
Clean Stale Vial Fixtures
Remove fixture files previously generated by Vial that no longer have a matching vial definition:
bin/rails vial:cleanVial tracks generated fixture files in test/fixtures/.vial_manifest.yml and only removes files listed there.
Analyze Fixtures
Get detailed fixture-to-model mapping information:
bin/rails vial:analyze_fixturesThis shows:
- Mapped fixtures with their corresponding models
- Detection method used (fixture directive, set_fixture_class, or path inference)
- Unmapped fixtures that may need attention
- Any errors encountered during analysis
Validate Fixtures
Check for orphaned fixtures without models:
bin/rails vial:validate_fixturesThis task:
- Returns success (exit 0) if all fixtures are mapped
- Returns failure (exit 1) if unmapped fixtures are found
- Provides suggestions for fixing unmapped fixtures
Analyze Fixture IDs
Check if fixtures use proper ActiveRecord::FixtureSet.identify:
bin/rails vial:analyze_fixture_idsThis shows:
- Primary key type distribution (UUID, integer, string, etc.)
- Fixtures that need ID standardization
- Current vs expected ID values
Standardize Fixture IDs
Convert hardcoded IDs to use ActiveRecord::FixtureSet.identify:
# Preview changes without applying
bin/rails vial:standardize_fixture_ids:dry_run
# Apply changes (with confirmation prompt)
bin/rails vial:standardize_fixture_idsThis will:
- Replace hardcoded integer IDs with
<%= ActiveRecord::FixtureSet.identify(:label) %> - Replace hardcoded UUIDs with
<%= ActiveRecord::FixtureSet.identify(:label, :uuid) %> - Skip string primary keys (often manually managed like slugs)
- Ensure consistent, deterministic IDs across all fixtures
How Fixture Detection Works
Vial uses three methods to determine which model a fixture belongs to:
-
_fixture Directive: Checks for
_fixture: model_class:in YAML files -
set_fixture_class: Uses mappings from
ActiveSupport::TestCase.set_fixture_class - Path Inference: Infers models from fixture paths using Rails naming conventions
Example Output
=== Vial Fixture Analysis ===
Total fixtures found: 114
Mapped to models: 93
Unmapped fixtures: 21
=== Mapped Fixtures ===
platform/countries:
Model: Platform::Country
Table: platform_countries
Detection: path_inference
active_llm_llms:
Model: ActiveLLM::LLM
Table: active_llm_llms
Detection: set_fixture_class
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
License
The gem is available as open source under the terms of the MIT License.