単体テストの構造パターン

この記事は、[ラクス Advent Calendar 2024](https://qiita.com/advent-calendar/2024/rakus) 3日目の記事です。

今年、チームに配属され、単体テストの実装タスクを担当しました。その際、ベテランの方から単体テストの構造について教わり、学んだ内容をもとに様々な構造パターンを調べ、まとめましたので紹介します。
以下では、javaとjunit5を用いて説明いたします。

## テスト対象コード
テストの具体例を示すために、以下の`BankAccount`クラスを用意しました。withdrawメソッドに対して、テストを書いていきます。

```BankAccount.java
public class BankAccount {
    private int balance;

    public BankAccount(int balance) {
        this.balance = balance;
    }

    /**
     * 預金残高から引き出す
     * @param amount 引き出す額
     * @return 預金残高
     */
    public int withdraw(int amount) {
        if(amount > balance) {
            throw new IllegalArgumentException("Insufficient balance");
        }
        balance -= amount;
        return balance;
    }

    public int getBalance() {
        return balance;
    }
}
```

## 単体テストの構造の種類
私が調べたテスト構造パターンは以下の通りになります。

* Arrange-Act-Assert (3A)
* Setup-Exercise-Verify-Teardown
* Given-When-Then
* Setup-Act-Assert

それぞれの構造について、テストコードの具体例を用いながら、そのメリットとデメリットを紹介します。

### Arrange-Act-Assert (3A)

#### メリット
* シンプルでわかりやすい
* 直感的で、多くの開発者が馴染みやすい
* 小規模で単純なテストに適している
#### デメリット
* テストの規模が大きくなると可読性が低下しやすい
* 再利用性が低く、冗長になる場合がある

```java
@Test
public void withdraw_ShouldDecreaseBalance_WhenSufficientBalance() {
    // Arrange: 初期残高が100円の銀行口座を用意
    BankAccount account = new BankAccount(100);

    // Act: 50円を引き出す
    int newBalance = account.withdraw(50);

    // Assert: 残高が50円になることを確認
    assertEquals(50, newBalance);
}
```

このように、3A構造はシンプルな記述になるため、これくらいのテストでは読みやすいという特徴がありますが、クラス内のメソッドが多かったり、メソッド内で分岐が多い場合は、可読性を担保することが難しかったりします。

### Setup-Exercise-Verify-Teardown

#### メリット
* 再利用性が高い（`@BeforeEach`や`@AfterEach`で共通のセットアップ/後処理が可能）
* テストコードがスッキリするため、可読性が向上
* クリーンアップ処理を明示できる
#### デメリット
* セットアップやクリーンアップのコードが複雑になりやすい
* 小規模なテストではやや冗長に感じる場合がある

```java
private BankAccount account;

@BeforeEach
public void setup() {
    // Setup: 初期残高100円の銀行口座を作成
    account = new BankAccount(100);
}

@Test
public void withdraw_ShouldDecreaseBalance_WhenSufficientBalance() {
    // Exercise: 50円を引き出す
    int newBalance = account.withdraw(50);

    // Verify: 残高が50円になることを確認
    assertEquals(50, newBalance);
}

@AfterEach
public void teardown() {
    // Teardown: リソースを解放
    account = null;
}
```

今回のBankAccountクラスのテストでは恩恵を感じにくいですが、テスト対象のクラスが膨大で毎回同じ処理が必要な場合、Setup内に処理をまとめることで有効に活用できます。

### Given-When-Then

#### メリット
* 自然言語に近い構造で、仕様をそのままテストに反映できる
* 可読性が高いため、非技術者にも意図を伝えやすい
* BDD（振る舞い駆動開発）との親和性が高い
#### デメリット
* コードがやや冗長に感じる場合がある
* シンプルなテストでは、過剰な構造に思えることがある

```java
@Test
@DisplayName("正常系 - 残高が十分な場合に引き出しが成功する")
public void withdraw_ShouldDecreaseBalance_WhenSufficientBalance() {
    // Given: 初期残高が100円の銀行口座
    BankAccount account = new BankAccount(100);

    // When: 50円を引き出す
    int newBalance = account.withdraw(50);

    // Then: 残高が50円になることを確認
    assertEquals(50, newBalance);
}
```

Given-When-Thenは振る舞いや仕様に焦点を当てたテストで、シナリオベースの理解が重要な場面では非常に有用です。一方で、簡単なメソッドのテストにはやや冗長になる可能性があり、テストの粒度に応じて使い分ける必要があります。

### Setup-Act-Assert

#### メリット
* 再利用可能なセットアップの恩恵を受けつつ、簡潔な構造を維持できる
* セットアップ後すぐにテストが読めるため、テストコードの意図を把握しやすい
#### デメリット
* Verifyの明示がないため、テストの意図が分かりづらくなる場合がある
* シンプルなテストには適しているが、大規模なテストではやや制約を感じる場合がある

```java
private BankAccount account;

@BeforeEach
public void setup() {
    // Setup: 初期残高100円の銀行口座を作成
    account = new BankAccount(100);
}

@Test
public void withdraw_ShouldDecreaseBalance_WhenSufficientBalance() {
    // Act: 50円を引き出す
    int newBalance = account.withdraw(50);

    // Assert: 残高が50円になることを確認
    assertEquals(50, newBalance);
}
```

Setup-Act-Assertは簡潔でありながら明確な構造を提供するため、複雑すぎないテストに適しています。ただし、Setup部分が繰り返しになる場合はリファクタリングや共有化を検討する必要があります。テスト対象やシナリオの規模に応じた最適な使い分けが重要です。

## まとめ
最後にそれぞれの構造のメリット/デメリットの比較表を示します。

| 構造                           | メリット                                                     | デメリット                                                                             | 
| :----------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------- | 
| Arrange-Act-Assert             | シンプルで直感的、小規模テストに最適                         | 大規模テストでは可読性が低下、冗長になりやすい    | 
| Setup-Exercise-Verify-Teardown | 再利用性が高く、テストコードがスッキリする                   | セットアップやクリーンアップが複雑になりやすい                                         | 
| Given-When-Then                | 可読性が高く、仕様をそのまま反映できる	 | 冗長になりやすく、シンプルなテストには過剰な構造と感じる場合がある                     | 
| Setup-Act-Assert               | セットアップ後のテストが簡潔で、意図が把握しやすい           | `Verify`がないため、明示的な検証が欠ける場合があり、テストの意図が曖昧になる可能性がある | 

つまり、
**小規模でシンプルなメソッド → 3A構造**
**再利用性が重要 → Setup-Exercise-Verify-Teardown**
**仕様の振る舞い重視 → Given-When-Then**
**セットアップが少し必要な簡潔なテスト → Setup-Act-Assert**
という感じに分類出来ます。

ただし、1つの構造パターンに固執すると、テストがかえってやりづらくなる場合もあります。そのため、1つの構造パターンをベースにしながら、必要に応じて拡張していく方法が良いと考えます。