【Ruby】オブジェクト指向設計実践ガイドを読み始めた #9章 費用対効果の高いテストを設計する
これまで、オブジェクト指向に関する設計周りを中心でしたが、
ついにテスト周りのお話です。
あるコードを追加(変更)したらテストが失敗したみたいなケースがあると思います。
テストが壊れないための設計というところを読んでみました。
費用対効果の高いテストを設計する
テストとは少しそれてしまいますが、変更可能なコードを書くには3つのスキルが必要
- オブジェクト指向設計の理解
- これまでに書いた継承、ダックタイプ、コンポジションを適切に使えているか
- リファクタリングのスキル
- 外部の振る舞いを保ったままで、内部の構造を改善してく作業
- 新たな振る舞いを追加することはない
効果的なテストを書く
スキル- リファクタによるコードの変更によってテストを書き直さないようにする
テストの意図
テストを書く目的は、コストの削減
です。
つまり、テストがなかった場合にかかるコスト(バグ修正やdoc作成)に比べて、
テストを書く場合にかかるコストの方が多いのであれば、テストを書く価値がなくなる。
テストの実施によって得られる価値は下のものが挙げられます。
バグ/設計の欠陥を見つける
- 初期段階でバグを見つけることでコストの削減に繋がる
仕様書になる
- テストが伝える説明は、時間がたっても正しいものとなる
設計の決定を遅らせる
- 意図的にインタフェースにテストを依存させる
設計の欠陥を見つける
テストは設計上の欠陥を見つけることにも有効です。
- テストのセットアップ(rspecでいうbeforeとか)に苦痛が伴う
- コードはコンテキストを要求しすぎている
- 1つのオブジェクトをテストするために、他のオブジェクトをいくつも引き込む必要がある
- 依存関係を持ちすぎている
テストコードを書くのが大変なのであれば、他のオブジェクトからみても再利用が難しいみたいな感じです。
テストの設計を考える
上で、アプリの設計に欠陥がある場合はテストが書きにくいみたいなことを書いていますが、
アプリの設計が良くても、テストを実装するコストが非常にかかる場合もあります。
低コストでテストからの恩恵を受けるためには
- より少ないテストを書く
- テストから重複を取り除くことでアプリケーションの変更に伴うテストの変更コストが下がる
- テストを適切な場所に書く
- Model Specなのに他のModelもテストしているみたいな状況は避ける
ことが必要です。
つまり、アプリ側だけではなく、テスト側の設計も考える必要があります。
テストもアプリの1つのオブジェクトと考えると、アプリ側のクラスに依存すればするほど、
変更時の影響を受けやすくなります。
不要なカバレッジを狙いにいってギチギチに結合したテストコードを書くよりも、
安定しているパブリックインタフェース
を対象にしてテストコードを書くことで、
変更による影響を受けににくします。
テスト対象のクラスが、別のクラスに依存している場合のテストコードはどう設計するか。
下のような感じです。
class Foo attr_accessor :foo def hogehoge # barの何かしらのパブリックメソッドを呼び出す foo.fugafuga # なんかしらの処理 end end class Bar def fugafuga # ログ記録だけの処理 end end
この場合は、FooがBarに対して送った送信メッセージには、
副作用がありません。(ただのログ記録なので)
副作用がない送信メッセージについては、テストは不要です。
逆に副作用がある送信メッセージについては、テストが必要です。
よく分かっていないのですが、例をあげるとRequest Specみたいな感じでしょうか。
Controllerに対するテストですが、データの登録・削除とかの回数は、
Controllerごとに変わってきます。
こういった場合は、コントローラ側でテストが担保されるべきみたいな感じになるんだと思います。
いつテストを書く(実行)するのか
テストを最初に書くと、オブジェクトに初めから多少の再利用性を持たせることになります。
テストコード自体が、テスト対象のオブジェクトを再利用しているので、これは当然のこと。
なので、僕のような初級の設計者はテストファーストでコードを書くことがいいようです。
そうすれば、少なくともテストが可能なコードが実装できます。
テストの方法
Rubyでメインのテストフレームワークはminitest
とRSpec
です。
(RailsだとRSpec使っている人の方が多いのではないでしょうか)
フレームワークの選定と同じように、テストの様式の選定も必要です。
- TDD(テスト駆動開発)
- 内から外へのアプローチ
- ドメインオブジェクトのテストから始めて、隣接するレイヤーのテストで再利用
- BDD(振る舞い駆動開発)
- 外から内へのアプローチ
- アプリの境界でオブジェクトを作る
- まだ書かれていないオブジェクトを用意するためMockが必要
経験や好みで変わってくるので、どちらかが効果を発揮してくれるようです。
個人的にはTDD方がなんとなくイメージがつきやすい気がします。
受信メッセージのテスト
受信メッセージは、パブリックインタフェースを構成します。
他のオブジェクトから依存されていなさそうな受信メッセージは削除の候補に入れます。
どこからも使われていない場合は、推測で作られた可能性が非常に高いコードです。
テストに影響する依存を取り除く
先ほどのコードを少し変えた例です。
Bar
は依存されているだけなので、コードの変更によるテストへの影響は少ないです。
Foo
の場合は、メソッド内で依存しているオブジェクトの生成をしています。
これだと、Foo
は特定のコンテキスト(Bar
)に依存し、他のクラスを利用したいという要件も実現できなくなります。
class Foo attr_accessor :var_1, :var_2 # コンストラクタがあるとする def hogehoge # barの何かしらのパブリックメソッドを呼び出す hoge * Bar.new(var_fuga).fugafuga end def hoge var_1 * var_2 end end class Bar attr_accessor :var_fuga def fugafuga var_fuga * 2 end def fuga_square var_fuga * var_fuga end end
ダックタイプで学んだように、同じ振る舞いをもつオブジェクトをパラメータとして持つようにします。
依存を取り除くことによって、コードが変更された時にテストが壊れる可能性を少なくしていきます。
class Foo attr_accessor :var_1, :var_2, :bar # コンストラクタがあるとする def hogehoge # barの何かしらのパブリックメソッドを呼び出す hoge * bar.fugafuga end def hoge var_1 * var_2 end end class Bar attr_accessor :var_fuga def fugafuga var_fuga * 2 end def fuga_square var_fuga * var_fuga end end # テストコードからは、Fooを生成時にBarも生成してあげる
プライベートメソッドはテストから無視する
下記、理由でプライベートメソッドをテストから無視していいとされています。
- パブリックメソッド経由でプライベートメソッドは実行される
- プライベートメソッドは他のオブジェクトから見えないものです
- パブリックメソッドがしっかりテスト時に、動作は確かめれているはず
- プライベートメソッドは不安定(変更されやすい)
- 最初の方に、安定している方に依存した方がいいことを学びました
- テストも同じで、不安定なところに依存しない方がいいという意味です
- プライベートメソッドをテストすることで、他の人がパブリックメソッドと勘違いする可能性がある
- テストは設計や実装の文書になる
- 一部のプライベートメソッドがテストされていると、パブリックなのか区別が付きにくなる
ダックタイプをテストする
共有の振る舞いを役割(ロール)として実装するために、
ダックタイプを選択することがあると思います。(先ほどのコードも少しありましたが)
オブジェクトがロールとして振舞うかをテストするために、下記のようなコードを書きます
# helperみたいな感じで実装 shared_examples_for "hogeable" it "object have to implement hoge" do it { expect(hogeable).to be_respond_to(:hoge) } end end # ModelSpec 設定ファイルでhelperをincludeするようにしておく RSpec.describe "fuge_hoge" do let!(:hogeable) { HogeFuga.new(hoge_args) } it_behaves_like "hoge_able" end
Helperで実装しているか、確かめるコードを書いておき、
Model側のスペックで実装しているかテストする。
継承されたコードをテストする
継承関係を使ったコードをテストする場合にはどうすれば良いでしょうか。
テストコードも上のダックタイプと同様に、共通するテスト部分をヘルパーとして実装します。
小クラスでヘルパーをインクルードして、特化した部分のテストを加えていきます。
まとめ
ようやく全ての章を読み終えました。
業務にすぐに適用できた部分や、まだ正直理解が追いついていない部分があります。
(特にダックタイプは業務でめちゃくちゃ効果を発揮した)
1度だけではおそらく身についていないので、Rubyに関わっている以上は、
半年に1回ペースで見直して行こうと思います。
最近は読書記録が多めになってしまいましたが、GW中にやってみた系を書こう!