「メソッドに対してテストをするな」という話題について - その手の平は尻もつかめるさ
記事の内容が間違ってるとまでは言いませんが、これだけではミスリードしてしまう可能性が高いと思いましたので、簡単にですがアンサーエントリ書いておきます。タイトルでは「反論」と書きましたがどちらかというと「補足」に近いかもです。
前提の整理
- 「メソッド」という言葉が使われているため、オブジェクト指向的なパラダイムを持つ言語に限定した話であるという認識で書きます。その前提自体が間違っていたらご指摘ください。
- 元エントリの主張である「テストが説明的であるべきだ」ということを否定するわけではありません。ただ、それと「メソッドに対してテストをするな」という意図が異なるのではないかと主張したいだけです。
- 元エントリは Perl でサンプルコードで書かれているようですが、僕が Perl にそれほど明るくないため、このエントリでは Ruby でサンプルコードを書かせていただきます。
ユニットテストにおける「ユニット」とは何か?
オブジェクト指向設計においてユニットとは通常「メソッド」ではなく「オブジェクト」です。
先日のDHHの「TDDは死んだ」というエントリに始まる一連の議論の中でも「ユニットテストとは一体何か?」という議論は多々ありましたが、それはモックの使い方に関する議論が中心だったと認識しており、単一のメソッドを「ユニット」と呼ぶことはないのではないかと思います。
つまり「メソッドに対するテストをするな」というのは「オブジェクトに対するテスト」をしろということと同義であり、メソッドを通して起こる副作用をオブジェクトの外から観測する方法について気を配りなさいということなのだと認識しています。
「メソッドに対するテスト」はカプセル化を破壊する
あるオブジェクトのあるメソッドが値を返すだけのときにはユニットとはオブジェクトであるということを非常に意識しにくいです。例えば以下のようなコードです。
user.rb
class User def initialize(first_name, last_name) @first_name, @last_name = first_name, last_name end def full_name @first_name + ' ' + @last_name end end
user_spec.rb
require './user.rb' describe User do describe "#full_name" do subject { User.new('Akira', 'SUENAMI') } it 'should be the string that concatenated the first name and the last name.' do expect(subject.full_name).to eq 'Akira SUENAMI' end end end
ユーザのフルネームは苗字と名前を連結した文字列であるというテストですが、この程度であればこれが「メソッドのテスト」か「オブジェクトのテスト」なのか意識しづらいです。メソッドのロジックをテストしているという感覚になってもおかしくはありません。
ただし、副作用がある、つまりオブジェクトの内部状態の変化を伴うメソッドの場合、「メソッドのテスト」と「オブジェクトのテスト」は明確に異なります。(というか、「メソッドのテスト」を書きたいと思ったら設計がよくないと疑ったほうがよいです。)
例えばスタックを実装する*1としましょう。こんな感じに書いてみました。
stack.rb
class Stack attr_accessor :elements def initialize @elements = [] end def push new_element @elements.push new_element end def pop @elements.pop end end
stack_spec.rb
require './stack.rb' describe Stack do let(:stack) { Stack.new } describe "#push" do it "should add new element" do stack.push 'stacked element' expect(stack.elements[-1]).to eq 'stacked element' end end describe "#pop" do it "should return the element in top of the stack" do stack.elements.push 'stacked element' expect(stack.pop).to eq 'stacked element' end end end
スタックは当然ですが「現在スタックに積まれている要素」という状態を持ちます。そして「スタックに新たに要素を追加する」(push
)という振る舞いも「スタックの一番上の要素を取り出す」(pop
)という振る舞いもその結果が現在の状態に依存しますし、push
に関しては値を返さないため、戻り値に対するアサーションではテストになり得ません。
それに対してどうやってテストコードを書くか考えた場合によく陥りやすいアンチパターンが上記のコードです。
これは@elements
という配列を外部に公開してしまっており、push
とpop
のテストにおいてそれぞれに直接アクセスしてしまっています。
この状態だと例えば@elements
の変数名を変えただけでもテストが落ちてしまいます。スタックというオブジェクトを使う際に通常期待することはpush
したらその要素がスタックの一番上に積まれ、pop
した際に一番上の要素が取り出せるということであるはずで、決して内部の変数名や詳細な実装を気にしてはいないはずです。にも関わらず、そういった内部の詳細実装の変更によって成功していたはずのテストが失敗するということはあってはならないことですし、それはまったくリファクタリングを支援しないどころかリファクタリングを阻害します。
これが「メソッドに対してテストをするな」という言葉の意味です。 おそらく「オブジェクトのテスト」はこうなるでしょう。
stack.rb
class Stack def initialize @elements = [] end def push new_element @elements.push new_element end def pop @elements.pop end end
stack_spec.rb
require './stack.rb' describe Stack do let(:stack) { Stack.new } it "should be LIFO container" do stack.push 'first element' stack.push 'last element' expect(stack.pop).to eq 'last element' expect(stack.pop).to eq 'first element' end end
内部状態を外部に公開する必要はなくなり、push
とpup
だけを外部へのインターフェースとして公開する本来あるべきスタックの姿になりました。
これによってリファクタリングができるようになり、スタックとしての責務を果たしつつ内部の実装をどんどん改善していけるのです。
オブジェクトにだって「振る舞い」はある
元エントリでは「振る舞い」という言葉を「ビジネスが要求するシナリオ(を満足しているかどうか)」という意味で使っているように見受けられます。そのため、「振る舞いテスト」はどうやらエンドツーエンドに近い上位テストのことを指しているようですし、「振る舞いテスト」と「ユニットテスト」を別のものとして説明されています。
しかし実際にはそうではないはずです。 「振る舞い」には必ず「誰にとって」という観点が必要です。
最終的にそのソフトウェアを利用するエンドユーザにとっての振る舞いは元エントリに書かれている通りかと思いますが、例えばあるライブラリの利用者にとってみればそのライブラリの入出力関係や状態遷移、例外の発生ケース等がそのライブラリの振る舞いになりますし、何らかのAPIだったり、ミドルウェアだったり、あらゆるものはそれを使う側から見れば「それがどう振る舞うか」が興味の対象になります。
当然誰かが実装したオブジェクトは他の誰かが利用するわけで、その利用する側の人にとって内部の実装は通常興味の対象ではなくどういう振る舞いをするかが興味の対象なわけです。
「メソッドに対するテスト」はこういった利用者(エンドユーザに限らない)目線を失いやすくさせ、カプセル化を破壊するからよくないと言われるのです。
メソッドのテストをしたいと思ったら責務過多を疑うべし
ある単一のメソッドに対してテストをしたいと思ったときには少し立ち止まってそのメソッドあるいはオブジェクトが責務を持ちすぎていることを疑ったほうがいいかもしれません。
非常に手続き的な長いメソッドであったり、多数の依存オブジェクトと何パターンもの内部状態を持つ複雑なオブジェクトのメソッドだと確かにメソッドのテストを書きたくなるかもしれません。ただ、その場合はそれに対するテストをがんばって書くよりも、設計を見直してより小さくSOLID原則を満たしたオブジェクトに振る舞いを委譲していくほうがトータルでは幸せになれることが多いかと思います。
そのテストは「誰のための」テスト?
ということですね。すごく簡単にまとめると。(まとめすぎ)
最後までお読みいただきありがとうございました。 簡単に書くつもりが長くなっちゃいました。その分ポカもありそうな気がするのでご意見ご感想誹謗中傷などありましたら、ブコメでもTwitterでもコメントでもいいのでどしどしお願いします。