📘

RSpecでFactoryBotのtraitの良さに少し気づけた

2022/12/28に公開

RSpecでFactoryBotの trait の何が良いか分かっていなかった

Railsエンジニアの皆様、RSpecでテスト書いていますか?
FactoryBotの trait の良さについて、自身で考えた内容になります。

私はRailsでWeb系の開発を行っている駆け出しエンジニアです。
未経験からの転職で1年半が経過しました。

誤りやより良い方法があれば教えて頂けると幸いです。

(結論) trait の組み合わせ自由度の高さに惚れた

パターンA のテストデータが欲しいときに A = S + T としたり、
パターンB が必要なときには B = T + X としたり、
C = X + S + T とできるのが trait です。

小さく必要なテストデータを用意することで、
利用者(テスト作成者) が自由に組み合わせて必要なテストデータを用意することが可能になります。

参考資料

https://ja.wikipedia.org/wiki/トレイト

factoryと何が違うかわからなかった

例えば 20歳 が閾値のテストがある場合、
以下のように、trait を利用してテストデータを作っていました。

理由は、単純に 「 trait 使うといい」 そんなことを聞いたことがあるからでした。

factory :user do
  name { "taro" }

  trait :age_20 do
    age { 20 }
  end

  trait :age_19 do
    age { 19 }
  end
end

「いやいや、そんなん別のfactory作ればいいじゃん」

factory :user do
  name { "taro" }
  age { 20 }
end

factory :age_19_user, class: User do
  name { "hanako" }
  age { 19 }
end

「nameが同じで良いならfactoryの中に書けるよ」

factory :user do
  name { "taro" }
  age { 20 }

  factory :age_19_user do
    age { 19 }
  end
end

私はそう思っていました。
trait 使っても factory でも同じことができるのでは?

それはテストパターンの軸が1つの話

こんなメソッドがあったとしてテストを書いてみます。

class User < ApplicationRecord
  def method_A
    if age > 19
      "成人です"
    else
      "未成年です"
    end
  end
end

本題ではないので省略してますが、it には対象のテスト内容を書きましょうね。
trait を使わなくても特に問題はなさそうに思います。

describe User do
  context "ユーザーが20歳以上の場合" do
    it { expect(build(:user).method_A).eq("成人です") }
  end

  context "ユーザーが20歳未満の場合" do
    it { expect(build(:age_19_user).method_A).eq("未成年です") }
  end
end

テストの軸が2つあったら?

20歳以上と未満、好きな食べ物が野菜かそれ以外かの組み合わせで4パターンあるとします。

method_X 好きな食べ物が野菜 好きな食べ物が野菜ではない
20歳以上 パターン1 パターン2
20歳未満 パターン3 パターン4

さあ、factoryでテストデータを用意しましょう!

factory :user do
  name { "taro" }
  age { 20 }
  like { "野菜" }

  factory :like_fish_user do
    like { "魚" }
  end

  factory :age_19_user do
    age { 19 }

    factory :age_19_and_like_fish_user do
      like { "魚" }
    end
  end
end

「うーん、どこまでが age 20 のユーザーなんだろ?」
「age_19_user の好きな食べ物はなんだろ?」
「 ちょっと複雑になってきた、、」

  it { expect(build(:user).method_X).eq("パターン1") }
  it { expect(build(:like_fish_user).method_X).eq("パターン2") }
  it { expect(build(:age_19_user).method_X).eq("パターン3") }
  it { expect(build(:age_19_and_like_fish_user).method_X).eq("パターン4") }

3つ、4つとパターンの組み合わせが増えて行くと、私は「もう、分からん」となります。

そこでtraitの出番

閾値となるテストデータを trait で用意します。

factory :user do
  name { "taro" }

  trait :age_20 do
    age { 20 }
  end

  trait :age_19 do
    age { 19 }
  end

  trait :like_vegetables do
    like { "野菜" }
  end

  trait :like_fish
    like { "魚" }
  end
end

使い方は ([factory_name], [traitA], [traitB]) の形式で呼び出します。

  it { expect(build(:user, :age_20, :like_vegetables).method_X).eq("パターン1") }
  it { expect(build(:user, :age_20, :like_fish).method_X).eq("パターン2") }
  it { expect(build(:user, :age_19, :like_vegetables).method_X).eq("パターン3") }
  it { expect(build(:user, :age_19, :like_fish).method_X).eq("パターン4") }

すっきりと意図が明確なテストデータが用意できたように私は思います。
テスト作成者が自由に必要なデータを組み合わせることができます。

もちろん例のような簡単なケースなら、以下のように事前定義不要の可能性もあります。

build(:user, age: 20, like: "野菜")

しかし、汎用性がありそう、再利用される、初期化が手間、などの場合には、trait でテストデータを用意することも有益ではないでしょうか?

(まとめ) trait の組み合わせは無限大

必要なテストデータをtraitで小さく用意することで、自由に組み合わせ、様々なテストに対応可能なデータを作ることができると私は思います。
これからも、楽しくテストが書けるように色々なことを考えて行きたいと思います。

最後まで読んで頂きありがとうございました。

(補足) 重複した場合は後が勝ちます

同じ定義のある trait を呼び出したり、直接定義をした場合には後が勝ちます。
認知負荷を高めるので、個人的に重複を利用したtraitを用いたテストは作らない方が良いのかなと思います。

build(:user, :age_20, :age_19)
=> age: 19

build(:user, :age_19, age: 20)
=> age: 20

Discussion