Go test 傳遞命令列引數及解析所遇到的錯誤及解決

2020-10-13 11:00:52

Go test 傳遞命令列引數及解析所遇到的錯誤及解決

概述

這周的服務計算作業是使用 Golang 開發 selpg 命令列程式,selpg 程式詳見 開發 Linux 命令列實用程式,筆者對其的 Golang 實現詳見 gitee

在好不容易實現了 selpg 所需要的函數後,當然是要對其進行測試,但是在編寫測試函數過程中,遇到了許多問題,經過一番摸索嘗試後,才將其解決,在此將方法記錄下來,和大家分享;

以下將結合 selpg 中函數 ProcessArgs 來說明;selpg 中函數 ProcessArgs 負責檢查命令列引數合法性並解析,大概流程是先通過 os.Args 來檢查必選引數 -s-e 是否存在,再用 pflag.XxxVarP 繫結引數到相應變數,使用 pflag.Parse 解析;

Go test 傳遞命令列引數

一般情況下,我們是直接在終端輸入諸如 selpg -s1 -e1 input.txt 命令來呼叫 selpg ,但現在我們要在 go test 中傳遞 selpg -s1 -e1 input.txt ,在經過查閱 相關部落格 ,發現可以使用 go test-args 標籤,該標籤會把其後的所有字串當做引數傳入,所以我嘗試使用如下命令進行測試;

go test -v -args selpg -s1 -e1 input.txt
func TestProcessArgs(t *testing.T){
	var args selpgArgs
 	expectedArgs := selpgArgs{1, 1, "input.txt", 10, false, ""}
	ProcessArgs(&args)
	if args != expectedArgs {
		t.Errorf("got %v, expected %v", args, expectedArgs)
 	}
}

結果毫無疑問地出錯了,因為使用 go test -v -args selpg -s1 -e1 input.txt 命令,得到的 os.Args 會是這樣的(「…」代表前面的路徑資訊);

.../selpg.test -test.timeout=10m0s -test.v=true selpg -s1 -e1 input.txt

我們想要的引數位於 os.Args 最後,而函數 ProcessArgs 是從 os.Args 前面開始檢查必選引數 -s-e 是否存在,難道要修改原始碼中的函數 ProcessArgs 來適應測試?好像並不是很合適;

那麼如何才能拿到我們想要的引數呢?很簡單,既然需要的引數被放在了最後,那就把前面不需要的引數都去掉不就好了;

os.Args 本質上是一個 []string 陣列,,os 包被匯入時對其進行建立並賦予命令列引數值,那麼我們可以對其進行切片操作,將前面不需要的都去掉即可;所以做出如下修改

func TestProcessArgs(t *testing.T){
	var args selpgArgs
	expectedArgs := selpgArgs{1, 1, "input.txt", 10, false, ""}
	
	os.Args = os.Args[3:len(os.Args)]
	
	ProcessArgs(&args)
	if args != expectedArgs {
		t.Errorf("got %v, expected %v", args, expectedArgs)
  	}
}

這樣,測試就能順利執行,函數 ProcessArgs 能拿到需要的命令列引數並解析,但是這樣做的不足之處在於, go test 命令格式不能改變,多一個引數或者少一個引數,都可能使切片出錯,導致傳遞的引數不符合要求;

再仔細想一想,函數 ProcessArgsos.Args 裡拿引數,測試中 os.Args 格式不符合要求,對 os.Args 切片又不夠靈活;既然 os.Args 本質上是一個 []string 陣列,那直接寫一個新的 []string 陣列,賦給 os.Args 不就好了;所以做出如下修改

func TestProcessArgs(t *testing.T){
	var args selpgArgs
	cmd := []string{"selpg", "-s1", "-e1", "input.txt"}	
	expectedArgs := selpgArgs{1, 1, "input.txt", 10, false, ""}
	os.Args = cmd
	ProcessArgs(&args)
	if args != expectedArgs {
		t.Errorf("got %v, expected %v", args, expectedArgs)
	}
}

這樣,只使用 go test ,測試也能夠順利執行,不需要在使用 -args 標籤傳遞引數

Go test 引數解析

以上只實現了一個測例的測試函數,如果想增加多個測例,只需要簡單的修改即可

func TestProcessArgs(t *testing.T) {
    cases := []struct {
        cmd          []string
        expectedArgs selpgArgs
    }{
        {
            []string{"selpg", "-s1", "-e1"},
            selpgArgs{1, 1, "", 10, false, ""},
        },
        {
            []string{"selpg", "-s1", "-e1", "input.txt"},
            selpgArgs{1, 1, "input.txt", 10, false, ""},
        },
    }
    for i, c := range cases {
	var args selpgArgs
	os.Args = c.cmd
	ProcessArgs(&args)
	if args != c.expectedArgs {
	    t.Errorf("case %v: got %v, expected %v", i, args, c.expectedArgs)
	}
    }
}

但是,如果 go test 測試的話,會出現 flag redefined 引數解析錯誤

.../selpg.test flag redefined: ...

這個問題的解決思路來自 Golang : flag 包簡介 ,總的來說,flag 包被匯入時建立了 FlagSet 型別的全域性物件 CommandLine ,在程式中定義的所有命令列引數變數都會被加入到 CommandLine 的 formal 屬性中

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

所以在每個測例中,除了要更新 os.Args ,還需要更新 flag.CommandLine ,所以做出如下修改

func TestProcessArgs(t *testing.T) {
    cases := []struct {
        cmd          []string
        expectedArgs selpgArgs
    }{
        {
            []string{"selpg", "-s1", "-e1"},
            selpgArgs{1, 1, "", 10, false, ""},
        },
   	{
            []string{"selpg", "-s1", "-e1", "input.txt"},
            selpgArgs{1, 1, "input.txt", 10, false, ""},
        },
    }
    for i, c := range cases {
	var args selpgArgs
	os.Args = c.cmd
	pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
	ProcessArgs(&args)
	if args != c.expectedArgs {
	    t.Errorf("case %v: got %v, expected %v", i, args, c.expectedArgs)
	}
    }
}

總結

  • 在 Go test 傳遞命令列引數,可以將引數在測試函數中寫成 []string 陣列,再賦給 os.Args 即可;
  • 如果還使用了 flag 包解析引數時,還需要用 flag.CommandLine = NewFlagSet(os.Args[0], ExitOnError) 更新 flag.CommandLine