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'
end
Then run:
bundle install
Usage
Compile Vial Sources
Vial reads *.vial.rb files from test/vials and writes YAML fixtures to test/fixtures:
bin/rails vial:compile
If 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_run
Example test/vials/users.vial.rb:
vial :users do
sequence(:email) { |i| "user#{i}@test.local" }
base do
email sequence(:email)
active true
country "MA"
end
variant :admin do
role "admin"
end
variant :guest do
role "guest"
active false
end
generate 10, :admin
generate 50, :guest
end
Output: test/fixtures/users.yml with deterministic IDs and explicit records.
Note: sequence(:name) { ... } only defines a sequence. It applies to records
when an attribute references it — email sequence(:email) inside base or a
variant. The compiler warns about sequences that are defined but never referenced.
Global Validation
vial:compile validates the entire dataset before writing any fixtures. Any collision or duplicate label fails the compile with a precise error.
IDs are namespaced per record_type (one namespace per table): fixtures for
different tables may share numeric IDs, and collisions are only errors within
the same record type.
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
end
include_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
end
Explicit IDs are supported and never rewritten:
vial :users do
base do
id 1001
role "system"
end
generate 1
end
Explicit and derived IDs share one namespace per record_type, so two vials
targeting the same table cannot collide, while vials for different tables can
reuse the same numbers freely.
Explain 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
end
Example 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_fixtures
This 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:clean
Vial 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_fixtures
This 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_fixtures
This 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_ids
This 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_ids
This 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.