首頁 > 軟體

Go語言單元測試超詳細解析

2022-02-25 19:01:31

前言:

平時根據需求寫程式碼.人工進行測試往往不會面面俱到,還會因為需求的改變繁瑣的進行測試
通過完成一個測試函數,可以大大簡化測試的步驟,並且在需求該變的時候只需要改變一下測試的輸入與期望

一、單元測試分類及其概念

1.基本分類

  • 測試函數 函數字首為Test 主要用於測試程式的一些邏輯行為是否正確
  • 基準函數 函數名字首為Benchmark 主要測試函數的效能
  • 範例函數 函數的字首名為 Example 為檔案提示範例檔案

2.細說單元測試分類

①測試函數

  • 函數的基本測試
  • 函數的組測試
  • 函數的子測試
  • 測試函數覆蓋率(也就是被測試函數有多少程式碼用於了執行)
  • 在進行測試的時候,要保證被測函數覆蓋率為100%,測試函數覆蓋率為60%以上,否則寫的程式碼大部分用不到,需要優化
    • ①測試覆蓋率可以使用go test -cover
    • go test -cover -coverprofile=c.out(將測試的結果存入檔案c.out)
    • 然後使用go tool cover -html=c.out可以開啟檔案,顯示哪些程式碼沒有執行

②基準測試

  • 基準函數測試是做什麼的:基準函數會自定義一個時間段用於執行程式碼,如果程式碼簡潔,被測函數的執行次數需要成倍增加(直到達到基準測試函數給的預期,然後統計一共執行了多少輪,,每輪平均用時多少)
  • 執行基準函數時,首先要寫基準函數
  • 基準測試函數的引數是:*testing.B對應的指標變數
  • 在測試函數內部要進行迴圈,迴圈的終止條件是b.N

二、結合程式碼細說每一種測試

1.基準測試

(1)基準測試函數需要注意的點

進行基準測試時往往是對函數的演演算法進行測驗,有時後一個演演算法在測試資料的基量不同時測試出的效果會不同我們需要對不同數量級的樣本進行測試的時候。可以自己寫一個盒子作為跳板,測試什麼數量級的資料就只進行當前數量級的測試

 測試的時候命令為:go test -bench=.(將所有的跳板函數均執行一遍)或者=特定函數進行特定函數測試-benchtime=time  (在被測試函數在基準函數預設的時間內執行不完時可以拿這個引數進行時間的擴充)在進行基準函數測試的時候,可能需要進行一些前置工作,如果感覺前置工作浪費時間可以使用 b.ResetTimer()對計時器進行重置

(2)基準測試程式碼

測試函數程式碼如下:

//檔名 subStr_test.go
package main

import (
    "reflect"
    "testing"
)
func BenchmarkSubStr(b *testing.B) {
    for i := 0; i < b.N; i++ {
        res := subStr("qwe:qw:es:wqe", ":")
        if !reflect.DeepEqual(res, []string{"qwe", "qw", "es", "wqe"}) {
            b.Errorf("不匹配")
        }
    }

}

func benchmarkFib(b *testing.B, n int) {
    for i := 0; i < b.N; i++ {
        Fib(n)
    }
}

func BenchmarkFib5(b *testing.B)  { benchmarkFib(b, 5) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib15(b *testing.B) { benchmarkFib(b, 15) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }

被測函數程式碼如下:

//檔名 subStr.go
package main

import (
    "fmt"
    "strings"
)

func subStr(str, stre string) []string {
    index := strings.Index(str, stre)
    var theSub []string

    for index >= 0 {
        // 先將分隔符前面的資料進行儲存
        temp := str[:index]
        // 將字串往後移
        str = str[index+1:]
        // 重新獲取下標
        index = strings.Index(str, stre)
        if temp != "" {
            theSub = append(theSub, temp)
        } else {
            continue
        }
    }
    theSub = append(theSub, str)
    return theSub[:]
}

// 菲薄那契數列
func Fib(n int) int {
    if n < 2 {
        return n
    }
    return Fib(n-1) + Fib(n-2)
}

func main() {
    fmt.Println(subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
    fmt.Printf("%#vn", subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
    fmt.Println(123)
}

將以上兩個檔案放在同一目錄下,執行測試命令 go test
 

得到測試結果:

    goos: windows
    goarch: amd64
    cpu: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
    BenchmarkSubStr-8   1213681       1012 ns/op      352 B/op  14 allocs/op
    PASS
    ok      _/d_/Go語言學習筆記/go語言語法部分/go語言進階語法/8.單元測試/基準測試    2.410s

2.組測試與子測試

  • 對於一個函數的測試,不僅僅是一組測試用例,所以我們需要寫測試組, 對一個函數進行測試,測試的時候一般用集合鍵值對構成,每一個鍵對應一組測試資料
  • 進行組測試的時候可能會有個別的組出錯,所以使用子測試可以對某個案例進行單獨測試這就是測試組與子測試的產生背景

測試函數如下:

package main

import (
    "reflect"
    "testing"
)

type testS struct {
    str  string
    ste  string
    want []string
}

//進行組測試,批次進行測試,如果有哪個地方不滿足則進行異常丟擲
// func TestSubStr(t *testing.T) {
//     testMap := map[string]testS{
//         "case_1": {"123:eqwe:123", ":", []string{"123", "eqwe", "123"}},
//         "case_2": {"13:eqwe3:1@23", "3:", []string{"1", "eqwe", "1@23"}},
//         "case_3": {"12@3@:@eq@we@:1@23", "@:", []string{"12@3", "@eq@we", "1@23"}},
//         "case_4": {"123:eq@we:1@2@3", "q@", []string{"123:e", "we:1@2@3"}},
//         "case_5": {"123:eqwe:123", "2", []string{"1", "3:eqwe:1", "3"}},
//         "case_6": {"123:eqwe:123", "eqwe", []string{"123:", ":123"}},
//     }
//     for k, v := range testMap {
//         res := subStr(v.str, v.ste)
//         if !reflect.DeepEqual(res, v.want) {
//             t.Errorf("%v want:%#v got:%#v", k, v.want, res)
//         }
//     }
// }

// 進行子測試,可以針對某個子樣本進行測試
func TestSubStr(t *testing.T) {
    testMap := map[string]testS{
        "case_1": {"13:eqwe:123", ":", []string{"123", "eqwe", "123"}},
        "case_2": {"3:eqwe3:1@23", "3:", []string{"1", "eqwe", "1@23"}},
        "case_3": {"2@3@:@eq@we@:1@23", "@:", []string{"12@3", "@eq@we", "1@23"}},
        "case_4": {"123:eq@we:1@2@3", "q@", []string{"123:e", "we:1@2@3"}},
        "case_5": {"23:eqwe:123", "2", []string{"1", "3:eqwe:1", "3"}},
        "case_6": {"123:eqwe:123", "eqwe", []string{"123:", ":123"}},
    }
    for k, v := range testMap {
        t.Run(k, func(t *testing.T) {
            res := subStr(v.str, v.ste)
            if !reflect.DeepEqual(res, v.want) {
                t.Errorf("want:%#v got:%#v", v.want, res)
            }
        })

    }
}

待測函數如下:

package main

import (
    "fmt"
    "strings"
)

func subStr(str, stre string) []string {
    index := strings.Index(str, stre)
    var theSub []string

    for index >= 0 {
        // 先將分隔符前面的資料進行儲存
        temp := str[:index]
        // 將字串往後移
        str = str[index+len(stre):]
        // 重新獲取下標
        index = strings.Index(str, stre)
        if temp != "" {
            theSub = append(theSub, temp)
        } else {
            continue
        }
    }
    theSub = append(theSub, str)
    return theSub[:]
}

func main() {
    fmt.Println(subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":c"))
    fmt.Printf("%#vn", subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
    fmt.Println(123)
}

(1).組測試結果分析及命令:

  • 組測試命令仍使用 go test

--- FAIL: TestSubStr (0.00s)
    --- FAIL: TestSubStr/case_1 (0.00s)
       subStr_test.go:46: want:[]string{"123", "eqwe", "123"} got:[]string{"13", "eqwe", "123"}        
    --- FAIL: TestSubStr/case_2 (0.00s)
         subStr_test.go:46: want:[]string{"1", "eqwe", "1@23"} got:[]string{"eqwe", "1@23"}
    --- FAIL: TestSubStr/case_3 (0.00s)
         subStr_test.go:46: want:[]string{"12@3", "@eq@we", "1@23"} got:[]string{"2@3", "@eq@we", "1@23"}
    --- FAIL: TestSubStr/case_5 (0.00s)
         subStr_test.go:46: want:[]string{"1", "3:eqwe:1", "3"} got:[]string{"3:eqwe:1", "3"}
    FAIL
    exit status 1
    FAIL    _/D_/Go語言學習筆記/go語言語法部分/go語言進階語法/8.單元測試/組測試與子測試     0.155s

(2).子測試結果分析及命令:

  • 例如單獨測試case_1,使用的命令為go test -v -run=TestSubStr/case_1(等號後面跟的是上面組測試失敗案例FAIL後面的目錄)

=== RUN   TestSubStr
=== RUN   TestSubStr/case_1
    subStr_test.go:46: want:[]string{"123", "eqwe", "123"} got:[]string{"13", "eqwe", "123"}
--- FAIL: TestSubStr (0.00s)
    --- FAIL: TestSubStr/case_1 (0.00s)
FAIL
exit status 1
FAIL    _/D_/Go語言學習筆記/go語言語法部分/go語言進階語法/8.單元測試/組測試與子測試     0.186s

三、pprof偵錯工具

1.對主函數進行傳參

(1)os.Args
package main1

import (
    "fmt"
    "os"
)

func main() {
    // os.Args可以在執行函數的時候傳遞引數,但是對於-name=xxx沒有辦法將其解析
    if os.Args != nil {
        for index, temp := range os.Args {
            fmt.Println("第", index+1, "個引數是:", temp)
        }
    }
    fmt.Println("hello")
}

(2)flag.Args

  •    相對於os.Args來說,flag.Args使用起來更加方便
package main

import (
    "flag"
    "fmt"
    "time"
)

func main() {
    // 第一個引數為對應的引數,第二個引數為預設值,第三個引數為提示
    // 返回的是對應型別的指標
    // name := flag.String("name", "Tom", "輸入name")
    // sex := flag.Bool("sex", true, "是不是男性")
    // age := flag.Int("age", 10, "年齡")
    // flag.Parse()
    // fmt.Println(*name, *sex, *age)
    var name string
    var age int
    var sex bool
    flag.StringVar(&name, "name", "Tom", "輸入name")
    flag.BoolVar(&sex, "sex", true, "是不是男性")
    flag.IntVar(&age, "age", 10, "年齡")
    tim := flag.Duration("time", time.Hour, "時間")
    // 將輸入的資料進行解析,不使用這句話的話不能獲取到name sex age等屬性對應的值
    flag.Parse()
    fmt.Println(name, sex, age, *tim)
    fmt.Println(flag.Args())  //以切片的方式返回命令列之外的其他引數
    fmt.Println(flag.NArg())  //返回命令列之外的其他引數的個數
    fmt.Println(flag.NFlag()) //返回使用命令列的引數的個數
}

2.pprof效能調優

pprof偵錯工具進行偵錯,主要看待測模組在記憶體於時間上的效益偵錯的時候只會顯示耗費時間空間較多的程式碼段.

生成偵錯程式碼塊的檔案:go run xx.exe -cpu....
使用go語言工具檢視程式碼塊所存在的問題:go tool pprof cpu.pprof

package main

import (
    "flag"
    "fmt"
    "os"
    "runtime/pprof"
    "time"
)
// 一段有問題的程式碼
func logicCode() {
    var c chan int
    for {
        select {
        case v := <-c:
            fmt.Printf("recv from chan, value:%vn", v)
        default:

        }
    }
}

func main() {
    var isCPUPprof bool
    var isMemPprof bool
    //獲取接收到的引數
    flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on")
    flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on")
    flag.Parse()
    //判斷進行什麼測試,然後執行分支,並將資訊儲存到相應的檔案內。
    if isCPUPprof {
        file, err := os.Create("./cpu.pprof")
        if err != nil {
            fmt.Printf("create cpu pprof failed, err:%vn", err)
            return
        }
        pprof.StartCPUProfile(file)
        defer pprof.StopCPUProfile()
    }
    for i := 0; i < 8; i++ {
        go logicCode()
    }
    time.Sleep(20 * time.Second)
    if isMemPprof {
        file, err := os.Create("./mem.pprof")
        if err != nil {
            fmt.Printf("create mem pprof failed, err:%vn", err)
            return
        }
        pprof.WriteHeapProfile(file)
        file.Close()
    }
}

總結:

這裡對常用的測試函數,子測試進行了詳細的講解,對於pprof引數調優僅僅做了個介紹,可以根據自己的興趣在網上找一部分pprof的例子深入學習,進行自動化測試非常方便。

到此這篇關於Go語言單元測試超詳細解析的文章就介紹到這了,更多相關Go語言單元測試內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com