これは ZOZO Advent Calendar 2023 カレンダー Vol.5 の 4日目の記事です。
本記事では、client-goのkubernetes/fakeパッケージを使って、テスト関数を実装する例を紹介します。
テスト関数では、テスト対象の関数内の処理によりKubernetesオブジェクトが作成されているかどうかの確認を行います。
fake clientとは
kubernetes/fakeパッケージが提供するClientsetを利用することで、実際にKubernetes APIへリクエストを送ることなく実行マシンのメモリ上でclientsetによるオブジェクト取得や作成といった動きを模倣できます。
client-goが提供するkubernetesパッケージのClientsetには様々なKubernetesのデフォルトリソースに関してKubernetes APIにリクエストするためのクライアントが含まれており、PodやDeploymentといったデフォルトリソースをGo言語で操作する際に利用されます。。
kubernetesパッケージのClientsetは、同じくkubernetesパッケージのInterfaceインターフェースを満たしています。
一方で、kubernetes/fakeパッケージが提供するClientsetもkubernetesパッケージのInterfaceインターフェースを満たしています。
そのため、テスト対象の関数の引数にkubernetesパッケージのInterfaceインターフェースでクライアントを受け取るように実装していれば、kubernetesパッケージのClientsetとkubernetes/fakeパッケージのClientsetのどちらの型でも渡すことが可能です。
fake clientを使ったテストの実装例
テスト対象の関数
kubernetes/fakeパッケージのClientsetを使ったテストの例として、次のプログラムのテストを作成します。
テスト対象の関数はreplaceNameAndCreateNewPod()関数です。
この関数では引数で渡されたPodオブジェクトを元に、Nameフィールドの値をpodName引数の値で上書きした新しいKubernetesオブジェクトを作成します。
clientset引数の型がkubernetes.Interafaceになっている点がポイントです。
package main
import (
"context"
"flag"
"fmt"
"os"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
var (
kubeconfig string
podName string
)
func main() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
flag.StringVar(&podName, "name", "", "Pod Name")
flag.Parse()
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
clientset, _ := kubernetes.NewForConfig(config)
base := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "<REPLACE_THIS_FIELD>",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "sleep-container",
Image: "alpine",
Command: []string{"sleep", "3600"},
},
},
},
}
_, err := replaceNameAndCreateNewPod(clientset, base, podName)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func replaceNameAndCreateNewPod(clientset kubernetes.Interface, base *corev1.Pod, podName string) (*corev1.Pod, error) {
newPod := base.DeepCopy()
newPod.ObjectMeta.Name = podName
_, err := clientset.CoreV1().Pods("default").Create(context.TODO(), newPod, metav1.CreateOptions{})
if err != nil {
return nil, err
}
return newPod, nil
}
テスト関数
replaceNameAndCreateNewPod()関数の振る舞いとしてテストしたい観点は次の3点になります。
- 実行時にエラーが発生しないこと
- 新しいPodオブジェクトの
Nameフィールドの値が変更されていること - clientsetにより、新しいPodオブジェクトが作成されていること
テスト関数の実装は以下になります。
package main
import (
"context"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"github.com/stretchr/testify/assert"
)
func TestReplaceNameAndCreateNewPod(t *testing.T) {
clientset := fake.NewSimpleClientset()
original := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "<REPLACE_THIS_FIELD>",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "sleep-container",
Image: "alpine",
Command: []string{"sleep", "3600"},
},
},
},
}
tests := []struct {
name string
input string
expected string
}{
{
name: "check equal to expected name",
input: "sample-pod",
expected: "sample-pod",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
newPod, err := replaceNameAndCreateNewPod(clientset, original, tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.expected, newPod.ObjectMeta.Name)
_, err = clientset.CoreV1().Pods("default").Get(context.TODO(), tt.expected, metav1.GetOptions{})
assert.NoError(t, err)
})
}
}
TestReplaceNameAndCreateNewPod()関数の初めにkubernetes/fakeパッケージのNewSimpleClientset()関数を呼び、クライアントを初期化しています。
replaceNameAndCreateNewPod()を呼び出す際には、こちらで初期化したクライアントを渡していることがわかるかと思います。
テストではまず次の部分で、replaceNameAndCreateNewPod()の戻り値とエラーオブジェクトについて、想定した値が返されているか確認しています。
newPod, err := replaceNameAndCreateNewPod(clientset, original, tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.expected, newPod.ObjectMeta.Name)
次に以下の部分で、想定したPodオブジェクトが作成されているのかのチェックを行なっています。
fake.Clientsetが活躍しているのはこちらの部分です。
_, err = clientset.CoreV1().Pods("default").Get(context.TODO(), tt.expected, metav1.GetOptions{})
assert.NoError(t, err)
上記のコードでテストを実行するとテストは成功します。
go test .
ok main/create-pod 0.638s
fake clientの機能を実験
kubernetes/fakeパッケージのClientsetのCreate()メソッドが呼ばれた際に、本当にメモリ上でオブジェクトが作成されているのか実験してみます。
create_pod.goのreplaceNameAndCreateNewPod()関数の一部実装をコメントアウトしてテストを実行します。(contextパッケージの参照もなくなるためimport文もコメントアウトしてください)
この変更によりclientsetのCreate()メソッドは呼ばれなくなるため、上記の3つ目のテスト箇所は失敗する想定です。
func replaceNameAndCreateNewPod(clientset kubernetes.Interface, base *corev1.Pod, podName string) (*corev1.Pod, error) {
newPod := base.DeepCopy()
newPod.ObjectMeta.Name = podName
// _, err := clientset.CoreV1().Pods("default").Create(context.TODO(), newPod, metav1.CreateOptions{})
// if err != nil {
// return nil, err
// }
return newPod, nil
}
変更後にテストを実行すると以下の出力を得ます。
出力から、3つ目の観点のチェック箇所でnot foundなエラーが返され、テストに失敗していることがわかります。
go test . 19:22:34
--- FAIL: TestReplaceNameAndCreateNewPod (0.00s)
--- FAIL: TestReplaceNameAndCreateNewPod/check_equal_to_expected_name (0.00s)
create_pod_test.go:51:
Error Trace: /Users/yutookamoto/work/create_k8s_custom_resource/create-pod/create_pod_test.go:51
Error: Received unexpected error:
pods "sample-pod" not found
Test: TestReplaceNameAndCreateNewPod/check_equal_to_expected_name
FAIL
FAIL main/create-pod 0.549s
FAIL
このことから、kubernetes/fakeパッケージのClientsetはCreate()メソッドによるKubernetes APIへの操作をメモリ上で記録できており、テスト内でkubernetesパッケージのClientsetの動きを模倣できていることがわかりました。
Custom Resourceを扱う場合のfake client
今回は例としてPodオブジェクトの操作を題材にしため、kubernetes/fakeパッケージのClientsetを利用しました。
一方で、Custom Resourceのオブジェクトを扱う際にはclient-goが提供するdynamic/fakeパッケージのFakeDynamicClientを利用可能です。
こちらを利用することでdynamicパッケージのDynamicClientを模倣できます。
DynamicClientについては、別記事のclient-goでCustom ResourceのオブジェクトをGETするで説明しているため、気になる方は参考にしてみてください。
まとめ
本記事では、client-goのkubernetes/fakeパッケージを使ってKubernetesオブジェクトの作成をテストする方法を紹介しました。
kubernetes/fakeパッケージのClientsetオブジェクトを利用することでkubernetesパッケージのClientsetオブジェクトの動きを模倣してテストを行うことができました。
注意点として、kubernetes/fakeパッケージのClientsetを利用した場合、etcdへのリソースの書き込みやAdmision Webhookでのvalidation・mutationは対象のリソースに対して機能しないという点があります。こちらの点には注意してください。