【Java発展学習12日目】テストの分類とその品質保証、リファクタリングについて

# 品質段階
プログラムの開発品質を判定する段階は、以下の通り。

| 段階 | 状態 | 内容 | 判定方法 |
|:-:|:--|:--|:--|
| 1 | コンパイルエラー | 文法上の誤り | コンパイル |
| 2 | ランタイムエラー・異常終了 | プログラム実行中に異常終了が発生 | テスト |
| 3 | 仕様非準拠 | 正常終了するものの仕様通りでない | テスト |
| 4 | 完成 | 全機能が仕様通りに正常終了 | テスト |

# 4段階のテスト
`テスト`を**段階別**に分類すると、以下のようになる。

| テスト | テスト対象 |
|:--|:--|
| `単体テスト(UT; Unit Test)` | ある一つの「クラス」または「機能」 |
| `結合テスト(IT; Integration Test)` | 単体テストが完了した複数の「クラス」・「機能」 |
| `総合テスト(ST; System Test)` | 結合テストが完了した全ての「クラス」・「機能」 |
| `受け入れテスト(UAT; User Acceptance Test)` | システム一式 ( **発注者**が実施 ) |

# 3種類のテスト
`テスト`を**目的別**に分類すると、以下のようになる。

| テスト | 実施者 | 内容 |
|:--|:--|:--|
| 顧客要件テスト | 顧客またはプロジェクト | 仕様通りの機能であることを確認 |
| 品質保証テスト | プロジェクト | 性能および信頼性の改善・保証 |
| 開発者テスト | 開発者 | 設計および開発の洗練 |

# テストケース(test case)
`テストケース`は`正常系`と`異常系`の2種類を組み合わせて作成する。

`同値分割(equivalence partitioning)`は、**有効値**と**無効値**の集合の中から、任意の代表値を選択し使用する手法であり、
`境界値分析(boundary value analysis)`は、条件分岐の**境界値付近**の値を使用する手法である。

***

# MavenプロジェクトでのJUnitの利用(VSCode)
`Visual Studio Code`の`Mavenプロジェクト`で`JUnit`を導入する場合、
`テストクラス`で利用する`Assert`クラスの静的メソッドと、
`テストメソッド`を表す`@Test`アナテイションを定義する`Test`クラスをインポートする必要がある。

## サンプルコード

```src/main/TestClass.java
public class TestClass {
    private String str = "";
    private int i = 0;

    // コンストラクタ
    public TestClass(){}
    public TestClass(String str) {
        this.str = str;
    }
    public TestClass(int i) {
        this.i = i;
    }
    public TestClass(String str, int i) {
        this.str = str;
        this.i = i;
    }

    // ゲッタ
    public String getStr() {
        return this.str;
    }
    public int getI() {
        return this.i;
    }
    
    // セッタ
    public void setStr(String str) {
        this.str = str;
    }
    public void setI(int i) {
        this.i = i;
    }

    @Override
    public String toString() {
        return "(" + this.str + ", " + this.i + ")";
    }

    public void plus(int i) {
        this.i += i;
    }
    public void minus(int i) {
        this.i -= i;
    }
}
```

```src/test/TestClassTest.java
import static org.junit.Assert.*;
import org.junit.Test;

public class TestClassTest {
    // インスタンス化テスト
    // -> テストメソッドには@Testアナテイションを付与
    @Test
    public void instantiate() {
        // インスタンスの生成
        TestClass tc = new TestClass("ABC", 123);

        assertEquals("ABC", tc.getStr());
        assertEquals(123, tc.getI());
    }

    // 増減テスト
    @Test
    public void plusMinus() {
        // インスタンスの生成
        TestClass tc = new TestClass();

        tc.setI(123);

        tc.plus(27);
        assertEquals(150, tc.getI());
        tc.minus(27);
        assertEquals(123, tc.getI());
    }
}
```

```zsh:JUnitによるテスト
% mvn test
[INFO] Scanning for projects...
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running TestClassTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.02 s - in TestClassTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.314 s
[INFO] Finished at: 2021-07-15T13:10:04+09:00
[INFO] ------------------------------------------------------------------------
```

***

# アサーション(assertion)
`アサーション`を用いることで、`テスト対象クラス`の各**メソッド**に対して、想定値でない場合はプログラムを**強制停止**させることができる。

ただし、`アサーション機能`を有効するためには`-ea`オプションをつけてクラスファイルを実行する必要がある。

```zsh:アサーション機能を有効にしたプログラム実行
% java -ea <メインクラスのFQCN>
```

## 契約による設計(DbC; Design by Contract)
主な`アサーション`の利用場面は、以下の通り。

| 場面 | 内容 |
|:--|:--|
| 呼び出し時 | 引数が正常値(=`null`でない)であることを保証 |
| 処理の終了時 | 戻り値や実行結果が正常値であることを保証 |
| 常時 | インスタンスの内部状態が正常値であることを保証 |

## サンプルコード

```java:TestClass.java
public class TestClass {
    private String str = "";
    private int i = 0;

    // コンストラクタ
    public TestClass(){}
    public TestClass(String str) {
        this.str = str;
    }
    public TestClass(int i) {
        this.i = i;
    }
    public TestClass(String str, int i) {
        this.str = str;
        this.i = i;
    }

    // ゲッタ
    public String getStr() {
        return this.str;
    }
    public int getI() {
        return this.i;
    }
    
    // セッタ
    public void setStr(String str) {
        this.str = str;
    }
    public void setI(int i) {
        this.i = i;
    }

    @Override
    public String toString() {
        return "(" + this.str + ", " + this.i + ")";
    }

    public void plus(int i) {
        this.i += i;

        if (i >= 0) {
            // アサーションによるテスト
            assert this.i >= (this.i - i);
        }
        else {
            assert this.i < (this.i - i);
        }
    }
    public void minus(int i) {
        this.i -= i;

        if (i >= 0) {
            // アサーションによるテスト(エラーメッセージつき)
            assert this.i <= (this.i + i) : "calculation has failed.";
        }
        else {
            assert this.i > (this.i + i) : "calculation has failed.";
        }
    }
}
```

***

# メトリクス
ソフトウェア開発で利用される主な`メトリクス`は、以下の通り。

| メトリクス | 視覚化対象 |
|:--|:--|
| コードカバレッジ | テスト品質 |
| LOC(Lines of Code) | プログラム規模 |
| CC(Cyclomatic Complexity; 循環的複雑度) | メソッドの複雑度<br>※分岐・ループ処理によって数値が上昇 |
| WMC(Weighted Method Per Class) | クラス内の全メソッドのCCの合計値 |

# コードカバレッジ(code coverage)
**テスト品質**のメトリクスとなる`コードカバレッジ`の種類は、主に以下の3つ。

| レベル | カバレッジ | 計算式 |
|:--|:--|:--|
| C0 | 命令網羅率 | テストで通過する命令数 / コード全体の命令数 × 100 [%] |
| C1 | 分岐網羅率 | テストで通過する分岐経路数 / コード全体の分岐経路数 × 100 [%] |
| C2 | 条件網羅率 | テストで通過する単純分岐経路数 / コード全体の複数条件分岐経路数 × 100 [%] |

# Jacoco
`カバレッジ`を計測する場合、`OSS`である`Jacoco`ライブラリを利用する。

また、`Mavenプロジェクト`で`Jacoco`を利用する場合、`POMファイル`は以下のように追記することで、
`Mavenセントラル`から`Jacoco`のJARファイルを自動でダウンロードすることができる。

```xml:pom.xml
<!-- 追記箇所(dependenciesタグ) -->
<dependency>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.7</version>
</dependency>

<!-- 追記箇所(build/pluginManagement/pluginsタグ) -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.7</version>
</plugin>
```

```zsh:Jacocoによるカバレッジ計測
% mvn jacoco:prepare-agent test jacoco:report
[INFO] Scanning for projects...
...
[INFO] --- jacoco-maven-plugin:0.8.7:report (default-cli) @ maven ---
[INFO] Loading execution data file <Mavenプロジェクトのディレクトリパス>/target/jacoco.exec

[INFO] Analyzed bundle 'maven' with 2 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.804 s
[INFO] Finished at: 2021-07-15T15:43:33+09:00
[INFO] ------------------------------------------------------------------------
```

`Jacoco`によるカバレッジ計測を実行すると、`target/site/jacoco`フォルダに
カバレッジ計測結果が記述された`HTMLファイル`が自動生成される。

![Jacoco.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/7d0bc21e-2271-2301-b275-6cbab589e4e9.png)

![Jacoco2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/092eede6-fc07-b72b-4b33-5be44fbe52ba.png)

***

# リファクタリングの手法
主な`リファクタリング`手法は、以下の通り。

> 1. 重複部分を`メソッド`で再定義
> 2. `メソッド`や`クラス`の分割
> 3. `外部ライブラリ`や直近バージョンの`Javaライブラリ`での代用
> 4. `コメント`の修正

# 静的コード解析(static code analysis)
`静的コード解析`を行う主な開発ツールは、以下の通り。

| ツール | 解析内容 |
|:--|:--|
| `SpotBugs` | バグが生じる可能性のある記述 |
| `Checkstyle` | コーディングスタイル |

```xml:pom.xml
<!-- 追記箇所(dependenciesタグ) -->

<!-- SpotBugs -->
<dependency>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs-maven-plugin</artifactId>
  <version>4.2.3</version>
</dependency>

<!-- Checkstyle -->
<dependency>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-checkstyle-plugin</artifactId>
  <version>3.1.2</version>
</dependency>

<!-- 追記箇所(build/pluginManagement/pluginsタグ) -->

<!-- SpotBugs -->
<plugin>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs-maven-plugin</artifactId>
  <version>4.2.3</version>
</plugin>

<!-- Checkstyle -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-checkstyle-plugin</artifactId>
  <version>3.1.2</version>
</plugin>

<!-- 追記箇所(reporting/pluginsタグ) -->

<!-- SpotBugs -->
<plugin>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs-maven-plugin</artifactId>
  <version>4.2.3</version>
</plugin>

<!-- Checkstyle -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-checkstyle-plugin</artifactId>
  <version>3.1.2</version>
</plugin>
```

```zsh:SpotBugs/Checkstyleによる静的コード解析
% mvn site
[INFO] Scanning for projects...
...
[INFO] Generating "SpotBugs" report      --- spotbugs-maven-plugin:4.2.3:spotbugs
[INFO] Generating "Checkstyle" report    --- maven-checkstyle-plugin:3.1.2:checkstyle
...
```

![SpotBugs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/aeb9887f-1f4d-a39c-6cf2-20badf3534ec.png)

![Checkstyle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/adba5c4d-9450-e6b9-20da-1d35651845b6.png)

***

# 用語集

| 用語 | 内容 |
|:--|:--|
| スコープ(scope) | 開発プロジェクトの作業範囲。 |
| テストクラス(テストドライバ) | テスト対象クラスのメインクラスを担うクラス。 |
| テスティングフレームワーク(testing framework) | テストケースの記述・実行・検証方法を定めた機能群。 |
| パラメトリックテスト(parametric test) | テストデータを微調整しながら複数回実行するテスト。 |
| アサーション(assertion) | テストケースをメインコードに記述できる仕組み。 |
| コードカバレッジ(code coverage) | テストにおいて、各テストケースによる命令文(=コード)の網羅(=カバー)率を定量的に測定する指標。 |
| メトリクス(metrics) | 品質や事象を数値化したもの。 |
| 技術的負債(technical debt) | ソースコードの複雑さ。 |
| リグレッションテスト(regression test) | リファクタリング後に再度実施する単体テスト。 |
| デグレード(degrade) | リファクタリングの結果、バグが混入すること。 |