リーダブルテストコード

# はじめに
よく言われるように、ソースコードというものは書かれることよりも読まれることの方が多く、それゆえ読みやすいコードを書くということが非常に重要です。それはテストコードにおいても同様であり、プロダクトコードと同等に資産として扱う必要があります。

テストコードは具体的な値を用いて記述し、また複数の変数の値の組み合わせでテストケースを起こすため、プロダクトコードと比べて冗長になりがちです。

書籍『リーダブルコード』の14章でもテストコードの読みやすさについて触れられていますが、本稿では読みづらいテストコードをリファクタリングして読みやすくするためのテクニックを紹介したいと思います。

なおサンプルコードはJavaScriptで記述されており、そのテストコードはJest[^1]を用いて書いています。
ソースコードは[GitHub](https://github.com/yonetty/readable_testcode)にあります。

# リファクタリング(その壱)
以下の、決して読みやすいとはいえないテストコードをリファクタリングしていきます。
Scrumによるアジャイル開発プロセスを支援するソフトウェアの一部だと思ってください。

```javascript:HardToReadTest.spec.js
describe('Sprint', () => {
    it('ストーリーが正しい', () => {
        const sprint = new Sprint(1);
        expect(sprint.id).toBe(1);
        expect(sprint.stories).toHaveLength(0);

        const story = new Story('環境構築', '開発環境をセットアップする');
        sprint.addStory(story);
        expect(sprint.stories).toHaveLength(1);
    });

    it('アサイン状況を正しく取得できる', () => {
        const sprint = new Sprint(1);

        const story1 = new Story('環境構築', '開発環境をセットアップする', 3, '井上');
        sprint.addStory(story1);
        const story2 = new Story('サンプル開発', 'サンプルコードを書く', 2, '山田');
        sprint.addStory(story2);
        const story3 = new Story('ユニットテスト', 'テストコードを書く', 1, '町田');
        sprint.addStory(story3);
        const story4 = new Story('E2E', 'E2Eテストを作成する', 2, '山田');
        sprint.addStory(story4);
        // 人別に、ポイント合計の降順でソートされる
        expect(sprint.assignment).toEqual(
            ['山田', 4],
            ['井上', 3],
            ['町田', 1],
        ]);
    });
});
```

## テスト対象を明確化する
テストにおける検証対象コンポーネントを**SUT(System Under Test)**と呼びます。一方、SUTが依存するコンポーネントを**DOC(Depended-on Component)**と呼びます。
テストケースにおける主人公であるSUTをその他の登場人物と明確に区別するため、SUTを格納する変数を`sut`と命名するのはよいプラクティスです。

```javascript
describe('Sprint', () => {
    it('ストーリーが正しい', () => {
        const sut = new Sprint(1);
        expect(sut.id).toBe(1);
        expect(sut.stories).toHaveLength(0);

        const story = new Story('環境構築', '開発環境をセットアップする');
        sut.addStory(story);
        expect(sut.stories).toHaveLength(1);
    });
    ...
})
```

## テストケースではただ一つのことを検証する
一つのテストケースにいろいろなことを盛り込まないようにしましょう。先ほどのテストケースは2つに分割します。

```javascript
describe('Sprint', () => {
    it('初期状態が正しい', () => {
        const sut = new Sprint(1);
        expect(sut.id).toBe(1);
        expect(sut.stories).toHaveLength(0);
    });

    it('ストーリーを追加できる', () => {
        const sut = new Sprint(1);
        const story = new Story('環境構築', '開発環境をセットアップする');
        sut.addStory(story);
        expect(sut.stories).toHaveLength(1);
    });
});
```

## 共通するテストフィクスチャをセットアップメソッドで作成する
**テストフィクスチャ**とはテストの事前条件を指します。具体的にはSUTの状態、DOCの状態、環境の状態などのことです。
コードの重複を取り除くため、テストフィクスチャを作成するコードはセットアップメソッドへ移動させましょう。Jestの場合、`beforeEach`メソッドとなります。

```javascript
describe('Sprint', () => {
    let sut;

    beforeEach(() => {
        sut = new Sprint(1);
    });

    it('初期状態が正しい', () => {
        expect(sut.id).toBe(1);
        expect(sut.stories).toHaveLength(0);
    });

    it('ストーリーを追加できる', () => {
        const story = new Story('環境構築', '開発環境をセットアップする');
        sut.addStory(story);
        expect(sut.stories).toHaveLength(1);
    });
    ...
});
```

## AAA(またはGive-When-Then)を意識する
テストコードは以下の4つのフェーズから成り立ちます。
1. 準備
2. 実行
3. 検証
4. 後片付け

4つ目の後片付けが必要となるのはDB・ストレージ・ネットワーク等の共有リソースを使用するテストケースが中心ですので、一般的なテストケースでは準備・実行・検証が基本的な構成要素となります。
AAA(トリプルエー)とは、この3つをそれぞれ**Arrange**/**Act**/**Assert**と呼んでその頭文字を取ったものです。BDD[^2]流ならば**Given-When-Then**と呼ぶでしょう。
テストコードを書く際はこの3つを意識し、コメントを入れて論理分割すると見通しがよくなります。

```javascript
describe('Sprint', () => {
    ...
    it('ストーリーを追加できる', () => {
        // Arrange
        const story = new Story('環境構築', '開発環境をセットアップする');
        // Act
        sut.addStory(story);
        // Assert
        expect(sut.stories).toHaveLength(1);
    });
})
```

## 振舞いに影響を与える値とそうでない値を区別する
以下のテストケースを見ましょう。

```javascript
describe('Sprint', () => {
    ...
    it('アサイン状況を正しく取得できる', () => {
        const sprint = new Sprint(1);

        const story1 = new Story('環境構築', '開発環境をセットアップする', 3, '井上');
        sprint.addStory(story1);
        const story2 = new Story('サンプル開発', 'サンプルコードを書く', 2, '山田');
        sprint.addStory(story2);
        const story3 = new Story('ユニットテスト', 'テストコードを書く', 1, '町田');
        sprint.addStory(story3);
        const story4 = new Story('E2E', 'E2Eテストを作成する', 2, '山田');
        sprint.addStory(story4);
        // 人別に、ポイント合計の降順でソートされる
        expect(sprint.assignment).toEqual([
            ['山田', 4],
            ['井上', 3],
            ['町田', 1],
        ]);
    });
});
```

`Story`は複数の属性を持ちます(タイトル、記述、ポイント、担当者)が、この中でテスト対象の振舞いである`sprint.assignment`(アサイン状況)に影響を与えるのはポイントと担当者です。それ以外の属性の値はこのテストケースにおいてはノイズとなります。
このような属性値には`'any'`といった値を設定することで、本当に関心のある属性値を際立たせることが可能です。

```javascript
describe('Sprint', () => {
    ...
    it('アサイン状況を正しく取得できる', () => {
        // Arrange
        const story1 = new Story('any', 'any', 3, '井上');
        sut.addStory(story1);
        const story2 = new Story('any', 'any', 2, '山田');
        sut.addStory(story2);
        const story3 = new Story('any', 'any', 1, '町田');
        sut.addStory(story3);
        const story4 = new Story('any', 'any', 2, '山田');
        sut.addStory(story4);
        // Act
        const assignment = sut.assignment;
        // Assert
        // 人別に、ポイント合計の降順でソートされる
        expect(assignment).toEqual([
            ['山田', 4],
            ['井上', 3],
            ['町田', 1],
        ]);
    });
});
```

## 生成メソッドを活用する
しかしながら、`'any'`という値自体も少し邪魔ですね。`new`演算子で直接オブジェクトを生成するのではなく、オブジェクトを生成するヘルパーメソッドを用意しましょう。

`Story`オブジェクトを生成する関数を定義します。

```javascript
const defaults = (val, defaultVal) => val === undefined ? defaultVal : val;

const aStory = ({title, description, point, asignee} = {}) =>
    new Story(
        defaults(title, 'any'),
        defaults(description, 'any'),
        defaults(point, 0),
        defaults(asignee, 'any')
    );
```

引数に指定したオブジェクトに`Story`コンストラクタの属性がある場合はそれを、そうでなければデフォルト値を使って`Story`オブジェクトを生成するように実装しています。
このヘルパーメソッドを利用すると、前述のテストケースは以下のように書き直すことができます。

```javascript
describe('Sprint', () => {
    ...
    it('アサイン状況を正しく取得できる', () => {
        // Arrange
        const story1 = aStory({point: 3, asignee: '井上'});
        const story2 = aStory({point: 2, asignee: '山田'});
        const story3 = aStory({point: 1, asignee: '町田'});
        const story4 = aStory({point: 2, asignee: '山田'});
        [story1, story2, story3, story4].forEach(s => sut.addStory(s));
        // Act
        const assignment = sut.assignment;
        // Assert
        // 人別に、ポイント合計の降順でソートされる
        expect(assignment).toEqual([
            ['山田', 4],
            ['井上', 3],
            ['町田', 1],
        ]);
    });
});
```

ノイズが減り、少し読みやすくなったのではないでしょうか。

## 計算結果ではなく計算式を記述する
先ほどのコードの`Assert`の部分を抜き出します。

```javascript
        expect(assignment).toEqual([
            ['山田', 4],
            ['井上', 3],
            ['町田', 1],
        ]);
```

山田さんの4(ポイント)は、`Arrange`でセットした2つのストーリーのポイントの合計値です。

```javascript
        const story1 = aStory({point: 3, asignee: '井上'});
        const story2 = aStory({point: 2, asignee: '山田'}); // ←これ
        const story3 = aStory({point: 1, asignee: '町田'});
        const story4 = aStory({point: 2, asignee: '山田'}); // ←これ
``` 

ですが、その情報はテストコードからは失われています。以下のように、計算した結果ではなく計算式として記述することで意図がより明確となります。

```javascript
        expect(assignment).toEqual([
            ['山田', (2 + 2)], // 計算式で記述する
            ['井上', 3],
            ['町田', 1],
        ]);
```

さらに、以下のように定数を使用することでもっと見通しがよくなるでしょう。

```javascript
    it('アサイン状況を正しく取得できる', () => {
        // Arrange
        const [pt1, pt2, pt3, pt4] = [3, 2, 1, 2]; // 定数化
        const story1 = aStory({point: pt1, asignee: '井上'});
        const story2 = aStory({point: pt2, asignee: '山田'});
        const story3 = aStory({point: pt3, asignee: '町田'});
        const story4 = aStory({point: pt4, asignee: '山田'});
        [story1, story2, story3, story4].forEach(s => sut.addStory(s));
        // Act
        const assignment = sut.assignment;
        // Assert
        // 人別に、ポイント合計の降順でソートされる
        expect(assignment).toEqual([
            ['山田', (pt2 + pt4)],
            ['井上', pt1],
            ['町田', pt3],
        ]);
    });
```

## テストケース名を雄弁にする
先ほどのテストケースの、以下のコメントについて。

```javascript
        // 人別に、ポイント合計の降順でソートされる
        expect(assignment).toEqual([
            ['山田', (pt2 + pt4)],
            ['井上', pt1],
            ['町田', pt3],
        ]);
```

これは、テストケース名でちゃんと表現してあげましょう。

```javascript
    it('アサイン状況は人別にポイントが表示され、ポイント合計の降順でソートされる', () => {
        // Arrange
        const [pt1, pt2, pt3, pt4] = [3, 2, 1, 2];
        const story1 = aStory({point: pt1, asignee: '井上'});
        const story2 = aStory({point: pt2, asignee: '山田'});
        const story3 = aStory({point: pt3, asignee: '町田'});
        const story4 = aStory({point: pt4, asignee: '山田'});
        [story1, story2, story3, story4].forEach(s => sut.addStory(s));
        // Act
        const assignment = sut.assignment;
        // Assert
        expect(assignment).toEqual([
            ['山田', (pt2 + pt4)], //4ポイント
            ['井上', pt1], // 3ポイント
            ['町田', pt3], // 1ポイント
        ]);
    });
```

# リファクタリング(その弐)
`Story`に対する以下のテストコードをリファクタリングします。

```javascript
describe('Story', () => {
    it('未アサインは開始不可', () => {
        const story = new Story('環境構築', '開発環境をセットアップする', 3, null);
        expect(story.canBeStarted).toBe(false);
    });
    it('ポイントを振ってない場合は開始不可', () => {
        const story = new Story('環境構築', '開発環境をセットアップする', 0, '井上');
        expect(story.canBeStarted).toBe(false);
    });
    it('ポイントもアサインも入ってない場合は開始不可', () => {
        const story = new Story('環境構築', '開発環境をセットアップする', 0, null);
        expect(story.canBeStarted).toBe(false);
    });
    it('ポイントもアサインも入っている場合は開始可', () => {
        const story = new Story('環境構築', '開発環境をセットアップする', 3, '井上');
        expect(story.canBeStarted).toBe(true);
    });
});
```

いったん、これまでに述べたリファクタリングテクニックを適用します。

```javascript
describe('Story', () => {
    it('未アサインは開始不可', () => {
        // Arrange
        const sut = aStory({point: 3, asignee: null});
        // Act
        const canBeStarted = sut.canBeStarted;
        // Assert
        expect(canBeStarted).toBe(false);
    });
    it('ポイントを振ってない場合は開始不可', () => {
        // Arrange
        const sut = aStory({point: 0, asignee: '山田'});
        // Act
        const canBeStarted = sut.canBeStarted;
        // Assert
        expect(canBeStarted).toBe(false);
    });
    it('ポイントもアサインも入ってない場合は開始不可', () => {
        // Arrange
        const sut = aStory({point: 0, asignee: null});
        // Act
        const canBeStarted = sut.canBeStarted;
        // Assert
        expect(canBeStarted).toBe(false);
    });
    it('ポイントもアサインも入っている場合は開始可', () => {
        // Arrange
        const sut = aStory({point: 3, asignee: '山田'});
        // Act
        const canBeStarted = sut.canBeStarted;
        // Assert
        expect(canBeStarted).toBe(true);
    });
});
```

## パラメータ化テストを適用する
前述の4つのテストケースはとても似通っていて冗長な印象があります。パラメータ化テスト(Parameterized Test)というテスティングパターンを使ってすっきりまとめてみましょう。

```javascript
describe('Story', () => {
    test.each`
        point | asignee  | expected | desc
         ${3} | ${null}  | ${false} | ${'未アサインは開始不可'}
         ${0} | ${'山田'} | ${false} | ${'ポイントを振ってない場合は開始不可'}
         ${0} | ${null}  | ${false} | ${'ポイントもアサインも入ってない場合は開始不可'}
         ${3} | ${'山田'} | ${true}  | ${'ポイントもアサインも入っている場合は開始可'}
    `("開始可能か: $desc", ({point, asignee, expected}) => {
        // Arrange
        const sut = aStory({point, asignee});
        // Act
        const canBeStarted = sut.canBeStarted;
        // Assert
        expect(canBeStarted).toBe(expected);
    });
})
```

`test.each` の部分は**タグ付きテンプレートリテラル**[^3]を使って記述しており、Jestが提供するDSL[^4]です。テンプレートリテラル中の2行目以降の各行がテストケースに展開されます。
例えば2行目は以下のテストケースと同等ということになります。

```javascript
    // point = 3, asignee = null, expected = false
    it('開始可能か: 未アサインは開始不可', () => {
        // Arrange
        const sut = aStory({point: 3, asignee: null});
        // Act
        const canBeStarted = sut.canBeStarted;
        // Assert
        expect(canBeStarted).toBe(false);
    });
```

# 最終的なコード
リファクタリング後のテストコードは以下となります。

```javascript:ReadableTest.spec.js
const {Sprint, Story} = require('../entities');

const defaults = (val, defaultVal) => val === undefined ? defaultVal : val;

const aSprint = ({id, description} = {}) =>
    new Sprint(
        defaults(id, 1),
        defaults(description, 'any')
    );

const aStory = ({title, description, point, asignee} = {}) =>
    new Story(
        defaults(title, 'any'),
        defaults(description, 'any'),
        defaults(point, 0),
        defaults(asignee, 'any')
    );

describe('Sprint', () => {
    let sut;

    beforeEach(() => {
        sut = aSprint();
    });

    it('初期状態が正しい', () => {
        // Assert
        expect(sut.id).toBe(1);
        expect(sut.stories).toHaveLength(0);
    });

    it('ストーリーを追加できる', () => {
        // Arrange
        const story = new Story('環境構築', '開発環境をセットアップする');
        // Act
        sut.addStory(story);
        // Assert
        expect(sut.stories).toHaveLength(1);
    });

    it('アサイン状況は人別にポイントが表示され、ポイント合計の降順でソートされる', () => {
        // Arrange
        const [pt1, pt2, pt3, pt4] = [3, 2, 1, 2];
        const story1 = aStory({point: pt1, asignee: '井上'});
        const story2 = aStory({point: pt2, asignee: '山田'});
        const story3 = aStory({point: pt3, asignee: '町田'});
        const story4 = aStory({point: pt4, asignee: '山田'});
        [story1, story2, story3, story4].forEach(s => sut.addStory(s));
        // Act
        const assignment = sut.assignment;
        // Assert
        expect(assignment).toEqual([
            ['山田', (pt2 + pt4)], //4ポイント
            ['井上', pt1], // 3ポイント
            ['町田', pt3], // 1ポイント
        ]);
    });
});

describe('Story', () => {
    test.each`
        point | asignee  | expected | desc
         ${3} | ${null}  | ${false} | ${'未アサインは開始不可'}
         ${0} | ${'山田'} | ${false} | ${'ポイントを振ってない場合は開始不可'}
         ${0} | ${null}  | ${false} | ${'ポイントもアサインも入ってない場合は開始不可'}
         ${3} | ${'山田'} | ${true}  | ${'ポイントもアサインも入っている場合は開始可'}
    `("開始可能か: $desc", ({point, asignee, expected}) => {
        // Arrange
        const sut = aStory({point, asignee});
        // Act
        const canBeStarted = sut.canBeStarted;
        // Assert
        expect(canBeStarted).toBe(expected);
    });
});
```

参考までに、プロダクトコードは以下となります。

```javascript:entities.js
class Sprint {
    constructor(id, description) {
        this.id = id;
        this.description = description;
        this.stories = [];
    }

    addStory (story) {
        this.stories.push(story);
    }

    get assignment () {
        const perAsignee = this.stories.reduce((map, story) => {
            const key = story.asignee;
            if (map.has(key)) {
                map.set(key, map.get(key) + story.point);
            } else {
                map.set(key, story.point);
            }
            return map;
        }, new Map());
        return Array.from(perAsignee).sort((s1, s2) => s2[1] - s1[1]);
    }
}

class Story {
    constructor(title, description, point = 0, asignee) {
        this.title = title;
        this.description = description;
        this.point = point;
        this.asignee = asignee;
    }

    get canBeStarted () {
        return Boolean(this.asignee) && this.point > 0;
    }
}

module.exports.Sprint = Sprint;
module.exports.Story = Story;
```

# まとめ
テストコードが散らかっていき可読性が低下すると、テストコードの信頼性が失われ、ゆくゆくはメンテナンスされなくなってしまうリスクがあります。**テストコードの負債化**は、プロダクトコードの品質やメンテナンス性に大きな影響を与える由々しき問題です。
そのような状況の発生を防止するため、テストコードの可読性の大切さを知り、本稿で紹介したようなテクニックを用いて日々リファクタリングをしていくことを心がけましょう。

# 参考文献
* Dustin Boswell, Trevor Foucher 『The Art of Readable Code』 (O'REILLY, 2011)
* Gerard Meszaros 『xUnit Test Patterns』 (Addison-Wesley, 2007)

[^1]: Facebook社主導で開発されているオープンソースのテスティングフレームワーク
[^2]: 振舞い駆動開発(Behavior Driven Development)
[^3]: ECMAScript 6(ES6)で導入された機能
[^4]: ドメイン固有言語(Domain Specific Language)