Agones を使って開発をしている最中、Agonesの細かな挙動についてテストしたくなる時があった。
Agonesはクイックスタート的な小さなサーバーでも動かすのに結構な手間が必要で、パラメータを少しずつ変えながら意図した挙動になっているかチェックする…という作業も繰り返していると面倒になってくる。
そこでこの手作業でやっている検証を自動化できないか調べたところ、Agones本体の中にGo用のテストライブラリ (test/e2e/framework) が含まれていることに気づいた。
agones/test/e2e at ff6ec62569bda023176f0c7f2f4dab2430e11f8c · googleforgames/agones · GitHub
この framework パッケージでGoのコードから次のような処理ができる!
- Kuberenetes namespace の作成と削除
- Agones Fleet/GameServer の作成と削除
- Agones Fleet/GameServer が特定の状態になるまで待機
Minikubeの準備
Minikubeを使うとお手軽にローカル環境にAgonesの環境を構築できるので、まずはこのドキュメントに従って環境を準備する。
このようになったら準備完了。
$ kubectl get pods --namespace agones-system NAME READY STATUS RESTARTS AGE agones-allocator-7cd58db9dc-gdcdm 1/1 Running 0 28s agones-allocator-7cd58db9dc-qjqdd 1/1 Running 0 28s agones-allocator-7cd58db9dc-s28tc 1/1 Running 0 28s agones-controller-6f4458776f-cr9fk 1/1 Running 0 28s agones-ping-59d46d7fb5-85xzc 1/1 Running 0 28s agones-ping-59d46d7fb5-b8q4v 1/1 Running 0 27s
Goでコードを書く
Agonesの環境が整ったので、実際にGoのテストで
まずはAgonesのライブラリを導入して、
go get agones.dev/agones
次のように e2eframwork.Framework という構造体をグローバルに保持しておき、この構造体を使ってAgonesのさまざまな機能にアクセスできる。 TestMain でこのframeworkを初期化しておくとすべてのテストで自動的に初期化できて便利。
import ( "fmt" "log" "os" "testing" e2eframework "agones.dev/agones/test/e2e/framework" ) var ( framework *e2eframework.Framework ) func TestMain(m *testing.M) { fw, err := e2eframework.NewFromFlags() if err != nil { log.Fatalf("failed to init e2e e2eframework: %+v", err) } framework = fw var exitCode int defer func() { os.Exit(exitCode) }() exitCode = m.Run() }
Namespaceの作成と削除
テストごとに独立したKubernetes Namespaceを切って、テスト同士が影響しないように分離するとよい。
そのために、ランダムな名前のNamespaceを作って、テスト終了後に削除するという処理を用意しておく。 ランダムな値を作るにはUUID(v4)などを使うと便利。
テスト終了後に削除するのは defer を使ってもよいが、Go 1.14 で追加された t.Cleanup を使うとより便利。
import ( "github.com/google/uuid" ) func newRandomNamespace(t *testing.T) string { namespace := fmt.Sprintf("testns-%s", uuid.Must(uuid.NewRandom())) if err := framework.CreateNamespace(namespace); err != nil { panic(err) } t.Cleanup(func() { _ = framework.DeleteNamespace(namespace) }) return namespace }
GameServer と Fleetの作成
実際にGameServerやFleetを立ち上げるには、Fleet構造体を作り framework.AgonesClient.AgonesV1().Fleets(namespace).Create(...)
に渡す。
構造体のパラメータがやたら多く複雑に見えるが、これ実は手動でAgonesを使うときに定義するYAMLとまったく同じ構造である。 よってわからない場合はKubernetesのYAML定義の説明を見ればよい。
GameServerの中身は簡単のため、Agones公式が用意してくれてるtcp-server を使った。 ここを自分で作ったImageに差し替えると自作GameServerでもテストが可能。
import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "log" "testing" "time" agonesv1 "agones.dev/agones/pkg/apis/agones/v1" corev1 "k8s.io/api/core/v1" ) func TestCreateFleet(t *testing.T) { // ランダムなnamespaceをつくる namespace := newRandomNamespace(t) // GameServerの定義をつくる gsSpec := agonesv1.GameServerSpec{ Ports: []agonesv1.GameServerPort{ { ContainerPort: 7654, Name: "tcp", PortPolicy: agonesv1.Dynamic, Protocol: corev1.ProtocolTCP, }}, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "tcp-server", Image: "gcr.io/agones-images/tcp-server:0.4", ImagePullPolicy: corev1.PullIfNotPresent, Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("30m"), corev1.ResourceMemory: resource.MustParse("32Mi"), }, Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("30m"), corev1.ResourceMemory: resource.MustParse("32Mi"), }, }, }}, }, }, } // Fleetを作る fleetDef := &agonesv1.Fleet{ ObjectMeta: metav1.ObjectMeta{Name: "fleet-name", Namespace: namespace}, Spec: agonesv1.FleetSpec{ Replicas: 2, Template: agonesv1.GameServerTemplateSpec{ Spec: gsSpec, }, }, } flt, err := framework.AgonesClient.AgonesV1().Fleets(namespace).Create(fleetDef) if err != nil { t.Fatal(err) } log.Printf("fleet created! (name: %s)", flt.ObjectMeta.Name) }
このテストを実行すると、いくつかのログが出てFleetの作成に成功!
=== RUN TestCreateFleet {"message":"Namespace gameserver-test-1aacbb5a-1e32-42c3-a48e-0761c15d044f is created","severity":"info","time":"2020-07-27T23:09:48.764999+09:00"} {"message":"ServiceAccount gameserver-test-1aacbb5a-1e32-42c3-a48e-0761c15d044f/agones-sdk is created","severity":"info","time":"2020-07-27T23:09:48.770368+09:00"} {"message":"RoleBinding gameserver-test-1aacbb5a-1e32-42c3-a48e-0761c15d044f/agones-sdk-access is created","severity":"info","time":"2020-07-27T23:09:48.775277+09:00"} 23:09:48 fleet created! (name: fleet-name) {"message":"Namespace gameserver-test-1aacbb5a-1e32-42c3-a48e-0761c15d044f is deleted","severity":"info","time":"2020-07-27T23:09:48.821892+09:00"} --- PASS: TestCreateFleet (0.07s) PASS
これでNamespace作成→Fleet作成までがコード上で実現可能になったので、さまざまなテストを自動化できる。すごい!
Kubernetesの特性を考慮した便利な関数たち
さらに、frameworkパッケージには便利関数がいくつか用意されているので、少しだけ紹介。
AgonesというかKubernetesは、命令したら即座にその状態になるのではなく、内部でループを回して理想の状態にだんだん近づける といった動きをする。
よって、次のように「〜〜の状態になるまで待機する」系の関数が役立つ。これはAgones本体のテストでも多用されていてテストコードを読んでみるだけでも結構面白い。
// AssertFleetCondition: Fleetが指定した状態になることを確認するassertion // 例)Status: Readyなゲームサーバーが2つ揃うことを検証する framework.AssertFleetCondition(t, flt, func(flt *agonesv1.Fleet) bool { return flt.Status.ReadyReplicas == 2 }) // WaitFor* 系関数: Agonesのさまざまな要素が一定の状態になるまで待機する // Status: Readyなゲームサーバーが2つになるまで待機する framework.WaitForFleetCondition(t, flt, func(flt *agonesv1.Fleet) bool { return flt.Status.ReadyReplicas == 2 })