自動テストを書くとき、
- どのコードをテストすべきか?
- どのコードをテストすべきではないか?
- どのコードに対するテストをモックすべきか?
について迷ったことはありますか?
このようなケースに関する判断指針を Sandi Metz 氏 がまとめてくれています。
前提知識
メソッドの種類
メソッドは
- Query Method
- Command Method
に分類されます。
Query Method は副作用なくデータを返すようなメソッドです。
Command Method はオブジェクトの状態を更新し、データを返さないようなメソッドです。
これらの両方の特徴を持つメソッド(状態を更新しつつ、データを返すようなメソッド)
は設計すべきではないとしています。
この一連の考え方を Command query separation と呼びます。
詳細は Marktinfowler.com が詳しいです。
テスト指針
テストの種類
Message Type | Query Method | Command Method |
---|---|---|
Incoming | 💚 Assert result | 💚 Assert direct public side effect |
Send to self | 🔴 Ignore | 🔴 Ignore |
Outgoing | 🔴 Ignore | 💚 Expect to send |
Images
- Incoming Query
- Incoming Command
- Outgoing Command
Incoming Query
メソッドの結果に対して検証します。
require "spec_helper" class Person attr_reader :name, :age def initialize(name, age) @name, @age = name, age end def next_age @age += 1 end def to_s "#{name} (#{age})" end end describe Person do it do tanaka = Person.new("tanaka", 23) expect(tanaka.to_s).to eq("tanaka (23)") end end
Incoming Command
副作用に対して 検証します。
require "spec_helper" class Person attr_reader :name, :age def initialize(name, age) @name, @age = name, age end def next_age @age += 1 end def to_s "#{name} (#{age})" end end describe Person do it do tanaka = Person.new("tanaka", 23) tanaka.next_age expect(tanaka.age).to eq(24) end end
Send to self
Query, Command ともに private なメソッドをテストすべきではありません 。
Outgoing Query
外部のオブジェクトの Query の呼び出しはテストすべきではありません 。 このテストは外部の Query で行うべきものだからです。
require "spec_helper" class Dog attr_reader :name, :age def initialize(name, age) @name, @age = name, age end def next_age age += 1 end def to_s "#{name} (#{age})" end end class Person attr_reader :name, :age, :pet def initialize(name, age, pet) @name, @age, @pet = name, age, pet end def next_age @age += 1 end def next_pet_age @pet.age += @pet.age end def to_s "#{name} (#{age}) - #{pet.to_s}" end end describe Person do let(:subject) do pochi = Dog.new("pochi", 3) tanaka = Person.new("tanaka", 23, pochi) end it "test person" do expect(subject.to_s).to eq("tanaka (23) - pochi (3)") end it "test dog" do # Outgoing qury はテストしない expect(subject.pet.to_s).to eq("pochi (3)") end end
Outgoing Command
外部のオブジェクトを Mock して呼び出しが行われたことを検証します 。
require "spec_helper" class Dog attr_reader :name, :age def initialize(name, age) @name, @age = name, age end def next_age @age += 1 end def to_s "#{name} (#{age})" end end class Person attr_reader :name, :age, :pet def initialize(name, age, pet) @name, @age, @pet = name, age, pet end def next_age @age += 1 end def next_pet_age @pet.next_age end def to_s "#{name} (#{age}) - #{pet.to_s}" end end describe Person do it "test dog" do pochi = Dog.new("pochi", 3) allow(pochi).to receive(:next_age).and_return(4) tanaka = Person.new("tanaka", 23, pochi) tanaka.next_pet_age expect(pochi).to have_received(:next_age).once end end