Home End-to-End Testing for Kubernetes (Part I) - kubetest
Post
Cancel

End-to-End Testing for Kubernetes (Part I) - kubetest

現今 Kubernetes Cluster 部署方式越來越多種,部署門檻也越來越低,但是在部署完之後要如何確認自己的 Cluster 真的是正常可用的? 大多數的人(包含筆者以前)都是簡單部署一些 Deployment 或是 Service 看是否有 running 就認為 Cluster 是正常的,或是部署時沒有看到什麼錯誤訊息就覺得沒事,但是這樣是不太足夠的,因為有可能壞掉的是一些不常使用的功能或是人工比較難已發現的問題,這樣之後如果使用者部署應用時剛好使用到相關功能,發生問題將會很難查找。

為了確認 Cluster 功能是否正常,我們可以針對 Cluster 做完整的測試,測試各個API是否正常,但是筆者查了一下發現網路上比較少關於 Kubernetes 測試的詳細資訊,因此花了一點時間研究並紀錄在此系列文章。

注意, Kubernetes 的測試有非常多種層面,例如: Unit Testing 、 Integration Testing 、 E2E Testing 、 Performance/Benchmark Testing 、 Load Testing 、 Stress Testing 或是 Security Testing,但是在此系列文中,強調的是測試 Cluster 的功能是否正常,因此重點會放在 E2E Testing 上。

E2E Testing for Kubernetes:

另外筆者在 Cloud Native Taiwan User Group (CNTUG) Meetup #20 也有一場 talk 是在介紹這個主題,大家可以參考以下的簡報:

Software Testing

一般 Software Testing 可分成三種比較常見的測試方式:

Unit Testing
單元測試,測試單一功能或是某個函數,例如測試登入功能是否正常。這種測試維度比較小,測試程式的撰寫也比較單純一點。

Integration Testing
整合測試,這種通常是測試多個具有關聯或相依性的功能(或函數)整合之後是否正常,維度比單元測試還要大一點。

End to End (E2E) Testing
前面單元測試跟整合測試兩個都是比較偏向 Developer 驗證 Code 或是函數是否正常,而 E2E 測試比較偏向以使用者角度去操作系統,測試人員把自己當成一般使用者去對整個系統做操作,測試每個功能是否正常。

由於我們只是要確認 Kubernetes Cluster 功能是否正常,並不是要開發,因此此系列文會著重在 Kubernetes E2E Testing。

SIG-Testing

Special Interest Group (SIG) 是由一群志同道合來自各公司各領域的人組成的組織或是團體,主要是一起學習某個技術、對某個技術進行探討或是一起維護某個專案,類似社群的概念,這些 SIG 可能也會定期的舉辦 Meetup 或是 Conference 等。 Kubernetes Community 是由許多不同的 SIG 組成,例如: sig-network 是負責 Kubernetes 中網路部分、 sig-docs 是負責文件、而 sig-storage 是負責儲存部分等等。每個 SIG 都有自己負責的 Subproject ,這些 Subproject 可能是文件撰寫,也有可能是 Code 撰寫。除了 SIG 之外,Kubnernetes Community還有其他的 Subgroups 如 Committees 、Working Groups 跟 User Groups 等等,

如果對於 Kubernetes Community 組成有興趣,可以參考: Kubernetes Community Governance Model 。 如果想了解 Kubernetes 中各個 SIG,可以參考: Kubernetes SIGs and Working Groups。 如果想了解各個 SIG 負責的 Subproject ,可以參考: Subprojects of each SIGs

在眾多 SIG 當中,負責 Kubernetes 測試的是 sig-testing , sig-testing 負責許多知名的 subprojects ,如管理 Kubernetes project 的 CI/CD 系統 - prow ,以及部署 K8s in docker 的 kind 等等。在這裡我們主要會提到的是 - test-infra 。 test-infra 提供了許多供 Kubernetes 測試的工具,裡面包含了我們要介紹的 Kubernetes E2E Testing Tool - kubetest

Cluster E2E Testing

Kubernetes E2E Testing 主要是驗證所有的功能 (包含 API Server 以及 Controller ) 是否是正常可用的,且 API 行為必須跟 Spec 上一樣。透過這種測試能夠找出一些 Unit Test 、 Integration Test 或是人工難以找出的問題。

Kubetest Usage

test-infra 釋出了可以對 Kubernetes Cluster 做 E2E Test 的工具 - kubetest ,不過 kubetest 其實是一個整合的介面,他不只可以做一般的 E2E Test ,還整合了如 Conformance Test, Node E2E Test 以及 Performance Test 等等的測試,後續我們會再細部介紹 kubetest 的運作流程。

這邊需要注意的是: kubetest 在執行 E2E Test 時,會開啟一個乾淨的 Cluster 來做測試,這樣的用意是因為 Kubernetes 是想要針對整個 Kubernetes Source Code 做 E2E Test ,這樣才可以確保當前 Release 出來的 Kubernetes 版本是正常可用的。 因此 kubetest 在執行時會去尋找 Kubernetes Repository ,找到後再 Build Source Code ,接著開啟新的 Cluster 測試。

kubetest 其實也有被整合到 prow 裡面,任何 Pull Requet 在被 Merge 前,也都會透過 kubetest 進行測試。

Execution:

1
$ kubetest --build --provider <yourprovider> --deployment <yourdeployer> --up --test --test_args="--ginkgo.skip(focus)=xxx" --dump <folder> --down

Flag:
--build: Build binaries (e2e.test, e2e_node.test) for testing.
--provider: Specify an alternative provider (gce, local, gke, aks, etc.) for E2E testing, default value is gce.
--deployment: Deployment strategies of Kubernetes cluster. (gce, local, gke, aks, etc.)
--up: Turn up a new cluster.
--test: Run E2E testing.
--test_args: Test matching for ginkgo.
--down: Shutdown and delete the cluster.
--dump: Export the result. ( junit xml format )

這邊比較容易搞混的是 --provider 以及 --deployment ,官方文件對這兩個 flag 並沒有太多的解釋,經過筆者研究了之後,整理出來結論是: --provider 比較像是告訴 kubetest 要在哪個平台上做測試或著是告訴 kubetest 準備哪一個平台使用的 Test Lists,而 --deployment 比較像是告訴 kubetest 你的 Cluster 位置,讓 kubetest 可以存取到 Cluster 。舉例來說如果你設定 --deployment gke , kubetest 就會知道你的 Cluster 是用 gke 佈出來的,接下來就會等你再提供一些 gke 相關參數(例如提供 kubeconfig 或是一些 token 之類的),讓 kubetest 能夠存取得到你的 Cluster 。需要注意的是 --provider--deployment 兩個平台必須一致,否則 kubetest 會無法測試並且報錯,例如 --provider local 配上 --deployment gke 或是 --provider gke--deployment local 這種情況是不行的。

筆者實際執行完整的 E2E Test 大概花了一天左右,不過可能跟筆者 Homelab 硬體資源有關,實際上應該不需要這多時間。

使用 --dump flag 會將測試結果存成 JUnit 格式,裡面會寫所有執行的測項以及測試結果 (包括失敗訊息):

Kubetest Workflow

接下來解釋一下 kubetest 運作流程,到底執行 kubetest 之後它做哪些事情呢?

上圖為 kubetest 簡單的執行流程圖,基本上大致流程是 kubernetes/hack/e2e.go -> test-infra/kubetest -> kubernetes/hack/ginkgo-e2e.sh -> kubernetes/test/e2e/e2e_test.go -> kubernetes/test/e2e/framework/framework.go -> Start E2E testing

Stage 1: kubernetes/hack/e2e.go -> test-infra/kubetest

kubernetes/hack/e2e.go 在以往還沒有 kubetest 的時候, Kubernetes 測試都是各自分開的介面,而其中 E2E Testing 部分就是 kubernetes/hack/e2e.go 負責,但是 sig-testing 可能是為了整合這些介面,因此開發了 kubetest ,來讓測試人員能夠直接使用單一介面來做到各式各樣的測試( Cluster E2E Test 、 Node E2E Test 或 Performance Test 等等)。如果你直接去看 kubernetes/hack/e2e.go 的 Source Code,你會發現其實它也是會去抓 kubetest 並且執行 kubetest ,所以你可能會看到網路上一些文章介紹 E2E Testing 的時候是執行 hack/e2e.go --provider xxx ... 而不是 kubetest --provider ... ,這兩個其實是一樣的。

如果你去看 kubernetes/hack/e2e.go 的 history ,可以看到他在2017年的時候把 Code 整個改成 kubetest 介面,由此可以得知是從那個時候開始整合進去的。

Stage 2: test-infra/kubetest -> kubernetes/hack/ginkgo-e2e.sh

Ginkgo & Gomega
Kubernetes E2E Testing 所有測項其實都是用 Ginkgo 以及 Gomega 撰寫的, Ginkgo 是 Golang 的 Behavior-Driven Development (BDD) Testing Framework ,而 Gomaga 是 Golang 的 Matcher Library, 用在測項結果比對。

這邊簡單解釋一下 BDD 為何,一般在做 Software Testing 時, RD 或是 QA 可能會依照規格書撰寫出對應的測試 Code ,但是這些 Code 可能只有技術人員看得懂,這樣會導致技術與非技術人員很難協同作業或討論,因此很容易發生開發者誤解測項或是規格書制定不清楚的情況。而 BDD 就是為了解決這個問題,讓開發人員以及非技術人員能夠一同協作的一種開發方式,首先大家會共同制定一份規格書,規格書會使用更接近人類語意的自然語言來描述軟體功能和測試案例,制定完後, QA 能夠直接執行這份規格書來測試,無需再額外寫一些複雜的測試 Code,簡單來說 BDD 就是以軟體的行為來描述測試,讓大家都能看懂。

Ginkgo 主要由以下三個部分組成:

  1. Main Program: 主要要測的程式。
  2. Test Suite: 測試包,通常會包含很多 Spec (測項)。
  3. Spec: 測項 (可能一個或多個)。

以下使用簡單的範例來介紹如何使用 Ginkgo 和 Gomaga ,範例 Code 可以從筆者 GitHub 上下載,有興趣可以去載下來執行看看。

  1. 下載並安裝 Ginkgo 和 Gomega
    1
    2
    3
    4
    
    $ go get github.com/onsi/ginkgo/ginkgo
    $ go get github.com/onsi/gomega/...
    # Install Ginkgo CLI
    $ go install github.com/onsi/ginkgo/ginkgo
    
  2. 這邊撰寫一個簡單的 myproject package ,裡面會有兩個函數: Greeting()Calc()Greeting() 功能是是傳入一個字串,會回傳 Hello xxx 的打招呼函數; Calc() 是傳入一個數值,會回傳十倍的值給你。

    myproject.go

    1
    2
    3
    4
    5
    6
    7
    8
    
    package myproject
    
    func Greeting(name string) string{ 
       return "Hello " + name + "."
    }
    func Calc(number int) int{ 
       return number * 10
    }
    
  3. 新增 Test Suite , Test Suite 就是測試包的意思,測試包會去抓所有的測試檔,並執行裡面的測項 ( Spec )。
    1
    2
    3
    4
    5
    6
    
    $ cd myproject
    $ ls
    myproject.go
    $ ginkgo bootstrap
    $ ls
    myproject.go myproject_suite_test.go
    
  4. 查看 Test Suite ,會發現裡面會有一個 Testxxx() 函數。預設 Golang 本身就有支援測試的功能 ( go test ),當執行 go test 後, go 會去抓目錄底下所有 xxx_test.go 的檔案,並執行這些 go 檔裡面 Testxxx() 的函數,這些函數就是你寫的測項。 Ginkgo 也是使用 go test 這種特性,讓 go 先去執行 Testxxx(),只不過在這個 Testxxx() 函數裡執行的是 Ginkgo 的函數 RunSpecs()RunSpecs() 會去抓目錄底下所有的測項 ( Spec ) 。

    myproject_suite_test.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    package myproject_test 
    
    import (
          "testing"
          . "github.com/onsi/ginkgo"
          . "github.com/onsi/gomega"
    )
    
    func TestMyproject(t *testing.T) {
          RegisterFailHandler(Fail)
          RunSpecs(t, "Myproject Suite")
    }
    
  5. 接著就是產生測試檔
    1
    2
    3
    
    $ ginkgo generate mytest
    $ ls
    myproject.go myproject_suite_test.go mytest_test.go
    

    mytest_test.go

    1
    2
    3
    4
    5
    6
    7
    8
    
     package myproject_test 
     import (
             . "github.com/onsi/ginkgo"
             . "github.com/onsi/gomega"
             . "github.com/myproject"
     )
     var _ = Describe("Mytest", func() {
     })
    
  6. 撰寫測項 ( Spec ) ,如果你曾經有寫過其他語言的測試,你可能會發現 Ginkgo 架構與他們非常相似,都是由三個部分組成: Describe , Context 以及 It , Describe 是描述這個 Spec ,例如: 測試 Greeting() 函數 。 Context 是補充說明這個 Spec ,可以增加條件式或是任何規則還讓 Spec 行為更加明確,例如: 傳給它一個字串 或是 傳給它一個中英文夾雜的字串 等等。最後 It 是預期達到的結果,例如: 應該要跟我打招呼 , 要回傳什麼值要報錯 等等。

    在此範例中, Spec 1 是測試傳入一個字串給 Greeing() ,驗證是否會回傳 Hello <字串>。 Spec 2 是測試傳入一個數值給 Calc() ,驗證是否會回傳十倍的值。

    mytest_test.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
     ...
     var _ = Describe("Mytest", func() {
         var name string= "test"
         var number int= 99
    
         // Spec 1
         Describe("Test Greeting function", func() {
                 Context("Giva a name", func() {
                     It("Should greeting", func() {
                         Expect(Greeting(name)).To(Equal("Hello "+name+"."))
                     })
                 })
         })
         // Spec 2
         Describe("Test Calc function", func() {
                 Context("Give a number", func() {
                     It("Should get the correct result", func() {
                         Expect(Calc(number)).To(Equal(number*10))
                     })
                 })
         })
     })
    
    

    另一個範例是 Ginkgo 的一個函數 BeforeEach() ,由於 Kubernetes 的 E2E Test 中使用了很多 BeforeEach ,因此這裡特別解釋一下 BeforeEach  的作用。 BeforeEach 執行時機是在 每個 Spec 執行前 ,我們可以將一些參數初始化的步驟或是一些可能會被 Spec 影響的步驟放到 BeforeEach 裡面,這樣可以確保每次的測試都是公平乾淨且不受影響的。

    從底下例子來看,我們宣告一個 number 並給它值 99 , Spec 1 會去執行驗證 Calc() 函數,驗證完後會把 number 值改掉,如果沒有把 number = 99 放到 BeforeEach() 裡面,那 Spec 2 就會直接使用 被改過number ( 這時應該是 990 ) 去做測試,然後就會測失敗,因此必須將 number = 99 放到 BeforeEach() 裡面,讓 Ginkgo 執行每個 Spec 之前都重新指派一次 number 的值。

    mytest2_test.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
      
     var _ = Describe("Mytest", func() {
         var number int
         // 如果將 number 指派放到 BeforeEach 外面,則 number 的值會被 Spec 1 改掉。
         // number = 99
    
         BeforeEach(func(){
             // 將number 指派放到 BeforeEach 裡面可以確保值永遠都是 99
             number  = 99
         })
         Describe("Test Calc function again", func() {
             Context("Give a number 99", func() {
                 // Spec 1 
                 It("Should return 990", func() {
                     Expect(Calc(number)).To(Equal(990))
                     number = Calc(number2)
                 })
                 // Spec 2
                 It("Should return 990, too", func() {
                     Expect(Calc(number)).To(Equal(990))
                 })
             })
         })
     })   
    
  7. 執行測試,我們可以直接使用 ginkgo 或是 go test 指令來執行測試。執行完後 Ginkgo 會顯示執行了幾個 Spec ,然後幾個成功幾個失敗。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    $ ginkgo
    # Show all Specs
    $ ginkgo -v
    
    Running Suite: Myproject Suite
    ==============================
    Random Seed: 1568041760
    Will run 4 of 4 Specs
    
    •••
    Ran 4 of 4 Specs in 0.001 seconds
    SUCCESS! -- 4 Passed | 0 Failed | 0 Pending | 0 Skipped
    PASS
    
    Ginkgo ran 1 suite in 3.471827325s
    Test Suite Passed
    
    

    到這邊大概了解 Ginkgo 的作用後,接下來我們回到 test-infra/kubetest ,根據程式碼來看一下 kubetest 做的事。

    當我們執行 kubetest --test 的時候, test-infra/kubetest/main.go 會去執行 complete() 函數,接著根據指定的 --deployment 執行 run(deploy, *o)

    test-infra/kubetest/main.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    ...
    func main() {
       ...
       err := complete(o)
       ...
    }
    
    func complete(o *options) error {
       ...
       if err := run(deploy, *o); err != nil {
          return err
       }
       ...
    }
    ...
    

    test-infra/kubetest/e2e.go 裡的 run() 函數,會判斷是否有設定 --test flag,有得話就執行 Run() 來做 E2E test ,可以看到 Run() 函數裡會去呼叫 ginkgo-e2e.sh

    test-infra/kubetest/e2e.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    ...
    func run(deploy deployer, o options) error {
       ...
       // 判斷是否有設定 --test flag
       if o.test {
          // 判斷是否有 build 了 e2e.test
          if err := control.XMLWrap(&suite, "test setup", deploy.TestSetup); err != nil {
             ...
          } 
          ...
          else {
             if o.deployment != "conformance" {
                ...
             }
             ...
             else{
                ...
                if tester != nil {
                   // 開始測試
                   return tester.Run(control, testArgs);		
                }
                ...
             } 
          }
       }      
    }
    ...
    
    // Run executes ./hack/ginkgo-e2e.sh
    func (t *GinkgoScriptTester) Run(control *process.Control, testArgs []string) error {
       return control.FinishRunning(exec.Command("./hack/ginkgo-e2e.sh", testArgs...))
    }
    

    由此可以知道 kubetest 其實是使用 Ginkgo 來執行所有測項,並且會執行 ginkgo-e2e.sh

Stage 3: kubernetes/hack/ginkgo-e2e.sh -> kubernetes/test/e2e/e2e_test.go -> kubernetes/test/e2e/framework/framework.go

在 Stage 2 我們已經知道 kubetest 會執行 Ginkgo , 接下來解釋到底 Test Lists 在哪裡。 我們實際去看 kubernetes/hack/ginkgo-e2e.sh 原始碼,會看到 Ginkgo 會去執行 e2e.test

kubernetes/hack/ginkgo-e2e.sh

1
2
3
4
5
6
7
8
9
10
11
...
# Find the ginkgo binary build as part of the release.
ginkgo=$(kube::util::find-binary "ginkgo")
e2e_test=$(kube::util::find-binary "e2e.test")

...

"${ginkgo}" "${ginkgo_args[@]:+${ginkgo_args[@]}}" "${e2e_test}" -- \
  "${auth_config[@]:+${auth_config[@]}}" \
  --ginkgo.flakeAttempts="${FLAKE_ATTEMPTS}" \
...

e2e.test 就是 Test Suite 以及 測項的 Binary 檔,前面我們有提到 kubetest --build 會去 Build Kubernetes Source Code ,其中一個就是把 kubernetes/test/e2e Build 成 e2e.test (可參考 BUILD)。

這邊 Build 其實就是做 make 的動作,如果對於 make 細節有興趣,可以去研究 Kubernetes 目錄裡的 Makefile

我們再回到 kubetest 原始碼來看實際步驟,當執行 kubetest --build 時,test-infra/kubetest/main.go 會去執行 Build() 函數,而這個 BUild() 函數是位於 test-infra/kubetest/build.go 裡。

test-infra/kubetest/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func acquireKubernetes(o *options, d deployer) error {
   // Potentially build kubernetes
   // 判斷有沒有設定 --build flag
	if o.build.Enabled() {
		var err error
		// kind deployer manages build
		if k, ok := d.(*kind.Deployer); ok {
			err = control.XMLWrap(&suite, "Build", k.Build)
		} else {
         // o.build.Build 就是 kubetest --build="值" 指定的值,如果沒指定
         // 預設是  quick-release
			err = control.XMLWrap(&suite, "Build", o.build.Build)
		}
		...
   }
}

Build() 函數會實際去執行 make 來 Build Source Code 。

test-infra/kubetest/build.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (b *buildStrategy) Build() error {
	var target string
	switch *b {
	case "bazel":
		target = "bazel-release"
	case "e2e":
		//TODO(Q-Lee): we should have a better way of build just the e2e tests
		target = "bazel-release"
      ...
	case "host-go":
		target = "all"
	case "quick":
		target = "quick-release"
	case "release":
		target = "release"
	case "gce-windows-bazel":
		...
	default:
		return fmt.Errorf("Unknown build strategy: %v", b)
	}
   ...

	// 執行 make -C kubernetes <target>
	return control.FinishRunning(exec.Command("make", "-C", util.K8s("kubernetes"), target))
}

到這邊我們已經知道 kubetest --build 執行細節以及 e2e.test 的由來,接下來我們再討論 e2e.test 裡的 Test Suite 以及 Spec 到底是什麼?

Test Suite

前面有提到 Golang 在執行 go test 時會去找 xxx_test.go 的檔案,並且執行裡面的 Testxxx() 函數。因此當 Ginkgo 執行 e2e.test 時,會找到 kubernetes/test/e2e/e2e_test.go 檔,然後去執行 kubernetes/test/e2e/e2e.go 裡面的 TestE2E() 函數。

kubernetes/test/e2e/e2e_test.go

1
2
3
func TestE2E(t *testing.T) {
	RunE2ETests(t)
}

TestE2E() 函數會執行 Ginkgo 函數 RunSpec() 來執行所有 Spec,由此可以知道 e2e_test.go 以及 e2e.go 就是 Test Suite 。

kubernetes/test/e2e/e2e.go

1
2
3
4
func RunE2ETests(t *testing.T) {
   ...
   ginkgo.RunSpecsWithDefaultAndCustomReporters(t, "Kubernetes e2e suite", r)
}

Specs

一般來說使用 Ginkgo 來做測試,為了方便 Test Suite 去抓到所有的 Spec ,會在 Test Suite 檔案 Import Spec 的 {ackage (除非 Spec 與 Test Suite 同個 Package ),所以如果直接從 e2e_test.go 以及 e2e.go 去看,就會發現在 e2e_test.go 裡 Import 了所有 E2E Test 用到的 Spec ,可以發現這些測項就是位於 kubernetes/test/e2e/ 底下。

kubernetes/test/e2e/e2e_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package e2e

import (
   "flag"
   ...

   // test sources
	_ "k8s.io/kubernetes/test/e2e/apimachinery"
	_ "k8s.io/kubernetes/test/e2e/apps"
	_ "k8s.io/kubernetes/test/e2e/auth"
	_ "k8s.io/kubernetes/test/e2e/autoscaling"
	_ "k8s.io/kubernetes/test/e2e/cloud"
	_ "k8s.io/kubernetes/test/e2e/common"
	_ "k8s.io/kubernetes/test/e2e/instrumentation"
	_ "k8s.io/kubernetes/test/e2e/kubectl"
	_ "k8s.io/kubernetes/test/e2e/lifecycle"
	_ "k8s.io/kubernetes/test/e2e/lifecycle/bootstrap"
	_ "k8s.io/kubernetes/test/e2e/network"
	_ "k8s.io/kubernetes/test/e2e/node"
	_ "k8s.io/kubernetes/test/e2e/scheduling"
	_ "k8s.io/kubernetes/test/e2e/servicecatalog"
	_ "k8s.io/kubernetes/test/e2e/storage"
	_ "k8s.io/kubernetes/test/e2e/storage/external"
	_ "k8s.io/kubernetes/test/e2e/ui"
	_ "k8s.io/kubernetes/test/e2e/windows"
)
...

從裡面挑了一個 Spec 來看,可以看到 Spec 格式就是使用 Ginkgo 以及 Gomega 寫出來的。

kubernetes/test/e2e/ui/dashboard.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package ui
...

var _ = SIGDescribe("Kubernetes Dashboard [Feature:Dashboard]", func() {
	ginkgo.BeforeEach(func() {
		// TODO(kubernetes/kubernetes#61559): Enable dashboard here rather than skip the test.
		framework.SkipIfProviderIs("gke")
	})

	const (
		uiServiceName = "kubernetes-dashboard"
		uiAppName     = uiServiceName
		uiNamespace   = metav1.NamespaceSystem

		serverStartTimeout = 1 * time.Minute
	)

	f := framework.NewDefaultFramework(uiServiceName)

	ginkgo.It("should check that the kubernetes-dashboard instance is alive", func() {
		ginkgo.By("Checking whether the kubernetes-dashboard service exists.")
		err := framework.WaitForService(f.ClientSet, uiNamespace, uiServiceName, true, framework.Poll, framework.ServiceStartTimeout)
		framework.ExpectNoError(err)

		ginkgo.By("Checking to make sure the kubernetes-dashboard pods are running")
		selector := Labels.SelectorFromSet(Labels.Set(map[string]string{"k8s-app": uiAppName}))
		err = testutils.WaitForPodsWithLabelRunning(f.ClientSet, uiNamespace, selector)
		framework.ExpectNoError(err)

		ginkgo.By("Checking to make sure we get a response from the kubernetes-dashboard.")
		err = wait.Poll(framework.Poll, serverStartTimeout, func() (bool, error) {
			var status int
			proxyRequest, errProxy := e2eservice.GetServicesProxyRequest(f.ClientSet, f.ClientSet.CoreV1().RESTClient().Get())
			if errProxy != nil {
				framework.Logf("Get services proxy request failed: %v", errProxy)
			}

			ctx, cancel := context.WithTimeout(context.Background(), framework.SingleCallTimeout)
			defer cancel()

			// Query against the proxy URL for the kubernetes-dashboard service.
			err := proxyRequest.Namespace(uiNamespace).
				Context(ctx).
				Name(utilnet.JoinSchemeNamePort("https", uiServiceName, "")).
				Timeout(framework.SingleCallTimeout).
				Do().
				StatusCode(&status).
				Error()
			if err != nil {
				if ctx.Err() != nil {
					framework.Failf("Request to kubernetes-dashboard failed: %v", err)
					return true, err
				}
				framework.Logf("Request to kubernetes-dashboard failed: %v", err)
			} else if status != http.StatusOK {
				framework.Logf("Unexpected status from kubernetes-dashboard: %v", status)
			}
			// Don't return err here as it aborts polling.
			return status == http.StatusOK, nil
		})
		framework.ExpectNoError(err)
	})
})

此 Spec 是檢查 kubernetes dashboard 是否正常執行

Kind of Tests

從前面小節我們得知 Kubernetes E2E Testing 的測項,但是這些測項非常的多,要如何區別這些測項呢? Kubernetes 針對這部份採用了賦予 Label 的方式來區別不同種類的測項。

  • [Slow] - 執行時間超過兩分鐘的測項。
  • [Serial] - 需要依序執行,而不能平行執行的測項。
  • [Disruptive] - 具有破壞性或是會影響其他測試的測項,例如重開機 node ,或是砍掉 kube-system 相關的 pod 等等。
  • [Internet] - 會需要連到外部網路的測項。
  • [Conformance] - Conformace Testing 的測項。
  • [LinuxOnly] - 只能跑在 Linux node 的測項。
  • [Privileged] - 會需要 privileged container 的測項。
  • [Alpha] - 測試 Alpha 功能的測項。 …

詳細 Label 資訊可以參考 - Kinds of tests

一個測項 ( Spec ) 可以貼上一個或是多個 Label ,貼的位置可以在 SIGDescribe 或是 Ginkgo.It ,如以下範例:

1
2
3
4
5
6
7
8
ginkgo.It("should provide Internet connection for containers [Feature:Networking-IPv6][Experimental][LinuxOnly]", func() {
		// IPv6 is not supported on Windows.
		framework.SkipIfNodeOSDistroIs("windows")
		ginkgo.By("Running container which tries to connect to 2001:4860:4860::8888")
		framework.ExpectNoError(
			framework.CheckConnectivityToHost(f, "", "connectivity-test", "2001:4860:4860::8888", 53, 30))
})
...
1
2
3
4
5
var _ = SIGDescribe("Kubernetes Dashboard [Feature:Dashboard]", func() {
	ginkgo.BeforeEach(func() {
		framework.SkipIfProviderIs("gke")
   })
...

Execute Specific Kind of Tests

在 kubetest 使用上,要過濾或是執行特定的測項,只需要使用 --test_args flag 然後裡面再設定 --ginkgo.focus/skip 就可以了。--test_args 是讓 kubetest 可以使用 Ginkgo CLI 的 flag ,因此不單單可以用 focus/skip ,只要是 Ginkgo CLI 支援的 flag ,基本上都可以使用。focus/skip 可以透過正規表示式來比對 Describe 或是 ginkgo.It 描述裡的 Label。

1
2
3
4
5
# Only execute tests with LinuxOnly Label.
$ kubetest --test --test_args="--ginkgo.focus=\[LinuxOnly\]" --provider local --deployment local

# Skip the tests with LinuxOnly Label.
$ kubetest --test --test_args="--ginkgo.skip=\[LinuxOnly\]" --provider local --deployment local

focus/skip 可以用來比對 Describe 或是 ginkgo.It 描述裡的 Label。

framework.go

kubernetes/test/e2e/framework/framework.go 是用來執行一些 E2E test 會使用到的函數,如後續會提到的 Conformance Testing 就是在 framework.go 裡執行 framework.ConformanceIt() 來做貼 Label 的動作。其餘 framework.go 細節筆者就沒深入去瞭解,因此這部分就不深入解釋。

Write Your Own Test

在了解 kubetest 流程之後,接下來我們來試著撰寫自己的測試。

Specifications

Kubernetes Community 在 Writing good e2e tests for Kubernetes 裡提到一些撰寫測試前需要注意的事項以及一些規範,大致為以下幾項:

  • Debuggability : 在寫測試時盡量將所有訊息寫清楚,例如測試失敗時,失敗訊息要寫明確說是哪裡錯誤,不要就含糊的顯示「測試失敗」之類的訊息,必須讓測試人員容易去 Debug 。

  • Ability to run in non-dedicated test clusters : 不要限定測試只能跑在特定的 Cluster,這裡的意思是撰寫測試時不要有假設的情況,任何測項都要寫清楚。例如:假設這個 Cluster 是乾淨的,上面沒有跑任何服務、假設環境裡已經有 xxx 服務或是假設 Cluster 跑在什麼硬體、環境等等。這裡官方舉了一個例子: 假如今天寫了一個測試「確認你的 Pod 能跑在所有 Node 上」來驗證所有 Node 有沒有問題,這個測項看起來似乎沒問題,但是實際上做了一個「同時間內只有你這個測項跑在 Node 上或是同時間沒有其他服務在 Node 上」的假設。假如你在跑這項測試前, Cluster 正在同時跑其他測試(或是服務),導致某個 Node 資源被佔滿或是執行到 [Disruptive] 的測項,這樣這個測試就會被影響而失敗,但是他的失敗並不是 Node 本身有問題,而是被其他東西影響,這樣測試出來就會不準確。

    另外是盡量避免撰寫 [Disruptive] 的測項,我們前面有提到 [Disruptive] 是具破壞性的測項,可能會影響其他測試,例如:重開機、刪掉服務等等。這裡避免的意思一樣是不要假設:「同時間只有你這個測項在測試」,避免影響到其他測試或服務,如果真的要撰寫該類型的測試,一定要加 [Disruptive] Label,並且把測項描述寫清楚。

    最後是盡量不要使用非 Kubernetes 官方的 API 來做測試,因為這樣測試失敗時很難找出是 API 問題還是 Cluster 問題。

  • Speed of execution : 在寫測試時,盡量提升測試的效率、壓低測試的時間,不要放一些如 sleep 這種耗時間的函數,如果測試時間會超過兩分鐘以上就加上 [Slow] Label,讓測試者知道這個是比較耗時的測項。

    另外除了測試花的時間之外,還必須設定好測試失敗的時間,例如:你的測試很簡單只需要兩分鐘內完成,但是可能光是等 Pod Ready 就等超過兩分鐘甚至是 Pod 已經卡住了,如果沒有設定測試失敗的時間 (如10分鐘後就認定測試失敗) ,這個測試就會永遠卡在那。

  • Resilience to relatively rare, temporary infrastructure glitches or delays : 在寫測試時,要彈性一點,當遇到失敗的情況,多試幾次,有可能只是剛好一些情況導致失敗。舉例來說:你的測項是 「測試 Cluster 能不能跑起來 Nginx 的 Pod 」,有可能第一次在測剛好網路比較不穩, Image 抓比較慢或是抓不下來,導致測試失敗,但是第二次網路就恢復重抓就抓下來了,因此不要在第一次失敗就直接認定測試失敗,過個幾秒再重抓 Image 一次,測試就會正常了。

這裡需要注意的是,雖然說要給測項一些彈性,不過也是要根據你的測項來判斷適不適用。

Add New Test

了解測試撰寫規範之後,接下來就來實際撰寫測試,以下範例可以從我的 GitHub - kubernetes-e2e-practice 裡取得,有興趣者,可以載下來測試。

該範例適用於 Kubernetes v1.15.x 版本

在這個範例裡, 我們會撰寫一個小測試驗證是否能在 Cluster 部署一個名為 pohsien 的 Pod ,並確認這個 Pod 是否成功執行起來。

  1. 首先在 kubernetes/test/e2e 新增一個 pohsien 資料夾,裡面就是放置我們自己撰寫的測試檔。
    1
    
    $ mkdir -c kubernetes/test/e2e/pohsien
    
  2. 接著撰寫自己的測試,並放置在剛剛建立的 kubernetes/test/e2e/pohsien/ 裡面,基本上這邊都是使用 client-go 來操作 Kubernetes 資源。我們定義一個 [Pohsien] Label ,然後把我們的測項貼上這個 Label ,以方便之後執行測試。
    1
    2
    
    $ vim kubernetes/test/e2e/pohsien/pohsien.go
    $ vim kubernetes/test/e2e/pohsien/framework.go
    

    pohsien.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
     package pohsien
     ...
     var _ = SIGDescribe("Kubernetes Pohsien's Pod [Pohsien]", func() {
             f := framework.NewDefaultFramework("pods")
             var podClient *framework.PodClient
                     ginkgo.BeforeEach(func() {
                     podClient = f.PodClient()
             })
    
             ginkgo.It("Make sure the pohsien pod can be deployed", func(){
                 // 建立 pod 
                 ginkgo.By("Create Pod")
                 pod := &corev1.Pod{
                             ObjectMeta: metav1.ObjectMeta{
                                     Name: "pohsien",
                                     Labels: map[string]string{
                                             "name": "pohsien",
                                     },
                             },
                             Spec: corev1.PodSpec{
                                     Containers: []corev1.Container{
                                             {
                                                     Name:  "nginx",
                                                     Image: "nginx:1.17.3",
                                             },
                                     },
                             },
                     }
                 podClient.Create(pod)
                    
                 // 檢查 pod 是否成功運行起來
                 ginkgo.By("Get the pod")
                     podGetting, err := podClient.Get(pod.Name, metav1.GetOptions{})
                 framework.ExpectNoError(err, "Failed to get the pod")
                 framework.ExpectNoError(f.WaitForPodRunning(podGetting.Name))
                 gomega.Expect(podGetting.Name, "pohsien")
    
             })
     })
    

    framework.go 是用來宣告 SIGDescribe() 函數,而其他的測試檔會呼叫這個函數並且傳遞相關參數(例如要設定的 Label 以及 Spec 等等),一般來說會在 framework.go 設定一些 Global 的設定或變數,例如幫整個該資料夾的測項貼上對應的 SIG Label。雖然 framework.go 應該不是必要的,但是為了與其他測項一致,因此在此範例我們也新增了 framework.go ,並且加上了我們虛構的 [pohsien-testing] Label 。 framework.go

    1
    2
    3
    4
    5
    6
    7
    8
    
     package pohsien
    
     import "github.com/onsi/ginkgo"
    
     // SIGDescribe annotates the test with the SIG Label.
     func SIGDescribe(text string, body func()) bool {
         return ginkgo.Describe("[pohsien-testing] "+text, body)
     }
    

    由於我們範例只是 Demo 用,因此在這裡並沒有特別嚴謹的指定 Label ,但請記得在撰寫自己的測試時一定要加上對應 Label ,如 [Slow][Serial][Disruptive] 等等,這樣可以讓測試人員更加了解你的測項的特性,如果不確定要加上哪些 Label 可以在 slack 或是 發佈 Issue / PR 時,標註 #kinds-of-tests 來詢問。

  3. 接下來在 kubernetes/test/e2e/e2e_test.go 裡面 Import 我們自己的測項。 framework.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    package e2e
    
    import (
       "flag"
       "fmt"
       ...
    
       // test sources
       _ "k8s.io/kubernetes/test/e2e/apimachinery"
       _ "k8s.io/kubernetes/test/e2e/apps"
       ...
       _ "k8s.io/kubernetes/test/e2e/pohsien"
    )
    ...
    
  4. 接下來撰寫及修改 BUILD 檔來讓 kubetest --build 或是 make 時能抓到我們寫的測試檔。主要有兩個地方: 一個是在 kubernetes/test/e2e/pohsien/ 底下新增 BUILD 檔,另一個是在 kubernetes/test/e2e/BUILD 裡加上我們寫的測試檔。

    BUILD 檔寫法可以參考其他 E2E 測項。

    1
    2
    
    $ vim kubernetes/test/e2e/pohsien/BUILD
    $ vim kubernetes/test/e2e/BUILD
    
  5. 完成後,接下來我們就可以執行看看。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    $ kubetest --build
    $ kubetest --test --test_args="--ginkgo.focus=\[Pohsien\]"
    
    
    [pohsien-testing] Kubernetes Pohsien's Pod [Pohsien]
    Make sure the pohsien pod can be deployed
    /root/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/test/e2e/pohsien/pohsien.go:35
    ...
    STEP: Create Pod
    STEP: Get the pod
    [AfterEach] [pohsien-testing] Kubernetes Pohsien's Pod [Pohsien]
    /root/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/test/e2e/framework/framework.go:151
    Oct 25 09:33:54.652: INFO: Waiting up to 3m0s for all (but 0) nodes to be ready
    STEP: Destroying namespace "pods-1457" for this suite.
    Oct 25 09:34:16.682: INFO: Waiting up to 30s for server preferred namespaced resources to be successfully discovered
    Oct 25 09:34:16.740: INFO: namespace pods-1457 deletion completed in 22.085125288s
    Oct 25 09:34:16.742: INFO: Running AfterSuite actions on all nodes
    Oct 25 09:34:16.742: INFO: Running AfterSuite actions on node 1
    
    Ran 1 of 4414 Specs in 28.369 seconds
    SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 4413 Skipped
    PASS
    
    Ginkgo ran 1 suite in 29.513161526s
    Test Suite Passed
    2019/10/25 09:34:16 process.go:155: Step './hack/ginkgo-e2e.sh --ginkgo.focus=\[Pohsien\]' finished in 29.787466628s
    

到這裡 Kubernetes E2E Testing 解釋就告一段落,下一篇文章會介紹如何在自己建立的 Cluster 做 E2E Test 。

References

  1. Types Of Software Testing: Different Testing Types With Details
  2. WIKIPEDIA - Special Interest Group
  3. Kubernetes SIGs and Working Groups
  4. Kubernetes Community Governance Model
  5. Subprojects of each SIGs
  6. Sig-Testing
  7. test-infra
  8. Kubernetes E2E Testing
  9. Kubetest
  10. Ginkgo
  11. Gomega
  12. End-To-End Testing in Kubernetes
  13. Writing good e2e tests for Kubernetes

文章內容的轉載、重製、發佈,請註明出處: https://blog.phshih.com/

This post is licensed under CC BY 4.0 by the author.