In general, when you’re unit testing something file fixtures are frowned upon. There’s a couple reasons:

  • They abstract away the details of how they were created.
    • This makes them “magic”, especially if your test code selectively overwrites parts of the fixture to simulate changing data.
  • If your data source or implementation changes, your fixture won’t reflect that change and will become stale.
    • This is particularly egregious if you’re fixturing payloads from an API. Seriously, don’t do this.
    • In the case of an API, it’s better to use something like VCR to record HTTP interactions and “play them back” at runtime.
    • This would let you “fixture” your API responses (with the side effect of making them faster), yet give you flexibility to easily update them if needed.

RSpec does provide an easy way to call file fixtures, and this could be helpful in limited use cases, but today I want to talk about a fixturing pattern utilizing FactoryBot that has been successful for us. To understand why we use it a little better, I should explain the goal.


Our Use Case

We work mainly on middleware between two large monoliths. There’s a whole ecosystem of microservices facilitating data transfer between the two, so a lot of the communication is done via API.

One of the systems we interact with is provided by a 3rd party, so we treat it as a black box considering we have no control over it. We do, however, expect payloads to come from that system in a certain shape, since it has a GraphQL API and we can be reasonably sure that our existing requests will just “work” as long as we keep the schema in lock step with theirs.

Now, our interactions with that system are not very simple. Most of our work is data transformation from the 3rd party monolith to ours, so it can take multiple requests to grab all the data we need in order to build a complete payload. While we do run complete integration tests against both systems in their staging environments, doing multiple API requests per unit test (even when using VCR) simply isn’t feasible.

To facilitate this, we’ve started fixturing the request responses. This isn’t particularly useful by itself, but when you combine this with Ruby’s powerful metaprogramming features and something like FactoryBot, you get a super clean way to build arbitrary data that fits seamlessly into RSpec.


Examples

Lets take a look at some code for setting up these fixture factories. As an example, I’ll be using MLB’s StatsAPI, which I’ve talked a little about before.

For reference, this might be around the right size for something we’d want to fixture in our use case: EXAMPLE

# frozen_string_literal: true

FactoryBot.define do
  factory :response_base do
    skip_create
    initialize_with do
      new(attributes, type)
    end

    transient do
      type { nil }
    end
  end

  %i[
    game
    scoreboard
    standings
  ].each do |entity_type|
    factory "#{entity_type}_response".to_sym, parent: :response_base do
      type { entity_type }
    end
  end
end

class Response
  def initialize(args, type)
    @payload = JSON.parse(File.read(Rails.root.join("spec/fixtures/response/#{type}.json"))).deep_symbolize_keys

    undefined_attributes = args.keys - @payload.keys
    raise ResponseFactoryError, "#{undefined_attributes} not defined in #{type} fixture" if undefined_attributes.present?

    @payload.merge!(args)
  end

  attr_reader :payload
end

class ResponseFactoryError < StandardError; end

Code Walkthrough

First, let’s look at the Response class. If you’re used to using FactoryBot you may have more experience using it with a model class backed by ActiveRecord. In that case, Rails magic would automatically let FactoryBot hook into all the columns defined at the database level, and you could specify defaults for those in your factory.

In our case, this is a Plain Old Ruby Object (PORO), so we have to tell FactoryBot how to build a new instance of Response.

In our first factory, we first make sure to add skip_create, which won’t work anyway since this can’t get written into the DB.

Next, we write an initialize_with block to tell FactoryBot how to actually initialize our object. We’ll just call the regular new method with attributes and type. Attributes are passed into a FactoryBot.create or FactoryBot.build method:

FactoryBot.create(:game_response, 
                  winner: 'St. Louis Cardinals') # These are the attributes

type will be defined on each child factory and used to load our fixture file.

Next, we’ll define each child factory we need (game, scoreboard, standings in this case), and define the appropriate type. Notice that the type is used to load the correct fixture file in the Response class.

Finally, with this in mind, our Response class can merge any attributes required into the fixture, which is then accessible via the payload instance attribute! Our code does a little extra to ensure that the attribute is actually valid, so you might not need this if you weren’t that concerned about adding arbitrary fields.


Usage

You’ve seen a small portion of how this is used in the above example, so let’s write a test that uses it:

{
  "_comment": "Simple fixture",
  "winner": "Chicago Cubs",
  "loser": "St. Louis Cardinals"
}
Rspec.describe Game do
  let(:game_response) do
    FactoryBot.create(:game_response,
                      winner: 'St. Louis Cardinals',
                      loser: 'Chicago Cubs')
  end
  let(:expected_payload) { game_response.payload }

  # Passes
  it 'has the correct attributes' do
    expect(expected_payload[:winner]).to eq('St. Louis Cardinals')
    expect(expected_payload[:loser]).to eq('Chicago Cubs')
  end
end

Notice that we have to access the Response.payload attribute from the instance we create in order to get to the actual hash we need.


Issues

It is obvious from even a cursory glance at this code that there are glaring issues. We don’t support arrays, nested data structures are hard to access, etc.

This was meant to solve a singular use case, and to that end we’ve mostly been able to do so. It could definitely be extended to support more complicated payloads–FactoryBot associations are powerful and can probably be used here if anything more complicated is necessary.

For now, this suits our needs, but I will be sure to update if any other useful changes are made!

That’s all for now. Thanks for reading!