0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TDD入門】Hello, World!から学ぶテスト駆動開発の基本

Last updated at Posted at 2025-12-04

Hello, World!から学ぶテスト駆動開発

記事の目的

プログラマなら一度は書いたことがあるであろうHello, World!をprintするだけのプログラム…。
この簡単なプログラムを例にとって、テスト駆動開発で重要な考え方を整理したい。

テスト駆動開発とは

テスト駆動開発とは以下のサイクルを繰り返す開発方法である。

  1. RED: テストコードを作成し、失敗することを確認する
  2. GREEN: テスト対象のコードを実装/修正をして、テストが通過することを確認する
  3. REFACTOR: テスト対象のコード/テストコードを改善する

まず、特に重要な部分がテストコードの実装である。
なぜなら、テストコードとは「こうあるべき」という関数の仕様を表すものだからだ。
REDのステップでは、「このような文字列を返して欲しい」「演算結果はこの数値であって欲しい」というゴールを決める。
次に、このゴールを達成するためのコードを実装する。
どんなに汚い実装でも構わない。
重複する処理が関数として切り出されていなくても問題無い。
とにかく、テストコードの結果がGREENとなれば良い。
テストが通過したら、最後はRefactorである。
ここまでで実装したコードはきっと汚い。
しかし安心してほしいことは、テストコードは変わらないため「関数の入出力はこうあるべき」という指標は見えているという点だ。
だから関数が壊れることを恐れずに、安心してリファクタを進めることができるのである。

具体例: Hello, World!から学ぶテスト駆動開発

ここで例として取り上げる言語はGoである。
Goを知らない方でもわかるように整理していくため、「Goを知らないこと」がノイズにならないよう心掛ける。

まず最も簡単なHello, World!をprintするだけのプログラムを書こう。
Goではmain関数を定義することでプログラムのエントリーポイントとなる。
適当なディレクトリにhello.goというファイルを作成しよう。

"Hello, World!"と出力するだけの関数
package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}

さて、このmain関数が正しく動いているかをテストしたい。
このプログラムを実行するとコンソールにHello, World!と表示される。
しかし、どうやってテストしよう…と考えたとき、実はこのままだとテストが難しいことに気づく。
なぜなら、このmain関数は主要な機能と副次的な機能が組み合わさっているためだ。

ドメインと副作用という概念

ここで主要な機能を「ドメイン」、副次的な機能を「副作用」と呼ぶ。

  • ドメイン
    • プログラムが解決したい目的そのもののこと
    • 何かの文字列を返す、何かを演算する…などの純粋な処理であることが多い
    • この例では"Hello, World!"を返す文字列そのものがドメインに該当する
  • 副作用
    • 外界との接続のこと
    • 例えば以下
      • 標準出力をする
      • ファイルを読み書きする
      • ネットワークを介してデータを送受信する
      • データベースにアクセスする

副作用はテストがしにくいって話

標準出力をテストすることは容易ではない。目視確認をする必要が出てくる。
したがって、ドメインと副作用は分離してテストをする必要性が出てくる。
副作用となる既存の関数fmt.Println()の挙動は信じて、自分が実装した関数のテストにのみ集中しようというわけだ。
また、ドメインと副作用を分けて実装することで、出力先をコンソールからファイルに変更することも容易に行えるという利点もある。

RED

では、まずテストが失敗するテストコードを書く。
hello.goと同じディレクトリにhello_test.goというファイルを作成する。
GoではXX.goのテストをしたいときは、XX_test.goという命名をすることが義務付けられている。

失敗するテストコード
package main

import "testing"

func TestHello(t *testing.T) {
	got := hello("World")
	want := "Hello, World!"
	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}

引数のt *testing.Tなど細かいことは気にしないでほしい。
重要なのは以下の点である。

  1. 変数gotに実際の値を代入する
  2. 変数wantに期待する値を代入する
  3. gotwantが等しいかを確認する
    • 等しくなければ、テストが失敗する
    • 等しい場合は、テストが成功する

テストコードの中で、未定義のhello関数を呼び出していることに気づく。
これはまさにドメインと対応している関数である。
got, wantを比較すると、この関数は文字列Worldを引数として受け取って、文字列Hello, World!を返すという処理を期待していることがわかる。

さて、このテストを実行すると当然失敗する。
当然だ。hello.goにはhello関数が定義されていないからだ。

テスト結果
.\hello_test.go:6:9: undefined: hello
FAIL helloWorld [build failed]

だが、これでいいのだ。
「この関数はこうあるべき」という思いを込めた失敗するテストコードを書くだけで、最初のREDステップは達成される。

GREEN

次にテストを通過するコードを書く。
まずは上記のテスト結果を参考に、hello.goを修正する。
Goではテスト結果をみて、指摘事項を直していくだけでGREENとなる。
まずundefined: helloと指摘されているため、hello.gohello関数を定義する。

通過するテストコード step.1
package main
import "fmt"

// ドメイン
func hello() string {
	return "Hello, World!"
}

// 副作用
func main() {
	fmt.Println(hello())
}

しかしこれではまだ、テストが失敗する。

テスト結果その2
.\hello_test.go:6:15: too many arguments in call to hello
        have (string)
        want ()
FAIL    helloWorld [build failed]

too many arguments in call to helloと指摘されているように、定義されているhello関数は引数を受け取ることを想定していないからだ。

最後に修正しよう。すでに言及している通り、hello関数は文字列Worldを引数として受け取って、文字列Hello, World!を返すという処理を期待している。

通過するテストコード step.2
package main
import "fmt"

// ドメイン
func hello(str string) string {
	return "Hello, " + str + "!"
}

// 副作用
func main() {
	fmt.Println(hello("World"))
}

これはテストが成功する。
hello関数は文字列"World"を受け取って文字列"Hello, World!"を返す。これでテストコードの期待値wantと合致した。

Refactor

今回は大きくないコードだったためリファクタの必要性は低い。
しかし、もしコードが大きくなったり複雑になったりする場合は、第3ステップとしてリファクタを行うことが重要である。

まとめ

テストコードから書くことが重要である[RED]。
このテストは転んでもいい。
とにかく、関数のあるべき姿から決めて、その思いをテストコードへ入れ込むことを意識する。
これを後追いするように関数を実装する[GREEN]。
実装は汚くても良い。テストを通過することだけを考える。
テストがしやすい実装も重要である。
ドメインと副作用をごっちゃにしていると、テストがしにくいため保守性が下がるリスクがある。
最後に、コードを成型する[REFACTOR]。
DRY原則を守っているか? 関数の責任は単一か? 関数や変数の命名は適切か?…などなど。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?