---
title: Tips for Using FactoryBot Without an ORM
teaser: 'Exploring how to use FactoryBot effectively with models backed by plain Ruby
  objects.

  '
tags: factory_girl,factory_bot,ruby,rails,web
author: Oli Peate
published_on: 2022-04-28
---

[FactoryBot] is one of my favourite testing tools — I was a fan prior to
joining thoughtbot. It's one of those tools I miss immediately when working
outside the Ruby ecosystem.

[FactoryBot]: https://github.com/thoughtbot/factory_bot

Recently I've been working on a Rails project that doesn't need any database
persistence and therefore doesn't use an object-relational mapper like
ActiveRecord. Instead data is fetched from an external JSON API and parsed into
value objects written in plain Ruby.

At the outset, the value objects needed for unit tests were manually written for
each example. As further attributes were added to the models this quickly became
tedious; the value objects created for previous tests also needed updating.

Fortunately FactoryBot can be used with plain Ruby objects, bringing all the
advantages of using factories in tests.

## A gentle start...

*Side note: The examples use inline classes and factory definitions for brevity.
They're also written as specs for self-proof each step works as intended.*

Let's begin with the simplest factory for a plain Ruby object:

```ruby
it "supports building a PORO object" do
  class User
    attr_accessor :name
  end

  FactoryBot.define do
    factory :user do
      name { "Amy" }
    end
  end

  user = FactoryBot.build(:user)

  expect(user.name).to eq "Amy"
end
```

The notable point here is the use of the `build` strategy with the factory.
Using the `create` strategy raises an exception:

<pre>
<samp>NoMethodError:
  undefined method &#96;save!' for #<User:0x007ff042882a90 @name="Amy"></samp>
</pre>

This exception occurs because the `User` class doesn't implement `#save!`. When
using ActiveRecord this would have been defined by inheriting from
`ApplicationRecord`, which includes
[`ActiveRecord::Persistence`][active-record-persistence].

[active-record-persistence]: http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-save-21

## Immutability

Next let's update the `User` model to…

- Be immutable by switching the `attr_accessor` to an `attr_reader`.
- Be instantiated with a `Hash` since the real data will be populated from JSON.

The updated model looks like this:

```ruby
class User
  attr_reader :name

  def initialize(data = {})
    @name = data[:name]
  end
end
```

With the original factory definition an exception is raised:

<pre>
<samp>NoMethodError:
  undefined method &#96;name=' for #<User:0x007fec9a9f3d08 @name=nil></samp>
</pre>

FactoryBot's default approach to initializing an object is to call `#new` and
then assign each attribute. This behaviour can be overridden by specifying
`initialize_with` in the factory definition:

```ruby
it "supports custom initialization" do
  class User
    attr_reader :name

    def initialize(data)
      @name = data[:name]
    end
  end

  FactoryBot.define do
    factory :user do
      name { "Amy" }

      initialize_with { new(attributes) }
    end
  end

  user = FactoryBot.build(:user)

  expect(user.name).to eq "Amy"
end
```

## Parsing nested resources

Let's imagine the external API response includes nested resources similar to the
following JSON structure:

```json
{
  "name": "Bob",
  "location": {
    "city": "New York"
  }
}
```

The nested `location` object is parsed by handing the relevant attributes to a
dedicated model:

```ruby
class Location
  attr_reader :city

  def initialize(data)
    @city = data[:city]
  end
end

class User
  attr_reader :name, :location

  def initialize(data)
    @name = data[:name]
    @location = Location.new(data[:location])
  end
end
```

To create this nested structure in tests it's necessary to combine the `:user`
factory attributes with attributes from a `:location` factory:

```ruby
it "supports constructing nested models" do
  class Location
    attr_reader :city

    def initialize(data)
      @city = data[:city]
    end
  end

  class User
    attr_reader :name, :location

    def initialize(data)
      @name = data[:name]
      @location = Location.new(data[:location])
    end
  end

  FactoryBot.define do
    factory :location do
      city { "London" }

      initialize_with { new(attributes) }
    end

    factory :user do
      name { "Amy" }

      location { attributes_for(:location) }

      initialize_with { new(attributes) }
    end
  end

  user = FactoryBot.build(:user)

  expect(user.name).to eq "Amy"
  expect(user.location.city).to eq "London"
end
```

It becomes clearer why this works by printing the factory-generated attributes
for a user:

```ruby
puts FactoryBot.attributes_for(:user)
# => {:name=>"Amy", :location=>{:city=>"London"}}
```

The attributes mimic the nested structure and this `Hash` is passed to
`User#initialize` by the custom `initialize_with` block.

## Time to lint

The final piece of the FactoryBot + plain Ruby objects puzzle is setting up
factory linting. Linting is typically run before the test suite to avoid a sea
of red test failures if a factory isn't valid.

Using the built-in `FactoryBot.lint` method from a Rake task produces:

    rake aborted!
    FactoryBot::InvalidFactoryError: The following factories are invalid:

    &#22; user - undefined method &#96;save!' for #<User:0x007fc890ae0e88 @name="Amy"> (NoMethodError)

The undefined `#save!` method is being called because the `#lint` method ends up
constructing objects with the `create` strategy. Here are two options to work
around this:

### Option A: `#skip_create`

Adding [`#skip_create`][skip-create] to each factory definition makes them
compatible with `FactoryBot.lint`:

[skip-create]: https://thoughtbot.github.io/factory_bot/ref/build-and-create.html#to_create

```ruby
FactoryBot.define do
  factory :location do
    city { "London" }

    skip_create
    initialize_with { new(attributes) }
  end

  factory :user do
    name { "Amy" }

    location { attributes_for(:location) }

    skip_create
    initialize_with { new(attributes) }
  end
end
```

Introducing `#skip_create` also allows for the `create` strategy to be used
within tests:

```ruby
FactoryBot.create(:user)
```

### Option B: Lint by building

The built-in lint can be mimicked by defining a custom Rake task to build
each factory:

```ruby
namespace :factory_bot do
  desc "Lint factories by building"
  task lint_by_build: :environment do
    if Rails.env.production?
      abort "Can't lint factories in production"
    end

    FactoryBot.factories.each do |factory|
      lint_factory(factory.name)
    end
  end

  private

  def lint_factory(factory_name)
    FactoryBot.build(factory_name)
  rescue StandardError
    puts "Error building factory: #{factory_name}"
    raise
  end
end
```

Illustrated with a deliberately broken factory like-so:

```ruby
class User
  attr_reader :name

  def initialize(data)
    @name = data.fetch(:name) # <- Name is now required
  end
end

FactoryBot.define do
  factory :user do
    # name { "Amy" } <- Uh oh

    initialize_with { new(attributes) }
  end
end
```

Running the `factory_bot:lint_by_build` Rake task outputs:

<pre>
<samp>Error building factory: user
rake aborted!
KeyError: key not found: :name
/path/models.rb:5:in &#96;fetch'
/path/models.rb:5:in &#96;initialize'
/path/factories.rb:8:in &#96;block (3 levels) in <top (required)>'
/path/Rakefile:15:in &#96;lint_factory'</samp>
</pre>

The output names the invalid factory and provides a stack trace, then the task
exits with a status of `1`. The non-zero exit status is important — if this Rake
task is used in shell script with `set -e` or composed with other tasks — it'll
skip the remaining steps and fail fast:

```ruby
# Rakefile
task(:default).clear
task default: ["factory_bot:lint_by_build", :spec]
```

## To recap

For FactoryBot + plain Ruby object bliss:

- Use `FactoryBot.build` instead of `FactoryBot.create`.
- Use [`initialize_with`][custom-construction] to customize initialization.
- Outer factories can use `attributes_for` to build nested resources.
- For factory linting either:
  - Add `#skip_create` to each factory definition.
  - Mimic the built-in linting by calling `FactoryBot.build` on each factory.

[custom-construction]: https://thoughtbot.github.io/factory_bot/ref/build-and-create.html#initialize_with

* * *

**Disclaimer:**

Looking for FactoryGirl? The library was renamed in 2017.
[Project name history can be found here.](https://github.com/thoughtbot/factory_bot/blob/master/NAME.md)
