😺

Rspec社内運用まとめ その2 テストデータの量産とFactoryBot

2024/09/13に公開

はじめに

弊社のサービスである ecforce は、 Rspec を用いてテストコードを実装しています。
現在はグループ内でルールや方針を定めたため、それらに則って各チームがテストコードを実装するようになっています。
数年前はルールや方針が明確に定まっておらず、可読性や拡張性が低下していました。
今回はテストコードにおいて数年前の状態から今に至るまで、何をどのように変えていったのかをご紹介していきます。

本題

欠損データを作らない

まず、テストコードを書く上で、実際に作成されるデータに可能な限り近づけることが重要だと考えています。
なぜなら、欠損データの状態でテストコードを書くと、意図せずテストコードがパスしたり失敗したりする可能性が高くなるからです。

欠損データを作成しないためには、 画面操作 をイメージすることが良いと考えています。
いくつか以下に例を示します。

  • 顧客作成画面は存在する
  • 定期受注の作成画面は存在せず、購入画面から注文が入った際に自動で定期受注が作成される
  • 在庫一覧画面では在庫の更新はできるものの、在庫単独での登録はできない。必ず商品を作成した後に在庫を登録できる仕様になっている

テストデータの読み書きを容易にするFactoryBot

Rspecだけでもテストデータを作成することは可能です。
例えば、30日間隔で配送される定期商品は、以下のように作成できます。

let(:product){
  Product.create!(
    name: '定期商品',
    number: 'test_number',
    is_single: false,
    shipping_cycle: 'term',
    delivered_every_n_day: 30,
  )
}

ただ、この記述を毎回書くとコード量が多くなってしまったり、可読性が落ちたりしてしまいます。
そこでRspecとセットでよく使用される FactoryBot を導入しました。
FactoryBot導入後のテストデータの作成は以下の通りです。

let(:product){ create(:product, :recurring, :cycle_term, delivered_every_n_day: 30) }

左から順に

  • 商品
  • 定期
  • 配送サイクルは間隔
  • 30日おき

と読み進めることができ、FactoryBot導入前よりも直感的に 30日間隔で配送する定期商品 と認識することができます。

テストデータの読み書きを容易にする方法

ここからは、どのような手順を踏めばテストデータの作成を容易にできるかについてご紹介します。
結論から先に述べると、 どのような仕様があるのかを洗い出すことこういうふうに呼び出したいという理想形を策定すること が良いと考えます。

どのような仕様があるのかを洗い出す

ecforceにおいては、一口に「商品」といっても様々な仕様が考えられます。

  • 単品商品と定期商品のどちらか
  • 配送サイクルが 日付で指定 間隔で指定 曜日で指定
  • 販売しているか、販売していないか
  • 商品一覧画面への表示/非表示
  • アップセル商品(対象商品よりもワンランク上の商品)が設定されているか
  • クロスセル商品(対象商品と一緒に購入してもらいたい商品)が設定されているか
  • 同梱物が設定されているか
  • 販売価格がいくらか
  • 配送業者ごとに許容する大きさ

呼び出したいインターフェースの理想形の策定

仕様が洗い出せた後は、読み書きが容易になりそうなインターフェースの理想形を考えます。
この時点で動作するかどうかは気にしなくても大丈夫です。

販売価格が1,000円の定期商品

create(:recurring_product, price: 1000)

指定の日付で販売終了した定期商品

create(:recurring_product, :unsale, :cycle_date)

複数個のラベルが紐づいた定期商品

create(:recurring_product, :with_labels)

2つの同梱物が紐づいた単品商品

create(:product, params: [{ item_id: 1, quantity: 1 }, { item_id: 2, quantity: 1 }])

どのDSLに任せるのかを考える

上記のようなインターフェースの理想形ができたら、最後にどのDSL(ドメイン固有言語)に任せるかを考えます。
具体的には以下4つです。

  • factoryのデフォルト値
  • trait
  • アソシエーションメソッド
  • nested attributesメソッド

販売価格が1,000円の定期商品

以下の price: 1000 については、FactoryBotが導入されていればそのまま使用することができます。
recurring_product については、 product を継承する形でFactoryを実装する必要があります。

create(:recurring_product, price: 1000)

指定の日付で販売終了した定期商品

こちらの unsalecycle_datetrait として、Factoryを実装する必要があります。

create(:recurring_product, :unsale, :cycle_date)

複数個のラベルが紐づいた定期商品

こちらの with_labels については何かしらの実装が必要です。

create(:recurring_product, :with_labels)

ただ、Railsのアソシエーションメソッドの

has_many :labels

が定義されているので、インターフェースそのものを微修正し、以下のように実装しました。

create(:recurring_product, labels: create_list(:product_labels, 3))

labels: xxx の「labels」は、アソシエーションメソッド has_many :labels の「labels」です。
すでに定義されているアソシエーションメソッドを活用することで、Factoryの実装を最小限に抑えることができます。

2つの同梱物が紐づいた単品商品

以下の params: xxx については何かしらの実装が必要です。

create(:product, params: [{ item_id: 1, quantity: 1 }, { item_id: 2, quantity: 1 }])

ただし、RailsのアソシエーションメソッドとNestedAttributesメソッドの

has_many :bundled_items
accepts_nested_attributes_for :bundled_items

が定義されているので、こちらもインターフェースそのものを微修正し、以下のように実装しました。

create(:product, bundled_items_attributes: [{ item_id: 1, quantity: 1 }, { item_id: 2, quantity: 1 }])

こちらもすでに定義されているアソシエーションメソッドとNestedAttributesメソッドを活用することで、Factoryの実装を最小限に抑えることができます。

Factoryの実装

上記を踏まえて、Productモデルに関するFactoryを実装しました。

FactoryBot.define do
  factory :product, class: Product do
    # 単品商品
    is_single{ true }
    sequence(:name){ |n| "product_#{n}" }
    number{ SecureRandom.uuid }

    trait :sale do
      sale{ true }
    end

    trait :unsale do
      sale{ false }
    end

    factory :recurring_product do
      # 定期商品
      is_single{ false }

      trait :cycle_term do
        shipping_cycle{ 'term' }
      end

      trait :cycle_date do
        shipping_cycle{ 'date' }
      end

      trait :cycle_day_of_week do
        shipping_cycle{ 'day_of_week' }
      end
    end
  end
end

このように、

  • 仕様の洗い出し
  • 理想のインターフェースの策定
  • DSLの役割分担

の3つを意識することで、高い可読性や拡張性を維持しています。

まとめ

いかがでしたでしょうか。
今回はテストデータの作成に対する考え方と、テストデータの作成を容易するための設計についてご紹介しました。
テストデータの作成において FactoryBot はもちろん大事なのですが、Rails本体に定義したメソッドと組み合わせることで、設計をよりシンプルにし、可読性を向上させることができます。

次回は、 パスしないテストコードの調査に工数が取られてしまったケース に対して、どのようなアプローチをしたのかお伝えしていきます。

SUPER STUDIOの採用について

SUPER STUDIOでは、積極的にエンジニアを採用しています。
少しでも興味がありましたら、以下の記事をご覧ください。

https://hrmos.co/pages/superstudio/jobs/0000400
https://hrmos.co/pages/superstudio/jobs/0000414
https://hrmos.co/pages/superstudio/jobs/0010025

また、下記はSUPER STUDIOで年に一度開催されるKICKOFFイベントにて、社内表彰されたエンジニアの受賞インタビューです。SUPER STUDIOのエンジニア組織についてより理解を深められる内容となっておりますので、ぜひご一読ください。

https://www.wantedly.com/companies/super-studio/post_articles/497997
https://www.wantedly.com/companies/super-studio/post_articles/487617

SUPER STUDIOテックブログ

Discussion