{"id":45679652,"url":"https://github.com/rubymonolith/fixturebot","last_synced_at":"2026-04-04T01:38:21.713Z","repository":{"id":337136961,"uuid":"1151968001","full_name":"rubymonolith/fixturebot","owner":"rubymonolith","description":"The speed of fixtures with the syntactic sugar of factories.","archived":false,"fork":false,"pushed_at":"2026-03-11T07:03:39.000Z","size":102,"stargazers_count":185,"open_issues_count":3,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-04T01:38:14.225Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rubymonolith.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-07T06:34:47.000Z","updated_at":"2026-03-27T18:51:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rubymonolith/fixturebot","commit_stats":null,"previous_names":["rubymonolith/fixturebot"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/rubymonolith/fixturebot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Ffixturebot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Ffixturebot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Ffixturebot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Ffixturebot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rubymonolith","download_url":"https://codeload.github.com/rubymonolith/fixturebot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Ffixturebot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31384845,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T01:22:39.193Z","status":"ssl_error","status_checked_at":"2026-04-04T01:22:33.970Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-24T14:00:36.908Z","updated_at":"2026-04-04T01:38:21.705Z","avatar_url":"https://github.com/rubymonolith.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# FixtureBot\n\nThe syntactic sugar of factories with the speed of fixtures.\n\nFixtureBot lets you define your test data in a Ruby DSL and compiles it into standard Rails fixture YAML files. The generated YAML is deterministic and should be checked into git, just like a lockfile. Your tests never see FixtureBot at runtime; Rails just loads the YAML fixtures as usual.\n\n**Features:**\n\n- **Ruby DSL** for defining records, associations, and join tables\n- **Generators** for filling in required columns (like email) across all records\n- **Stable IDs** so foreign keys are consistent and diffs are clean across runs\n- **UUID support** for tables with UUID primary keys\n- **Polymorphic associations** with automatic type column resolution\n- **Schema auto-detection** from your Rails database (no manual column lists)\n- **Auto-generates** before your test suite runs (RSpec and Minitest)\n\n## Read the article\n\nMore background at [BeautifulRuby.com](https://beautifulruby.com/code/fixturebot).\n\n[![Screenshot of Don't throw the specs out with the factories article](https://immutable.terminalwire.com/frotvIWicKOgyNTufNGKSjpaI3RecS7IoO4hAWFycOC4zMopBylYljvcg62Br7NGHwsMikw3U5eLxQ0CpI7aVaVkLNLhCYK6OYI9.jpeg)](https://beautifulruby.com/code/fixturebot)\n\n## Quick example\n\n```ruby\n# spec/fixtures.rb\nFixtureBot.define do\n  # Generators fill in required columns so you don't have to repeat yourself.\n  # |fixture| gives you the fixture key; bare methods give column values.\n  user.email { |fixture| \"#{fixture.key}@example.com\" }\n\n  user :brad do\n    name \"Brad\"\n    email \"brad@example.com\"  # overrides the generator\n  end\n\n  user :alice do\n    name \"Alice\"              # email filled in by generator: \"alice@example.com\"\n  end\n\n  user :deactivated do\n    name \"Ghost\"\n    email nil                 # explicit nil, generator skipped, email set to null\n  end\n\n  post :hello_world do\n    title \"Hello World\"\n    body \"Welcome to the blog!\"\n    author :brad              # sets author_id to brad's stable ID\n    tags :ruby, :rails        # creates rows in posts_tags\n  end\n\n  tag.name { |fixture| fixture.key.to_s.capitalize }\n\n  tag :ruby                   # name: \"Ruby\"\n  tag :rails                  # name: \"Rails\"\nend\n```\n\nThis generates YAML files like `users.yml`, `posts.yml`, `tags.yml`, and `posts_tags.yml` in your fixtures directory. Use them in tests like any other fixture:\n\n```ruby\n# RSpec\nRSpec.describe Post, type: :model do\n  fixtures :all\n\n  it \"belongs to an author\" do\n    post = posts(:hello_world)\n    expect(post.author).to eq(users(:brad))\n  end\nend\n\n# Minitest\nclass PostTest \u003c ActiveSupport::TestCase\n  def test_belongs_to_author\n    post = posts(:hello_world)\n    assert_equal users(:brad), post.author\n  end\nend\n```\n\n## Installation\n\nAdd to your Gemfile:\n\n```ruby\ngem \"fixturebot-rails\"\n```\n\n### Rails generator\n\nThe easiest way to get started:\n\n```bash\nrails generate fixturebot:install\n```\n\nThis creates `spec/fixtures.rb` (RSpec) or `test/fixtures.rb` (Minitest) with a skeleton DSL file and adds the appropriate require to your test helper.\n\n### Manual setup\n\n#### RSpec\n\nAdd to `spec/rails_helper.rb`:\n\n```ruby\nrequire \"fixturebot/rspec\"\n```\n\nCreate `spec/fixtures.rb` with your fixture definitions.\n\nFixtures are auto-generated before each suite run, no rake task needed.\n\n#### Minitest\n\nAdd to `test/test_helper.rb`:\n\n```ruby\nrequire \"fixturebot/minitest\"\n```\n\nCreate `test/fixtures.rb` with your fixture definitions.\n\nFixtures are auto-generated when the helper is loaded, no rake task needed.\n\n### Rake task\n\nA `fixturebot:compile` rake task is also available if you prefer manual control:\n\n```bash\nbundle exec rake fixturebot:compile\n```\n\n### Configuration\n\nFixtureBot auto-detects your fixtures file (`test/fixtures.rb` or `spec/fixtures.rb`) and derives the output directory by stripping `.rb` (e.g. `spec/fixtures.rb` writes to `spec/fixtures/`). To override:\n\n```ruby\n# config/application.rb or config/environments/test.rb\nconfig.fixturebot.fixtures_file = \"test/my_fixtures.rb\"\nconfig.fixturebot.output_dir = \"test/fixtures\"\n```\n\n## The DSL\n\n### Records\n\nDefine named records with literal column values:\n\n```ruby\nFixtureBot.define do\n  user :brad do\n    name \"Brad\"\n    email \"brad@example.com\"\n  end\nend\n```\n\nRecords without a block get an auto-generated ID and any generator defaults:\n\n```ruby\nFixtureBot.define do\n  user.email { |fixture| \"#{fixture.key}@example.com\" }\n\n  user :alice\n  # =\u003e { id: \u003cstable_id\u003e, email: \"alice@example.com\" }\nend\n```\n\n### Generators\n\nGenerators set default column values. They run for each record that doesn't explicitly set that column. Generators are never created implicitly; columns without a value or generator are omitted from the YAML output (Rails uses the database column default).\n\n```ruby\nFixtureBot.define do\n  user.email { |fixture| \"#{fixture.key}@example.com\" }\nend\n```\n\nThe generator block receives a `fixture` object with a `key` method (the record's symbol name). Bare methods inside the block refer to column values set on the record:\n\n```ruby\nFixtureBot.define do\n  user.display_name { |fixture| \"#{fixture.key} (#{email})\" }\nend\n```\n\nWhen a generator covers all the columns you need, records don't need a block at all:\n\n```ruby\nFixtureBot.define do\n  tag.name { |fixture| fixture.key.to_s.capitalize }\n\n  tag :ruby     # name: \"Ruby\"\n  tag :rails    # name: \"Rails\"\n  tag :testing  # name: \"Testing\"\nend\n```\n\nA literal value shadows the generator. An explicit `nil` also shadows it:\n\n```ruby\nFixtureBot.define do\n  user.email { |fixture| \"#{fixture.key}@example.com\" }\n\n  user :brad do\n    name \"Brad\"\n    email \"brad@hey.com\"  # literal, skips the generator\n  end\n\n  user :alice do\n    name \"Alice\"\n    # no email set, generator produces \"alice@example.com\"\n  end\n\n  user :deactivated do\n    name \"Ghost\"\n    email nil              # explicit nil, skips the generator, sets email to null\n  end\nend\n```\n\n### Associations\n\nReference other records by name for `belongs_to`:\n\n```ruby\nFixtureBot.define do\n  post :hello_world do\n    title \"Hello World\"\n    author :brad  # sets author_id to brad's stable ID\n  end\nend\n```\n\n### Polymorphic associations\n\nFor polymorphic `belongs_to`, reference the target using its table helper to set both `_id` and `_type` columns:\n\n```ruby\nFixtureBot.define do\n  post :hello do\n    title \"Hello\"\n  end\n\n  comment :nice do\n    body \"Nice post\"\n  end\n\n  vote :upvote_post do\n    votable post(:hello)       # sets votable_id and votable_type: \"Post\"\n  end\n\n  vote :upvote_comment do\n    votable comment(:nice)     # sets votable_id and votable_type: \"Comment\"\n  end\nend\n```\n\nIn the manual schema, declare polymorphic associations with `polymorphic`:\n\n```ruby\nFixtureBot::Schema.define do\n  table :posts, singular: :post, columns: [:title]\n  table :comments, singular: :comment, columns: [:body]\n\n  table :votes, singular: :vote, columns: [:votable_id, :votable_type, :voter_id] do\n    polymorphic :votable\n    belongs_to :voter, table: :users\n  end\nend\n```\n\nIn Rails, polymorphic associations are auto-detected from `_id`/`_type` column pairs that don't have a foreign key constraint.\n\n### Join tables (HABTM)\n\nReference multiple records for join table associations:\n\n```ruby\nFixtureBot.define do\n  post :hello_world do\n    title \"Hello World\"\n    tags :ruby, :rails  # creates rows in posts_tags\n  end\nend\n```\n\n### Hardcoded IDs\n\nBy default, FixtureBot generates stable IDs deterministically from the table and record name. You can override this with an explicit value:\n\n```ruby\nFixtureBot.define do\n  user :admin do\n    id 42\n    name \"Admin\"\n  end\n\n  post :hello do\n    title \"Hello\"\n    author :admin  # author_id resolves to 42\n  end\nend\n```\n\n### UUID primary keys\n\nTables with UUID primary keys work automatically in Rails (detected from the column type). In the manual schema, pass `key: FixtureBot::Key::Uuid`:\n\n```ruby\nFixtureBot::Schema.define do\n  table :projects, singular: :project, columns: [:name], key: FixtureBot::Key::Uuid\nend\n```\n\nFixtureBot generates deterministic UUID v5 values, so the output is stable across runs.\n\nYou can also provide your own key strategy — any object (or module) that responds to `generate(table_name, record_name)`. For example, Stripe-style prefixed IDs:\n\n```ruby\nmodule PrefixedKey\n  module_function\n\n  def generate(table_name, record_name)\n    hash = Zlib.crc32(\"#{table_name}:#{record_name}\").to_s(36)\n    \"#{table_name.to_s.chomp('s')}_#{hash}\"\n  end\nend\n\nFixtureBot::Schema.define do\n  table :customers, singular: :customer, columns: [:name], key: PrefixedKey\n  # =\u003e customer_id: \"customer_1a2b3c\"\nend\n```\n\n### Custom primary key column\n\nIf your table uses a column other than `id` as the primary key:\n\n```ruby\nFixtureBot::Schema.define do\n  table :users, singular: :user, columns: [:name], primary_key: :uid\nend\n```\n\nIn Rails, this is auto-detected from the database.\n\n### Multiple files\n\nFor larger apps, split fixtures across multiple files using `FixtureBot.require`:\n\n```ruby\n# spec/fixtures.rb\nFixtureBot.require \"spec/fixtures/**/*.rb\"\n\nFixtureBot.define do\n  user.email { |fixture| \"#{fixture.key}@example.com\" }\nend\n```\n\n```ruby\n# spec/fixtures/users.rb\nFixtureBot.define do\n  user :brad do\n    name \"Brad\"\n  end\nend\n```\n\n```ruby\n# spec/fixtures/posts.rb\nFixtureBot.define do\n  post :hello do\n    title \"Hello\"\n    author :brad\n  end\nend\n```\n\nEach file calls `FixtureBot.define` with its own block. Files are loaded in alphabetical order. References across files work because everything is resolved after all files are loaded.\n\n### Implicit vs explicit style\n\nBy default, the block is evaluated implicitly. Table methods like `user` and `post` are available directly:\n\n```ruby\nFixtureBot.define do\n  user :brad do\n    name \"Brad\"\n  end\nend\n```\n\nIf you prefer an explicit receiver (useful for editor autocompletion or clarity in large files), pass a block argument:\n\n```ruby\nFixtureBot.define do |t|\n  t.user :brad do\n    name \"Brad\"\n  end\nend\n```\n\nBoth styles are equivalent. Record blocks (the inner `do...end`) are always implicit.\n\n### Schema\n\nFixtureBot reads your database schema automatically in Rails. Outside of Rails, you can define it by hand:\n\n```ruby\nFixtureBot::Schema.define do\n  table :users, singular: :user, columns: [:name, :email]\n\n  table :posts, singular: :post, columns: [:title, :body, :author_id] do\n    belongs_to :author, table: :users\n  end\n\n  table :tags, singular: :tag, columns: [:name]\n\n  join_table :posts_tags, :posts, :tags\nend\n```\n\n## Migrating from FactoryBot\n\nFixtureBot provides `build`, `create`, `attributes_for`, and related methods that mirror FactoryBot's API. The key difference is that you pass both a table name and a fixture name instead of just a factory name:\n\n```ruby\n# FactoryBot                    # FixtureBot\nbuild(:user)                    # build(:user, :brad)\ncreate(:user)                   # create(:user, :brad)\nbuild(:user, name: \"X\")        # build(:user, :brad, name: \"X\")\nattributes_for(:user)           # attributes_for(:user, :brad)\nbuild_list(:user, 3)            # build_list(:user, :brad, :alice, :bob)\ncreate_list(:user, 3)           # create_list(:user, :brad, :alice, :bob)\nbuild_pair(:user)               # build_pair(:user, :brad, :alice)\ncreate_pair(:user)              # create_pair(:user, :brad, :alice)\nbuild_stubbed(:user)            # build_stubbed(:user, :brad)\n```\n\n### Method reference\n\n| Method | Behavior |\n|---|---|\n| `build(:user, :brad, **attrs)` | Duplicates the fixture, applies overrides. Returns unpersisted. |\n| `create(:user, :brad, **attrs)` | Without overrides: returns the fixture (already persisted). With overrides: `build` + `save!`. |\n| `build_stubbed(:user, :brad, **attrs)` | Like `build` but retains `id`. Looks persisted without touching DB. |\n| `attributes_for(:user, :brad, **attrs)` | Returns attributes hash, strips `id`/`created_at`/`updated_at`. |\n| `build_list(:user, :brad, :alice, **attrs)` | Maps each name through `build`. |\n| `create_list(:user, :brad, :alice, **attrs)` | Maps each name through `create`. |\n| `build_pair(:user, :brad, :alice, **attrs)` | Alias for `build_list` with 2 names. |\n| `create_pair(:user, :brad, :alice, **attrs)` | Alias for `create_list` with 2 names. |\n| `build_stubbed_list(:user, :brad, :alice, **attrs)` | Maps each name through `build_stubbed`. |\n| `build_stubbed_pair(:user, :brad, :alice, **attrs)` | Alias for `build_stubbed_list` with 2 names. |\n\nThese methods are automatically available in your tests when you require `fixturebot/rspec` or `fixturebot/minitest`. They call the standard Rails fixture accessors under the hood, so `build(:user, :brad)` is equivalent to `users(:brad).dup`.\n\n## Prior art\n\n### [Rails fixtures](https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures)\n\nRails fixtures are YAML files that get loaded into the database once before your test suite runs. Each test wraps in a transaction and rolls back, so the data is always clean and tests are fast. This is the approach FixtureBot builds on.\n\nThe problem is writing YAML by hand. Foreign keys are magic strings (`author: brad`), there's no way to DRY up repeated columns, and large fixture files are hard to read. FixtureBot gives you a Ruby DSL that compiles down to the same YAML, so you keep the speed of fixtures without the pain of maintaining them.\n\n### [FactoryBot](https://github.com/thoughtbot/factory_bot)\n\nFactoryBot creates records on the fly inside each test. You call `create(:user)` and it inserts a row. This makes tests self-contained and easy to read, but it's slow. Every test that needs data pays the cost of inserting records, and complex object graphs lead to cascading `create` calls.\n\nFixtureBot borrows the DSL feel of FactoryBot (named records, associations by symbol, default generators) but compiles to fixtures instead of inserting at runtime. You get the ergonomics of factories with the speed of fixtures.\n\n### [Oaken](https://github.com/kaspth/oaken)\n\nOaken and FixtureBot share the same motivation: replace hand-written YAML fixtures with a Ruby DSL. They take very different approaches.\n\n**Oaken inserts records into the database** at runtime using `ActiveRecord::Base#create!`. It also supports loading different seed files per test case (`seed \"cases/pagination\"`), which means your data set can vary across tests. This flexibility comes at a cost: you lose the \"load once, wrap every test in a transaction\" speed advantage that makes Rails fixtures fast. It's closer to factories in that regard, with more structure around organizing seed scripts.\n\n**FixtureBot is more opinionated.** One fixture file, one data set, compiled to plain YAML and checked into git. At test time, FixtureBot is out of the picture entirely. Rails loads the YAML fixtures once and wraps each test in a transaction as usual. No runtime dependency, no per-test seeding, no seed file organization to manage.\n\n| | FixtureBot | Rails fixtures | FactoryBot | Oaken |\n|---|---|---|---|---|\n| **Define data in** | Ruby DSL | YAML | Ruby DSL | Ruby scripts |\n| **Output** | YAML files in git | YAML files in git | Database rows per test | Database rows at boot |\n| **Runtime dependency** | None | None | Required per test | Required at boot |\n| **Data set** | One set, loaded once | One set, loaded once | Built per test | Per-test via seed files |\n| **Speed** | Fast (fixtures) | Fast (fixtures) | Slow (inserts per test) | Varies |\n| **Stable IDs** | Deterministic | Deterministic | Database-assigned | Database-assigned |\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.\n\nTry the playground without Rails:\n\n```bash\nbundle exec exe/fixturebot show ./playground/blog\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubymonolith%2Ffixturebot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frubymonolith%2Ffixturebot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubymonolith%2Ffixturebot/lists"}