状態遷移テストとは

## 状態遷移テストとは
状態遷移テストは、ソフトウェアテストの手法の一つで、システムが異なる状態間を遷移する際の挙動を検証するためのテストです。システムの状態とそれらの間の遷移を視覚的に表した図を**状態遷移図**といい、状態遷移テストはこの状態遷移図や状態遷移表を用いたテストの総称を指します。
これはシステムが多くの状態を持ち、それらの状態間を遷移する場合に特に有効です。状態遷移テストは、システムが正しい遷移を行い、適切な状態に留まることを確認するのに役立ちます。

### 基本概念
- **状態**
    システムやオブジェクトがある時点で持つ特定の条件やステータス
    　
- **状態遷移**
    ある状態から別の状態への変化すること
    　
- **イベント**
    状態遷移するきっかけとなる操作や条件のこと

## 状態遷移テストのプロセス

### 1. 状態と遷移の特定
まず、システムが持つ全ての状態と、それらの状態間を移動するためのイベントを特定します。
　
**状態の特定**
状態とはシステムが特定の条件や状況にある時のことです。例えば、自動販売機の場合、以下のような状態が考えられます。

- `待機中` (WAITING): コインが投入されるのを待っている状態
- `コイン投入済み`  (COIN_INSERTED): コインが投入された状態
- `商品選択中`  (PRODUCT_SELECTED): 商品が選択されるのを待っている状態
- `商品提供中`  (PRODUCT_DISPENSED): 商品を提供している状態

　
**遷移の特定**
遷移とは、一つの状態から別の状態に変わることを指します。遷移は通常、特定のイベントや条件が発生したときに起こります。自動販売機の場合、以下のような遷移が考えられます。

- `待機中`から`コイン投入済み`への遷移: コインが投入されたとき
- `コイン投入済み`から`商品選択中`への遷移: 商品が選択されたとき
- `商品選択中`から`待機中`への遷移: 商品提供が完了したとき

　
**状態遷移表**
| 現在の状態 | イベント | 次の状態 |
| --- | --- | --- |
| 待機中 (WAITING) | コイン投入 (Coin Inserted) | コイン投入済　(COIN_INSERTED) |
| コイン投入済み (COIN_INSERTED) | 商品選択 (Product Selected) | 商品選択中 (PRODUCT_SELECTED) |
| 商品選択中 (PRODUCT_SELECTED) | 商品提供完了 (Product Dispensed) | 待機中 (WAITING) |

---

### 2. 状態遷移図の作成
次に、特定した状態と遷移を視覚的に表した状態遷移図を作成します。状態遷移図は、状態を円や長方形で、遷移を矢印で表現します。

　
**状態遷移図の例**
以下に自動販売機の状態遷移図の例を示します。
```text
┌───────────┐      コインを投入           ┌──────────────┐
│   待機中   │ ───────────────────────→ │  コイン投入済   │
└───────────┘                          └──────────────┘
     ↑                                       │
     │                                       │ 商品選択
     │                                       ↓
     │                                 ┌───────────────┐
     └──────────────────────────────── │   商品選択中    │
                 商品提供完了            └───────────────┘
```

---

### 3. テストケースの作成
状態遷移図を作成しただけでは、状態遷移テストを行うことが出来ません。状態遷移図をもとに、テストケースを作成する必要があります。
　
1. **明確なテスト目的の設定**
各テストケースの目的を明確に定義します。何をテストするのか、どの機能や部分を確認するのかを明確にすることで、効果的なテストが可能となります。
　
2. **初期状態の設定**
テスト開始時のシステムの状態を明確にします。初期状態が明確でないと、テスト結果が一貫しなくなる可能性があります。
　
3. **具体的な入力と操作**
テストする際の具体的な入力や操作を明確にします。入力データや操作手順を詳細に記述することで、再現性のあるテストが可能となります。
　
4. **期待される結果の定義**
テスト実行後に期待される結果を明確に定義します。これにより、実際の結果が期待される結果と一致するかどうかを正確に評価できます。
　
5. **境界値分析**
システムが取り得る最小値や最大値、またはその近傍の値をテストケースに含めます。これにより、境界条件でのシステムの動作を確認できます。
　
6. **エラーパスのテスト**
異常な入力や操作に対するシステムの動作を確認します。エラーハンドリングや例外処理が正しく機能するかをテストすることが重要です。
　
7. **カバレッジの確認**
すべての機能や状態遷移をカバーするテストケースを設計します。状態遷移図や状態遷移表を利用して、すべての遷移をテストすることができるようにします。
　
8. **依存関係の管理**
テストケース間の依存関係を考慮し、独立したテストケースを設計します。依存関係がある場合、その関係を明確にし、適切に管理します。
　
9. **再現性と自動化**
テストケースは再現性があり、自動化しやすい形で記述します。自動化テストツールを利用して、繰り返し実行できるようにします。

---

#### テストケース1: 待機中からコイン投入済みへの遷移
- **テスト目的:** システムが「待機中」から「コイン投入済み」に正しく遷移することを確認する
- **初期状態**: 待機中 (WAITING)
- **イベント**: コイン投入 (Coin Inserted)
- **入力**: ユーザーがコインを投入する
- **期待される結果**: 状態がコイン投入済み (COIN_INSERTED) に変わる

```jsx
[待機中 (WAITING)] --(コイン投入)--> [コイン投入済み (COIN_INSERTED)]
```
　
#### テストケース2: コイン投入済みから商品選択中への遷移

- **テスト目的:**  システムが「コイン投入済み」から「商品選択中」に正しく遷移することを確認する
- **初期状態**: コイン投入済み (COIN_INSERTED)
- **イベント**: 商品選択 (Product Selected)
- **入力**: ユーザーが商品を選択する
- **期待される結果**: 状態が商品選択中 (PRODUCT_SELECTED) に変わる

```jsx
[コイン投入済み (COIN_INSERTED)] --(商品選択)--> [商品選択中 (PRODUCT_SELECTED)]
```

#### テストケース3: 商品選択中から待機中への遷移

- **テスト目的**: システムが「商品選択中」から「待機中」に正しく遷移することを確認する
- **初期状態**: 商品選択中 (PRODUCT_SELECTED)
- **イベント**: 商品提供完了 (Product Dispensed)
- **入力**: ユーザーが商品を受け取る
- **期待される結果**: 状態が待機中 (WAITING) に変わる

```jsx
[商品選択中 (PRODUCT_SELECTED)] --(商品提供完了)--> [待機中 (WAITING)]
```

---

### 状態遷移テストのコード
テストケースを基に状態遷移テストを行うためのコードを示します。まず、システムのクラスを定義し、その後テストコードを記述します。
　
**システムのクラス**
```jsx
// VendingMachine.js

class VendingMachine {
    constructor() {
        this.state = 'WAITING';
    }

    coinInserted() {
        if (this.state === 'WAITING') {
            this.state = 'COIN_INSERTED';
        }
    }

    productSelected() {
        if (this.state === 'COIN_INSERTED') {
            this.state = 'PRODUCT_SELECTED';
        }
    }

    productDispensed() {
        if (this.state === 'PRODUCT_SELECTED') {
            this.state = 'WAITING';
        }
    }

    getState() {
        return this.state;
    }
}

module.exports = VendingMachine;
```

　
**テストコード**

```jsx
// VendingMachine.test.js

const VendingMachine = require('./VendingMachine');

describe('VendingMachine State Transition Testing', () => {
    let vm;

    beforeEach(() => {
        vm = new VendingMachine();
    });

    test('should transition from WAITING to COIN_INSERTED on coinInserted', () => {
        expect(vm.getState()).toBe('WAITING');
        vm.coinInserted();
        expect(vm.getState()).toBe('COIN_INSERTED');
    });

    test('should transition from COIN_INSERTED to PRODUCT_SELECTED on productSelected', () => {
        vm.coinInserted();
        expect(vm.getState()).toBe('COIN_INSERTED');
        vm.productSelected();
        expect(vm.getState()).toBe('PRODUCT_SELECTED');
    });

    test('should transition from PRODUCT_SELECTED to WAITING on productDispensed', () => {
        vm.coinInserted();
        vm.productSelected();
        expect(vm.getState()).toBe('PRODUCT_SELECTED');
        vm.productDispensed();
        expect(vm.getState()).toBe('WAITING');
    });
});
```