account.go
package domain
import "errors"
// エラー定義
var (
ErrAccountNameRequired = errors.New("account name is required")
ErrAccountEmailRequired = errors.New("account email is required")
)
// アカウントID
type AccountID string
// アカウントリポジトaリのインターフェース
type AccountRepository interface {
Create(Account) (Account, error) // アカウントを作成
FindAll() ([]Account, error) // すべてのアカウントを取得
}
// アカウントエンティティ
type Account struct {
id AccountID
name string
email string
}
// コンストラクタ
func NewAccount(id AccountID, name, email string) (Account, error) {
if name == "" {
return Account{}, ErrAccountNameRequired
}
if email == "" {
return Account{}, ErrAccountEmailRequired
}
return Account{id: id, name: name, email: email}, nil
}
// Getter: アカウントIDを取得
func (a Account) ID() AccountID {
return a.id
}
// Getter: 名前を取得
func (a Account) Name() string {
return a.name
}
// Getter: メールアドレスを取得
func (a Account) Email() string {
return a.email
}
account_test.go
package domain
import (
"testing"
)
// TestNewAccount は NewAccount の動作をテストする
func TestNewAccount(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id AccountID
accountName string
email string
expectError bool
}{
{
name: "成功: アカウントが作成される",
id: "account1",
accountName: "Taro",
email: "taro@example.com",
expectError: false,
},
{
name: "エラー: アカウント名が空",
id: "account2",
accountName: "",
email: "hanako@example.com",
expectError: true,
},
{
name: "エラー: メールアドレスが空",
id: "account3",
accountName: "Jiro",
email: "",
expectError: true,
},
}
for _, tt := range tests {
tt := tt // 並列テスト用に変数をキャプチャ
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
account, err := NewAccount(tt.id, tt.accountName, tt.email)
if tt.expectError {
// エラーが発生することを期待する場合
if err == nil {
t.Errorf("[TestCase '%s'] 期待したエラーが発生しませんでした", tt.name)
}
} else {
// エラーが発生しないことを期待する場合
if err != nil {
t.Errorf("[TestCase '%s'] 予期しないエラー: %v", tt.name, err)
}
if account.Name() != tt.accountName {
t.Errorf("[TestCase '%s'] アカウント名が異なります (期待: %s, 実際: %s)", tt.name, tt.accountName, account.Name())
}
if account.Email() != tt.email {
t.Errorf("[TestCase '%s'] メールアドレスが異なります (期待: %s, 実際: %s)", tt.name, tt.email, account.Email())
}
}
})
}
}
Goのテストコード解説: TestNewAccount
この記事では、Go言語の初心者向けに、以下のテストコードの文法と動作を解説します。
package domain
import (
"testing"
)
// TestNewAccount は NewAccount の動作をテストする
func TestNewAccount(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id AccountID
accountName string
email string
expectError bool
}{
{
name: "成功: アカウントが作成される",
id: "account1",
accountName: "Taro",
email: "taro@example.com",
expectError: false,
},
{
name: "エラー: アカウント名が空",
id: "account2",
accountName: "",
email: "hanako@example.com",
expectError: true,
},
{
name: "エラー: メールアドレスが空",
id: "account3",
accountName: "Jiro",
email: "",
expectError: true,
},
}
for _, tt := range tests {
tt := tt // 並列テスト用に変数をキャプチャ
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
account, err := NewAccount(tt.id, tt.accountName, tt.email)
if tt.expectError {
if err == nil {
t.Errorf("[TestCase '%s'] 期待したエラーが発生しませんでした", tt.name)
}
} else {
if err != nil {
t.Errorf("[TestCase '%s'] 予期しないエラー: %v", tt.name, err)
}
if account.Name() != tt.accountName {
t.Errorf("[TestCase '%s'] アカウント名が異なります (期待: %s, 実際: %s)", tt.name, tt.accountName, account.Name())
}
if account.Email() != tt.email {
t.Errorf("[TestCase '%s'] メールアドレスが異なります (期待: %s, 実際: %s)", tt.name, tt.email, account.Email())
}
}
})
}
}
1. package domain
このコードは domain
というパッケージに属しています。Goでは、パッケージごとにコードを管理します。
2. import "testing"
Goの標準ライブラリに含まれる testing
パッケージをインポートして、テスト機能を利用します。
3. func TestNewAccount(t *testing.T)
-
TestNewAccount
はt *testing.T
を引数に取る関数で、go test
コマンドによって実行されます。 - 関数名は
Test
で始める必要があります。
4. t.Parallel()
このテストを 並列実行 可能にします。並列実行すると、複数のテストを並行して動作させることができます。
5. テストケースのリスト (tests
)
tests := []struct {
name string
id AccountID
accountName string
email string
expectError bool
}{ ... }
-
tests
という スライス(リスト) を作成し、3つのテストケースを定義しています。 - 各テストケースは、
-
name
(テストの名前) -
id
(アカウントID) -
accountName
(アカウント名) -
email
(メールアドレス) -
expectError
(エラーが発生するか)
を持つ構造体です。
-
より詳しい解説はこちら
6. t.Run()
によるサブテスト
t.Run(tt.name, func(t *testing.T) {
-
t.Run()
を使うと、各テストケースを 個別に実行 できます。 -
tt.name
を渡すことで、テスト結果に名前が表示されます。
7. NewAccount
の実行
account, err := NewAccount(tt.id, tt.accountName, tt.email)
-
NewAccount
関数を実行し、account
(生成されたアカウント)とerr
(エラー)を取得します。
8. expectError
に応じたエラーチェック
エラーが 発生することを期待 する場合:
if tt.expectError {
if err == nil {
t.Errorf("[TestCase '%s'] 期待したエラーが発生しませんでした", tt.name)
}
}
エラーが 発生しないことを期待 する場合:
if !tt.expectError {
if err != nil {
t.Errorf("[TestCase '%s'] 予期しないエラー: %v", tt.name, err)
}
}
9. 期待値と実際の値の比較
if account.Name() != tt.accountName {
t.Errorf("[TestCase '%s'] アカウント名が異なります (期待: %s, 実際: %s)", tt.name, tt.accountName, account.Name())
}
-
account.Name()
の値がtt.accountName
と一致するか確認 - もし一致しなければ
t.Errorf
でエラーメッセージを出力
出力
koichi@koxo JobAtlas_go % go test -v ./domain -run TestNewAccount
=== RUN TestNewAccount
=== PAUSE TestNewAccount
=== CONT TestNewAccount
=== RUN TestNewAccount/成功:_アカウントが作成される
=== PAUSE TestNewAccount/成功:_アカウントが作成される
=== RUN TestNewAccount/エラー:_アカウント名が空
=== PAUSE TestNewAccount/エラー:_アカウント名が空
=== RUN TestNewAccount/エラー:_メールアドレスが空
=== PAUSE TestNewAccount/エラー:_メールアドレスが空
=== CONT TestNewAccount/成功:_アカウントが作成される
=== CONT TestNewAccount/エラー:_メールアドレスが空
=== CONT TestNewAccount/エラー:_アカウント名が空
--- PASS: TestNewAccount (0.00s)
--- PASS: TestNewAccount/成功:_アカウントが作成される (0.00s)
--- PASS: TestNewAccount/エラー:_メールアドレスが空 (0.00s)
--- PASS: TestNewAccount/エラー:_アカウント名が空 (0.00s)
PASS
ok JobAtlas_go/domain (cached)
Goのテスト出力の読み方解説
テストを実行した際の以下の出力結果について、どのように読み解けば良いのかを解説します。
テスト実行コマンド
koichi@koxo JobAtlas_go % go test -v ./domain -run TestNewAccount
-
go test
: Goのテストを実行するコマンド -
-v
: 詳細なログを表示(verbose
モード) -
./domain
:domain
パッケージのテストを実行 -
-run TestNewAccount
:TestNewAccount
に一致するテスト関数のみを実行
テスト出力の各行の意味
=== RUN TestNewAccount
=== PAUSE TestNewAccount
=== CONT TestNewAccount
-
=== RUN TestNewAccount
→TestNewAccount
の実行が開始されたことを示します。 -
=== PAUSE TestNewAccount
→t.Parallel()
の影響で、一旦テストの実行が保留されたことを示します。 -
=== CONT TestNewAccount
→TestNewAccount
の実行が再開されたことを示します。
=== RUN TestNewAccount/成功:_アカウントが作成される
=== PAUSE TestNewAccount/成功:_アカウントが作成される
=== RUN TestNewAccount/エラー:_アカウント名が空
=== PAUSE TestNewAccount/エラー:_アカウント名が空
=== RUN TestNewAccount/エラー:_メールアドレスが空
=== PAUSE TestNewAccount/エラー:_メールアドレスが空
=== CONT TestNewAccount/成功:_アカウントが作成される
=== CONT TestNewAccount/エラー:_メールアドレスが空
=== CONT TestNewAccount/エラー:_アカウント名が空
-
=== RUN TestNewAccount/成功:_アカウントが作成される
→TestNewAccount
のサブテスト"成功: アカウントが作成される"
が開始。 -
=== PAUSE TestNewAccount/成功:_アカウントが作成される
→t.Parallel()
によって、一旦テストが保留。 -
=== CONT TestNewAccount/成功:_アカウントが作成される
→ 並列処理が再開され、テスト実行。
ポイント
- 並列実行のため、
RUN
→PAUSE
→CONT
の流れがある。
--- PASS: TestNewAccount (0.00s)
--- PASS: TestNewAccount/成功:_アカウントが作成される (0.00s)
--- PASS: TestNewAccount/エラー:_メールアドレスが空 (0.00s)
--- PASS: TestNewAccount/エラー:_アカウント名が空 (0.00s)
PASS
ok JobAtlas_go/domain (cached)
-
--- PASS: TestNewAccount (0.00s)
→TestNewAccount
のテストが成功 (PASS
) し、実行時間は0.00s
。 -
--- PASS: TestNewAccount/成功:_アカウントが作成される (0.00s)
→"成功: アカウントが作成される"
のサブテストが成功 (PASS
) し、実行時間は0.00s
。 -
--- PASS: TestNewAccount/エラー:_メールアドレスが空 (0.00s)
→"エラー: メールアドレスが空"
のサブテストが成功 (PASS
) し、実行時間は0.00s
。 -
--- PASS: TestNewAccount/エラー:_アカウント名が空 (0.00s)
→"エラー: アカウント名が空"
のサブテストが成功 (PASS
) し、実行時間は0.00s
。 -
PASS
→TestNewAccount
のすべてのサブテストが成功したため、全体としてPASS
。 -
ok JobAtlas_go/domain (cached)
→domain
パッケージのテストが問題なく成功 (ok
) し、前回のキャッシュが利用された (cached
) 。
テストが失敗した場合の例
例えば、NewAccount("", "", "test@example.com")
で AccountID
が空でも通ってしまうバグがあると仮定します。
その場合、以下のような出力になります。
=== RUN TestNewAccount/エラー:_アカウントIDが空
account_test.go:45: [TestCase 'エラー:_アカウントIDが空'] 期待したエラーが発生しませんでした
--- FAIL: TestNewAccount (0.00s)
--- FAIL: TestNewAccount/エラー:_アカウントIDが空 (0.00s)
FAIL
exit status 1
FAIL JobAtlas_go/domain 0.005s
-
account_test.go:45: [TestCase 'エラー:_アカウントIDが空'] 期待したエラーが発生しませんでした
→NewAccount("", "", "test@example.com")
に対してエラーが発生しなかったため、テストが失敗 (FAIL
) 。 -
--- FAIL: TestNewAccount (0.00s)
→TestNewAccount
の実行全体が失敗 (FAIL
) 。 -
exit status 1
→ プログラムがエラーを伴って終了。 -
FAIL JobAtlas_go/domain 0.005s
→domain
パッケージのテストが失敗 (FAIL
) し、実行時間は0.005s
。