単体テストでstatic変数いじったのに初期化しないのは時限爆弾を仕込むようなものだよ、というお話

# まず言いたいこと

単体テストの中で静的（`static`）なフィールド変数に変更を加える必要がある場合は、そのクラスでのテスト終了時に初期化しましょう。


# はじめに

本記事のタイトルは以下記事をオマージュしてます。読み物としても面白くてオススメなのでぜひ読んでみてください。

https://qiita.com/moromi25/items/61a8ae8750bec3078c10

で、本記事で伝えたいのは、`Mockito.mockStatic` と同様に **静的（`static`）なフィールド変数も時限爆弾化する** ということです。

※ 正しくは `static` なフィールドが時限爆弾化するんだから `mockStatic` も同様に...のロジックだけど上記の記事ありきの本記事だから許せサスケ（デコトン）

開発者のローカル環境やテストを差分実行しているCI上でテストが通ったとしても、日次のテスト全件実行で落ちることがあります（実行順とかがうまくかみ合えば落ちないんだけどね）

`static` なんだから当たり前でしょ、はそうなのですが意外とわかってない人も多いのでは、と思ったので記事化しました。


# 開発環境

- JUnit 5
- Java 8 以降

# 例えばこういうやつ

どこかに定義された `static` なフィールド変数があったとします。

```java:MyClass.java
// テスト対象クラス
public class MyClass {

    // staticなフィールド変数
    public static int staticValue = 5;

    // いろんなメソッドなど
}
```

既存のテストケースでは上記の `static` なフィールド変数を使っていたとします。

```java:ExistingTest.java
// 既存のテストクラス
public class ExistingTest {

    @Test
    public void existingTest() {
        // この時点でstaticValueは 5
        assertThat(MyClass.staticValue, is(5));
    }
}

```

今までは `MyClass.java` に定義された `staticValue` を使っていたテストケースが `ExistingTest.java` の１ケースのみでしたが、今回新たに `staticValue` を使うテストケースが追加されたとします。

```java:AddedTest.java
// 新たにどこかで追加されたテストクラス
public class AddedTest {

    @Test
    public void addedTest() {
        MyClass.staticValue = 10;
        // このテストが実行された時点でstaticValueは 10
        assertEquals(MyClass.staticValue, is(10));
    }
}
```

開発環境では `AddedTest.java` のテスト結果がグリーンで、PRのコミットで実行されるCIのテスト（差分実行）もグリーンになりました。

レビュワー『はい、問題なさげなので *Merge* するよ～』

# 翌日、全件テスト実行ジョブが日次実行され、全く手を入れていない箇所で突然テストが Fail する事態が発生

特に製品コードにもテストコードにも手を入れていないテストが突然 `Fail` する事態が複数箇所で発生し、私は「？？？」となりつつも調査していました。[^JUnitJupiter]

こういうときはだいたい `Mockito.mockStatic` の close漏れが原因なのですが、特にcloseを漏らしているコミットもなく、ん～全然分からん...。

どうにもならないのでパワープレイで解決を試みました。（調査方法は[後述](#全件実行で-fail-したときの調査方法)）

[^JUnitJupiter]:JUnit Jupiterはデフォルトではシングルスレッドで順番にテストが実行されます。設定することでマルチスレッドで並列実行が可能らしいですね。

# 原因はstatic変数の初期化忘れ

前の [例えばこういうやつ](#例えばこういうやつ) に戻って説明します。

テストの実行環境で運悪く（？） `AddedTest #addedTest` が先に実行されると、`staticValue` の値が 10 に設定されます。その後 `ExistingTest #existingTest` が実行されますが、`staticValue`の値は依然として 10 であるため、期待値である 5 とは一致しません。

これは、**`staticValue` が 静的（`static`）な変数であり、テスト実行環境上のすべてのインスタンスで共有されるため** です。

上記の例は簡単なテストケースになっているのでテストが `Fail` しても調査は簡単にできますが、

- テスト対象メソッドの中で静的変数をいじったりしている場合
- テスト対象クラスのインスタンス化の際に静的変数をいじったりしている場合

だと原因特定が一筋縄ではいかなくなることがあります。（初見だとなおさら）

# 共有依存は排除すべき

単体テストには、満たすべき３つの要件があります。[^３つの要件]

- １単位の振る舞いを検証すること
- 実行時間が短い
- （テストケースが）他のテストケースから隔離された状態で実行される

`static` な変数の初期化忘れは要件の１つである、「（テストケースが）他のテストケースから隔離された状態で実行される」を破壊します。**ほかのテストケースから隔離すべき共有依存が隔離されていないから** です。

こういった、複数のテストケース間で互いの検証に影響を及ぼしあう状態を共有依存な状態と言ったりします。静的（`static`）なフィールドはまさに、共有依存を生み出す典型例（他にはデータベースとかシングルトンインスタンスとかがある）で、同じプロセス内で複数のテストケースが同時実行されているときにどこかでフィールドの値を書き換えるとほかのテストケースに影響を与えてしまうわけです。

[^３つの要件]:引用元：[単体テストの考え方 / 使い方](https://www.amazon.co.jp/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9-%E4%BD%BF%E3%81%84%E6%96%B9-Vladimir-Khorikov/dp/4839981728)


# static変数いじるときは最後に必ず初期化しよう

では、単体テストが満たすべき３つの要件のうちの１つ「（テストケースが）他のテストケースから隔離された状態で実行される」を満たすためにはどうしたらいいでしょうか。

簡単です、テストケースの事後処理で `static` なフィールド変数を初期化すればいいのです。

事前ではなく事後であるのは、各テストの独立性を保つためです。テストケースは、**他のテストケースの結果に影響を受けず、また他のテストケースに影響を与えないように設計されるべきです。** 影響があるかもしれないから事前に初期化するのではなく、そもそも別のテストケースに影響を与えないように各テストケースで事後処理すべきでしょ、という話です。

事後処理でやることはとてもシンプルで、

```java:MyClass.java
// テスト対象クラス
public class MyClass {

    // staticなフィールド変数
    public static int staticValue = 5; // 変数宣言されるのはここ

    // いろんなメソッドなど
}
```

`@AfterAll` アノテーションで初期化するだけ。

```java:YourClassTest.java
import org.junit.jupiter.api.AfterAll;

// どこかで追加するテストクラス
public class YourClassTest {

    // いろんなテストメソッド

    @AfterAll
    public static void tearDown() {
        MyClass.staticValue = 5; // 初期化
    }
}
```

テスト実行後に初期化をかますことで、今回追加したテストケースが他のテストケースに影響を与えることはなくなります。



# 全件実行で Fail したときの調査方法

以下の記事、天才的で感動したので載せておきます。

トリガーとなったマージコミットを二分探索で効率的に探せる賢いgitコマンド...。

https://qiita.com/usamik26/items/cce867b3b139ea5568a6

これでテストの全件実行で `Failed` になったトリガーを特定して、`Failed` になる場合とそうでない場合（マージされる前後）の `Stack Trace` の差分を１つ１つデバッグで確かめることでなんとか調査できました（パワープレイすぎる）。

ほかにもっと効率的な方法あれば知りたい。

# おわりに

単体テスト書くときは「（テストケースが）他のテストケースから隔離された状態で実行される」を満たせるように気を遣いましょう。また、レビューで指摘しましょう。（CIで検知できたら一番いいんだけどね...）