テストで身につける保守性のテクニック

# はじめに

## この記事で書きたいこと

タイトルにもあるように、テストを書くことで下記のようなことを意識した開発ができ、システムの保守性を高めることができると思っています。
- プレゼンテーション・ドメイン分離
- Dependency Injection (依存性注入)
- 参照透過性

<!-- textlint-disable -->
また、テストファーストの手法を取ることで、無意識的に上記のことをクリアできるのではないかと思っています。  

あくまで私見であり、トレードオフや足りないところもあると思います。  
<!-- textlint-enable -->

それぞれについての詳細な説明は省かせていただきますが、どれも保守性の高い設計をするには大切なことだと思っています。  
この記事では TypeScript と Jest を使ってサンプルアプリを作りながら紹介しています。

## 経緯
<!-- textlint-disable -->
私が以前いた会社は企業したてで、エンジニアの人数はまだ少ないドスタートアップでした。
要件が固まってきているフェーズで、動くものはまだ何もありませんでした。
今後仕様変更が生じる可能性は高かったことと、今後参加したエンジニアにとって読みやすく、書き換えやすいこと、さらには自身の経験の浅さから自分が書いたコードに自信を持てなかったことから、保守性の高い設計の必要性を感じ、改めて学習し始めました。
その時に学んだ事が、テストファーストで開発する事で実践しやすいと思いこの記事を書いています。
<!-- textlint-enable -->

# 実践
<!-- textlint-disable -->
実行した時間帯によって異なるメッセージを表示するサンプルアプリケーションを開発しながら紹介していこうと思います。
<!-- textlint-enable -->

## 使用技術とバージョン

- TypeScript: 4.6.X
- Jest: 28.1.X
- ts-node: 10.8.X

## サンプルアプリの仕様

実行した時間帯によって異なるメッセージを表示する。
- 0 時以降 4 時までの場合 `Good night`
- 4 時以降 12 時までの場合 `Good morning`
- 12 時以降 18 時までの場合 `Good afternoon`
- 18 時以降 21 時までの場合 `Good evening`
- 21 時以降 24 時までの場合 `Good night`

## 愚直にコードを書いてみる

まず最初はテストを書かずに愚直にコードを書いてみます。
現在の時刻の数値を取得し、分岐してそれぞれの時刻にあったメッセージを console.log で表示します。

```ts 
function main(): void {
	const hour = new Date().getHours();
	if (4 <= hour && hour < 12) {
     console.log("Good morning");
	} else if (12 <= hour && hour < 18) {
     console.log("Good afternoon");
	} else if (18 <= hour && hour < 21) {
     console.log("Good evening");
	} else if ((21 <= hour && hour < 24) || (0 <= hour && hour < 4)) {
    return "Good night";
	}

	throw new Error("Date is not defined");
}
 
main();
```

実行
`$ ts-node main.ts`  

これではインターフェースに依存しているためテストをすることが難しいです。  
動くには動きますが、本当に合っているかと言われると確信を持って YES と答えられません。

## プレゼンテーション・ドメイン分離

### Presentation
- 「表現」のこと
- ユーザーインターフェース
	- Web ブラウザ / スマートフォン / WebAPI / CLI 

### Domain
- 「領域」のこと
- ソフトウェア設計において、「そのアプリケーションが対象としてる課題そのもの」


プレゼンテーション・ドメイン分離とは、その名の通りプレゼンテーションとドメインを別々のモジュールに分割しすることです。  
それぞれのモジュールに関心ごとを閉じ込めることで、再利用性が高くなります。

参考： http://martinfowler.com/bliki/PresentationDomainSeparation.html

どのように分離するかというと、以下のようにしていきます。  
<!-- textlint-disable -->
ドメイン： 現在時刻ごとの異なるメッセージを返すだけ  
プレゼンテーション： 関数の結果を出力するだけ  
<!-- textlint-enable -->

### コード

```ts
// getMessage.ts
export function getMessage(): string {
   const hour = new Date().getHours();
 
   if (4 <= hour && hour < 12) {
     return "Good morning";
   } else if (12 <= hour && hour < 18) {
     return "Good afternoon";
   } else if (18 <= hour && hour < 21) {
     return "Good evening";
   } else if ((21 <= hour && hour < 24) || (0 <= hour && hour < 4)) {
     return "Good night";
   }
 
   throw new Error("Date is not defined");
 }

// main.ts
import { getMessage } from "./getMessage";
 
console.log(getMessage());
```

実行
`$ ts-node main.ts`  

プレゼンテーションとドメインを分離できたことで CLI 以外の環境でも関数を利用できます。

```tsx
import { getMessage } from "./getMessage";

const RectComponent: React.FC = () => {
   const message = getMessage();
 
   return (
     <div>
       <p>現在の時刻: {new Date().toISOString()}</p>
       <p>挨拶: {message}</p>
     </div>
   );
 };
```
このように React などのフロントエンドでも使えます。

しかし、これだけではまだテストを書くのが難しいままです。
なぜなら、実行する時間ごとに決まった結果へとなってしまうため、時刻が変わるのを待ってテストを実行しなければなりません。


## Dependency Injection (依存性注入)

> コンポーネント間の依存関係をプログラムのソースコードから排除するために、外部の設定ファイルなどでオブジェクトを注入できるようにするソフトウェアパターンである。
> (wikipedia参照)

https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5

モジュール内部でインスタンスを作成すると依存性が密結合になり、片方を修正するともう片方の修正も必要になってくるため、`依存性注入` を行うと片方の修正だけで済みます。

関数の内部で現在時刻を取得していましたが、その処理を外に持たせて、引数として受け取り、受け取った数値によってそれぞれの時刻のメッセージを返すように変更します。
### コード

```ts
// getMessageByHour.ts
export function getMessageByHour(hour: number): string {
  if (4 <= hour && hour < 12) {
    return "Good morning";
  } else if (12 <= hour && hour < 18) {
    return "Good afternoon";
  } else if (18 <= hour && hour < 21) {
    return "Good evening";
  } else if ((21 <= hour && hour < 24) || (0 <= hour && hour < 4)) {
    return "Good night";
  }

  throw new Error("Date is not defined");
}

// main.ts
import { getMessageByHour } from "./getMessageByHour";
  
console.log(getMessageByHour(new Date().getHours()));
```

実行
`$ ts-node main.ts`  

関数の外部で取得した現在時刻を引数として関数に与える事で、それぞれのパターンのテストが可能になります。  


## 参照透過性

同じ入力に対して同じ作用と同じ出力とを持つプログラムになることです。  
テストコードは"いつ", "どの順番で", "何度"実行しても同じ結果になるべきなので、参照透過性のあるコードがテストしやすいと言えます。  

参考： https://ja.wikipedia.org/wiki/%E5%8F%82%E7%85%A7%E9%80%8F%E9%81%8E%E6%80%A7


プレゼンテーションのコードを以下のように変更したとしても、どの時間帯であれ、何度実行しても結果は変わらなくなります。

```ts
console.log(getMessageByHour(9)); 	// "Good morning"
console.log(getMessageByHour(9)); 	// "Good morning"
console.log(getMessageByHour(9)); 	// "Good morning"
console.log(getMessageByHour(9)); 	// "Good morning"
```

## テストコード

最後にテストコードを書いていきます。  
それぞれの境界値を検証していけば十分でしょう。

```ts
import { getMessageByHour } from "./getMessageByHour";

describe("getMessageByHour.ts", () => {
  test("4時以降12時までの場合`Good morning`を返す", () => {
    expect(getMessageByHour(4)).toBe("Good morning");
    expect(getMessageByHour(11)).toBe("Good morning");
  });
  test("12時以降18時までの場合`Good afternoon`を返す", () => {
    expect(getMessageByHour(12)).toBe("Good afternoon");
    expect(getMessageByHour(17)).toBe("Good afternoon");
  });
  test("18時以降21時までの場合`Good evening`を返す", () => {
    expect(getMessageByHour(18)).toBe("Good evening");
    expect(getMessageByHour(20)).toBe("Good evening");
  });
  test("21時以降24時までの場合`Good night`を返す", () => {
    expect(getMessageByHour(21)).toBe("Good night");
    expect(getMessageByHour(23)).toBe("Good night");
  });
  test("0時以降4時までの場合`Good night`を返す", () => {
    expect(getMessageByHour(0)).toBe("Good night");
    expect(getMessageByHour(3)).toBe("Good night");
  });
});
```

テストを実行すると全て通ります。

## 最後に

今回は先に実装してからテストコードを書きましたが、テストから先に書き始めるテストファーストの手法を取っていれば初めから保守性の高いコードを実装できたはずです。  
テストコードとしては難しいことは全くしていません。単に関数の結果が、関数に渡す引数によって異なることを検証しているだけです。  

<!-- textlint-disable -->
テストを書くとなると、難しい・コストがかかると思っている方もまだいるかと思いますが、是非テストを書いて品質を高め、ついでに保守性も高めてしまいましょう！  
<!-- textlint-enable -->

最後まで読んでいただきありがとうございます。至らない箇所や間違っている箇所がありましたら是非コメントいただけたら嬉しいです。