0
0

earn-go-with-tests Goをインストールする〜Hello, World

Posted at

はじめに

Goを学習していて以下のサイトに出会い、学習していますが一通りgoの文法を理解できた方にとてもおすすめです。

Goをインストールする

モジュールは、依存関係管理、バージョン選択、再現性のあるビルドに関連する問題を解決することを目的としており、ユーザは GOPATH の外で Go コードを実行することもできます。

が指定されていない場合、go mod initはディレクトリ構造からモジュールのパスを推測しようとしますが、引数を与えることでそれを上書きすることもできます。

mkdir my-project
cd my-project
go mod init <modulepath>

modulepathとは、Goプログラミング言語におけるモジュールシステムでモジュールを一意に識別するための文字列です。モジュールパスは、そのモジュールがどこにホストされているかを示すURL形式のパスであり、依存関係の解決やパッケージのインポートに使用されます。

例えば、githubにホストされる場合は、以下のようなmodulepathが一例です。
これを実行することで、go.modが作成されます。

go mod init github.com/username/repository

go.modの初期状態は先ほど指定したmodulepathとgoのバージョンが書かれています。
このファイルはプロジェクトが依存する他のモジュール(ライブラリやパッケージ)の情報やバージョン管理を行うために使用されます。

module github.com/username/my-project

go 1.21.6

Goリンター

次のGoのリンターを以下コマンドでインストールします。

brew install golangci/tap/golangci-lint

そして、以下コマンドで実行してコードを解析することができます。

golangci-lint run

.golangci.ymlファイルを作成してカスタマイズも可能です。
https://golangci-lint.run/usage/configuration/

Hello, World

まずはHello, Worldという文字列を表示してみます。
好きな場所にフォルダを作成します。
その中に hello.go という名前の新しいファイルを作り、その中に以下のコードを記述します。

package main

import "fmt"

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

次のように実行します。

go run hello.go
// Hello, World と表示される

テスト方法

先ほどの関数のテストをするには、副作用をなくすことが重要です。
副作用(Side Effect)という概念は、プログラムが外部の状態に影響を与えることを指します。

fmt.Printlnのような関数はコンソールに出力を行うため、副作用を持つと言われます。この副作用があると、関数の動作が入力だけでなく、外部の状態にも依存するため、予測が難しくなります。

したがって、純粋関数に切り分けていきます。
以下のようにHello関数に切り分けることでよりテストが容易になります。

package main

import "fmt"


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

では、テストを行うためにhello_test.goという新しいファイルを作成しましょう。
基本的にテストは同じディレクトリ内に_ファイル名_test.goという命名で作成します。

または、関数を右クリックしGenerate Unit Tests For Functionを選択することで
スクリーンショット 2024-06-24 23.20.10.png

以下が自動生成されたテストです。
TODOと書かれた箇所にテストケースを追記していきます。

テストにはいくつかのルールがあります。

  • xxx_test.goのような名前のファイルにある必要があります。
  • テスト関数はTestという単語で始まる必要があります。
  • テスト関数は1つの引数のみをとります。 t *testing.T
  • *testing.T 型を使うには、他のファイルの fmt と同じように import "testing" が必要です。

ただ、自動作成の時点でこれらのルール通りに作成されるので頭の片隅に置いておけばいい気がします。

hello_test.go
package main

import "testing"

func TestHello(t *testing.T) {
	tests := []struct {
		name string
		want string
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Hello(); got != tt.want {
				t.Errorf("Hello() = %v, want %v", got, tt.want)
			}
		})
	}
}

ただ、最初は上の構造化したテストではなく、よりシンプルなテストで試してみます。
まずは、失敗するテストを書いてみます。

package main

import "testing"

func TestHello(t *testing.T) {
    got := Hello("Chris")
    want := "Hello, Chris"

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

現時点ではHello関数は引数を取らないので、これは失敗するはずです。
実行すると以下のように引数が多すぎると怒られます。

./hello_test.go:6:18: too many arguments in call to Hello
        have (string)
        want ()

そこで、文字列型の引数を受け入れるようにHello関数を編集します。

func Hello(name string) string {
	return "Hello, World"
}

テストを実行すると次のように表示されます。

%go test
--- FAIL: TestHello (0.00s)
    hello_test.go:10: got "Hello, World" want "Hello, Chris"
FAIL
exit status 1

これは"Hello, Chris"となって欲しいところが"Hello, World"となっているため失敗しています。

次にHello関数の引数を使用してHello,と連結してみます。

func Hello(name string) string {
	return "Hello, " + name
}

これでテストを実行すると無事テストに合格しました。

%go test
PASS
ok    

通常、TDDサイクルの一部として、 refactor を実行する必要があります。と書かれていた通り、Red=>Green=>Refactorのプロセスを通してテストをパスすることが重要です。

しかし、まだリファクタリングはできていないので、上述のHello関数をリファクタリングします。

リファクタリングする程ではありませんが、定数に"Hello, "を切り出してテストを実行すると壊れずにテストがパスします。

定数は、 Helloが呼び出されるたびに"Hello、"文字列インスタンスを作成する手間を省くため、アプリケーションのパフォーマンスを向上させるはずです。

const englishHelloPrefix = "Hello, "

func Hello(name string) string {
    return englishHelloPrefix + name
}

次の要件は、関数が空の文字列で呼び出されたときに、デフォルトで"Hello、"ではなく"Hello、World"を出力することです。

以下の通り、引数が空文字の場合にHello, Worldが返却されるテストを書きます。
ただし、現時点ではこれは通らないはずです。

func TestHello(t *testing.T) {
    t.Run("saying hello to people", func(t *testing.T) {
        got := Hello("Chris")
        want := "Hello, Chris"

        if got != want {
            t.Errorf("got %q want %q", got, want)
        }
    })
    t.Run("say 'Hello, World' when an empty string is supplied", func(t *testing.T) {
        got := Hello("")
        want := "Hello, World"

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

テストを実行すると想定通りテストはパスしません。

--- FAIL: TestHello (0.00s)
    --- FAIL: TestHello/say_'Hello,_World'_when_an_empty_string_is_supplied (0.00s)
        hello_test.go:20: got "Hello, " want "Hellom World"
FAIL
exit status 1

したがって、Hello関数を修正し、空文字の場合はHello, Worldが返るように修正します。
しかし、その前にテストで共通しているエラーメッセージの表示を共通化しておきます。

func TestHello(t *testing.T) {
	assertCorrectMessage := func(t testing.TB, got, want string) {
        t.Helper()
        if got != want {
            t.Errorf("got %q want %q", got, want)
        }
    }
    t.Run("saying hello to people", func(t *testing.T) {
        got := Hello("Chris")
        want := "Hello, Chris"
        assertCorrectMessage(t, got, want)
    })
    t.Run("empty string defaults to 'World'", func(t *testing.T) {
        got := Hello("")
        want := "Hello, World"
        assertCorrectMessage(t, got, want)
    })
}

次に、引数が空文字の場合はHello, WorldとなるようにHello関数を書き換えます。
これでテストを実行するとパスするはずです。

func Hello(name string) string {
	if name == "" {
		name = "World"
	}
    return englishHelloPrefix + name
}

ここで、サイクルをもう一度確認です。

  1. テストを書く
  2. コンパイラーをパス
  3. テストを実行し、失敗することを確認し、エラーメッセージが意味があることを確認
  4. テストに合格するのに十分なコードを記述
  5. リファクタリング

次に、スペイン語にも対応し、それ以外はデフォルトで英語にします。
まずはテストを記述しましょう。当然パスしません。

t.Run("in Spanish", func(t *testing.T) {
	got := Hello("Elodie", "Spanish")
	want := "Hola, Elodie"
	assertCorrectMessage(t, got, want)
})

次にこれをパスするために関数を修正します。
第二引数を追加したので、既存のテストの第二引数に空文字を渡すと全てのテストがパスします。

const englishHelloPrefix = "Hello, "
const spanishHelloPrefix = "Hola, "

func Hello(name, language string) string {
	if name == "" {
		name = "World"
	}

	if language == "Spanish" {
    return spanishHelloPrefix + name
	}
    return englishHelloPrefix + name
}

続いてフランス語も追記していきます。まずはテストを書きます。
当然失敗します。

t.Run("in French", func(t *testing.T) {
	got := Hello("Mike", "French")
	want := "Bonjour, Mike"
	assertCorrectMessage(t, got, want)
})

次にHello関数を修正します。これでテストがパスします。

func Hello(name, language string) string {
	if name == "" {
		name = "World"
	}

	if language == "Spanish" {
    return spanishHelloPrefix + name
	}

	if language == "French" {
		return frenchHelloPrefix + name
	}
	
    return englishHelloPrefix + name
}

それでは、リファクタリングします。
特定の値をチェックする多くの ifステートメントがある場合、代わりにswitchステートメントを使用するのが一般的です。

func Hello(name, language string) string {
	if name == "" {
		name = "World"
	}

	prefix := englishHelloPrefix

	switch language {
		case "French":
			prefix = frenchHelloPrefix
		case "Spanish":
			prefix = spanishHelloPrefix
	}

  return prefix + name
}

これでもテストが通りますが、さらに関数に切り分けてリファクタリングします。
以下のようにgreetingPrefixを切り分けて、prefixを返す関数を作成します。

func Hello(name, language string) string {
	if name == "" {
		name = "World"
	}
  return greetingPrefix(language) + name
}

func greetingPrefix(language string) (prefix string) {
	switch language {
		case "French":
			prefix = frenchHelloPrefix
		case "Spanish":
			prefix = spanishHelloPrefix
		default:
			prefix = englishHelloPrefix
	}

  return
}

Goドキュメント

個人的にいいなと思ったのは、godoc -http=:8000を実行するとhttp://localhost:8000/pkg/にアクセスし、ローカルでドキュメントを見られることです。

例えばtestingについては以下のページで説明を見ることができます。
http://localhost:8000/pkg/testing/

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