Thursday, August 18, 2016

依賴注入 - dependency injection

寫單元測試時,常常會遇到一問題,原始的程式碼本身的結構,難以加入單元測試。

最近我遇到了一個問題,我寫的程式會呼叫 git clone ,而 git clone 在網路環境不佳時,甚至會執行超過24小時,將程式的 go routine 整個卡住。所以解法就是需要將 golang 的外部指令呼叫,改成有 timeout 的。

要加上這個 timeout 並不難,困難點是,要如何測試這個功能?因為其實要模擬出 git clone 長時間執行的環境,並不是很簡單的事。

解法是這樣子:
加上這個功能時,必須將這個「為指令加上 timeout 功能」實作成一個獨立的函數,使它的功能獨立,並不「依賴」於 git clone 。換言之,「 git clone 」對應的指令,會成為這個函數的輸入變數(input argument),由外部「注入」。於是這個功能就可以獨立地來寫單元測試來測。測試它的時候,就可以用「 sleep 500 」對應的指令,來做為它的輸入變數(input argument)

原始程式碼的修改
// Original -> New
cmd := exec.Command("git", "clone", gitRemoteAddr, file.Basename(pluginDir))
cmd.Dir = parentDir
- err2 := cmd.Run()
+ err2 := RunCmdWithTimeout(cmd, 600)

新增的程式碼和單元測試
func RunCmdWithTimeout(cmd *exec.Cmd, timeout int64) (err error) {
if err = cmd.Start(); err != nil {
log.Println("Start shell command error:", err)
}
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
d := time.Duration(time.Duration(timeout) * time.Second)
select {
case err = <-done:
case <-time.After(d):
// timed out
if err = cmd.Process.Kill(); err != nil {
log.Printf("failed to kill: %s, error: %s", cmd.Path, err)
}
err = fmt.Errorf("Command %s time out", cmd.Path)
}
return
}
// Unit test of RunCmdWithTimeout
func TestRunCmdWithTimeout(t *testing.T) {
cmd := exec.Command("sleep", "500")
t1 := time.Now()
RunCmdWithTimeout(cmd, 3)
t2 := time.Now()
t.Log("Time spent should less than 4 second: ", t2.Sub(t1))
}