This page looks best with JavaScript enabled
⚠️

『オブジェクト志向設計実践ガイド』9章

 ·   ·  ☕ 7 分で読めます
✏️

9章

テストを書く目的=コストの削減

  • バグを見つける
  • 仕様書となる
    • 動くものだからこそ、最新であることを保証する
  • 設計の決定を遅らせる
    • 暫定コードの動作を保証するので、現時点では超具体的過ぎるコードでも、今後リファクタリングできる
  • 抽象を支える
  • 設計の欠陥を明らかにする

テストは最小限にする。重複すると直すのが二度手間になるから。

適切に設計されていれば、オブジェクトは「ほかのオブジェクトから受け取ったメッセージ」と「ほかのオブジェクトに送られたメッセージ」によってのみ外部とつながる。
(内部では、内部同士「自身に送られたメッセージ」とつながっている / これは外部からは見れない)
なので、その外部とつながっているメッセージ(感覚的にインターフェースと言い換えてよさげ)のみテストする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Foo
  def foo # 受信メッセージ
    Bar.new.bar # 送信メッセージ
  end
end

class Bar
  def bar # 受信メッセージ
  end
end

Foo.new.fooのイメージ
1220

受信メッセージがパブリックインターフェース
↑これに対してはテストする責任がある。メッセージの戻り値を表明(アサーション)する。

FooBar#barについてはテストしないべき。

送信メッセージの種類

クエリ(質問)

副作用のない送信メッセージ。
戻り値のみが重要で、メッセージを送ったか送っていないかはどうでもいい。
→受け手オブジェクトの受信メッセージとしてテストすれば十分。

コマンド(命令)

副作用(ファイルの書き込み・データベースへの記録の保存・アクションが起こされるなど)のある送信メッセージ。端的に言うと「なにかが起こる」。
ちゃんとそのメッセージが送られたことは送り手オブジェクトの責任。

振る舞いのテストとして、メッセージが送られた回数使われた引数をテストする(何が起こるかはテストしない / 受信側で)。
送られることに対してのみのテストなので、結合が緩くなる。

テストをいつ書くか

今でしょ!!
最初にテストを書くと、利用する側でコードを書くので再利用性を持たせることができる(not 適切な設計)。t_wadaさんがよく言ってること。

テストの種類

フレームワーク

  • MiniTest
  • RSpec

テスティング

  • テスト駆動開発 - TDD(Test Driven Development)
    • より内から外へ
    • ドメインオブジェクトのテストから始まる。そこから隣接したオブジェクトのテストを書いていって広げる。
    • だからDDDの文脈でTDDが出てくるのか(感想)
  • 振る舞い駆動開発 - BDD(Behaver Driven Development)
    • より外から内へ
    • まだ書かれていないオブジェクトを用意するためにモックが必要。
    • 外からこんなメソッドで呼ぶよ、どうなってる?どうなってる?と掘っていく(自分なりのイメージ)

BDDについて自分なりにまとめてみた - UKSTUDIO

雑に言うとxUnit系はTDDの思想、xSpec系はBDDの思想に基づいてます
by アジャイルおじさん

受信メッセージのテスト

受信メッセージには必ず依存するものがある(絶対にどこかから呼ばれている)。
呼ばれていないなら、そのメソッドは不要なメソッドなので削除する。

テストする項目

基本:考えられうる全ての状況において正しい値を返すこと。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Gear
  def initialize(args)
    @chainring = args[:chainring]
    @cog = args[:cog]
    @rim = args[:rim]
    @tire = args[:tire]
  end

  def gear_inches
    ratio * Wheel.new(rim, tire).diameter
  end
end

このメソッドは別のオブジェクトが絡まっているが、外部からはWheelを使っていることはわからない。テストからももちろんわからない。
→リスクになりうる(Gearが正しくてもWheelが壊れていたらダメ)。

依存を隔離する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Gear
  def initialize(args)
    @chainring = args[:chainring]
    @cog = args[:cog]
    @wheel = args[:wheel]
  end

  def gear_inches
    ratio * wheel.diameter # もはやこのwheelはWheelクラスである必要もない
  end
end

このwheelオブジェクトはDiameterizableロール(#diameterに応える)を担う。ちなみに今のところはロールはWheelだけだし、コード上でWheelDiameterizableの違いについてはわからない。

まあとりあえず、Wheel.new(rim, tire)を注入する設計にしたことで複雑さが解消された。

メソッド名を変更したら……??

Wheel#diameterWheel#widthにしたら、Gear#gear_incheswheel.diameterがエラーになる(当然)。

とはいえ、Wheelを用意するコストが高いとか、いろいろ具体が増えてきたならDiameterizableを用意しよう。

テストダブルを作る

テストダブルはスタブとモックの両方を指す。場合によって、スタブにもなるしモックにもなるという認識。

1
  test_double = double('Twitter client')

スタブは「受信メッセージのテスト」のために使うオブジェクト。
updateメソッド持ってますかー?呼んでいいですかー?」
スタブ「ええで」

1
  allow(test_double).to receive(:update)

モックは「送信メッセージのテスト」のために使うオブジェクト。
nameはなーにー?」
モック「ほれ、名前や。名前しか返さんで」

1
  allow(test_double).to receive(:name).and_return('名前')

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita

とはいえ、ダブルを使っていると、実際の実装と変わってくるよね……。

プライベートメソッドのテスト

プライベートメソッドのテストはしない!!

  • パブリックメソッドでそのロジックは使われているはずだから(そして、プライベートが壊れたらパブリックでも影響が出るはずだから)。
  • プライベートメソッドは変わりやすいから。
  • プライベートメソッドは内部のためのものなのに、外部が使いかねないから(?よくわからんかった。意味はわかるけど、privateは呼び出せなくね?Ruby以外の話?)

解決策

  • プライベートメソッドを作らない
    • プライベートメソッドになった時点で別オブジェクトに切り出す。
    • まあゆーて中身変わっていないから、不安定なことは変わらない。
  • パブリックメソッドの動作で担保する

(呟き)結局、パブリックメソッドに対して「〇〇のときは〜、〇〇のときは〜」ってやるのしんどいよなあ。

送信メッセージのテスト

1
2
3
4
5
6
class Gear
  ...
  def gear_inches
    ratio * wheel.diameter
  end
end
  • クエリメッセージwheel.diameter)を無視する。
    実行したとて、決まった返り値が返ってくるだけなので(そこはwheelの方でテストする)。特にほかのオブジェクトにも依存していないので。

  • selfに送られたメッセージ(ratio)も無視する。
    外に出ていくクエリメッセージも無視する。

コマンドメッセージのテスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Gear
  attr_reader :chainring, :cog, :wheel, :observer
  def initialize(args)
    @chainring = args[:chainring]
    @cog = args[:cog]
    @wheel = args[:wheel]
    @observer = args[:observer] # 追加
  end

  def gear_inches
    ratio * wheel.diameter
  end

  # ↓追加
  def set_cog(new_cog)
    @cog = new_cog
    changed
  end

  def set_chainring(new_chainring)
    @chainring= new_chainring
    changed
  end

  def changed
    observer.changed(chainring, cog)
  end
end

cogchainringが変わったときにはobserverchangeを送るので、それをテストする必要がある。
changedの戻り値については、observerの責任なのでGearがテストする必要はない。ただ、observerにchangedを送ったことだけテストすればいい。

モック

モックは振る舞いのテスト(←→状態のテスト)。
メッセージが送られればオッケー。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def test_notifies_observers_when_chainrings_change
  observer = MiniTest::Mock.new
  gear = Gear.new(
    chainring: 52,
    cog: 11,
    observer: observer
  )

  observer.expect(:changed, true, [42, 11])
  gear.set_chainring(42)
  observer.verify
end

RSpecだとこんな感じになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
it 'test_notifies_observers_when_chainrings_change' do
  observer = double('observer')
  gear = Gear.new(
    chainring: 52,
    cog: 11,
    observer: observer
  )

  expect(observer).to receive(:changed).with(42, 11)
  gear.set_chainring(42)
end

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita

ここで大事なのは、モックはあくまでも送ったことのみテストしていること。実際の中身はobserverの方でテストしてください案件。
必要なら返り値も設定できるが、それはただのおまけで中身はないようなもの。

依存オブジェクトを外部から注入していれば、テスト時にモックに置き換えるだけなので簡単。

RSpecだとinstance_doubleを使えばいい。

ダックタイプをテストする

インターフェイスに対するメソッドをモジュールで作る。
それをコピペ的な感じでincludeする。

……受信のテスト

1
2
3
4
5
module PreparerInterfaceTest
  def test_implements_the_preparer_interface
    assert_respond_to(@object, :prepare_trip)
  end
end

これをそれぞれのクラスのテストでincludeする。

あとは、prepare_tripを送信していることをテストすればいいので、モックを使う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class TripTest < MiniTest::Unit::TestCase
  def test_request_trip_preparation
    @preparer = MiniTest::Mock.new
    @trip = Trip.new
    @preparer.expect(:prepare_trip, nil,[@trip])

    @trip.prepare([@prepare_trip, nil, [@trip]])
    @prepare.verify
  end
end

(省略)

RSpec における double / spy / instance_double / class_double のそれぞれの違いについて - Qiita

Share on

END
END
@aiandrox

 
目次