これは 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
は対象のリソースに対して機能しないという点があります。こちらの点には注意してください。