Tbpgr Blog

Organization Development Engineer tbpgr(てぃーびー) のブログ

Sandi Metz 氏に学ぶ自動テストの実装分類。どれをテストする?どれをテストしない?どれをモックする?

自動テストを書くとき、

  • どのコードをテストすべきか?
  • どのコードをテストすべきではないか?
  • どのコードに対するテストをモックすべきか?

について迷ったことはありますか?
このようなケースに関する判断指針を Sandi Metz 氏 がまとめてくれています。

前提知識

メソッドの種類

メソッドは

  1. Query Method
  2. Command Method

に分類されます。

Query Method は副作用なくデータを返すようなメソッドです。
Command Method はオブジェクトの状態を更新し、データを返さないようなメソッドです。

これらの両方の特徴を持つメソッド(状態を更新しつつ、データを返すようなメソッド)
は設計すべきではないとしています。

この一連の考え方を Command query separation と呼びます。
詳細は Marktinfowler.com が詳しいです。

martinfowler.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

f:id:tbpg:20170324024353p:plain

  • Incoming Command

f:id:tbpg:20170324024359p:plain

  • Outgoing Command

f:id:tbpg:20170324024404p:plain

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

関連資料