Oaken

repository·main·Indexed 19 days ago

https://github.com/kaspth/oaken

A data seeding and fixture replacement tool for Rails that uses Ruby scripts to define object graphs. Oaken combines the speed of fixtures with the flexibility of factories, allowing developers to manage shared datasets via Ruby-based recipes instead of YAML. It provides a DSL for creating and upserting records, supports environment-specific seeding, and integrates with Minitest, RSpec, FactoryBot, and Fabrication.

Tokens
7.1K
Snippets
25
Records
33
Agent score
63%

What's inside Oaken

  1. What is Oaken?

    main

    Oaken is a data management tool for Rails development and test environments that combines the benefits of fixtures, factories, and seeds. It allows you to define your application's object graph using Ruby-based data scripts instead of YAML, providing a more readable and maintainable way to manage shared datasets.

    Key benefits include:

    • In Development: Provides a way to group data by scenario, reveal your domain model through sequential object graph descriptions, and use Ruby-based recipes for seeding db/seeds.rb.
    • In Testing: Enables the reuse of development seed data in tests. Because Oaken seeds data before tests run and relies on Rails' transaction rollbacks, it maintains the speed of fixtures while offering the flexibility of Ruby.
  2. Oaken vs. Factories vs. Fixtures

    main

    Oaken is designed to bridge the gap between the two most common Rails data strategies:

    Compared to Factories

    • Speed: Oaken seeds shared records once before tests run and uses transactions to roll back, whereas factories often recreate data per test, which can significantly slow down CI.
    • Visibility: Factories optimize for isolation, which can hide the complexity of the object graph. Oaken focuses on shared datasets that reveal the system's structure.
    • Reduced Setup: Oaken allows you to write a shared seed for common cases once, rather than repeating setup code in every test file.

    Compared to Fixtures

    • UX: Replaces cumbersome YAML files with sequential Ruby scripts.
    • Organization: Instead of splitting data across many files (e.g., users.yml, accounts.yml), you can describe a complete scenario in a single Ruby script.
    • Flexibility: Allows for complex scenarios and edge cases to be broken out into separate scripts without creating massive, unmanageable YAML files.
    • Ease of Use: You don't have to manually ensure every single record has a unique label to avoid clashes; you can rely on associations to connect objects.
  3. How to create and access records in Oaken

    main

    Oaken uses a DSL that mirrors your model names (e.g., accounts.create, users.create) to build an object graph. You can provide an optional symbol label to a create call to make that specific record easily accessible in your tests.

    When you provide a label, you can access the record in a test setup like this: setup { @user = users.kasper } (assuming you used users.create :kasper).

    If you do not provide a label, you can still access the record through its associations, for example: accounts.kaspers_donuts.menus.first.menu_items.first.

    # Creating a root-level model with a label
    account = accounts.create :kaspers_donuts, name: "Kasper's Donuts"
    
    # Creating associated models with labels
    kasper   = users.create :kasper,   name: "Kasper",   email_address: "kasper@example.com",   accounts: [account]
    coworker = users.create :coworker, name: "Coworker", email_address: "coworker@example.com", accounts: [account]
    
    # Creating models without labels (accessible via associations)
    menu = menus.create(account:)
    plain_donut     = menu_items.create menu:, name: "Plain",     price_cents: 10_00
    sprinkled_donut = menu_items.create menu:, name: "Sprinkled", price_cents: 10_10
  4. Compatibility with FactoryBot and Fabrication

    main
    Oaken is designed to be compatible with existing factory libraries like FactoryBot and Fabrication. You can use Oaken for shared, stable datasets and use factories for one-off, highly specific data requirements within individual tests.
  5. Use the `setup` phase for defaults and helpers

    main

    When you call Oaken.seed or Oaken.loader.seed, Oaken automatically executes a seed :setup call once. This phase is intended for defining common defaults and helper methods rather than creating actual records via create or upsert.

    Recommended directory structure for setup:

    • db/seeds/setup.rb: General starting point.
    • db/seeds/setup/defaults.rb: Loader-level and type-specific defaults.
    • db/seeds/setup/users.rb: Type-specific helpers/defaults for Users.
    • db/seeds/test/setup.rb: Setup specific to the test environment.
  6. Setup and load seed files with Oaken

    main

    Oaken uses a Loader to manage seed files. By default, Oaken.loader is available. You can load seed directories or specific files using the .seed method.

    Oaken searches within db/seeds/ and db/seeds/#{Rails.env}/ using a glob pattern. For example, calling .seed :accounts will match accounts.rb, accounts/some_file.rb, or any nested path matching accounts{,**/*}.rb.

    Key Loading Rules:

    • Order: Files are loaded in the order specified (e.g., Oaken.loader.seed :data, :accounts loads :data first).
    • Environment Sharing: Files in the top-level db/seeds/ are shared across all environments. Files in db/seeds/development/ or db/seeds/test/ are environment-specific.
    • Transactions: Each file load is wrapped in an ActiveRecord::Base.transaction; if a file fails, the changes in that file roll back.
    # Load specific directories/identifiers
    Oaken.loader.seed :accounts
    Oaken.loader.seed :data, :accounts, :cases
    
    # Load a specific file path
    Oaken.seed "cases/pagination"
  7. Integrate Oaken into Rails tests (Minitest and RSpec)

    main

    To use Oaken's seed loading capabilities within your test suite, follow these integration steps:

    Minitest (Rails default): Include Oaken.loader.test_setup in your ActiveSupport::TestCase.

    RSpec: Require the Oaken RSpec setup in your spec/rails_helper.rb.

    Loading specific cases in tests: Instead of loading all seeds, you can load specific scenarios within a test block to maintain isolation.

    # test/test_helper.rb
    class ActiveSupport::TestCase
      include Oaken.loader.test_setup
    end
    
    # spec/rails_helper.rb
    require "oaken/rspec_setup"
    
    # Usage in Minitest
    class PaginationTest < ActionDispatch::IntegrationTest
      setup { seed "cases/pagination" }
    end
    
    # Usage in RSpec
    RSpec.describe "Pagination", type: :feature do
      before { seed "cases/pagination" }
    end
  8. Migrate from Rails fixtures to Oaken

    main

    If you are moving from standard Rails fixtures to Oaken, you can automate the conversion of your YAML fixture files into Oaken seed files.

    Convert fixtures

    Run the following generator to convert everything in test/fixtures to db/seeds (e.g., test/fixtures/users.yml becomes db/seeds/users.rb):

    bin/rails generate oaken:convert:fixtures

    Disable fixture generation

    Once fully migrated, you can prevent Rails generators from creating new fixtures by updating config/application.rb. This ensures your workflow stays focused on Oaken.

    Note: If you are already using FactoryBot, you do not need to disable fixtures manually as FactoryBot typically replaces them.

    module YourApp
      class Application < Rails::Application
        # We prefer Oaken to fixtures, so we disable them here.
        config.app_generators { _1.test_framework _1.test_framework, fixture: false }
      end
    end
  9. Migrate from factories (e.g., FactoryBot) to Oaken

    main

    You can improve test suite performance by migrating highly shared, static records from factories to Oaken seeds. Instead of recreating common objects (like a standard Account) in every test, seed them once.

    Step 1: Seed Oaken in your test environment

    In your db/seeds.rb, trigger the Oaken seeding when in the test environment:

    # db/seeds.rb
    if Rails.env.test?
      Oaken.seed :accounts
      return
    end

    Step 2: Define shared scenarios

    Create a seed file for your shared records. You can use your existing factory logic to define the attributes for the Oaken seed:

    # db/seeds/test/accounts/basic.rb
    accounts.create :basic, **FactoryBot.attributes_for(:account)

    Step 3: Use the seeded records in tests

    Your tests can now reference these pre-seeded records directly, for example, by passing them to other factories:

    # Example usage in a test
    create(:user, account: accounts.basic)

    Best Practice: Do the minimum and go slow. Only migrate records that are 100% safe to share across the entire suite to avoid side effects.

  10. Configure the Oaken loader

    main

    You can customize the loader's behavior, such as changing the lookup paths or the provider, using the .with method. You can then replace the global Oaken.loader with your custom instance.

    # config/initializers/oaken.rb
    loader = Oaken.loader.with(lookup_paths: "test/seeds")
    # Or with custom locator/provider/context
    loader = Oaken.loader.with(locator: Oaken::Loader::Type, provider: Oaken::Stored::ActiveRecord, context: Oaken::Seeds)
    
    Oaken.loader = loader
  11. Use Oaken's delegated API

    main
    The Oaken module acts as a proxy to its internal loader instance. Most public methods used to interact with seed data are delegated directly to Oaken.loader. This allows you to call loader methods directly on the Oaken module.