Golang專案的測試實踐

軒脈刃發表於2019-05-22

Golang專案的測試實踐

最近有一個專案,鏈路涉及了4個服務。最核心的是一個配時服務。要如何對這個專案進行測試,保證輸出質量,是最近思考和實踐的重點。這篇就說下最近這個實踐的過程總結。

測試金字塔

按照Mike Cohn提出的“測試金字塔”概念,測試分為4個層次

test

最下面是單元測試,單元測試對程式碼進行測試。再而上是整合測試,它對一個服務的介面進行測試。繼而是端到端的測試,我也稱他為鏈路測試,它負責從一個鏈路的入口輸入測試用例,驗證輸出的系統的結果。再上一層是我們最常用的UI測試,就是測試人員在UI介面上根據功能進行點選測試。

單元測試

對於一個Golang寫的服務,單元測試已經是很方便了。我們在寫一個檔案,函式的時候,可以直接在需要單元測試的檔案旁邊增加一個_test.go的檔案。而後直接使用 go test 直接跑測試用例就可以了。

一般單元測試,最直接的衡量標準就是程式碼覆蓋率。

單元測試一般測試的物件是一個函式,一個類。

這個部分已經有很多實踐例子了,就沒什麼好聊的。

整合測試

思考和需求

對於一個服務,會提供多個介面,那麼,測試這些介面的表現就是整合測試最重要的目標了。只有通過了整合測試,我們的這個服務才算是有保障。

手頭這個配時專案,對外提供的是一系列HTTP服務,基本上程式碼是以MVC的形式架構的。在思考對它的整合測試過程中,我希望最終能做到下面幾點:

首先,我希望我手上這個配時服務的整合測試是自動化的。最理想的情況下,我能呼叫一個命令,直接將所有case都跑一遍。

其次,衡量整合測試的達標指標。這個糾結過一段時間,是否需要有衡量指標呢?還是直接所有case通過就行?我們的服務,輸入比較複雜,並不是簡單的1-2個引數,是一個比較複雜的json。那麼這個json的構造有各種各樣的。需要實現寫一些case,但是怎麼保證我的這些case是不是有漏的呢?這裡還是需要有個衡量指標的,最終我還是選擇用程式碼覆蓋率來衡量我的測試達標情況,但是這個程式碼覆蓋率在MVC中,我並不強制要求所有層的所有程式碼都要覆蓋住,主要是針對Controller層的程式碼。controller層主要是負責流程控制的,需要保證所有流程分支都能走到。

然後,我希望整合測試中有完善的測試概念,主要是TestCase, TestSuite,這裡參考了JUnit的一些概念。TestCase是一個測試用例,它提供測試用例啟動和關閉時候的注入函式,TestSuite是一個測試套件,代表的是一系列類似的測試用例集合,它也帶測試套件啟動和關閉時候的注入函式。

最後,視覺化需求。我希望這個測試結果很友好,能有一個視覺化的測試介面,我能很方便知道哪個測試套件,哪個測試用例中的哪個斷言失敗了。

整合測試實踐

Golang 只有_test.go的測試,其中的每個Test_XXX相當於是TestCase的概念,也沒有提供測試case啟動,關閉執行的注入函式,也沒有TestSuite的概念。首先我需要使用 Golang 的test搭建一個測試架子。

整合測試和單元測試不一樣,它不屬於某個檔案,整合測試可能涉及到多個檔案中多個介面的測試,所以它需要有一個單獨的資料夾。它的目錄結構我是這麼設計的:

tester_folder

suites

存放測試套件

suites/xxx

這裡存放測試套件,測試套件資料夾需要包含下列檔案:

before.go存放有

  • SetUp() 函式,這個函式在Suite執行之前會執行
  • Before() 函式,這個函式在所有Case執行之前執行

after.go存放有

  • TearDown() 函式,這個函式在Suite執行之後會執行
  • After() 函式,這個函式在Suite執行之後執行

run_test.go檔案

這個檔案是testsuite的入口,程式碼如下:

package adapt

import "testing"
import . "github.com/smartystreets/goconvey/convey"

func TestRunSuite(t *testing.T) {
    SetUp()
    defer TearDown()
    Convey("初始化", t, nil)

    runCase(t, NormalCasePEE001)
    runCase(t, PENormalCase01)
    runCase(t, PENormalCase04)
    runCase(t, PENormalCase11)
    runCase(t, PENormalCase13)
    runCase(t, PENormalCase14)
    runCase(t, NormalCasePIE001)
    runCase(t, NormalCasePIE002)
    runCase(t, NormalCase01)
    runCase(t, NormalCase02)
    runCase(t, NormalCase07)
    runCase(t, NormalCase08)
    runCase(t, NormalCasePIN003)
    runCase(t, NormalCasePIN005)
    runCase(t, NormalCasePIN006)
    runCase(t, NormalCasePIN015)

}

func runCase(t *testing.T, testCase func(*testing.T)) {
    Before()
    defer After()

    testCase(t)
}

envionment

初始化測試環境的工具

當前我這裡面存放了初始化環境的配置檔案和db的建表檔案。

report

存放報告的地址

程式碼覆蓋率需要額外跑指令碼

在tester目錄下執行:

sh coverage.sh 會在report下生成coverage.out和coverage.html,並自動開啟瀏覽器

引入Convey

關於視覺化的需求。

我引入了Convey這個專案,http://goconvey.co/ 。第一次看到這個專案,覺得這個專案的腦洞真大。

下面可了勁的誇一誇這個專案的優點:

斷言

首先它提供了基於原裝go test的斷言框架;提供了Convey和So兩個重要的關鍵字,還提供了 Shouldxxx等一系列很好用的方法。它的測試用例寫下來像是這個樣子:

package package_name

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestIntegerStuff(t *testing.T) {
    Convey("Given some integer with a starting value", t, func() {
        x := 1

        Convey("When the integer is incremented", func() {
            x++

            Convey("The value should be greater by one", func() {
                So(x, ShouldEqual, 2)
            })
        })
    })
}

很清晰明瞭,並且超讚的是很多引數都使用函式封裝起來了,go中的 := 和 = 的問題能很好避免了。並且不要再絞盡腦汁思考tmp1,tmp2這種引數命名了。(因為都已經分散到Convey語句的func中了)

Web介面

其次,它提供了一個很讚的Web平臺,這個web平臺有幾個點我非常喜歡。首先它有一個case編輯器

convy-edit

什麼叫好的測試用例實踐? 我認為這個編輯器完全體現出來了。寫一個完整的case先考慮流程和斷言,生成程式碼框架,然後我們再去程式碼框架中填寫具體的邏輯。這種實踐步驟很好解決了之前寫測試用例思想偷懶的問題,特別是斷言,基本不會由於偷懶而少寫。

其次它提供很讚的測試用例結果顯示頁面:
goconvey-img
很贊吧,哪個case錯誤,哪個斷言問題,都很清楚顯示出來。

還有,goconvey能監控你執行測試用例的目錄,當目錄中有任何檔案改動的時候,都會重新跑測試用例,並且提供提醒

goconve-notice

這個真是太方便了,可以在每次儲存的時候,都知道當前寫的case是否有問題,能直接提高測試用例編寫的效率。

TestSuite初始化

Web服務測試的環境是個很大問題。特別是DB依賴,這裡不同的人有不同的做法。有使用model mock的,有使用db的。這裡我的經驗是:整合測試儘量使用真是DB,但是這個DB應該是私有的,不應該是多個人共用一個DB。

所以我的做法,把需要初始化的DB結構使用sql檔案匯出,放在目錄中。這樣,每個人想要跑這一套測試用例,只需要搭建一個mysql資料庫,倒入sql檔案,就可以搭建好資料庫環境了。其他的初始化資料等都在TestSuite初始化的SetUp函式中呼叫。

關於儲存測試資料環境,我這裡有個小貼士,在SetUp函式中實現 清空資料庫+初始化資料庫 ,在TearDown函式中不做任何事情。這樣如果你要單獨執行某個TestSuite,能保持最後的測試資料環境,有助於我們進行測試資料環境測試。

TestCase編寫

在整合測試環境中,TestCase編寫呼叫HTTP請求就是使用正常的 httptest包,其使用方式沒有什麼特別的。

程式碼覆蓋率

goconvey有個小問題,測試覆蓋率是根據執行goconvey的目錄計算的,不能額外設定,但是go test是提供的。所以程式碼覆蓋率我還額外寫了一個shell指令碼

#!/bin/bash

go test  -coverpkg xxx/controllers/... -coverprofile=report/coverage.out ./...
go tool cover -html=report/coverage.out -o report/coverage.html
open report/coverage.html

主要就是使用converpkg引數,把程式碼覆蓋率限制在controller層。

tester-coverage

整合測試總結

這套搭建實踐下來,對介面的程式碼測試有底很多了,也測試出不少controller層面的bug。

端到端測試

這個是測試金字塔的第二層了。

關於端到端的測試,我的理解就是全鏈路測試。從整個專案角度來看,它屬於一個架構的層次了,需要對每個服務有一定的改造和設計。這個測試需要保證的是整個鏈路流轉是按照預期的。

比如我的專案的鏈路通過了4個服務,一個請求可能在多個服務之間進行鏈路呼叫。但是這個專案特別的是,這些服務並不都是一個語言的。如何進行測試呢?

理想的端到端測試我的設想是這樣的,測試人員通過postman呼叫最上游的服務,構造不同的請求引數和case,有的case其實可能無法通到最下游,那麼就需要有一個全鏈路日誌監控系統,在這個系統可以看到這個請求在各個服務中的流轉情況。全鏈路日誌監控系統定義了一套tag和一個traceid,要求所有服務在打日誌的時候帶上這個traceid,和當前步驟的tag,日誌監控系統根據這些日誌,在頁面上能很好反饋出這個鏈路。

然後測試人員每個case,就根據返回的traceid,去日誌中查詢,並且確認鏈路中的tag是否都全齊。

關於如何在各個服務中傳遞traceid,這個很多微服務監控的專案中都已經說過了,我也是一樣的做法,在http的header頭中增加這個traceId。

關於打日誌的地方,其實有很多地方都可以打日誌,但是我只建議在失敗的地方+請求的地方打上tag日誌,並且是由呼叫方進行tag日誌記錄,這樣主要是能把請求和返回都記錄,方便除錯,查錯等問題。

UI測試

這個目前還是讓測試人員手動進行點選。這種方式看起來確實比較low,但是貌似也是目前大部分網際網路公司的測試方法了。

總結

這幾周主要是在整合測試方面做了一些實踐,有一些想法和思路,所以拿出來進行了分享,肯定還有很多不成熟的地方沒有考慮到,歡迎評論留言討論。

測試是一個費時費力的工作,大多數情況下,業務的迭代速度估計都不允許做很詳細的測試。但是對於複雜,重要的業務,強烈建議這四層的測試都能做到,這樣程式碼上線才能有所底氣。

相關文章