と同僚にオラついてしまい勉強会を開ことになったので作った資料。
本当は「ユニットテストを作る前に考えてほしいこと」が正しい題(JARO案件)
この記事の概要
この記事を読むとユニットテストとは何か、ユニットテストを作るときにどんなことを考えればよいか、が分かるようになる。具体的には下記のことを説明できるようになる。はず。
- なぜユニットテストをするのか
- ユニットテストでできること・できないこと
- ユニットテストを補完する別のテスト
前半はなぜなにユニットテスト、後半はハウツーユニットテスト。
※この記事にかいてあるすべてが正解とは限らないけど、考え方の一つとして参考になれば良いと思います。
対象読者
ある程度の開発・テストの経験があって
- これからユニットテストを始めようとしている若人
- ユニットテストをやりすぎて疲れちゃったおじさん
事前課題
- ユニットテストとはなにか140字以内で説明しろ
- ユニットテスト以外のテスト手法(名称)を最低5つ挙げろ(テストの規模やカテゴリ等は問わない)
- どの言語でもよいのでxUnitでユニットテストを作成・実行して、ふんいきをつかんでおいてね。参考:xUnit.net でユニットテストを始める
ユニットテストってなに
この記事では xUnit のようなテスティングフレームワークを使用した開発を想定し、下記の特徴を持ったテストをユニットテストとよぶ。
- 開発者自身が作成する
- コード(通常はクラスやメソッドの単位)を対象としている
- 自動化されている
※実際は文脈や何を「ユニット」とするかによって意味は変わるし、ユニットテストフレームワークを使って複数のクラスを結合したテストや半自動的なテストを作成することもあるけど許して。
ユニットテストは主に次の目的で実施される。
- コードに欠陥を作り込むのを予防する
- コードの欠陥を発見する
- コードが正しく変更されたことを確認する
ユニットテストの特徴
まずはユニットテストを他のテスト手法と比較して特徴を把握しよう。特徴を捉えておくと、活用できような場面、逆に役に立たなそうな場面を考えられるようになる。
今回は下記の3つの観点で比較する。
- ユニットテスト vs インテグレーションテスト
- 自動テスト vs 手動テスト
- 動的テスト vs 静的テスト
ユニットテスト vs インテグレーションテスト
クラスやメソッドを単体でテストすることをユニットテスト、他のクラスや外部モジュールと結合してテストすることをインテグレーションテストという。
ユニットテストはインテグレーションテストと比較して、下記のような特徴がある。
比較ポイント | ユニットテスト | インテグレーションテスト |
---|---|---|
テスト規模 | 小 | 大 |
テスト開始可能時期 | 早い | 遅い |
テスト実行時間 | 短い | 長い |
原因調査の難易度 | 容易 | 困難 |
インタフェースのテスト | 不可 | 可 |
ユニットテストは「小さい」。小さいので、早く・速く実行でき、テストに失敗した場合の原因調査も比較的容易。
ただし、ユニットテストはあくまで「単体」でしかないので、様々な部品が組み合わさって動くソフトウェアのテストとしては不十分である。
自動テスト vs 手動テスト
ユニットテストのようにあらかじめテストコードを書いて機械に実行させる自動テストと、人間がプログラムを実行する手動テストがある。
自動テストは手動テストと比較して、下記のような特徴がある。
比較ポイント | 自動テスト | 手動テスト |
---|---|---|
作成コスト | 大 | 小 |
実行コスト | 小 | 大 |
テスト実行の品質 | 一定 | 不定 |
検出可能な欠陥 | 既知 | 既知・未知 |
ユニットテストは「自動」である。自動なので手間なく何度も実行できる。大量のテストケースが必要な場合、ユニットテストの恩恵は大きい。特に継続的にソフトウェアをバージョンアップし、リリースするタイプの開発には必須の手法だろう。
テストが常に決まった手順で実行されることも自動テストの特徴。これは人間がテストするときの実行ミスがない反面、未知の欠陥は発見できないということでもある。新しい欠陥を発見するには、実施手順をアレンジしてみたり、勘で突いてみたり、といった人間の手を使うことも必要になる。(未知の欠陥、特に脆弱性を発見するファジングという自動化が進んでる手法もあるが、使用できる分野が限られる)
また、ユニットテストにはテストコードの作成という初期投資が必要な点にも注意。コストを比較したとき、テスト作成コスト>テスト実行コスト
になりそうな場合、無理にユニットテストを作成せず、手動テストで済ませるのも手。
動的テスト vs 静的テスト
ユニットテストのようにコードを実際に動作させてテストすることを動的テストと言い、対してコードを動作させずにテストすることを静的テストと言う。例えばコードレビューはコードに対して実行される静的テストの1つである。
ユニットテストは静的テストと比較し、下記のような長所短所がある。
比較ポイント | 動的テスト | 静的テスト |
---|---|---|
本当に動くか確認できるか | 可 | 不可 |
パフォーマンス測定 | 可 | 不可 |
実行環境 | 必須 | 不要 |
テスト開始可能時期 | 遅い | 早い |
検出可能な欠陥の傾向 | 動作させてみないとわからないこと | 注意深い観察が必要なこと |
ユニットテストは「動的」である。実際に動く(または動かない)ことが分かるのは開発者にとって非常に重要なフィードバックとなる。パフォーマンス(CPU, Memory性能など)の測定も動的なテストでなければできない。
ただしユニットテストを実行するには、コードが完成しており、なおかつ実行環境が整っている必要がある。反面、コードレビューは未完成のものでもコードさえ見れればいつでもどこでも可能である。(iPhoneでもできるし、コードを印刷した紙を眺めてでもできる)
また、動的テストでは発見しづらい欠陥もある。マルチスレッドプログラムを例に挙げると、競合(レース)という複数のスレッドが同時に同じ処理を実行してしまうときにのみ障害となる欠陥がある。この種の障害はコードを動作させてもなかなか再現しないことが多い。そこで動的テストの代わりに、コードを「スレッドセーフでないオブジェクトへの同時書き込み操作」という観点をもってレビューすると欠陥を発見しやすくなる。
プログラミング熟練者の場合、プログラムを動かすよりもソースコードを眺めるほうが早く多くの欠陥を発見できることもある。
ユニットテストまとめ
ユニットテストの特徴として「小さい」「自動」「動的」を挙げた。これらの特徴を把握して、場面に適したテスト手法を選択する必要がある。
ユニットテストってどうやるの?
ユニットテストはだれが書くの?
原則コードを変更する人自身が書くのが効率が良いと思う。ただしブラックボックス的なユニットテスト(後述)を作成する際は他の開発者にも協力してもらうと、勘違いや思い込みに早く気づける可能性がある。
ユニットテストはいつ書くの?
いつ?
- コードを変更する前
- コードを変更しながら
- コードを変更した後
いつでもよい。1や2のことをテストファーストやTest Driven Development(TDD)と言ったりする。テストファーストは仕様を先に明確にしておくことで、実装が迷走しにくくなるという点ではよい方法だと思う。
ただ、これは個人的な経験ではあるが、新しい機能のコードを書く場合、テストファーストはうまく機能しないことが多かった。新しい機能場合、一度仕様を明確にしたつもりでも、あとで矛盾に気づいて変更を余儀なくされることが少なくない。仕様変更によってそれまでに書いたユニットテストは使えなくなり、無駄となってしまった。
これは、仮説を立てる・検証する・分析する・改良する(PDCA)をどう回すのがやりやすいかという話なので、各々合う方法でやればよいと思う。
テストケースはどうやって作るの?
次のメソッドに対するユニットテストを考える。
/// <summary>
/// numが素数ならtrue, そうでないならfalseを返す
/// </summary>
public bool IsPrime(int num)
{
if (num == 2) return true;
if (num < 2) return false;
if (num % 2 == 0) return false;
for (int i = 2; i < num; i++)
{
if (num % i == 0) return false;
}
return true;
}
ユニットテストのテストケースを作るアプローチは大きく分けて2つある。
ホワイトボックステスト
1つ目のアプローチは、書いたコードのすべてが実行されるようなユニットテストを作成することである。つまりIsPrimeメソッドの「実装方法」に注目する。
public bool IsPrime(int num)
{
if (num == 2) return true;
if (num < 2) return false;
if (num % 2 == 0) return false;
for (int i = 2; i < num; i++)
{
if (num % i == 0) return false;
}
return true;
}
IsPrimeメソッドのすべての命令と分岐が実行されるようにユニットテストを作成すると、下記のようになる。
[Theory]
[InlineData(2, true)]
[InlineData(1, false)]
[InlineData(4, false)]
[InlineData(6, false)]
[InlineData(3, true)]
public void IsPrimeWhiteBox(int num, bool isPrime)
{
Assert.Equal(isPrime, IsPrime(num));
}
このように「実装方法」に基づいてテストをすることをホワイトボックステストという。ホワイトボックステストでは、プロダクトコードの変更に伴ってユニットテストコードの変更も必要になる。
ブラックボックステスト
もう1つのアプローチは、メソッドの入出力に基づいてユニットテストを作成することである。つまりIsPrimeメソッドの「仕様」に注目する。
/// <summary>
/// numが素数ならtrue, そうでないならfalseを返す
/// </summary>
public bool IsPrime(int num)...
IsPrimeメソッドが仕様通り正しく実装されていることを確かめるユニットテストを作成すると、下記のようになる。
[Theory]
[InlineData(0, false)]
[InlineData(1, false)]
[InlineData(2, true)]
[InlineData(3, true)]
// ...ほか入力値 num がとりうるパターン
public void IsPrimeWhiteBox(int num, bool isPrime)
{
Assert.Equal(isPrime, IsPrime(num));
}
このように「仕様」に基づいてテストすることをブラックボックステストという。
ブラックボックステストでは、メソッドの仕様が変わらない限りプロダクトコードを変更してもユニットテストコードの修正は必要ない。
ところでIsPrimeが「仕様通り」であることを確かめるには、intがとりうるパターンすべてを試験する必要がある。しかしintには取りうる値が約40億通りもあるので、そんなにテストデータを作ることはできないし、実行する時間もない。
そこでブラックボックステストでは、境界値分析や同値分割といった手法を使って必要なテストのパターンを減らすことになる。が、ここでは扱わない。ブラックボックステストで検索すれば良い記事はいっぱい見つかるだろう。
改めてホワイトボックステスト vs ブラックボックステスト
比較ポイント | ホワイトボックス | ブラックボックス |
---|---|---|
テストの寿命 | 短い | 長い |
テストの作りやすさ | 容易 | 困難(分析が必要) |
基本的にはブラックボックスで作成し、網羅されなかったコードをホワイトボックスで補うなど、ホワイト・ブラック両方の視点でテストするのが良い。
コストパフォーマンスを意識したテスト
ユニットテストにこだわりすぎるとコストパフォーマンス悪くなるよという話。
ユニットテストのコードカバレッジ
テスト対象コードのうち、どれくらいの命令や条件分岐が実行されたかを計測した値(%)をコードカバレッジという。コードカバレッジはテストで実行されなかった個所を明らかにし、テストを追加するための情報を提供してくれる。
通常、コードカバレッジを上げようとすればするほどユニットテストのコストが跳ね上がる。つまりカバレッジ100%を目指すのは非常にコストパフォーマンスが悪い。
※図はイメージです
というよりカバレッジ100%にあまり意味はない(と思う)。カバレッジ100%=バグ0というわけでもないし、そもそもテストコードにだってバグは入りうるし。
複数のテストを組み合わせる
じゃあ書いたコードをテストしなくていいんかい、というとそういうわけでもない。ユニットテストにこだわらずいろいろなテスト手法を検討すればよいというだけ。
簡単に思いつく方法を挙げると、
- 手動テストで通す
- コードレビューで担保する
- インテグレーションテストで通る(はずだ)からこのままでよい
- 残業してユニットテストを追加する
- 別にバグってもいいのでテストしない...など
こんな感じでカバレッジレポートをもとに手動テストやコードレビューで重点的にテストする判断をしたり、もちろんユニットテストを追加してもよい。
どの方法が良いかは当然プロジェクトによって変わるが、いろいろなテスト手法を良いとこどりをすればコスパよく開発できる可能性が高い。
※テストがガバガバすぎる
まとめ
この記事にはユニットテストはなにか、ユニットテストを作るときにどんなことを考えればよいかを書いた。
ユニットテストは比較的低コストで、うまく実施すれば早期に欠陥を防止・検出・排除できるテスト手法である。
ただしユニットテストはあくまでソフトウェアテスティングの1手法であり、ユニットテストだけでソフトウェアの品質を担保できるわけではない。
ユニットテストにこだわらず、様々なテスト手法を組み合わせてつかうことが、低予算・短期間・高品質を実現するために必要になる。
また、ユニットテストやソフトウェアテスティングに関するキーワードもいくつか挙げた。
- インテグレーションテスト
- 動的テスト
- 静的テスト
- コードレビュー
- 自動テスト
- 手動テスト
- テストファースト
- Test Driven Development(TDD)
- コードカバレッジ
- ホワイトボックステスト
- ブラックボックステスト
- 境界値分析
- 同値分析
あとはがんばれ!