> For the complete documentation index, see [llms.txt](https://andmorefine.gitbook.io/learn-go-with-tests/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://andmorefine.gitbook.io/learn-go-with-tests/meta/why.md).

# なぜユニットテストなのか、そしてユニットテストを役立てる方法

なぜユニットテストなのか、そしてユニットテストを役立てる方法

[この話題について雑談している私の動画へのリンクです](https://www.youtube.com/watch?v=Kwtit8ZEK7U)

動画が苦手な方はこちらの文章版。

## ソフトウェア

ソフトウェアが約束してくれるのは、変更できるということです。だからこそ *ソフト* ウェアと呼ばれるのであり、ハードウェアと比べて柔軟に形を変えられます。優れたエンジニアリング・チームは、価値を提供し続けるために、ビジネスと共に進化できるシステムを書き、会社にとって素晴らしい資産となるはずです。

では、なぜ私たちはそれが苦手なのでしょうか？完全に失敗したプロジェクトの話を、どれだけ耳にするでしょうか？ あるいは「レガシー」になって、全面的に書き直さなければならなくなる（そしてその書き直しも、しばしば失敗します！）という話は？

そもそも、ソフトウェアシステムはどうやって「失敗」するのでしょうか？ 正しくなるまで変更し続ければいいだけではないのでしょうか？ そう約束されていたはずです！

多くの人がシステムの構築にGoを選んでいるのは、Goがレガシー化しにくくなることが期待できるような選択を数多くしているからです。

* 私が以前使っていたScala（[首を吊るのに十分なロープがある、と説明したことがあります](http://www.quii.dev/Scala_-_Just_enough_rope_to_hang_yourself)）と比べると、Goのキーワードはわずか25個で、標準ライブラリといくつかの小さなライブラリだけで *非常に多くの* システムを構築できます。Goなら、コードを書いて半年後に戻ってきても、まだ理解できるだろうと期待できます。
* テスト、ベンチマーク、リント、出荷に関するツールは、他の多くの選択肢と比較しても一級品です。
* 標準ライブラリは素晴らしいです。
* タイトなフィードバックループのための非常に速いコンパイル速度
* Goの下位互換性の約束。Goは将来的にジェネリックやその他の機能を手に入れるようですが、設計者は5年前に書いたGoのコードでもビルドできると約束しています。私は文字通り、プロジェクトをScala 2.8から2.10にアップグレードするのに数週間を費やしました。

これだけの素晴らしい特性があっても、ひどいシステムを作ってしまうことはあり得ます。

ですから、私たちは過去に目を向けて、あなたの言語がどれだけ輝いていても（あるいは輝いていなくても）適用されるソフトウェア工学の教訓を理解しなければなりません。

1974年、[Manny Lehman](https://en.wikipedia.org/wiki/Manny_Lehman_%28computer_scientist%29)という賢いソフトウェアエンジニアが[リーマンのソフトウェア進化の法則](https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution)を書きました。

> この法則は、一方では新しい開発を推進する力と、他方では進歩を遅らせる力との間のバランスを記述している。

これらの力は、レガシーになって何度も何度も書き直されるシステムを出荷するという終わりのないサイクルに陥らないようにするためには、理解しておくべき重要なことのように思われます。

## 継続的変化の法則

> 実世界で使用されるソフトウェアシステムは、変化し続けなければ、その環境においてますます有用でなくなっていく

システムは変化 *しなければならず* 、さもなければ有用性が下がっていくというのは自明に思えますが、これはどれほど頻繁に無視されているでしょうか？

多くのチームは、プロジェクトを特定の期日までに完了させ、次のプロジェクトに移るようにインセンティブを与えられています。

ソフトウェアが「幸運」なら、少なくとも保守を担当する別の人たちへ何らかの引き継ぎがありますが、もちろん彼らはそれを書いた本人ではありません。

人々はしばしば、「素早く納品」できるフレームワーク選びには気を配りますが、システムがどう進化していく必要があるかという観点での、システムの寿命には目を向けていません。

たとえあなたが素晴らしいソフトウェア・エンジニアであっても、システムの将来のニーズを知り得ないという問題からは逃れられません。 ビジネスが変化すると、あなたが書いた素晴らしいコードのいくつかは、もはや関連性がないものになってしまいます。

リーマンは70年代に絶好調で、私たちがじっくり考えるべき法則をもう1つ与えてくれました。

## 複雑さが増す法則

> システムが進化すると、それを減らすための作業が行われない限り、その複雑さは増加します。

彼がここで言っているのは、長期的に生き残ることを願ってソフトウェアにどんどん機能を積み上げていくだけの、盲目的な機能工場のようなソフトウェアチームではいけない、ということです。

私たちは、ドメインの知識が変化するにつれて、システムの複雑さを管理し続け **なければなりません** 。

## リファクタリング♪

ソフトウェアの柔軟性を保つためのソフトウェアエンジニアリングの側面は、次のように *たくさん* あります。

* 開発者のエンパワーメント
* 一般的に「良い」コード。関心の適切な分離など
* コミュニケーション能力
* アーキテクチャ
* 可観測性
* デプロイアビリティ
* 自動化されたテスト
* フィードバックループ

今回はリファクタリングに焦点を当ててみたいと思います。「これはリファクタリングが必要だ」というのはよく飛び交うフレーズで、プログラミングを始めて初日の開発者に対してさえ、深く考えずに口にされます。

このフレーズはどこから来たのでしょうか？リファクタリングはコードを書くこととどう違うのでしょうか？

私も、そして他の多くの人も、リファクタリングをしていると *思っていた* のに、実は間違っていました。

[マーティン・ファウラーは、人々がどのようにしてそれを間違っているかを説明しています](https://martinfowler.com/bliki/RefactoringMalapropism.html)

> しかし、「リファクタリング」という言葉は、適切でない場面でよく使われます。もし誰かが、リファクタリングをしている間にシステムが数日間壊れていると話していたら、その人はリファクタリングをしていないとほぼ確信してよいでしょう。

では、リファクタリングとは何でしょうか？

### 因数分解

学校で数学を習っているとき、因数分解について学んだことがあるでしょう。とても簡単な例を挙げてみましょう。

`1/2 + 1/4`を計算する

これを行うには、分母を *因数分解* して、式を

`2/4 + 1/4`に変えます。そうすると`3/4`にできます。

このことから、いくつかの重要な教訓を得ることができます。 *式を因数分解* するとき、 **式の意味は変えていません** 。どちらも`3/4`に等しいですが、私たちにとって扱いやすくなりました。`1/2`を`2/4`に変えることで、私たちの「ドメイン」にフィットしやすくなったのです。

コードをリファクタリングするとき、あなたは自分のコードをより理解しやすくし、システムが何をする必要があるかについての現在の理解に「適合」させる方法を見つけようとしています。重要なのは、**動作を変更してはいけないということです。**

#### Goでの例

特定の`language`で`name`に挨拶する関数は以下の通りです。

```
func Hello(name, language string) string {

  if language == "es" {
     return "Hola, " + name
  }

  if language == "fr" {
     return "Bonjour, " + name
  }

  // imagine dozens more languages

  return "Hello, " + name
}
```

何十個もの`if`文があるのは気持ちの良いものではありませんし、言語固有の挨拶を`,`と`name.`で連結するという重複があるので、コードをリファクタリングしてみます。

```
func Hello(name, language string) string {
      return fmt.Sprintf(
          "%s, %s",
          greeting(language),
          name,
      )
}

var greetings = map[string]string {
  "es": "Hola",
  "fr": "Bonjour",
  //etc..
}

func greeting(language string) string {
  greeting, exists := greetings[language]

  if exists {
     return greeting
  }

  return "Hello"
}
```

このリファクタリングの性質は実際には重要ではなく、重要なのは私が動作を変更していないことです。

リファクタリングでは、インターフェースの追加、新しい型の追加、関数、メソッドの追加など、好きなことをすることができます。唯一のルールは、動作を変更しないことです。

### コードをリファクタリングする際には、挙動を変えてはいけません。

これはとても重要なことです。同時に振る舞いも変えているなら、一度に *2つ* のことをしていることになります。ソフトウェアエンジニアとして、私たちはシステムを別々のファイル、パッケージ、関数などに分割することを学びます。大きな塊を理解しようとするのは難しいとわかっているからです。

一度にたくさんのことを考えなければならない状況は避けたいものです。そういうときにこそ間違いを犯すからです。開発者が手に負えないほど多くのことを抱え込んだために、リファクタリングの試みが失敗するのを、私は何度も目の当たりにしてきました。

数学の授業で因数分解を紙とペンでやっていたときは、式の意味を変えていないかどうかを頭の中で手作業で確認しなければなりませんでした。コードを扱う場合、特に単純ではないシステムで、リファクタリング時に振る舞いを変えていないことをどうやって知ればよいのでしょうか？

テストを書かないことを選択した人は、典型的には手動テストに依存することになります。小さなプロジェクト以外では、これはとてつもなく時間を浪費することになり、長期的にはスケールしません。

**安全にリファクタリングを行うためには、ユニットテストが必要です**。ユニットテストは次のものを与えてくれるからです。

* 振る舞いが変わることを心配せずにコードを作り変えられるという自信
* システムがどう振る舞うべきかについての、人間向けのドキュメント
* 手動テストよりもはるかに速く、信頼性の高いフィードバック

#### Goでの例

`Hello`関数のユニットテストは次のようになります。

```
func TestHello(t *testing.T) {
  got := Hello(“Chris”, es)
  want := "Hola, Chris"

  if got != want {
     t.Errorf("got %q want %q", got, want)
  }
}
```

コマンドラインで`go test`を実行して、リファクタリングの努力が挙動を変えたかどうかのフィードバックをすぐに得ることができます。 実際には、エディタ/IDE内でテストを実行するための魔法のボタンを覚えるのがベストです。

次のような進め方ができる状態を目指しましょう。

* 小さなリファクタ
* テストの実行
* リピート

これらすべてを非常に短いフィードバックループの中で行うことで、深みにはまってミスをすることを防げます。

すべての主要な動作がユニットテストされ、1秒をはるかに下回る時間でフィードバックが得られるプロジェクトを持つことは、必要なときに大胆なリファクタリングを行うための非常に強力なセーフティネットとなります。これは、リーマンが説明するような、押し寄せてくる複雑さの力に対処するのに役立ちます。

## ユニットテストがそれほど素晴らしいものであるならば、なぜユニットテストを書くことに抵抗が生じることがあるのでしょうか？

一方には、（私のように）ユニットテストはシステムの長期的な健全性にとって重要だと言う人がいます。ユニットテストがあれば、自信を持ってリファクタリングし続けられるからです。

他方には、ユニットテストが実際にはリファクタリングを *妨げた* という経験を語る人がいます。

自問自答してみてください。リファクタリングを行う際に、どれくらいの頻度でテストを変更しなければならないのでしょうか？ 私は長年にわたり、非常に良いテストカバレッジを持つ多くのプロジェクトに参加してきましたが、エンジニアたちはテストを変更する手間を嫌って、リファクタリングに消極的でした。

これは私たちが約束されていたことと正反対です！

### なぜこんなことが起きるのでしょうか？

正方形の開発を依頼されて、それを達成するための最良の方法は、2つの三角形をくっつけることだと考えたとします。

![直角三角形を2つくっつけて正方形を作る](https://i.imgur.com/ela7SVf.jpg)

正方形に対するユニットテストを書いて辺が等しいことを確認し、さらに三角形に対するテストも書きます。三角形が正しく描画されることを確認したいので、角度の合計が180度であることをアサートしたり、三角形を2つ作っていることを確認したりします。テストカバレッジは本当に重要ですし、これらのテストを書くのはとても簡単なので、書かない手はありませんよね？

数週間後、「継続的変更の法則」がシステムを襲い、新しい開発者がいくつかの変更を行いました。彼女は、2つの三角形ではなく、2つの長方形で正方形を形成した方が良いと考えています。

![2つの長方形で正方形を作る](https://i.imgur.com/1G6rYqD.jpg)

彼女はこのリファクタリングをしようとすると、失敗した多数のテストから紛らわしいシグナルを受け取ります。彼女は実際にここで重要な動作を壊してしまったのでしょうか？ 彼女は三角形のテストを掘り下げて、何が起こっているのかを理解しようとしなければなりません。

*正方形が三角形から作られていることは、実は重要ではありません* 。しかし、 **私たちのテストは実装の詳細の重要性を不当に高めてしまっていた** のです。

## 実装の詳細よりも振る舞いをテストすることを優先する

ユニットテストについて文句を言う人の話を聞くと、それはテストの抽象化レベルが間違っているからだということがよくあります。彼らは、実装の詳細をテストしたり、協調するオブジェクトを過度にスパイしたり、過剰にモックしたりしています。

私は、ユニットテストとは何かを誤解して、見栄えのためだけの指標（テストカバレッジ）を追いかけていることに起因すると考えています。

「振る舞いだけをテストせよ」と言うのであれば、システムテスト/ブラックボックステストだけを書けばよいのではないでしょうか？ この種のテストは主要なユーザージャーニーを検証するという点では大きな価値がありますが、一般に書くのにコストがかかり、実行にも時間がかかります。

そのため、フィードバックループが遅く、 *リファクタリング* にはあまり役に立ちません。さらに、ブラックボックステストは、ユニットテストと比較して根本原因についてはあまり役に立たない傾向があります。

では、 *正しい* 抽象化レベルとは何でしょうか？

## 効果的なユニットテストを書くことは設計上の問題です。

テストのことはちょっと忘れて、システム内には、ドメイン内の重要な概念を中心とした、自己完結的で分離された「ユニット」があることが望ましいです。

私は、これらのユニットを単純なレゴブロックのように想像したいのですが、このレゴブロックには首尾一貫した API があり、他のブロックと組み合わせてより大きなシステムを作ることができます。これらのAPIの下には、何十ものもの（型や関数など）があり、それらが必要なように動作するように連携している可能性があります。

例えば、Goで銀行を書いている場合、"account"パッケージがあるかもしれません。これは、実装の詳細を漏らさず、統合しやすいAPIを提供します。

こうした性質を備えたユニットがあれば、それらの公開APIに対してユニットテストを書くことができます。 *定義上* 、これらのテストは有用な振る舞いしかテストし得ません。 これらのユニットの下では、必要に応じて実装を自由にリファクタリングすることができますし、ほとんどの部分でテストが邪魔になることはありません。

### これらはユニットテストですか？

**はい**です。ユニットテストは、私が説明したように「ユニット」に対して行われます。ユニットテストは、単一のクラス/関数/何かだけを対象にするものでは *決して* ありませんでした。

## これらの概念をまとめる

ここまで次のことを扱ってきました。

* リファクタリング
* ユニットテスト
* ユニット設計

見えてくるのは、ソフトウェア設計のこれらの側面が互いに補強し合っているということです。

### リファクタリング♪

* ユニットテストについてのシグナルを提供してくれます。手動チェックをしなければならないなら、より多くのテストが必要です。テストが間違って失敗している場合、テストは間違った抽象化レベルにあります(または、価値がないので削除されるべきです)。
* ユニット内とユニット間の複雑さを処理するのに役立ちます。

### ユニットテスト

* リファクタリングのためのセーフティネットを提供する。
* ユニットの動作を検証し、文書化する。

### (よく設計された)ユニット

* *意味のある* ユニットテストを簡単に書ける。
* リファクタリングが簡単。

複雑さを管理し、システムを柔軟に保つために、コードを常にリファクタリングできるようなポイントに到達するのを助けるプロセスはありますか？

## なぜテスト駆動開発(TDD)なのか？

ソフトウェアは変化しなければならないというリーマンの言葉を受けて、手の込んだ設計を考えすぎ、「完璧な」拡張可能なシステムを作ろうと最初に多くの時間を浪費し、結局間違ったものを作ってどこにもたどり着けない、という人もいるかもしれません。

これは昔のソフトウェアの悪い時代の話で、アナリストチームが半年間かけて要件書を書き、アーキテクトチームがさらに半年間かけて設計を考え、数年後にはプロジェクト全体が失敗してしまうというものです。

「悪しき昔」と言いましたが、このようなことは今でも起こっています！

アジャイルでは、ソフトウェアの設計や、それが実際のユーザーに対してどう機能するかについて素早いフィードバックを得るために、小さく始めてソフトウェアを進化させ、反復的に作業する必要があることを教えてくれます。

TDD はこのアプローチを強制します。TDD は、常にリファクタリングを行い、反復的に納品する方法論を奨励することで、リーマンが語った法則や、歴史の中で学んだその他の教訓に対応しています。

### 小さな一歩を踏み出す

* 少量の望ましい動作のための小さなテストを書く
* 明確なエラーでテストが失敗することを確認する（赤）
* テストを通過させるための最小限のコードを書く（緑）。
* リファクタリング
* リピート

慣れてくれば、この作業の進め方は自然で素早いものになります。

このフィードバックループにそれほど時間がかからないことを期待するようになり、システムが「緑」ではない状態になると、ウサギの穴に落ちているかもしれないという不安を感じるようになります。

テストからのフィードバックに支えられながら、小さく有用な機能を常に安心して駆動していけるようになります。

## まとめ

* ソフトウェアの強みは、我々がそれを変えることができるということです。 *ほとんどの* ソフトウェアは、予測不可能な方法で時間の経過とともに変化を必要とします。しかし、将来を予測するのはあまりにも難しいので、過剰なエンジニアリングはしないでください。
* その代わりに、ソフトウェアの柔軟性を保てるようにする必要があります。ソフトウェアを変更するためには、進化に合わせてリファクタリングしなければなりません。さもないと、めちゃくちゃになってしまいます。
* 良いテストスイートがあれば、より速く、ストレスの少ない形でリファクタリングできます。
* 良いユニットテストを書くことは設計上の問題なので、レゴブロックのように統合できる意味のあるユニットを持つようにコードを構造化することを考えてください。
* TDDは、うまく因数分解（ファクタリング）されたソフトウェアを、今後の作業を助けるテストに支えられながら反復的に設計する手助けをし、またそうするよう強制してくれます。
