RSpecのletの遅延評価を利用してよりコンテクスチュアルなSpecを書く
皆さんこんにちは。もう今年も残すところわずか3日となりました。早いものです。
さて、年末感のかけらもないネタですが、今回はRSpecのletについて、最近自分の中で1つの理解が得られた(気がする)ので書いてみようと思います。
これまでのletへの理解
ちょっと賢い単なる変数への束縛だと思っていました。
公式ドキュメントであるRelishのLet and let!によると、let
は、
$count = 0
RSpec.describe "let" do
let(:count) { $count += 1 }
it "memoizes the value" do
expect(count).to eq(1)
expect(count).to eq(1)
end
it "is not cached across examples" do
expect(count).to eq(2)
end
end
同じexampleの中(👆で言うと “it” の中)では常に値が一緒である事が保証されるようです。
また、 let!
は、
$count = 0
RSpec.describe "let!" do
invocation_order = []
let!(:count) do
invocation_order << :let!
$count += 1
end
it "calls the helper method in a before hook" do
invocation_order << :example
expect(invocation_order).to eq([:let!, :example])
expect(count).to eq(1)
end
end
exampleが実行される前に評価&束縛されるようです。
また、日本語Googleで「RSpec let」と検索すると、次のような記事がヒットします。
- RSpec の letとlet!とbeforeの挙動と実行される順番 – Qiita
- RSpecのletを使うのはどんなときか?(翻訳) – Qiita
- letとlet!を使う (Better Specs { rspec guidelines with ruby })
ご覧の通り、大抵の記事では let
と let!
の評価順の違いと、メリットとしては公式ドキュメント同様にexmaple内での値の一貫性と、記事によっては可読性や、typoに気づきやすい等が挙げられています。
しかし、いずれも単に(以下のように)example内で変数を自前でバインドする事に比べて、大したメリットに感じません。
$count = 0
RSpec.describe "let" do
it "memoizes the value" do
count = $count += 1
expect(count).to eq(1)
expect(count).to eq(1)
end
it "is not cached across examples" do
count = $count += 1
expect(count).to eq(2)
end
end
さらにはこんな記事まであります。
👆ではパフォーマンスの問題や、他で挙げられていた可読性が逆に損なわれていると言われています。
確かに、単なる変数束縛との違いが分からない状態では、初めてRSpecのコードを見た時のカオス感の大きな要因の一つとも思えます(実際思ってました)
理解した事
先程挙げた公式ドキュメントの中には、こんな言及もあります。
Note that let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked.
なるほど、どうやら let
は遅延評価されるようです。ここで、ある思いが頭をよぎりました。
「letはsubjectを使った時に効果を発揮するのでは・・・?」
試しに書いてみた
テスト対象のクラスは、引数として与えられた同クラスのインスタンスの性別から異なった文言を使って挨拶を返す次のような物とします:
class Person
SEX_MALE = 1
SEX_FEMALE = 2
attr_reader :name, :sex
def initialize(name, sex)
@name = name
@sex = sex
end
# @param [Person] to_be_greeted
def greet(to_be_greeted)
title = to_be_greeted.sex == SEX_MALE ? "Mr." : "Ms."
"Hi, #{title}#{to_be_greeted.name}, my name is #{@name}. It's a pleasure to meet you."
end
end
これに対して、RSpecは次のような感じになりました:
RSpec.describe Person do
describe "#greet" do
let(:person) { Person.new("Issei", Person::SEX_MALE) }
subject { person.greet(to_be_greeted) }
context "with a male person" do
let(:to_be_greeted) { Person.new("Taro", Person::SEX_MALE) }
it { is_expected.to eq "Hi, Mr.Taro, my name is Issei. It's a pleasure to meet you." }
end
context "with a female person" do
let(:to_be_greeted) { Person.new("Hanako", Person::SEX_FEMALE) }
it { is_expected.to eq "Hi, Ms.Hanako, my name is Issei. It's a pleasure to meet you." }
end
end
end
主題である person.greet
の呼び出しだけは先に宣言しておいて、example毎に、引数である to_be_greeted
を別途用意しています。これは、 let
が遅延評価であるから実現できています。
もちろんメソッドの呼び出し自体を、各exampleの it
の中で行っても良いのですが、このくらいシンプルな振る舞いの検証であれば、この書き方の方がDSLを使っている感があって(RSpecの意図としても)良いように思います。好みの問題かもしれませんが。
因みに、スタブを使う場合も同様にできます。同じくPersonを例にすると:
(同じクラスのインスタンスを引数に取るので、この場合本来スタブを使うまでも無いのですが)
RSpec.describe Person do
describe "#greet" do
let(:person) { Person.new("Issei", Person::SEX_MALE) }
subject { person.greet(to_be_greeted) }
context "with a male person" do
let(:to_be_greeted) { double(:to_be_greeted, name: "Taro", sex: Person::SEX_MALE) }
it { is_expected.to eq "Hi, Mr.Taro, my name is Issei. It's a pleasure to meet you." }
end
# ...
end
end
まとめ
RSpecはDSLを使っていて読みやすいSpecを書けるのはいいのですが、書き方が多岐に渡りすぎていて統率が難しいのです。
ですが、綺麗な書き方を発見すると楽しいので皆さんも色々探してみて下さい。
それでは良いお年を🎅
コメントを残す