FactoryBot を使って JSON 文字列を生成する


2023年 08月 24日

こんにちは。SI部の r_maeda です。

みなさん、FactoryBot gem はご存知でしょうか?

https://github.com/thoughtbot/factory_bot

FactoryBot は、Ruby オブジェクトを生成するための factory を、簡単な DSL で定義できる gem です。

RSpec gem と共に、Ruby (on Rails) で書いたアプリケーションのテストコードを書くために広く利用されている gem の1つではないでしょうか。

この FactoryBot gem ですが、生成できるオブジェクトは ActiveRecord モデルのインスタンスだけではありません。任意のクラスのインスタンスを生成することが可能です。

そんな FactoryBot gem の面白い使い方を発見したので、ご紹介したいと思います。

JSON 文字列を生成する

FactoryBot を使って、JSON 文字列を生成してみます。まずは json factory を以下の通り定義します。

# frozen_string_literal: true

require "factory_bot"
require "json"

FactoryBot.define do
  factory :json, class: "String" do
    hoge { :foo }
    fuga { :bar }

    initialize_with do
      JSON.generate(attributes)
    end
  end
end

この factory を使って実際に JSON 文字列を生成してみます。

 % irb
irb(main):001:0> require "./json_factory"
=> true
irb(main):002:0> FactoryBot.build(:json)
=> "{\"hoge\":\"foo\",\"fuga\":\"bar\"}"
irb(main):003:0>

できました。

解説

今回作成した json factory のポイントは、initialize_with ブロックです。

このブロックを定義することで、「どのような手順で、生成したインスタンスに値を設定するか」を上書きすることが出来ます。

initialize_with ブロックを定義しない場合、FactoryBot は以下のようなコードを実行しようとします。

s = String.new
s.hoge = :foo
s.fuga = :bar

String class に #hoge= メソッドは存在しないため、当然このコードはエラーになります。

しかし、initialize_with ブロックを定義してあげることで、FactoryBot に「どうやってインスタンスを生成すればよいか、またどうやって生成したインスタンスに値を設定すればよいか」を指示することが出来ます。

で、なにが嬉しいの?

私が普段関わっているシステムには、「他のシステムに HTTP リクエストを行う」処理が多く存在しています。

これらの処理のテストを書くために WebMock gem を導入しているのですが、この gem を使って「特定のリクエストに対するダミーレスポンスを設定する」場面などで使えるんじゃないかなと考えています。

# JSON 文字列を生成
response_body = FactoryBot.build(:get_api_v1_users_response)

# HTTP リクエストに対する stub の設定
WebMock
  .stub_request(:get, "https://example.com/api/v1/users")
  .to_return(status: 200, headers: { "Content-Type": "application/json" }, body: response_body)

最後に

今回は FactoryBot gem を使って JSON 文字列を生成する方法を紹介しました。

他にも Hash のインスタンス、StructData のサブクラスのインスタンスなども、同様に initialize_with ブロックを定義してあげることで生成できるようになります。

なお association を使うこともできるので、「ネストが存在するなど、それなりに複雑な JSON を生成したい」場合には、適宜 factory を分割していくのがよいと思います。

# frozen_string_literal: true

require "factory_bot"
require "json"

FactoryBot.define do
  factory :json, class: "String" do
    hoge { :foo }
    fuga { :bar }

    association :child

    initialize_with do
      JSON.generate(attributes)
    end
  end

  factory :child, class: "Hash" do
    piyo { :baz }

    initialize_with do
      attributes
    end
  end
end

この記事が、ここまで読んで頂いた皆様のお役に立ちますと幸いです。

2023/08/30 追記

WebMock gem には WebMock::RequestStub#to_return_json といったメソッドが存在するそうです。これを使えば、簡単に JSON WebAPI のダミーレスポンスを設定できるみたいですね。

https://github.com/bblimke/webmock#response-with-json-body

body = { hoge: :foo, fuga: :bar }

# 中で body.to_json してくれる
# Content-Type: "application/json" リクエストヘッダも勝手に追加してくれる
WebMock
  .stub_request(:get, "https://example.com")
  .to_return_json(status: 200, body: body)

「FactoryBot で JSON 文字列を生成したところで何に使えるの?」についてはその用途が1つ減ってしまいましたが、「#initialize_with を使えば様々な class のインスタンスが生成できる」といった話は頭の片隅に置いておいてもらえれば、役に立つこともあるのかなと思います。

#to_return_json に渡す body (Hash) を FactoryBot を使って生成する」なんてこともできますしね。