テストを書くタイミングは、かなり大きく分けて、開発と同時に書くテスト(今回開発時テストと呼ぶ)と、開発がある程度進んでから書くテスト(今回品質保証時テストと呼ぶ)があると思いますが、7:3 ぐらいの割合で開発時テストが重要だと言う事を、最近知ったので記事にします。
開発時テストを書かないとどうなるか
だいたい機能実装時にテストを書かない時の理由は以下だと思います。
- 時間が無いから
- コンソール・ブラウザ画面で動作確認できたから
実体験なのですが、こういった事をすると、社内確認・クライアント確認・リリース前後の終盤のタイミングで苦しむことになります。
(苦しむどころかリリースできるくらいの品質担保ができない)
すごくコードが読みづらく、デバックがしづらい事に気づき、
既にその時には、改修する時間など無く手遅れの状態になります。
先輩にも教わった事ですが、
開発時テストを書くことの重要な意味は、
必然とテストが書けるようなコードを書くしかなくなるため、品質が良くなる
という事だと知りました。
開発時テストを書くようにした結果として、品質が保証できると知りました。
以下がメリットと感じています。
- コードの品質がよくなる
- コードの品質が良くなるとサービスの品質がよくなる
- 開発終盤になるにつれ、テストを書かない人に比べて開発スピードが上がる
重視すべきテスト
次は、単体テスト・結合テストを考えた時に、何を重視すべきか考えてみます。
- 単体テスト
重要。これさえしっかりやっておけば順分に品質を担保できると言っても過言ではないくらい
=> 最小単位のメソッドに関してはパターン網羅
=> 最小単位がいくつか合わさったメソッドに関しては必要数パターンの網羅
- 結合 テスト/API テスト
=> ざっくり言うと必要最低限で良い。重要なのは単体テスト
API は部品の寄せ集めであり、個々の部品の機能の品質が保証されていれば、完全ではないにしても API 単位での品質はかなり保証されるため、優先すべきは個々の部品の品質です。
案件の限られた時間の中では、単体テストで品質を担保し、API テストは必要最低限でやるのが良いと考えています。
テストが書きやすい・書きにくいとは
では、テストが書きやすいコード、書きにくいコードとは何かを考えてみます。
テストが書きづらいコードとは何か
- 一つのメソッドで複数のことをしている(最小単位のメソッドなのに)
- メソッドの名前がつけづらい
- 引数が多い(前提となる条件が多いため、context が多くなる)
テストが書きやすいコードとは何か
- 最小単位のメソッドでは、一つのことのみ行なっている
- メソッドの名前がつけやすい
- 引数が少ない(前提となる条件が少ないため、context が少なく、テストが書きやすい)
書くとあたり前の事のように思えるのですが、意外とできていないんですよね。。
具体例で見てみる
簡易的な具体例で考えてみます。
以下2つの事を行いたいとします。
- ある名前を持つエントリー全てを取得する
- それらのエントリーの全てのステータスを更新する
一番小さい単位(最小単位)のメソッドを実装する例をRailsのコードで記載します。
テストが書きづらいコード
1つのメソッドで2つの事を行います。
def self.get_same_names_and_update_status(name, status)
entries = where(name: name)
entries.update_all(status: status)
end
パターンを洗い出します。
-
name が存在する時
- status が status1 の時
- status が status2 の時
- status が status3 の時
- status が status4 の時
-
name が nil の時
- status が status1 の時
- status が status2 の時
- status が status3 の時
- status が status4 の時
-
name が空白の時
- status が status1 の時
- status が status2 の時
- status が status3 の時
- status が status4 の時
-
name が空文字の時
- status が status1 の時
- status が status2 の時
- status が status3 の時
- status が status4 の時
パターン数が多く(name ✖️status の数のパターン必要)、メソッド名が長く、わかりづらいコードです。
簡易的な例なので、あまりそう感じないかもしれませんが、次を見ると、少し意味を感じ取れると思います。
テストが書きやすいコード
def self.get_same_names(name)
where(name: name)
end
def self.update_status(status)
update_all(status: status)
end
パターンを洗い出します。
#get_same_names
- name が存在する時
- name が nil の時
- name が空白の時
- name が空文字の時
#update_all_status
- status が status1 の時
- status が status2 の時
- status が status3 の時
- status が status4 の時
個々のロジックのパターン数が少なく、メソッド名が短く、わかりやすいコードです。
ロジックのミスなどがあれば、すぐに気づけるでしょう。
呼び出し側を見てみます。
呼び出し側
entries = Entry.get_same_names('name1')
entries.update_status('status2')
部品を組み合わせたメソッドを作成する場合、
必要パターン数の網羅をすれば良いこととします。
-
name が存在する時
- status が status1 の時
-
name が nil の時
- status が status1 の時
-
name が空白の時
- status が status1 の時
-
name が空文字の時
- status が status1 の時
実際は、この例の 2 種類の項目の組み合わせくらいならば全パターンについてテスト書きますが、3 種類・4 種類の項目の組み合わせならば、必要パターン数の網羅で担保するケースも出てくると思います。
最小単位のメソッドならば、メソッド名は具体的な名前をつけると思いますが上記テストが書きづらいコードの例のように、get_same_names_and_update_status
という長くて一目見てわかりづらい名前となってしまってい ます。さらにひどくなると、メソッド名をつける事すらできないという状況に陥ります。
また、引数が 2 つ存在する事で context が増え、最小単位のメソッドのはずなのにシンプルで無い形となってしまいます。
テストが書きやすいコードの例のように、しっかり一つの役割ごとにメソッド分割し、
それを組み合わせる形で実装する方針でいくべきだと考えています。
まとめ
小さな事かもしれませんが、こういったテストが書きづらいコードの積み重ねが、最終的に品質の悪いコードを生み出し、プロジェクト終盤にバグ探しが辛くなる状況を生み出すと感じています。
なので、しっかり開発時点から意識して改善していこうと思う所存です。
今回述べた内容を踏まえての RSpec の具体的な書き方をいくつか記事にしようと考えていますが、それについては次回にしたいと思います。