RSpecでFactoryBotのtraitの良さに少し気づけた
trait
の何が良いか分かっていなかった
RSpecでFactoryBotの Railsエンジニアの皆様、RSpecでテスト書いていますか?
FactoryBotの trait
の良さについて、自身で考えた内容になります。
私はRailsでWeb系の開発を行っている駆け出しエンジニアです。
未経験からの転職で1年半が経過しました。
誤りやより良い方法があれば教えて頂けると幸いです。
trait
の組み合わせ自由度の高さに惚れた
(結論) パターンA のテストデータが欲しいときに A = S + T
としたり、
パターンB が必要なときには B = T + X
としたり、
C = X + S + T
とできるのが trait
です。
小さく必要なテストデータを用意することで、
利用者(テスト作成者) が自由に組み合わせて必要なテストデータを用意することが可能になります。
参考資料
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