<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    Go單元測試從入門到放棄—0.單元測試基礎

    一顆小胡椒2022-07-20 11:00:42

    go test工具

    Go語言中的測試依賴go test命令。編寫測試代碼和編寫普通的Go代碼過程是類似的,并不需要學習新的語法、規則或工具。

    go test命令是一個按照一定約定和組織的測試代碼的驅動程序。在包目錄內,所有以_test.go為后綴名的源代碼文件都是go test測試的一部分,不會被go build編譯到最終的可執行文件中。

    *_test.go文件中有三種類型的函數,單元測試函數、基準測試函數和示例函數。

    類型格式作用測試函數函數名前綴為Test測試程序的一些邏輯行為是否正確基準函數函數名前綴為Benchmark測試函數的性能示例函數函數名前綴為Example為文檔提供示例文檔

    go test命令會遍歷所有的*_test.go文件中符合上述命名規則的函數,然后生成一個臨時的main包用于調用相應的測試函數,然后構建并運行、報告測試結果,最后清理測試中生成的臨時文件。

    單元測試函數

    格式

    每個測試函數必須導入testing包,測試函數的基本格式(簽名)如下:

    func TestName(t *testing.T){
        // ...
    }
    

    測試函數的名字必須以Test開頭,可選的后綴名必須以大寫字母開頭,舉幾個例子:

    func TestAdd(t *testing.T){ ... }
    func TestSum(t *testing.T){ ... }
    func TestLog(t *testing.T){ ... }
    

    其中參數t用于報告測試失敗和附加的日志信息。testing.T的擁有的方法如下:

    func (c *T) Cleanup(func())
    func (c *T) Error(args ...interface{})
    func (c *T) Errorf(format string, args ...interface{})
    func (c *T) Fail()
    func (c *T) FailNow()
    func (c *T) Failed() bool
    func (c *T) Fatal(args ...interface{})
    func (c *T) Fatalf(format string, args ...interface{})
    func (c *T) Helper()
    func (c *T) Log(args ...interface{})
    func (c *T) Logf(format string, args ...interface{})
    func (c *T) Name() string
    func (c *T) Skip(args ...interface{})
    func (c *T) SkipNow()
    func (c *T) Skipf(format string, args ...interface{})
    func (c *T) Skipped() bool
    func (c *T) TempDir() string
    

    單元測試示例

    就像細胞是構成我們身體的基本單位,一個軟件程序也是由很多單元組件構成的。單元組件可以是函數、結構體、方法和最終用戶可能依賴的任意東西。總之我們需要確保這些組件是能夠正常運行的。單元測試是一些利用各種方法測試單元組件的程序,它會將結果與預期輸出進行比較。

    接下來,我們在base_demo包中定義了一個Split函數,具體實現如下:

    // base_demo/split.go
    package base_demo
    import "strings"
    // Split 把字符串s按照給定的分隔符sep進行分割返回字符串切片
    func Split(s, sep string) (result []string) {
     i := strings.Index(s, sep)
     for i > -1 {
      result = append(result, s[:i])
      s = s[i+1:]
      i = strings.Index(s, sep)
     }
     result = append(result, s)
     return
    }
    

    在當前目錄下,我們創建一個split_test.go的測試文件,并定義一個測試函數如下:

    // split/split_test.go
    package split
    import (
     "reflect"
     "testing"
    )
    func TestSplit(t *testing.T) { // 測試函數名必須以Test開頭,必須接收一個*testing.T類型參數
     got := Split("a:b:c", ":")         // 程序輸出的結果
     want := []string{"a", "b", "c"}    // 期望的結果
     if !reflect.DeepEqual(want, got) { // 因為slice不能比較直接,借助反射包中的方法比較
      t.Errorf("expected:%v, got:%v", want, got) // 測試失敗輸出錯誤提示
     }
    }
    

    此時split這個包中的文件如下:

    ? ls -l
    total 16
    -rw-r--r--  1 liwenzhou  staff  408  4 29 15:50 split.go
    -rw-r--r--  1 liwenzhou  staff  466  4 29 16:04 split_test.go
    

    在當前路徑下執行go test命令,可以看到輸出結果如下:

    ? go test
    PASS
    ok      golang-unit-test-demo/base_demo       0.005s
    

    go test -v

    一個測試用例有點單薄,我們再編寫一個測試使用多個字符切割字符串的例子,在split_test.go中添加如下測試函數:

    func TestSplitWithComplexSep(t *testing.T) {
     got := Split("abcd", "bc")
     want := []string{"a", "d"}
     if !reflect.DeepEqual(want, got) {
      t.Errorf("expected:%v, got:%v", want, got)
     }
    }
    

    現在我們有多個測試用例了,為了能更好的在輸出結果中看到每個測試用例的執行情況,我們可以為go test命令添加-v參數,讓它輸出完整的測試結果。

    ? go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestSplitWithComplexSep
        split_test.go:20: expected:[a d], got:[a cd]
    --- FAIL: TestSplitWithComplexSep (0.00s)
    FAIL
    exit status 1
    FAIL    golang-unit-test-demo/base_demo 0.009s
    

    從上面的輸出結果我們能清楚的看到是TestSplitWithComplexSep這個測試用例沒有測試通過。

    go test -run

    單元測試的結果表明split函數的實現并不可靠,沒有考慮到傳入的sep參數是多個字符的情況,下面我們來修復下這個Bug:

    package base_demo
    import "strings"
    // Split 把字符串s按照給定的分隔符sep進行分割返回字符串切片
    func Split(s, sep string) (result []string) {
     i := strings.Index(s, sep)
     for i > -1 {
      result = append(result, s[:i])
      s = s[i+len(sep):] // 這里使用len(sep)獲取sep的長度
      i = strings.Index(s, sep)
     }
     result = append(result, s)
     return
    }
    

    在執行go test命令的時候可以添加-run參數,它對應一個正則表達式,只有函數名匹配上的測試函數才會被go test命令執行。

    例如通過給go test添加-run=Sep參數來告訴它本次測試只運行TestSplitWithComplexSep這個測試用例:

    ? go test -run=Sep -v
    === RUN   TestSplitWithComplexSep
    --- PASS: TestSplitWithComplexSep (0.00s)
    PASS
    ok      golang-unit-test-demo/base_demo 0.010s
    

    最終的測試結果表情我們成功修復了之前的Bug。

    回歸測試

    我們修改了代碼之后僅僅執行那些失敗的測試用例或新引入的測試用例是錯誤且危險的,正確的做法應該是完整運行所有的測試用例,保證不會因為修改代碼而引入新的問題。

    ? go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestSplitWithComplexSep
    --- PASS: TestSplitWithComplexSep (0.00s)
    PASS
    ok      golang-unit-test-demo/base_demo 0.011s
    

    測試結果表明我們的單元測試全部通過。

    通過這個示例我們可以看到,有了單元測試就能夠在代碼改動后快速進行回歸測試,極大地提高開發效率并保證代碼的質量。

    跳過某些測試用例

    為了節省時間支持在單元測試時跳過某些耗時的測試用例。

    func TestTimeConsuming(t *testing.T) {
        if testing.Short() {
            t.Skip("short模式下會跳過該測試用例")
        }
        ...
    }
    

    當執行go test -short時就不會執行上面的TestTimeConsuming測試用例。

    子測試

    在上面的示例中我們為每一個測試數據編寫了一個測試函數,而通常單元測試中需要多組測試數據保證測試的效果。Go1.7+ 中新增了子測試,支持在測試函數中使用t.Run執行一組測試用例,這樣就不需要為不同的測試數據定義多個測試函數了。

    func TestXXX(t *testing.T){
      t.Run("case1", func(t *testing.T){...})
      t.Run("case2", func(t *testing.T){...})
      t.Run("case3", func(t *testing.T){...})
    }
    

    表格驅動測試

    介紹

    編寫好的測試并非易事,但在許多情況下,表格驅動測試可以涵蓋很多方面:表格里的每一個條目都是一個完整的測試用例,包含輸入和預期結果,有時還包含測試名稱等附加信息,以使測試輸出易于閱讀。

    使用表格驅動測試能夠很方便的維護多個測試用例,避免在編寫單元測試時頻繁的復制粘貼。

    表格驅動測試的步驟通常是定義一個測試用例表格,然后遍歷表格,并使用t.Run對每個條目執行必要的測試。

    表格驅動測試不是工具、包或其他任何東西,它只是編寫更清晰測試的一種方式和視角。

    示例

    官方標準庫中有很多表格驅動測試的示例,例如fmt包中的測試代碼:

    var flagtests = []struct {
     in  string
     out string
    }{
     {"%a", "[%a]"},
     {"%-a", "[%-a]"},
     {"%+a", "[%+a]"},
     {"%#a", "[%#a]"},
     {"% a", "[% a]"},
     {"%0a", "[%0a]"},
     {"%1.2a", "[%1.2a]"},
     {"%-1.2a", "[%-1.2a]"},
     {"%+1.2a", "[%+1.2a]"},
     {"%-+1.2a", "[%+-1.2a]"},
     {"%-+1.2abc", "[%+-1.2a]bc"},
     {"%-1.2abc", "[%-1.2a]bc"},
    }
    func TestFlagParser(t *testing.T) {
     var flagprinter flagPrinter
     for _, tt := range flagtests {
      t.Run(tt.in, func(t *testing.T) {
       s := Sprintf(tt.in, &flagprinter)
       if s != tt.out {
        t.Errorf("got %q, want %q", s, tt.out)
       }
      })
     }
    }
    

    通常表格是匿名結構體數組切片,可以定義結構體或使用已經存在的結構進行結構體數組聲明。name屬性用來描述特定的測試用例。

    接下來讓我們試著自己編寫表格驅動測試:

    func TestSplitAll(t *testing.T) {
     // 定義測試表格
     // 這里使用匿名結構體定義了若干個測試用例
     // 并且為每個測試用例設置了一個名稱
     tests := []struct {
      name  string
      input string
      sep   string
      want  []string
     }{
      {"base case", "a:b:c", ":", []string{"a", "b", "c"}},
      {"wrong sep", "a:b:c", ",", []string{"a:b:c"}},
      {"more sep", "abcd", "bc", []string{"a", "d"}},
      {"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},
     }
     // 遍歷測試用例
     for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) { // 使用t.Run()執行子測試
       got := Split(tt.input, tt.sep)
       if !reflect.DeepEqual(got, tt.want) {
        t.Errorf("expected:%#v, got:%#v", tt.want, got)
       }
      })
     }
    }
    

    在終端執行go test -v,會得到如下測試輸出結果:

    ? go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestSplitWithComplexSep
    --- PASS: TestSplitWithComplexSep (0.00s)
    === RUN   TestSplitAll
    === RUN   TestSplitAll/base_case
    === RUN   TestSplitAll/wrong_sep
    === RUN   TestSplitAll/more_sep
    === RUN   TestSplitAll/leading_sep
    --- PASS: TestSplitAll (0.00s)
        --- PASS: TestSplitAll/base_case (0.00s)
        --- PASS: TestSplitAll/wrong_sep (0.00s)
        --- PASS: TestSplitAll/more_sep (0.00s)
        --- PASS: TestSplitAll/leading_sep (0.00s)
    PASS
    ok      golang-unit-test-demo/base_demo 0.010s
    

    并行測試

    表格驅動測試中通常會定義比較多的測試case,在Go語言中很容易發揮自身并發優勢將表格驅動測試并行化,可以查看下面的代碼示例。

    func TestSplitAll(t *testing.T) {
     t.Parallel()  // 將 TLog 標記為能夠與其他測試并行運行
     // 定義測試表格
     // 這里使用匿名結構體定義了若干個測試用例
     // 并且為每個測試用例設置了一個名稱
     tests := []struct {
      name  string
      input string
      sep   string
      want  []string
     }{
      {"base case", "a:b:c", ":", []string{"a", "b", "c"}},
      {"wrong sep", "a:b:c", ",", []string{"a:b:c"}},
      {"more sep", "abcd", "bc", []string{"a", "d"}},
      {"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},
     }
     // 遍歷測試用例
     for _, tt := range tests {
      tt := tt  // 注意這里重新聲明tt變量(避免多個goroutine中使用了相同的變量)
      t.Run(tt.name, func(t *testing.T) { // 使用t.Run()執行子測試
       t.Parallel()  // 將每個測試用例標記為能夠彼此并行運行
       got := Split(tt.input, tt.sep)
       if !reflect.DeepEqual(got, tt.want) {
        t.Errorf("expected:%#v, got:%#v", tt.want, got)
       }
      })
     }
    }
    

    使用工具生成測試代碼

    社區里有很多自動生成表格驅動測試函數的工具,比如gotests等,很多編輯器如Goland也支持快速生成測試文件。這里簡單演示一下gotests的使用。

    安裝

    go get -u github.com/cweill/gotests/...
    

    執行

    gotests -all -w split.go
    

    上面的命令表示,為split.go文件的所有函數生成測試代碼至split_test.go文件(目錄下如果事先存在這個文件就不再生成)。

    生成的測試代碼大致如下:

    package base_demo
    import (
     "reflect"
     "testing"
    )
    func TestSplit(t *testing.T) {
     type args struct {
      s   string
      sep string
     }
     tests := []struct {
      name       string
      args       args
      wantResult []string
     }{
      // TODO: Add test cases.
     }
     for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
       if gotResult := Split(tt.args.s, tt.args.sep); !reflect.DeepEqual(gotResult, tt.wantResult) {
        t.Errorf("Split() = %v, want %v", gotResult, tt.wantResult)
       }
      })
     }
    }
    

    代碼格式與我們上面的類似,只需要在TODO位置添加我們的測試邏輯就可以了。

    測試覆蓋率

    測試覆蓋率是指代碼被測試套件覆蓋的百分比。通常我們使用的都是語句的覆蓋率,也就是在測試中至少被運行一次的代碼占總代碼的比例。在公司內部一般會要求測試覆蓋率達到80%左右。

    Go提供內置功能來檢查你的代碼覆蓋率。我們可以使用go test -cover來查看測試覆蓋率。例如:

    ? go test -cover
    PASS
    coverage: 100.0% of statements
    ok      golang-unit-test-demo/base_demo 0.009s
    

    從上面的結果可以看到我們的測試用例覆蓋了100%的代碼。

    Go還提供了一個額外的-coverprofile參數,用來將覆蓋率相關的記錄信息輸出到一個文件。例如:

    ? go test -cover -coverprofile=c.out
    PASS
    coverage: 100.0% of statements
    ok      golang-unit-test-demo/base_demo 0.009s
    

    上面的命令會將覆蓋率相關的信息輸出到當前文件夾下面的c.out文件中。

    ? tree .
    .
    ├── c.out
    ├── split.go
    └── split_test.go
    

    然后我們執行go tool cover -html=c.out,使用cover工具來處理生成的記錄信息,該命令會打開本地的瀏覽器窗口生成一個HTML報告。上圖中每個用綠色標記的語句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。

    testify/assert

    testify是一個社區非常流行的Go單元測試工具包,其中使用最多的功能就是它提供的斷言工具——testify/asserttestify/require

    安裝

    go get github.com/stretchr/testify
    

    使用示例

    我們在寫單元測試的時候,通常需要使用斷言來校驗測試結果,但是由于Go語言中沒有提供斷言,所以我們會寫出很多的if...else...語句。而testify/assert為我們提供了很多常用的斷言函數,并且能夠輸出友好、易于閱讀的錯誤描述信息。

    比如我們之前在TestSplit測試函數中就使用了reflect.DeepEqual來判斷期望結果與實際結果是否一致。

    t.Run(tt.name, func(t *testing.T) { // 使用t.Run()執行子測試
     got := Split(tt.input, tt.sep)
     if !reflect.DeepEqual(got, tt.want) {
      t.Errorf("expected:%#v, got:%#v", tt.want, got)
     }
    })
    

    使用testify/assert之后就能將上述判斷過程簡化如下:

    t.Run(tt.name, func(t *testing.T) { // 使用t.Run()執行子測試
     got := Split(tt.input, tt.sep)
     assert.Equal(t, got, tt.want)  // 使用assert提供的斷言函數
    })
    

    當我們有多個斷言語句時,還可以使用assert := assert.New(t)創建一個assert對象,它擁有前面所有的斷言方法,只是不需要再傳入Testing.T參數了。

    func TestSomething(t *testing.T) {
      assert := assert.New(t)
      // assert equality
      assert.Equal(123, 123, "they should be equal")
      // assert inequality
      assert.NotEqual(123, 456, "they should not be equal")
      // assert for nil (good for errors)
      assert.Nil(object)
      // assert for not nil (good when you expect something)
      if assert.NotNil(object) {
        // now we know that object isn't nil, we are safe to make
        // further assertions without causing any errors
        assert.Equal("Something", object.Value)
      }
    }
    

    testify/assert提供了非常多的斷言函數,這里沒辦法一一列舉出來,大家可以查看官方文檔了解。

    testify/require擁有testify/assert所有斷言函數,它們的唯一區別就是——testify/require遇到失敗的用例會立即終止本次測試。

    此外,testify包還提供了mock、http等其他測試工具,篇幅所限這里就不詳細介紹了,有興趣的同學可以自己了解一下。

    總結

    本文介紹了Go語言單元測試的基本用法,通過為Split函數編寫單元測試的真實案例,模擬了日常開發過程中的場景,一步一步詳細介紹了表格驅動測試、回歸測試和常用的斷言工具testify/assert的使用。在下一篇中,我們將更進一步,詳細介紹如何使用httptest和gock工具進行網絡測試。

    assert單元測試
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    編寫測試代碼和編寫普通的Go代碼過程是類似的,并不需要學習新的語法、規則或工具。go test命令是一個按照一定約定和組織的測試代碼的驅動程序。
    前言 OpenAI的ChatGPT智能AI引擎,在全世界范圍流行,各種ChatGPT應用場景也遍地開花。簡單測試一下,ChatGPT在軟件開發方面的功能是否好用,就用一個自動生成單元測試代碼作為例子。文章最后,提供一些好用的ChatGPT插件,覆蓋了日常最常用的ChatGPT的使用場景。0x02 ChatGPT自動生成代碼單元測試用例 源代碼,先寫一個簡單的Lua函數代碼,太復雜的理解的也慢,如下:function ChatGPTlocal ret = num1 + num2return retendChatGPT(5,7)
    disable_functionsdisable_functions是php.ini中的一個設置選項,可以用
    深入理解how2heap_2.23
    2023-09-25 10:35:41
    例題1:0CTF2017:babyheap(https://www.52pojie.cn/thread-1817311-1-1.html)
    manjusaka牛屎花是一款基于WEB界面的遠程主機管理工具,系統架構見下圖。
    CVE-2021-4034分析
    2023-08-28 09:45:48
    在 polkit 的 pkexec 工具中發現的本地權限升級漏洞,pkexec 應用程序是一個 setuid 工具,旨在允許非特權用戶按照預定義策略以特權用戶身份運行命令。當前版本的 pkexec 無法正確處理調用參數個數,并試圖將環境變量作為命令執行。攻擊者可以利用這一點,精心設計環境變量,誘使 pkexec 執行任意代碼。成功執行后,該攻擊可導致本地權限升級,賦予未授權用戶在目標計算機上的管理
    文章首發在:奇安信攻防社區https://forum.butian.net/share/2142前言 如果存
    House of Cat5月份偶然發現的一種新型GLIBC中IO利用思路,目前適用于任何版本,命名為House of cat并出在2022強網杯中。但是需要攻擊位于TLS的_pointer_chk_guard,并且遠程可能需要爆破TLS偏移。并且house of cat在FSOP的情況下也是可行的,只需修改虛表指針的偏移來調用_IO_wfile_seekoff即可。vtable檢查在glibc2.24以后加入了對虛函數的檢測,在調用虛函數之前首先會檢查虛函數地址的合法性。
    在滲透或者CTF中,總會有特殊函數被過濾,如'ls、cat、tac'。防火墻將這些函數加入黑名單,我們需要找一些方法來代替,用fuzz給他們替換,就出現了BYpass思路。學習就是先走一回別人的老路,知識點到量才可以開創自己的新路。
    菜刀、冰蝎、蟻劍、哥斯拉的流量特征
    一顆小胡椒
    暫無描述
      亚洲 欧美 自拍 唯美 另类