この記事の内容は技術書を読んだ感想や個人的な考察です。
はじめに
どうもこんにちは、もきお(@mokio_50)です。
今までなんとなく書いてきたテスト。テストの質について考えたことって意外とないですよね?えっ、それは自分だけだって?笑
そんな自分にテストの考え方を改めて考えさせてくれた本がこちら。今回から3部に渡ってこの本の要約というか感想文をつらつらと書き連ねたいと思います。
書籍情報
質の高いテストを行い、ソフトウェアに価値をもたらそう!
著作者名:Vladimir Khorikov
編集者名:須田智之
Unit Testing Principles, Practices, and Patternsの翻訳書。
1章 なぜ単体(unit)テストを行うのか?
1章では主に以下について記載されていました
・なぜ単体テストを行うのか
・テスト網羅率(カバレッジ)について
・価値のあるテストとはなにか
個人的に2つ目が読んでて興味深かったです。それではさっそく見てみましょう。
なぜ単体テストを行うのか
なぜ単体テストを行うのか、単体テストで成し遂げたいものはなにか。その問いがきたとき、バグを未然に防ぐという安易な考えくらいしか思い浮かびませんでしたがこの本ではこう書かれてました。
単体テストで成し遂げたいものはなにか、それはソフトウェア開発プロジェクトの成長を持続可能なものにする、ということです。
「持続可能」って言葉めっちゃ良くないですか?「そうでもなくね?」って言葉が返ってきそうですが個人的には響きました。
目先のメリットではなく単体テストの本質的なゴールを教えていただいた気がしました。
単体テストと設計との関係
単体テストと設計の関係については以下のように記載されていました。
単体テストを作成しづらいと感じるのであればあそのテスト対象となるコードは何かしらの改善を必要としている可能性が高いです。
こちらは概ね思った通りの記載がされていました。上手くメソッドに分割されていなければテスト書きづらいしそりゃそうだよなって感じです。一方で以下の記載は少し意外なものでした。
しかしながら、その逆に、単体テストを作成しやすいからと言って、プロダクションコードの質が良いという判断を下すことはできません。(中略)。仮にプロダクションコードが疎結合(異なるコード同士の結合が緩い状態)になったとしても、良い設計でないことはあるのです。
なるほど、個人的には基本的にテストが書きやすいコードは良いコードだと考えていたので、具体的に疎結合で質が良くないコードとはどんなものかあんまり理解できていませんが、先入観を変えていきたいと思います。
テスト網羅率(カバレッジ)について
網羅率と言われるとピンとこないですがテストカバレッジだと聞き覚えがある人は多いのでわないでしょうか?
網羅率が何%以上だったらいいとか良く議論されたりすると思うのですが、そもそも「網羅率はどのように算出しているかを意識されたことがある人は意外と少ないのでは?」っと思いました。
自身も網羅率は高いほうがいいよねーくらいしか考えられていませんでした。
ここではいくつかの網羅率算出方法の紹介がされていました
コード網羅率について
コード網羅率の算出方法は極めて単純でプロダクション・コードの行数をプロダクションコードの総行数で割ったものです。式で表すと以下
\displaylines{
\ コード網羅率 =
\frac{実行されたコードの行数}{総行数}
}
分岐網羅率について
プロダクション・コードに含まれている分岐経路がいくつ経由されるかを計測することで算出される。式で表すと以下。
\displaylines{
\ 分岐網羅率 =
\frac{経由された経路の数}{分岐経路の総数}
}
網羅率に関する問題
網羅率も評価自体は一方向です。テストコードの質が悪いことを判断するには効果があるが、テストコードの質が良いことを判断するには向いていないので網羅率が高いからテストコードの質が良いと判断しないよう意識していきたいですね。
価値のあるテストとはなにか
価値のあるテストの定義として以下の3つが定義されていました。
・テストすることが開発サイクルの中に組み込まれている。
・コードベースの特に重要な部分のみがテスト対象となっている。
・最小限の保守コストで最大限の価値を生み出すようになっている。
テストすることが開発サイクルの中に組み込まれている
テスト自動化ツールとして代表的なものといえばGithub ActionsやCircleCIかと思います。
それらが当たり前に導入されていたので特に気にすることはなかったですが、確かに自動テストツールが存在しない状態において毎度のようにテストを実行しテストが通ることを確認してからGitHubにコードをpushすることはかなり大変だと思いました。
よって開発サイクルの中に組み込まれていることが普通でしょと思いましたが、よくよく考えてみるとテストを自動化される環境がなければ難しいことなんだなと気付かされました。
コードベースの特に重要な部分のみがテスト対象となっている
コードベースの特に重要な部分とはどこか。それはやはりビジネスロジックを含む部分、具体的にはドメインモデルになります。なので基本モデルに対するテストが価値のあるテストを書くことに繋がりそうですね。
最小限の保守コストで最大限の価値を生み出すようになっている
最小限の保守コストで最大限の価値を生み出すためにはどうすれば良いのでしょうか?
・価値のあるテスト・ケースを認識できること(逆に言えば、価値の低いテスト・ケースも認識できること)
・価値のあるテスト・ケースを作成できること
これらのことは同じことのように見えますが、本質的には違います。まず価値のあるテスト・ケースを認識するためにはテスト・ケースを評価するための枠組みを知っておかなければなりません。一方、価値のあるテスト・ケースを作成するためにはこれらのことに加えて、設計のテクニックについても理解していなければなりません。
これを分かりやすく理解するために以下で例えられていました。
良い曲を認識できるからと言って、良い曲が作成できるとは限らない。作曲に求められる労力は良い音楽と悪い音楽を見分ける労力よりもはるかに大きくなります。
確かにそうだ!と感じましたね。なのでここで言いたいのは価値のあるテストを認識することができるようになったからといって価値のあるテストが作成できるようになるとは限らない。価値のあるテストを認識することができるようになっただけで満足しないでね!こんな感じのことが言いたいのではないでしょうか?個人的解釈ですが笑
なのでこの本では価値のあるテスト・ケースを作成するための設計についても深く触れられていました。
2章 単体テストとは何か?
技術的に役に立つところはあまりなかったですが、単体テストの歴史みたいなものを感じることができ、個人的には面白い章でした。
単体テストの定義についての解釈が異なることによって、単体テストをどのように行うべきかということに関して異なる見解を持った二つの学派が生まれたみたいです。それが以下の二つ
- ロンドン学派
- 古典学派
筆者は古典学派を推奨していました。この二つの学派はあとで違いをそれぞれ並べるとして単体テストにおいて三つの重要な性質が記載されていました。
単体テストの定義
単体テストにおいて三つの重要な性質は以下の3つ
・単体(unit)と呼ばれる少量のコードを検証する
・実行時間が短い
・隔離された状態で実行される
特に三つ目の「隔離」が意味するものは何か?隔離の定義によって二つの学派の意見が分かれています。二つの学派の隔離に対しての考え方を見ていきましょう。
ロンドン学派が考える隔離
色々書いてありましたが個人的な解釈だとクラス単位で隔離したいよね。なので単体テストで他の関連するクラスも含まれる場合は全てテスト・ダブル(モック、スタブ等)を使って置き換えちゃおうね。こんな感じなのかと。
例えばUserモデルにおける単体テストを書くとして、Userモデルに関連付けされたBookモデルを使う機会がある場合は全てモックに置き換えるみたいな感じなんだと思います。
古典学派が考える隔離
古典学派における単体テストの隔離とはテスト対象となるクラスを共有依存から隔離することを意味する。そのためプライベート依存であれば、テスト・ダブルに置き換えずそのまま使っても問題はない
一つの問題が一つのテストケースだけではなく、多くのテストケースに影響を与えるのであれば、その問題のあったコードは多くのクラスに依存されるような重要な価値があることの証明となるのです。
「単体」の定義 | テスト対象が依存する概念の扱い | |
---|---|---|
ロンドン学派(モック主義者) | 1つのクラス | すべてテスト・ダブルに置き換えなくてはならない |
古典学派 | 1つのテスト・ケース | 他のテスト・ケースに影響を与える共有依存だけをテスト・ダブルに置き換える |
3章 単体テストの構造的解析
この章では基本的な単体テストの構造、書き方が解説されていました。
また、軽いアンチパターンに関しても触れられていました。
- 単体テストの構造
- 単体テストにおいて回避すべきこと
単体テストの構造
単体テストの構造としてAAAパターン(3Aパターンとも呼ばれる)なる物があるらしいです。初めて聞きましたねー。
このパターンは準備(Arange)、実行(Act)、確認(Assert)の3段階に分かれており、それぞれのフェーズで実装において意識することがあるみたいです。これは後ほど記載してあります。
実装コードだと以下。本書だとC#で書かれているためrspecで書き起こしてみました(以降のコードもrspecで記載)。
# calculator.rbでメソッド定義
class Calculator
def sum(first, second)
first + second
end
end
# calculator_spec.rb
RSpec.describe Calculator do
describe '#sum' do
it 'returns the sum of two numbers' do
# 準備(Arrange)
first = 10
second = 20
calculator = Calculator.new
# 実行(Act)
result = calculator.sum(first, second)
# 確認(Assert)
expect(result).to eq(30)
end
end
end
書き起こしてみて思ったんですがrspecって実行と確認一緒にやってないか?と思いました。なので以下で修正。
RSpec.describe Calculator do
describe '#sum' do
it 'returns the sum of two numbers' do
# 準備(Arrange)
first = 10
second = 20
calculator = Calculator.new
# 実行と確認(Act and Assert)
expect(calculator.sum(first, second)).to eq(30)
end
end
end
準備(Arange)、実行(Act)、確認(Assert)それぞれの役割は以下
・準備(Arange)フェーズ
テスト実行に必要なオブジェクトを事前に生成したり、関連付けたりするフェーズ。事前準備。
・実行(Act)フェーズ
テスト対象システムのメソッドを実際に呼び出すことによってテスト対象の振る舞いを実行させるフェーズ。
・確認(Assert)フェーズ
実行結果が想定した結果であることを確認するフェーズ。実行結果は戻り値であることもあれば、テスト対象システムやその協力者オブジェクトの実行後の状態のこともある。
単体テストにおいて回避すべきこと
単体テストの基本構造を学んだところで実装するにあたって注意する点が色々と記載されていました。以下にまとめます。
一つのテストケースで同じフェーズが複数回出てくること
準備(Arange)、実行(Act)、確認(Assert)フェーズと記載する中で、同じフェーズが複数回出現した場合は単体テスト構造を逸脱してしまいます。
例
準備フェーズ→実行フェーズ→確認フェーズ→実行フェーズ→確認フェーズ
このようなテストだと単体テストではなく統合テストに属する。同じフェーズが複数回でてきた場合はメソッドをまず見直して違うメソッドに切り出せないか考える必要がありそうです。
if文の使用
テストの中で条件分岐をしてしまうのはアンチパターンみたいです。条件分岐がある場合はそれぞれテストケースを分けてテストを作成する必要がありますね。特に考えないでやってたけど改めて気付かされました。
あとこれは業務の中のレビューで指摘されたことなんですが、そもそもの実装において、条件分岐をなるべく減らす意識ができているか?と指摘されたことがありました。具体例で言うとボッチ(&.)使ったりした時ですね。ボッチを使うことで2通りの条件分岐だけでいいのに、4通りの条件分岐になってしまったことがありました。
無駄なテストケースを増やさないという意識で実装側も日々行っていきたいと思いました。
各フェーズのサイズはどのくらいが適切なのか
AAAパターンに即してテストを記載するとして、各フェーズのサイズがどのくらいが適切なのかが記載されており参考になりました。
まず第一に、準備フェーズが一番大きくなります。そして実行フェーズは基本1行に収まるとのこと。よって実行フェーズが1行を超えた場合はそのテストケースが正しいものかを改めて考える必要があるみたいです。
最後に確認フェーズですが、1つのテストケースで1つの確認しかすべきでないという意見が巷ではあるみたいなんですが、本書では1つのテストケースは1単位のコードではなく1単位の振る舞いで区切っているため、確認する項目は複数ある可能性があります。
よって確認フェーズは実行フェーズのように1行で収めないといけない!と言うわけではないみたいです。しかしながら、確認フェーズが大きくなりすぎるのであれば、危機感を持たなければいけないとのことでした。そのようなことが起きるようであればコードの抽象化がうまくいっていないサインです。
これらの視点を持ってより価値のあるコード、テストを書いていきたいですね!
最後に
いかがだったでしょうか?今回は「単体テストの考え方/使い方」の第一部の要約というか感想文に近いものを書き連ねてみました。
この本は新しい知識をバンバン与えてくれると言うよりは、今までなんとなくやっていたこと事を再認識させてくれたり、テストの良し悪しに対する違和感を覚えるような気付きを与えてくれる本のような気がしています。
この本良い本なのは間違い無いんですが分厚くてなかなか読むきになりませんよね笑
自分も記事を書いて理解が深まりましたし、この記事を読んでこの本の理解が少しでも深まれば幸いです。少しでも良い記事だと思っていただけましたら下のいいねポチッとしてもらえると今後も記事を書く励みになります。次回は1ヶ月後くらいになるかもですがまたダラダラと記事書いていきたいと思います笑