実践で学ぶオブジェクト指向④ 効果的なテストを行う



今回はテストについて考えたいと思います。
オブジェクト指向設計 実践ガイド』の9章の部分です。

なぜテストを書くのか

変更可能なコードを書くための3つのスキル

価値の高いテストを書くことによって、行おうとしている変更によってソフトウェアが壊れてしまうことに怯えることなく自信を持ってリファクタリングすることができる。

目的は「コストの削減」


  • バグの修正に必要な時間

  • 文書を書く時間

  • 設計をする時間

テストを書くことでこれらにかかる時間コストを下げることができる。

何をテストするべきか



ここがなかなか難しいです。


クラスの中で定義されるメソッドを受信メッセージとプライベートメソッドに分けます。

・他のオブジェクトから呼び出されるものは受信メッセージ(パブリックインターフェース)です。
・そのメソッドを自身のオブジェクトしか呼び出さないのであれば、プライベートメソッドです。

オブジェクトを指定してメソッドを呼び出すものを「送信メッセージ」と考え、これを2つに分けます。

・そのメッセージが送信されたことによって影響を及ぼすのが、送ったオブジェクトのみなのであればクエリ(質問)メッセージです。
・そのメッセージが送信されたことによって他のオブジェクトに影響を及ぼすのであれば、コマンド(命令)メッセージです。



テストの有無は以下です。


  • メソッド
    ・受信メッセージ(パブリックインターフェース )
    テストを書く
    ・プライベートメソッド
    テストは書かない

  • 送信メッセージ(メソッドの呼び出し)
    ・クエリ(質問)メッセージ
    テストは書かない
    ・コマンド(命令)メッセージ
    テストを書く



例を挙げて考える


Judgeクラスのテストを例に考えてみましょう。
考える行について、コメントで処理が発動する順に番号を降ってあります(1~3)

class Judge

  def judge(card1, card2) #パブリックインターフェース #2
    card1 == card2 
  end

end


class Game
  attr_reader :player
省略
  def judge_2_cards(card_id) #プライベートメソッド
    if counter.limit?(opened) #送信クエリ
      flip_over(index(card_id)) #selfに対する送信
    end
  end

  def flip_over(i) #プライベートメソッド 
    if Judge.new.judge(cards[opened[0]+2], cards[i+2])   #送信クエリ #1
      player.face_down #送信コマンド #3
    end
  end

end



Judgeクラスのjudgeメソッドはクラスのパブリックインターフェースです。
1でGameからメッセージが送信され、2でそのメッセージを受信して処理を実行してtrue/falseを返し、trueなら3が実行されます

2の受信メッセージ(クラスのパブリックインターフェース)

まずJudgeクラスのjudgeメソッド(2)についてはパブリックインターフェースであるのでテストをすべきです。
与えられた2つのカード(数字)が一致すればtrueを返す、ということをテストします。


1の送信クエリメッセージ

1の送信メッセージ(クエリ)ですが、この送信メッセージによる影響はflip_overメソッドにおけるif分岐が受けます。この送信クエリ自体が他のオブジェクトへ影響を及ぼすわけではないのでテストはしません。

気持ちとしては、「ここのif分岐によって、playerオブジェクトへface_downメッセージが送信されてopenedという情報が書き換わることがあるし影響はあるんじゃないの」と思うんですが、このクエリメッセージのテストはする必要はありません。

3の送信コマンドメッセージ

3の送信メッセージはクエリ(質問)ではなくコマンド(命令)です。
メッセージを送信することでplayerオブジェクトがface_downを実行します。そしてその影響は

class Player
    attr_accessor :opened

    def face_down #(これもパブリックインターフェース)
        opened.clear #openedが空になる
    end
end

送信したGameオブジェクトではなく、送信されたPlayerオブジェクトのopenedが空になるという影響を受けます。

そのメッセージを送ることによって他のオブジェクトへ影響を及ぼすものなので、3はコマンド(命令)メッセージだというわけです。
なのでテストは書かれるべきです。

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

3はコマンドメッセージなのでテストが書かれるべきなんですが、ここでもポイントがあります。

テストをするのはplayerオブジェクトへface_downメッセージが送信されたことのみです。

送信された後、つまり受信メッセージ(メソッド)として実行された結果(openedが空になるかどうか)についての確認はPlayerのface_down受信メッセージのテストで行われるべきです。

この、オブジェクトAからオブジェクトBへ送信されるメッセージは、オブジェクトBの受信メッセージであるという構造が重要です。
なので、他オブジェクトへ送信されるコマンドメッセージのテストは、「それが間違いなく送信されたという事実の確認」のみで良いのです。

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


プライベートメソッドのテストは必要ありません。
なぜ必要ないか、flip_overメソッドを見ながら考えましょう。

  def flip_over(i) #プライベートメソッド 
    if Judge.new.judge(cards[opened[0]+2], cards[i+2])   #送信クエリ #1
      player.face_down #送信コマンド #3
    end
  end


flip_overメソッドの責任は「Judgeオブジェクトにjudgeメッセージを送ったとき、trueが返ってくればplayerオブジェクトにface_downメッセージを送る」というものです。

flip_overではなくアプリケーションとして考えたときにテストすることとして挙がるのは、

①Judgeが正しい判定をすること
②if分岐が正しく行われること
③playerにface_downメッセージが送られること
④face_downメソッドが正しく実行されること


です。それぞれについて考えてみると、

①はJudgeのテストが行います。
②はrubyのオブジェクトであり堅牢なのでテストを書く必要はありません。
③は送信コマンドメッセージのテストが行います。
④はPlayerのテストが行います。

すべてがカバーされているので、プライベートメソッドのテストは必要がないというわけです。
無駄にたくさんのテストを書くと保守が大変になりコストが上がります。それではテストの本来の目的から遠ざかる結果になります。


そもそもプライベートメソッドの存在は好ましくないらしい



p266には、「そもそもプライベートメソッドを大量に持つようなオブジェクトは責任を持ちすぎている必要があり、それらを別のオブジェクトとして切り出すことを考えるべき」とあります。
増えていったプライベートメソッドを新しいオブジェクトとして切り出すのか、判断を遅らせるのか。
このあたりのさじ加減が難しいですね。経験を積まないと分からないことかもしれません。



メモ

Rspecでテストを書くにあたって躓いたところをメモしておきます。

  • ApplicationRecordを継承していないモデルはRspecのテストを受けられない
    これは間違いでした。
    initialize時のテストを書こうとしたときにAcriveRecordを継承してないとvalid?でエラーが出る、ということです。
    superの記述が必要という情報も見かけました。

  • Rspecインストール後にアプリケーション直下にできる.rspecという隠しファイルに--format documentationを追記すると、成功した場合もドキュメントが出るようになる