47.3K Views
October 13, 22
スライド概要
JJUG CCC 2022 Fallの資料です。
テスト技法およびJUnitの使い方の基礎中の基礎を解説しています。
Java、Spring、IntelliJ IDEA
入門: テスト技法とJUnit (株)クレディセゾン 多田真敏 2022年11月27日 JJUG CCC 2022 Fall1
このセッションについて ▸ テスト技法とJUnitをさっくりと解説することで、 今後の勉強へのとっかかりを提供します ▸ 更に勉強するための参考資料も紹介します ▸ 対象者 この資料だけでは 十分ではありません ▸ テスト技法を初めて学ぶ方、JUnitを初めて使う方 ▸ 前提知識 ▸ 入門書レベルのJava基本文法を知っている、 Maven(or Gradle)および何らかのJava IDEを 使ったことがある 2
自己紹介 ▸ 多田真敏 (@suke_masa) ▸ 金融事業会社のJavaプログラマー ▸ 社内システムの内製化+AWS化 ▸ IntelliJ IDEAが好き ▸ 日本Springユーザ会(JSUG) スタッフ 3
サンプルコード ▸ GitHubで公開しています 4
注意点 ▸ この資料内で使われている用語は、 多田が一般的と判断したものを 使っています ▸ しかし、会社やプロジェクトによって 用語は異なる可能性がありますので、 ご承知おきください 5
使用しているバージョン ▸ JDK 17 ▸ JUnit 5.9.0 ▸ Maven 3.8.3 多少バージョンが違っていても、 内容はほぼ変わらないと思います (JUnit 4は全く異なるので注意) 6
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 7
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 8
なぜテストは必要か? ▸ プログラムは人間が書く + 人間は間違いを犯す → プログラムは間違っていることがある ▸ プログラムの間違いは、 人や企業に損害を与える事がある ▸ 業務の中断、人手による代替、損害賠償、 企業イメージの低下、人の財産や生命への損害、・・・ ▸ なのでリリース前に間違いを検知+修正する 必要がある 9
手作業でのテストだと…💀 ▸ ビジネス目線 ▸ テストの実行に多くの時間+人手がかかる → プログラムの変更に莫大なコストがかかる → ビジネス環境の変化に対応できなくなる → 企業のビジネス競争力が低下する ▸ 開発者目線 ▸ テストを気軽に実行しにくい → コードの変更による悪影響を検知しにくい → 機能追加やリファクタリングがしづらくなる 10
テストをプログラムで書くと😆 ▸ ビジネス目線 ▸ テストを短時間+少ない人手で実行できる → プログラムの変更コストが小さくなる → ビジネス環境の変化に対応できる → 企業のビジネス競争力がアップする! ▸ 開発者目線 ▸ テストを気軽に実行できる → コードの変更による悪影響をすぐ検知できる → 機能追加やリファクタリングがしやすくなる! 11
テストのプロセス ① 単体テスト 本資料の主な範囲 ▸ 各クラスやメソッドがちゃんと動作するか確認する ② 結合テスト ▸ いくつかのクラスをつなげてテストする(機能単位など) ③ システムテスト ▸ システム全体のテスト ▸ パフォーマンスやセキュリティのテストも行う ④ ユーザーテスト ▸ ユーザーが行うテスト 12
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 13
例: ポイント計算画面 ▸ 仕様 ① 買物金額1〜999円では ポイント無し 買物金額 2000 ② 買物金額1000〜9999円では ポイントが3% ③ 買物金額10000円以上では ポイントが5% 計算 ポイント=60pt ④ すべて小数点以下は切り捨て ⑤ 入力は文字列(最大7文字) ⑥ 入力が0以下や数字以外の場合は 「エラー」と表示 14
全部テストするのは無理😇 ▸ 最大7文字なので、 正の数だけでもテストケースは9,999,999件。 0以下・ひらがな・カタカナ・漢字・英字などを 考慮すると、ケース数は天文学的になる → 時間と手間がかかりすぎて現実的には不可能 ▸ バグの出やすい箇所だけを効率的に テストする手法が必要 → そのための手法が同値分割と境界値分析 15
①同値分割 ▸ 同じ動作になる入力値のグループ (=同値パーティション)を作成して、 各グループごとに1つの代表値でテストする 16
①同値分割 ▸ 今回の同値パーティションは👇の通り No. 同値パーティション 代表値 1 1〜999円(ポイント0) 500 2 1000〜9999円(ポイント3%) 5000 3 10000円以上(ポイント5%) 50000 4 0以下または数字以外(エラー) あああ この4ケースを テストすれば 十分とするのが 同値分割 17
①同値分割 ▸ Q「本当にこの4ケースだけで十分なの?」 A「場合によるので、しっかり考えましょう」 ▸ 同値分割で十分な場合もあるし、 境界値分析(次で説明)や全数テストが 必要な場合もある 18
②境界値分析 ▸ 同値パーティションの端の値でテストする ▸ 「同値分割法の拡張であるが、 パーティションが数値または順序付け可能な値で 構成される場合だけ使用できる。」 (ISTQBテスト技術者認定制度シラバスより引用) 19
②境界値分析 ▸ なぜ「端の値」をテストするのか? ▸ 「端の値」を間違えたり、不等号に=を忘れたりしやすい // バグがあるプログラム① // バグがあるプログラム② String check(int age) { // 2022年4月から18歳! String check(int age) { // 不等号に=が付いてない! } if (age >= 20) { return "大人"; if (age > 18) { return "大人"; } else { return "子供"; } else { return "子供"; } } } 20
②境界値分析 ※最大文字数が7文字なので、数値の上限は9999999、下限は-999999 No. 同値パーティション 端の値 1 1〜999円(ポイント0) 1, 999 2 1000〜9999円(ポイント3%) 1000, 9999 10000円以上(ポイント5%) 10000, 9999999 0以下または数字以外(エラー) 0, -999999, あああ 3 4 この8ケースをテストすれば 十分とするのが境界値分析 21
この章のまとめ ▸ 全数テストは大変 → 同値分割や境界値分析で効率的にテストする ▸ 同値分割は、各同値パーティションから 代表値を1つずつテストする ▸ 境界値分析は、各同値パーティションの 端の値をテストする 22
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 23
カバレッジとは何か ▸ プロダクトコード内の処理や分岐のうち、 テストで確認(カバー)できたものの割合 ▸ レベルが3つある ▸ C0(処理網羅): 全ての処理がテストできたら100% ▸ C1(分岐網羅): 全ての分岐がテストできたら100% ▸ C2(条件網羅): 全ての条件のtrue/falseが テストできたら100% 24
C0: 処理網羅 ① ▸ 全ての処理が テストできたら100% ▸ 通らない分岐があっても良い ▸ 右の例の場合、 下記の1ケースで100% ① a = 1, b = 1 false a > 0 かつ b > 0 true 「両方とも正です」 と表示 25
C1: 分岐網羅 ① ② ▸ 全ての分岐が テストできたら100% ▸ true/falseの両方を テストしない条件があっても良い ▸ 右の例の場合、 下記の2ケースで100% false a > 0 かつ b > 0 true 「両方とも正です」 と表示 ① a = 1, b = 1 ② a = 0, b = 1 b > 0はどちらもtrueでよい (falseでもよい) 26
C2: 条件網羅 ① ② ▸ 全ての条件のtrue/falseが テストできたら100% false a > 0 かつ b > 0 ▸ 右の例の場合、条件は 「a > 0」「b > 0」の2つ true ▸ 右の例の場合、 下記の2ケースで100% 「両方とも正です」 と表示 ① a = 1, b = 1 ② a = 0, b = 0 全ての条件のtrue/falseを 少なくとも1回はテストする ※「C2では2×2=4パターンを網羅すべき」という説もあります 27
カバレッジの測定方法 ▸ 各種IDE ▸ IntelliJの場合は [Run 'Tests in 'xxx'' with Coverage] ▸ jacoco-mavenplugin ▸ HTML形式のレポートを 出力可能 28
カバレッジは何%を目指すか? ▸ カバレッジレベルによっても 変わるので注意 ▸ 個人的意見 ▸ C1で80%を目指しています ▸ 必ずしも100%じゃなくて いいと思います ▸ チームで目標を決めて 同意しておきましょう URLはこちら 29
カバレッジ100%でもテストが十分でない例 ▸ 仕様「18歳以上ならば大人、それ以外は子供」 ▸ 以下2つのテストケースでカバレッジ100% ① 年齢が20ならば「大人」 // バグがあるプログラム ② 年齢が15ならば「子供」 String check(int age) { // 不等号に=が付いてない! if (age > 18) { return "大人"; ▸ しかし、これらのテストでは 👉のバグを検出できない } else { return "子供"; } } 30
カバレッジの弱点 ▸ ちゃんと境界値分析ができているかどうかは 分からない ▸ 各処理や条件を通っていれば100%になるから ▸ 仕様に対して実装が足りているかどうかは 分からない ▸ 今の実装に対して全処理・全条件を通っていれば 100%になるから 31
カバレッジはあくまで「補助輪」 URLはこちら ※ご本人から引用許可はいただいています 32
この章のまとめ ▸ カバレッジは、プロダクトコードのうち テストでカバーできたものの割合 ▸ C0(処理網羅)・C1(分岐網羅)・C2(条件網羅) のレベルがある ▸ カバレッジが100%でも、 十分にテストできていない場合があるので注意 33
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 34
今回、紹介できなかったテスト技法 ▸ デシジョンテーブル ▸ 入力値が多いときに組み合わせを整理する技法 ▸ 状態遷移テスト ▸ 状態遷移をテストする技法 ▸ 組合せテスト ▸ 組み合わせが多いときにテスト項目を減らす技法 35
もっと勉強したい人へ ▸ JSTQB Foundation Levelを受験する ▸ ソフトウェアテストの教科書[増補改訂 第2版] ▸ JaSSTの資料 36
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 37
JUnit ▸ Javaでテストコードを作成・実行するための フレームワーク ▸ 最新バージョンは5.x ▸ オリジナルの作者は Kent Beck氏(『テスト駆動開発』の著者)と Erich Gamma氏(『デザインパターン』の著者) ▸ 飛行機でのフライト中に作ったらしい(URL) ▸ JUnit 4の開発がしばらく停滞 → JUnit 5はクラウドファンディングで作られた 38
JUnit 4と5 ▸ 互換性は無し ▸ 使い方もアーキテクチャも全て異なる ▸ ただし、4上で5のテストを動かしたり、 5上で4のテストを動かしたりすることは可能 ▸ 現在、新規にテストを書くなら5を選ぶべき ▸ 既存の4のテストコードを全て5に書き直すべきかは微妙 ▸ 5で書き直すことによるメリットはそんなにない ▸ しかし、4のメンテナンスがいつまで続くかも不明 39
準備 <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.0</version> <scope>test</scope> </dependency> ... Sure re 2.22.0以上必須※ </dependencies> (Spring Boot利用時は ... 既に組み込まれているため記述不要) <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> ... </plugins> </build> fi ※ https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven 40
今回のテスト対象 package com.example; public class PointCalculator { /** * 買物金額をもとにポイントを計算します。ポイント計算ルールは次のとおりです。 * 1. 買物金額1〜999円ではポイント無し * 2. 買物金額1000〜9999円ではポイントが3% * 3. 買物金額10000円以上ではポイントが5% * 4. すべて小数点以下は切り捨て * 5. 最大7文字 * 6. 0以下や数字以外の場合はエラー * * @param amountStr 買物金額 * @return ポイント * @throws RuntimeException amountが0以下や数字以外の場合 */ public int calculate(String amountStr) { /* 処理は省略 */ } } 41
テストの実行 ① IDE上で実行 ② mvn testコマンドで実行 ▸ どちらの方法も個別実行・一括実行の 両方が可能 42
テストクラスの書き方 ▸ JUnitのルール ▸ テストメソッドには@Testを付加する ▸ assertXxx()で、期待値と実際の値を検証する ▸ 慣習 ▸ テストクラスは、テスト対象と同じパッケージに作成する ▸ テストクラス名は、「テスト対象+Test」にする 43
@Testの注意点 ▸ JUnit 5と4でパッケージ名が違う ▸ JUnit 5 → org.junit.jupiter.api.Test ▸ JUnit 4 → org.junit.Test ▸ 「なんかテストの動きが変?」と思ったら import文を確認しましょう 44
assertXxx()メソッド ▸ Assertionsクラスのstaticメソッド ▸ 通常はstatic importして利用する ▸例 ▸ assertEquals(int expected, int actual) ▸ actual(実際のメソッド戻り値)の値がexpected(期待値)と異なる場合、 AssertionErrorをスローする。それ以外は何もしない。 ▸ assertThrows(Class<T> expected, Executable e) ▸ eのラムダ式内でexpectedの例外がスローされたら何もしない。 それ以外はAssertionErrorをスローする。 45
テストクラスの書き方 package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; 普通のJavaクラスなので 通常のフィールドやメソッドも 作成可能 public class PointCalculatorTest { PointCalculator calculator = new PointCalculator(); @Test public void test01() { int actual = calculator.calculate("1"); assertEquals(0, actual); } テストメソッドは 引数なし、戻り値なし } テスト対象メソッドを実行して、 期待値と比較する 46
テストケースに名前を付ける方法 ① メソッド名を詳細に書く @Test public void 買物金額1円ではポイント無し() { int actual = calculator.calculate("1"); assertEquals(0, actual); } ② @DisplayNameに書く @Test @DisplayName("買物金額1円ではポイント無し") 利用可能文字に制限が無いので こちらが個人的にオススメ public void test01() { int actual = calculator.calculate("1"); assertEquals(0, actual); } どちらの場合でも「こんな入力を与えたら、こんな出力になる」 という旨を明確に書きましょう 47
テストケースに名前を付けると・・・ 48
例外はスローされる場合の書き方
public class PointCalculatorTest {
PointCalculator calculator = new PointCalculator();
@Test @DisplayName("買物金額1円ではポイント無し")
public void test01() {
int actual = calculator.calculate("1");
assertEquals(0, actual);
}
@Test @DisplayName("買物金額0円ではRuntimeException")
public void test02() {
assertThrows(RuntimeException.class, () -> {
calculator.calculate("0");
});
}
}
49
テストメソッドの実行順序は? ▸ デフォルトの実行順序は、わざと明確にしていない → どんな実行順序でもテストが通るようにすべき ▸ 後述の@BeforeEachなどで、 テストの事前環境を整えれば大体は可能なはず ▸ どうしても実行順序を指定したい場合は、 @TestMethodOrderを付加する 50
テストメソッドを分類する方法 ▸ インナークラスを作成して@Nestedを付加する public class PointCalculatorTest { PointCalculator calculator = new PointCalculator(); @Nested @DisplayName("ポイント無し") public class Point_0 { @Test @DisplayName("買物金額1円ではポイント無し") public void test01() { ... } @Test @DisplayName("買物金額999円ではポイント無し") public void test02() { ... } } @Nested @DisplayName("エラー") public class Error { @Test @DisplayName("買物金額0円ではRuntimeException") public void test01() { ... } ... 51
各テスト共通の前処理・後処理 ▸ @BeforeAll ▸ テストクラス実行前に、1回だけ実行される ▸ @AfterAll ▸ テストクラス実行後に、1回だけ実行される ▸ @BeforeEach ▸ 各テストメソッド実行前に実行される ▸ @AfterEach ▸ 各テストメソッド実行後に実行される 52
テストクラス実行の流れ ▸ @BeforeAll → @BeforeEach → @Test(1つ目) → @AfterEach → @BeforeEach → @Test(2つ目) → @AfterEach → @AfterAll public class SampleTest { @BeforeAll public static void beforeAll() { ... } @AfterAll public static void afterAll() { ... } @BeforeEach public void beforeEach() { ... } @AfterEach public void afterEach() { ... } @Test public void test01() { ... } @Test public void test02() { ... } } 53
@BeforeXxx/@AfterXxxの用途 ▸ @BeforeAll/@BeforeEach ▸ テストメソッドのための事前条件を準備する (必要なデータをDBに投入するなど) ▸ @AfterAll/@AfterEach ▸ テストメソッドで作成したものの削除 (テキストファイルなど) ▸ DBデータの削除は@BeforeXxxでやることが多い (DELETEしてINSERT) 54
今回、紹介できなかったJUnitの機能 ▸ Extension ▸ 前処理・後処理などをまとめたもの ▸ Parameterized Test ▸ 引数を変えるだけのテストを効率的に書ける ▸ JUnit 4上でJUnit 5を実行 ▸ JUnit 5上でJUnit 4を実行 ▸ 詳細は公式ドキュメントをご確認ください 55
この章のまとめ ▸ テストクラスを作成して、メソッドに@Testを付加する ▸ assertEquals()で期待値と実際の戻り値を比較する ▸ assertThrows()で例外がスローされるかを検証できる ▸ @DisplayNameでテストケース名を付けられる ▸ @Nestedとインナークラスでテストを分類できる ▸ @BeforeAll/@BeforeEach/@AfterAll /@AfterEachで前処理・後処理を記述できる 56
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 57
テストしやすいコードにするために ▸ メソッド戻り値はなるべくvoidにしない ▸ voidだとassertEquals()を使った検証がやりにくい ▸ 副作用のあるメソッドはなるべく少なくする ▸ 副作用=戻り値を返す以外に外部に影響を及ぼすこと (例: 表示、ファイル出力、データの登録) ▸ 副作用の確認はやや手間 (何かが表示された、ファイルが出力された、など) 58
テストしやすいコードにするために ▸ 外部システムにアクセスする箇所は なるべく少なくする ▸ テストのときに外部システムが使えない場合がある。 その場合はモック化(仮のクラスに差替)する ▸ 日時・ランダム値・依存先クラスは 引数などで外から代入する ▸ テストが場合によってOK/NGが変わるのを防げる 59
目次 ▸ 前半: テスト技法 ▸ 後半: JUnit ① なぜテストは必要か ① テストコードの書き方 ② 同値分割と境界値分析 ② テストしやすいコード ③ カバレッジ ③ その他もろもろ ④ その他もろもろ 60
JUnit関連のその他もろもろ ▸ JaCoCo: カバレッジ測定+レポート出力 ▸ Selenium: 自動ブラウザテスト ▸ Selenide: Seleniumのラッパーライブラリ ▸ Mockito: 依存クラスのモック作成 ▸ Spring Test: Springと連携したテスト ▸ dbUnit: DBへのデータ投入、データの存在確認 61
CI/CD ▸ CI: Continuous Integration ▸ 自動的にテストやビルドを実行してくれる ▸ CD: Continuous Delivery ▸ CIでのビルド成果物を自動的にデプロイしてくれる ▸ 主なCI/CDツール ▸ Jenkins、GitHub Actions、GitLab CI/CD、 AWS Codeシリーズ・・・など AWS以外のクラウドにも 同様のサービスがあるはず 62
もっと勉強したい人へ ▸ JUnit 5使い方メモ ▸ テスト駆動開発 ▸ JUnit 5 User Guide 63
▸ 質の高いテストでハッピーなエンジニアライフを! 64