ユニットテストを書く習慣
特に開発しているアプリをビルドしてデプロイして画面で確認できるようになるまで、
余裕で5分10分かかるんですけど、という場合、
業務ロジックを書いたけどマニュアルでしかテストしません、という運用はつらみしか生みません。
本稿では、
なんとなく書いたほうがいいのはわかってるんだけど、正直今の構造だとどう書いたらいいのかわからない、
何がうれしいのかよくわからないのでそんなコストをかける気になれない、と思っているであろう開発者の皆さんに宛てて、
効果的にxUnitによるユニットテストを書くための設計方法を提供します。
基本的にはjava8とJUnit4ですが、あまり言語を限定しない内容です。
大前提
ユニットテストは「testing」ではない
ユニットテストはアサーションにより、関数の動作が仕様通りであることを確かめる(checking)ことしかできません。
未知の不具合を発見するためには、マニュアルテストが最も効率的です。
ユニットテストは開発を助けるツールである
品質を上げるというよりは、つまらないことで開発スピードを落としたり、思わぬ手戻りを後で起こさないために最低限のチェックを自動で行えるというものです。
当たり前ですが、本稿は普段ユニットテストを書かない人を対象にしているので念のため。
ユニットテストを書くメリット
手戻りが手前になる
上記の例のような、画面から確認できるようになるまで余裕で5分10分かかるんですけど、という場合、
複雑な業務ロジックについては、画面からのテストよりも前に手戻りを発生させた方が効率が良いですよね。
ユニットテストは、一般的にサーバを起動して画面で確認するよりも早く結果を確認することができます。
また、ユニット(単体)テストの名の通り、全ての機能をつなげる前から各関数のレベルでの検証が行えるため、
実装 => 検証のタイムラグがより小さいということも挙げられます。
つまり、
- 画面と繋げられる状態にするより手前の段階で検証ができるため、実装手順として手前で手戻りが発見できる。
- サーバー起動などを全て行ってから画面で叩くよりも、単純に検証自体の時間が短い。
という二点において、手戻りが早く検知できるというわけです。
仕様の記述が可能
TDDのメリットとしてよくあげられますが、
ユニットテストはアサーション(関数XXの返り値は引数YYのときZZであるはずだ、というような)を行う形で通常記述されます。
それにより、後続の開発者はうまく書けたユニットテストのテストコードを読むことにより、
「この関数はこういった仕様になっているんだな」ということを、最もクリアに知ることができるようになるわけです。
(うまく書けていることは非常に重要で、
テストケースとしての網羅性と、コードとしての可読性が兼ね備えられていないとかえって混乱します)
当然ですが、開発者が単に理解できるだけではなく、
その関数をよく理解していない開発者が下手に修正を加えてしまった場合、
CIがきちんと回ってさえいれば、
ユニットテストで自動的に「あるべき仕様から動きが変わってしまっていること」を検出することが可能です。
ユニットテストをきちんとメンテナンスすることは、
本来なら結合テスト段階でしか気付けないような不具合を手前で、しかも自動で検知できるということにつながります。
公開の関数の使い勝手が感覚としてわかりやすい
これもよくTDDのメリットとしてあげられますが、
テストコードは自分の実装する(予定の)公開の関数の最初の利用者になるわけです。
特にJavaにおいては、シグネチャの設計が拡張性や修正のリスクなどもろもろの要素を決めると個人的には考えていますが、
テストコードから実際にその関数を呼び出してみることで、関数の使い勝手が改めて掴めるということはよくあることです。
これが様々なクラスから呼び出されるようになってから気付くようでは、修正のリスクや影響範囲は甚大になってしまいます。
禁則をテストできる
組合せテストを行う場合、禁則というものがついてまわります。
要は、実際には画面から入力できない値というものを禁則としてテストケースから除外するわけですが、
この場合の「画面から入力できない」という判断は、えてして信用のおけないものです。
例えばフロントエンドでバリデーションをかけているからといっても、
送信する瞬間に悪意のあるユーザーに値を書き換えられてしまえば意味がありません。
ユニットテストは画面から入力できる・できないという制約を持たないため、
禁則ケースとされる入力パターンに対しても、
テストを行うことが可能です。
例外がスローされることを期待値とするテストが書けたりしているといいですね。
ユニットテストが書けない理由
さて、メリットとしてはいろいろあげましたが、
この記事を読んでいるあなたは先述の通りユニットテストを書いたことがないまま、
大規模なアプリの開発業務と日々戦っています。
なぜ書いたことがないのか、なぜ周りの先輩たちも、
見渡す限り誰も書いていないのか、ということを考えると、
ひょっとしたら実は、
そもそもプロダクトコードの側で、ユニットテストを書きづらい構造になっているせいなのかもしれません。
この項では、「ユニットテストを書けない理由」として、
ユニットテストを書きづらい構造になっている場合の特徴を説明します。
ここでは、あなたの職場によくある関数として、
何かの完了処理をつかさどる「complete」という名前のものを想定します。
public Map<String,Object> complete(BigBean bigBean) {
// ...
SomeKindOfConfig config = dao.loadConfig(someId);
this.computeSomeValue(bigBean);
if(config.getSomeAttribute() == 1) {
// ...
} else {
// ...
}
// ...
for(DtlBean dtl : bigBean.getManyDtls()) {
// ...
}
if(bigBean.getOtherAttribute() == 0) {
// ...
}
dao.updateSomething(bigBean);
return res;
}
関数が長すぎて、返り値の期待値が不明瞭
何かの完了処理を行うとき、
ボタン一つでポンといろいろな処理が走ってくれて便利という風の機能設計にすることはよくあることかと思いますが、
それらを全て一つの関数に書いてしまっているパターンです。
そういった処理は往々にして、
後でこの設定みて処理分岐しないと!みたいなつらい状況になったりするので、
DBアクセスも途中でアドホックに入ったりしてカオスです。
こういうレガシーな感じのする業務ロジックについては、
ユニットテストは非常に書きにくいです。
長すぎる以外にもいろいろと問題がありますが、
端的に一つの関数でやっていることが多い場合、
「インプットに対してアウトプットがこうあるべき」というアサーションの形式で書きにくいというのが
大きな特徴です。
分岐も多く、組み合わせとしても爆発してしまうため、テストクラスの記述量も膨大になってしまい非効率です。
引数を変えてしまっている
private void computeSomeValue(BigBean bigBean) {
//...
bigBean.setSomeValue(someValue);
//...
}
引数の内容が変わりうるvoidのメソッドがある場合、
それに対しての、
「インプットがこうあるとき、アウトプットとしてはこうあるべき」というアサーションがクリアに書けなくなるので、
できるだけ避けたほうがよいと考えます。
どうしても上記の要件を満たしたい場合は、
シグネチャ自体を、
private SomeValueClass computeSomeValue(BigBean bigBean) {
//...
return someValue;
}
というように書いておいたほうがベターです。
引数以外のものに依存している
単体テストの運用によりますが、
テスト環境みたいなものがサラッとリフレッシュできるようになっていない場合、
例えばDBの値依存のものをユニットテストでテストするのは簡単ではありません。
上記の処理の場合、
設定っぽい値をDBから途中で取りに行っていますが、
それにより同じ引数に対するcompleteメソッドのアサーションが
DBの状態によって成功したり失敗したりしてしまうことになります。
というかそもそも、後続の開発者のためにも、
しれっと途中でいきなり設定を見に行くのはあまりいけてない実装だなと思います。
この場合、ユニットテスト側でDaoをMockitoでmockするなど、
対応のとりようはあります。
ユニットテストが書きやすい設計
適切な分割
分岐の組合せが爆発しない程度の複雑さの関数が、
疎結合な状態で分割して実装されていると、
非常に効果的に単体テストを実装・実行できます。
例えば、ある日に複数月分の家賃を払う機能があったとしましょう。
その月にいつの分の家賃を支払うかというのを見れるプレビュー機能を実装します。
引数としては、
- いつ支払うか、という日付。
- 対象となる月が、YYYYMMの状態で入っている配列。
があるでしょう。
アウトプットとしては、
- 日付がYYYY/MM/DDで表示される。
- 対象月がMM月、という形式で表示される。
- 対象月が複数あるとき、その中で最初の月から最後の月にかけて、MM月 - MM月というように表示される。
- それぞれの対象月が、支払の日付より前の年であれば「前」、次の年であれば「翌」というように表示される。
という仕様だったとします。
普通に画面からすべて繋げて叩こうとすると、
- 複数のとき?単数のとき?
- 二桁月のとき?一桁月のとき?
- 前のとき?翌のとき?
と、いろいろあって組合せとしても多くなってしまいがちです。
ただし、これらは分割すれば単純な仕様の集合なので、
「単数の月をフォーマットするだけの関数」のユニットテストが書ければ、
月のフォーマットについてはある程度信頼してよいということがわかりますし、
「複数月を並べ替える関数」のユニットが書ければ、
「複数月の最初から最後までをハイフンでつなげて出力する関数」について順番を執拗にテストする必要はありません。
そして「日付と年月を比較して、年月の方が前なら「前」を、
年月の方が後なら「翌」を返す関数」のユニットテストがあれば、
最終的なプレビュー表示関数のユニットテストは、
それらが適切に組み合わせられていることの代表値のテストで済むわけです。
それぞれを小さく、引数に対して返り値を保証するユニットテストとセットで実装すると、
「ここまでは少なくともきちんと動作しているよね」ということを
確認しながら進められるので精神的に安心して開発できるというメリットもあります。
参照透過性
An expression is said to be referentially transparent if it can be replaced with its corresponding value without changing the program's behavior.
As a result, evaluating a referentially transparent function gives the same value for same arguments.
「ある関数が参照透過であるとは、その関数をその返り値と置き換えてもプログラムの挙動が変わらないことを指す。
結果として、参照透過である関数を実行すると、必ず同じ引数に対して同じ返り値を返す。」
気になる業務ロジックは参照透過に書きましょう。
個人的には、状態を持つクラスとインプットに対してアウトプットを返す関数とは明確に分けておくともろもろハッピーだと思います。
同じ引数に対して同じ返り値を常に返す状態なのであれば、ユニットテストが書きやすいだけではなく、
「ユニットテストで保証済の動きを、どの状態でも再現する」という点で、ユニットテストを効果的にしてくれます。
で、どれぐらい書けばいいの?
基本、publicのメソッド一つに対して最低一つは用意するといいようです
ここらへんは自分ルールみたいなのもあるとは思いますが。
効果的な書き方
テスト設計を素早く、きちんとやる
仕様の記述としてユニットテストを書く場合、
ただ闇雲に思いついた値を渡してアサーションするのは却って逆効果かもしれません。
まず最低限の、
- 境界値
- 同値分割
など、普通にテスト手法として名前の知られている手法はある程度かじっておいたほうが効果的です。
というかむしろこの二つでケースに落とし込めないような機能の場合、
ユニットテストでのテスト自体が不向きである可能性があるので、
他の手法による検証を検討してみてもいいかもしれません。
設計したケースを組合せて実行できるようにする
@Runwith(Theories.class)
や
@Runwith(Parameterized.class)
を利用すると、「引数と期待値のセット」を一つのテストコードに対していくつも渡しながらテストすることができます。
要は、インプットとアウトプットを網羅的に書きやすいつくりにできます。
テストスイートを用意して何度も回す
テストクラスが増えていった場合、
それを束ねるSuiteクラスをパッケージ単位で作り、
その配下のテストクラスをいつでもすべて一気に回しなおせるようにしましょう。
何か変更があったら、
すぐにそれを全て回しなおせば、
いったんユニットテストでアサーションされている範囲では今までの仕様のまま動作する、
というのがすぐにわかります。
Jenkinsやtravisなど、CIツールを運用しているプロジェクトであれば、
コードベースに変更がプッシュされた時点で回しなおすというのもいいかと思います。
まとめ
なぜユニットテストを書くのか?
- 手戻りが手前になる
- 仕様の記述が可能
- 公開の関数の使い勝手が感覚としてわかりやすい
- 禁則をテストできる
なぜ、にもかかわらずユニットテストが書けないのか?
- 関数が長すぎて、返り値の期待値が不明瞭
- 引数を変えてしまっている
- 引数以外のものに依存している
どうすればユニットテストが書きやすい構造になるのか?
- 適切な分割
- 参照透過性
以上でした。