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?

ポケモンバトルをDDDで実装したい(Step2: テストコードを実装する)

Posted at

はじめに

前回までにポケモンバトルの基礎を実装しました。
 今回はテストコードを実装していこうと思います。テストコードを書く目的は、長期的な保守、開発効率化、コードの可読性を上げることです。テストと聞くと長期的な保守のイメージが強いですが、開発実装中に動作確認を高速に回すことや、動かした時の動作をテストコードから理解する事ができます。

実装

ディレクトリ構成
.
├── cmd
│   └── battle_cli.go
├── docs
│   └── domain.puml
├── domain
│   ├── battle
│   │   └── battle.go
│   ├── move
│   │   └── move.go
│   ├── pokemon
│   │   ├── hp_test.go
│   │   ├── hp.go
│   │   ├── moveset_test.go
│   │   ├── moveset.go
│   │   ├── pokemon_test.go
│   │   ├── pokemon.go
│   │   ├── species.go
│   │   └── stats.go
│   └── services
│       ├── services_test.go
│       └── services.go
├── domain.md
└── go.mod

7 directories, 16 files

テストの実装する構成はIn-package testsを採用しています。Goとしても推奨されているテストですし、ドキュメントはプロダクトの近くにある方がよいでしょう。
逆に、JavaやPython、Dart等はTest Directory Patternが推奨されたりします。個人的には、In-package testsの方が扱い易いのでいいなぁとおもてっています。

domain/pokemon/hp_test.go
package pokemon

import "testing"

func TestHP_Apply(t *testing.T) {
	tests := []struct {
		name     string
		initial  HP
		damage   int
		expected HP
	}{
		{
			name:     "正常なダメージ適用",
			initial:  HP{Max: 100, Cur: 50},
			damage:   20,
			expected: HP{Max: 100, Cur: 30},
		},
		{
			name:     "HPが0になるダメージ適用",
			initial:  HP{Max: 100, Cur: 20},
			damage:   30,
			expected: HP{Max: 100, Cur: 0},
		},
		{
			name:     "負のダメージは適用されない",
			initial:  HP{Max: 100, Cur: 50},
			damage:   -10,
			expected: HP{Max: 100, Cur: 50},
		},
		{
			name:     "HPがすでに0の場合",
			initial:  HP{Max: 100, Cur: 0},
			damage:   10,
			expected: HP{Max: 100, Cur: 0},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.initial.Apply(tt.damage)
			if got != tt.expected {
				t.Errorf("Apply() = %v, want %v", got, tt.expected)
			}
		})
	}
}

func TestHP_IsFainted(t *testing.T) {
	tests := []struct {
		name     string
		hp       HP
		expected bool
	}{
		{
			name:     "HPが0の場合",
			hp:       HP{Max: 100, Cur: 0},
			expected: true,
		},
		{
			name:     "HPが0より大きい場合",
			hp:       HP{Max: 100, Cur: 1},
			expected: false,
		},
		{
			name:     "HPが最大の場合",
			hp:       HP{Max: 100, Cur: 100},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.hp.IsFainted()
			if got != tt.expected {
				t.Errorf("IsFainted() = %v, want %v", got, tt.expected)
			}
		})
	}
}
domain/pokemon/pokemon_test.go
package pokemon

import (
	"pokemon_ddd_v2/domain/move"
	"testing"
)

func TestNewMoveSet(t *testing.T) {
	tests := []struct {
		name      string
		moves     []move.Move
		expectLen int
		expectErr bool
	}{
		{
			name:      "正常な技の追加",
			moves:     []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}},
			expectLen: 2,
			expectErr: false,
		},
		{
			name:      "最大数を超える技での作成",
			moves:     []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}, {Name: "なきごえ"}, {Name: "でんこうせっか"}, {Name: "かみつく"}},
			expectLen: 0,
			expectErr: true,
		},
		{
			name:      "空の技の追加 (エラー)",
			moves:     []move.Move{},
			expectLen: 0,
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ms, err := NewMoveSet(tt.moves...)
			if (err != nil) != tt.expectErr {
				t.Errorf("NewMoveSet() error = %v, expectErr %v", err, tt.expectErr)
				return
			}
			if ms.Len() != tt.expectLen {
				t.Errorf("NewMoveSet() Len = %v, want %v", ms.Len(), tt.expectLen)
			}
		})
	}
}

func TestMoveSet_Len(t *testing.T) {
	tests := []struct {
		name     string
		ms       MoveSet
		expected int
	}{
		{
			name:     "2つの技を持つMoveSet",
			ms:       MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}}},
			expected: 2,
		},
		{
			name:     "空のMoveSet",
			ms:       MoveSet{list: []move.Move{}},
			expected: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.ms.Len(); got != tt.expected {
				t.Errorf("MoveSet.Len() = %v, want %v", got, tt.expected)
			}
		})
	}
}

func TestMoveSet_At(t *testing.T) {
	ms := MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}, {Name: "なきごえ"}}}

	tests := []struct {
		name       string
		index      int
		expectMove move.Move
		expectErr  bool
	}{
		{
			name:       "有効なインデックス",
			index:      0,
			expectMove: move.Move{Name: "たいあたり"},
			expectErr:  false,
		},
		{
			name:       "範囲外の小さいインデックス",
			index:      -1,
			expectMove: move.Move{},
			expectErr:  true,
		},
		{
			name:       "範囲外の大きいインデックス",
			index:      3,
			expectMove: move.Move{},
			expectErr:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ms.At(tt.index)
			if (err != nil) != tt.expectErr {
				t.Errorf("MoveSet.At() error = %v, expectErr %v", err, tt.expectErr)
				return
			}
			if got != tt.expectMove {
				t.Errorf("MoveSet.At() = %v, want %v", got, tt.expectMove)
			}
		})
	}
}

func TestMoveSet_All(t *testing.T) {
	originalMoves := []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}}
	ms := MoveSet{list: originalMoves}

	got := ms.All()

	// スライスの内容が同じことを確認
	if len(got) != len(originalMoves) {
		t.Errorf("MoveSet.All() length = %v, want %v", len(got), len(originalMoves))
	}
	for i, m := range got {
		if m != originalMoves[i] {
			t.Errorf("MoveSet.All() at index %d = %v, want %v", i, m, originalMoves[i])
		}
	}

	// 返されたスライスが元のスライスと異なるメモリを指していることを確認
	if &got[0] == &originalMoves[0] && len(originalMoves) > 0 {
		t.Errorf("MoveSet.All() returned slice is not a copy")
	}
}

func TestMoveSet_Learn(t *testing.T) {
	tests := []struct {
		name      string
		initial   MoveSet
		move      move.Move
		expectLen int
		expectErr bool
	}{
		{
			name:      "空きスロットに技を追加",
			initial:   MoveSet{list: []move.Move{}},
			move:      move.Move{Name: "たいあたり"},
			expectLen: 1,
			expectErr: false,
		},
		{
			name:      "満杯のスロットに技を追加",
			initial:   MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}, {Name: "なきごえ"}, {Name: "でんこうせっか"}}},
			move:      move.Move{Name: "新しい技"},
			expectLen: 4,
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ms := tt.initial
			err := ms.Learn(tt.move)
			if (err != nil) != tt.expectErr {
				t.Errorf("MoveSet.Learn() error = %v, expectErr %v", err, tt.expectErr)
				return
			}
			if ms.Len() != tt.expectLen {
				t.Errorf("MoveSet.Learn() Len = %v, want %v", ms.Len(), tt.expectLen)
			}
			if !tt.expectErr && ms.list[ms.Len()-1] != tt.move {
				t.Errorf("MoveSet.Learn() added move = %v, want %v", ms.list[ms.Len()-1], tt.move)
			}
		})
	}
}

func TestMoveSet_Replace(t *testing.T) {
	tests := []struct {
		name      string
		initial   MoveSet
		index     int
		newMove   move.Move
		expected  MoveSet
		expectErr bool
	}{
		{
			name:      "正常な技の差し替え",
			initial:   MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}}},
			index:     1,
			newMove:   move.Move{Name: "かみつく"},
			expected:  MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "かみつく"}}},
			expectErr: false,
		},
		{
			name:      "範囲外のインデックス",
			initial:   MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}}},
			index:     2,
			newMove:   move.Move{Name: "かみつく"},
			expected:  MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}}},
			expectErr: true,
		},
		{
			name:      "負のインデックス",
			initial:   MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}}},
			index:     -1,
			newMove:   move.Move{Name: "かみつく"},
			expected:  MoveSet{list: []move.Move{{Name: "たいあたり"}, {Name: "ひっかく"}}},
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ms := tt.initial
			err := ms.Replace(tt.index, tt.newMove)
			if (err != nil) != tt.expectErr {
				t.Errorf("MoveSet.Replace() error = %v, expectErr %v", err, tt.expectErr)
				return
			}
			if !tt.expectErr {
				for i, m := range ms.list {
					if m != tt.expected.list[i] {
						t.Errorf("MoveSet.Replace() at index %d = %v, want %v", i, m, tt.expected.list[i])
					}
				}
			}
		})
	}
}

domain/services/services_test.go
package services

import (
	"pokemon_ddd_v2/domain/move"
	"pokemon_ddd_v2/domain/pokemon"
	"testing"
)

// ダミーのSpecies, Stats, HPを準備
var (
	sp = pokemon.Species{Name: "ダミー"}
	hp = pokemon.HP{Max: 100, Cur: 100}
)

func newDummyPokemon(speciesName string, atk, def, spe int, moveName string) *pokemon.Pokemon {
	sp := pokemon.Species{Name: speciesName, Type: "ノーマル"}
	st := pokemon.Stats{Atk: atk, Def: def, Spe: spe}
	m, _ := pokemon.NewMoveSet(move.Move{Name: moveName, Power: 10, Priority: 0})
	p, _ := pokemon.NewPokemon(sp, st, hp, m.All()...)
	return p
}

func TestSimpleOrder_Decide(t *testing.T) {
	tests := []struct {
		name     string
		a        Acting
		b        Acting
		expected []Acting
	}{
		{
			name: "ピカチュウの優先度が高い技が先行",
			a: Acting{
				Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "でんこうせっか"),
				Move:     move.Move{Name: "でんこうせっか", Priority: 1},
			},
			b: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "ひっかく"),
				Move:     move.Move{Name: "ひっかく", Priority: 0},
			},
			expected: []Acting{
				{Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "でんこうせっか"), Move: move.Move{Name: "でんこうせっか", Priority: 1}},
				{Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "ひっかく"), Move: move.Move{Name: "ひっかく", Priority: 0}},
			},
		},
		{
			name: "ヒトカゲの優先度が高い技が先行",
			a: Acting{
				Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "ひっかく"),
				Move:     move.Move{Name: "ひっかく", Priority: 0},
			},
			b: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "でんこうせっか"),
				Move:     move.Move{Name: "でんこうせっか", Priority: 1},
			},
			expected: []Acting{
				{Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "でんこうせっか"), Move: move.Move{Name: "でんこうせっか", Priority: 1}},
				{Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "ひっかく"), Move: move.Move{Name: "ひっかく", Priority: 0}},
			},
		},
		{
			name: "優先度が同じでピカチュウの素早さが高い場合が先行",
			a: Acting{
				Attacker: newDummyPokemon("ピカチュウ", 10, 10, 20, "たいあたり"),
				Move:     move.Move{Name: "たいあたり", Priority: 0},
			},
			b: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "たいあたり"),
				Move:     move.Move{Name: "たいあたり", Priority: 0},
			},
			expected: []Acting{
				{Attacker: newDummyPokemon("ピカチュウ", 10, 10, 20, "たいあたり"), Move: move.Move{Name: "たいあたり", Priority: 0}},
				{Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "たいあたり"), Move: move.Move{Name: "たいあたり", Priority: 0}},
			},
		},
		{
			name: "優先度が同じでヒトカゲの素早さが高い場合が先行",
			a: Acting{
				Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "たいあたり"),
				Move:     move.Move{Name: "たいあたり", Priority: 0},
			},
			b: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 20, "たいあたり"),
				Move:     move.Move{Name: "たいあたり", Priority: 0},
			},
			expected: []Acting{
				{Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 20, "たいあたり"), Move: move.Move{Name: "たいあたり", Priority: 0}},
				{Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "たいあたり"), Move: move.Move{Name: "たいあたり", Priority: 0}},
			},
		},
		{
			name: "優先度も素早さも同じ場合 (ピカチュウが先行)",
			a: Acting{
				Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "たいあたり"),
				Move:     move.Move{Name: "たいあたり", Priority: 0},
			},
			b: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "たいあたり"),
				Move:     move.Move{Name: "たいあたり", Priority: 0},
			},
			expected: []Acting{
				{Attacker: newDummyPokemon("ピカチュウ", 10, 10, 10, "たいあたり"), Move: move.Move{Name: "たいあたり", Priority: 0}},
				{Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "たいあたり"), Move: move.Move{Name: "たいあたり", Priority: 0}},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := SimpleOrder{}
			got := s.Decide(tt.a, tt.b)
			// ポインタ比較ではなく、内容が等しいことを確認
			if len(got) != len(tt.expected) {
				t.Errorf("Decide() got = %v, want %v", got, tt.expected)
				return
			}
			for i := range got {
				if got[i].Attacker.Species.Name != tt.expected[i].Attacker.Species.Name ||
					got[i].Move.Name != tt.expected[i].Move.Name {
					t.Errorf("Decide() got[%d] Attacker Name = %s, Move Name = %s; want Attacker Name = %s, Move Name = %s",
						i, got[i].Attacker.Species.Name, got[i].Move.Name, tt.expected[i].Attacker.Species.Name, tt.expected[i].Move.Name)
				}
			}
		})
	}
}

func TestAlwaysHit_Hit(t *testing.T) {
	tests := []struct {
		name     string
		acting   Acting
		expected bool
	}{
		{
			name:     "常に命中する",
			acting:   Acting{}, // Actingの中身は関係ない
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := AlwaysHit{}
			if got := s.Hit(tt.acting); got != tt.expected {
				t.Errorf("AlwaysHit.Hit() = %v, want %v", got, tt.expected)
			}
		})
	}
}

func TestSimpleDamage_Calc(t *testing.T) {
	tests := []struct {
		name     string
		acting   Acting
		expected int
	}{
		{
			name: "ヒトカゲからピカチュウへの正常なダメージ計算",
			acting: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 10, 10, 10, "ひっかく"),
				Defender: newDummyPokemon("ピカチュウ", 10, 10, 10, "でんこうせっか"),
				Move:     move.Move{Power: 40},
			},
			expected: 4,
		},
		{
			name: "防御が0の場合、防御は1として計算 (ピカチュウ)",
			acting: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 10, 0, 10, "ひっかく"),
				Defender: newDummyPokemon("ピカチュウ", 10, 0, 10, "でんこうせっか"),
				Move:     move.Move{Power: 40},
			},
			expected: 48,
		},
		{
			name: "計算結果が1未満の場合、ダメージは1 (ヒトカゲ)",
			acting: Acting{
				Attacker: newDummyPokemon("ヒトカゲ", 1, 10, 10, "ひっかく"),
				Defender: newDummyPokemon("ピカチュウ", 10, 100, 10, "でんこうせっか"),
				Move:     move.Move{Power: 10},
			},
			expected: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := SimpleDamage{}
			if got := s.Calc(tt.acting); got != tt.expected {
				t.Errorf("SimpleDamage.Calc() = %v, want %v", got, tt.expected)
			}
		})
	}
}
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?