はじめに
前回までにポケモンバトルの基礎を実装しました。
今回はテストコードを実装していこうと思います。テストコードを書く目的は、長期的な保守、開発効率化、コードの可読性を上げることです。テストと聞くと長期的な保守のイメージが強いですが、開発実装中に動作確認を高速に回すことや、動かした時の動作をテストコードから理解する事ができます。
実装
ディレクトリ構成
.
├── 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)
}
})
}
}