如何對被 Kubernetes API 調用的代碼進行單元測試?
使用 Kubernetes 客戶端庫可以通過模擬集群來測試代碼。
作為在構建 kubernetes / minikube 時,第一批使用kubernetes / client-go 庫的用戶之一,我曾經詳盡的設計了服務、調度單元和部署來對我的代碼進行單元測試。現在,我發現有一種更簡單的方法可以用更少的代碼行來做同樣的事情。
我將演示如何測試一個列出了集群中運行的所有容器映像的簡單的函數。我建議你用 GKE 或 Docker 作為桌面,並且你還需要一個 Kubernetes 集群。
如果你想要運行命令並以交互方式繼續操作,請克隆示例倉庫 https://github.com/r2d4/k8s-unit-test-example 。
main.go>>>>package main
import (
"github.com/pkg/errors"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/typed/core/v1"
)
// ListImages returns a list of container images running in the provided namespace
func ListImages(client v1.CoreV1Interface, namespace string) ([]string, error) {
pl, err := client.Pods(namespace).List(meta_v1.ListOptions{})
if err != nil {
return nil, errors.Wrap(err, "getting pods")
}
var images []string
for _, p := range pl.Items {
for _, c := range p.Spec.Containers {
images = append(images, c.Image)
}
}
return images, nil
}
我們可以從測試用例的定義開始,運行一些測試的框架代碼。
func TestListImages(t *testing.T) {
var tests = []struct {}
發生了什麼>>>>
這種編寫測試的風格稱為「表驅動測試」,是Go語言中首選的樣式,實際測試代碼會迭代表條目並執行必要的測試。測試代碼只需要寫一次,卻可以適用於每種情況。我門可以注意到一些有趣的事情:
測試用例的定義是用匿名結構保存的,因此我們可以簡潔的定義測試用例。
對象切片運行時,objs將保存我們的模擬 API 伺服器保存的所有正在運行的對象,此處我們選擇用一些調度單元來填充它,但其實可以在這裡使用任何 Kubernetes 對象。
如果測試用例非常瑣碎,伺服器上將沒有調度單元,並且不返回任何圖像。
讓我們來為每個測試用例都寫一段實際測試代碼。
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
client := fake.NewSimpleClientset(test.objs...)
actual, err := ListImages(client.CoreV1(), test.namespace)
if err != nil {
t.Errorf("Unexpected error: %s", err)
return
}
if diff := cmp.Diff(actual, test.expected); diff != "" {
t.Errorf("%T differ (-got, +want): %s", test.expected, diff)
return
}
})
}
在這裡我們也可以注意到一些有趣的事情:
fake.NewSimpleClientset 返回一個使用提供的對象進行響應的客戶端集。它由一個非常簡單的對象跟蹤器支持,該跟蹤器按原樣處理創建、更新和刪除操作,而不應用任何驗證和默認值。
我們來創建一個調度單元助手函數,它將幫助我們提供一些測試的調度單元。由於我們關心的是命名空間和鏡像,所以我們可以創建一個可以基於這些參數創建新的調度單元的助手。
func pod(namespace, image string) *v1.Pod {
return &v1.Pod{ObjectMeta: meta_v1.ObjectMeta{Namespace: namespace}, Spec: v1.PodSpec{Containers: []v1.Container{{Image: image}}}}
}
我們來寫三個單元測試。第一個方法能確保我門獲取所有鏡像,這時我們需要使用特殊的命名空間值 「」 來列出命名空間中所有的調度單元。
{"all namespaces", "", []string{"a", "b"}, []runtime.Object{pod("correct-namespace", "a"), pod("wrong-namespace", "b")}}
第二種方法能確保我們按名稱空間正確篩選,忽略調度單元中的 wrong-namespace 。
{"filter namespace", "correct-namespace", []string{"a"}, []runtime.Object{pod("correct-namespace", "a"), pod("wrong-namespace", "b")}}
第三種方法能確保如果所需的命名空間中沒有調度單元,則不會返回任何內容。
{"wrong namespace", "correct-namespace", nil, []runtime.Object{pod("wrong-namespace", "b")}}
把上面這些代碼放在一起。
func TestListImages(t *testing.T) {
var tests = []struct {
description string
namespace string
expected []string
objs []runtime.Object
}{
{"no pods", "", nil, nil},
{"all namespaces", "", []string{"a", "b"}, []runtime.Object{pod("correct-namespace", "a"), pod("wrong-namespace", "b")}},
{"filter namespace", "correct-namespace", []string{"a"}, []runtime.Object{pod("correct-namespace", "a"), pod("wrong-namespace", "b")}},
{"wrong namespace", "correct-namespace", nil, []runtime.Object{pod("wrong-namespace", "b")}},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
client := fake.NewSimpleClientset(test.objs...)
actual, err := ListImages(client.CoreV1(), test.namespace)
if err != nil {
t.Errorf("Unexpected error: %s", err)
return
}
if diff := cmp.Diff(actual, test.expected); diff != "" {
t.Errorf("%T differ (-got, +want): %s", test.expected, diff)
return
}
})
}
}
上一篇:Kubernetes 源碼分析之 kubelet(四)
長按掃描二維碼,添加管理員微信
加入容器時代交流群吧~
記得備註「加群」字樣哦