現在需要你寫一個程式,從 3 開始依次向下,當到 0 時列印 「GO!」 並退出,要求每次列印從新的一行開始且列印間隔一秒的停頓。
Countdown
函數來處理這個問題,然後放入 main
程式,所以它看起來這樣:
package main func main() { Countdown() }
我們的軟體需要將結果列印到標準輸出介面。在 DI(依賴注入) 的部分,我們已經看到如何使用 DI 進行方便的測試。
func TestCountdown(t *testing.T) { buffer := &bytes.Buffer{} Countdown(buffer) got := buffer.String() want := "3" if got != want { t.Errorf("got '%s' want '%s'", got, want) } }
buffer
不熟悉,請重新閱讀前面的部分。Countdown
函數將資料寫到某處,io.writer就是作為 Go 的一個介面來抓取資料的一種方式。
main
中,我們將資訊傳送到 os.Stdout
,所以使用者可以看到 Countdown
的結果列印到終端bytes.Buffer
,所以我們的測試能夠抓取到正在生成的資料./countdown_test.go:11:2: undefined: Countdown
定義 Countdown
函數
func Countdown() {}
再次嘗試執行
./countdown_test.go:11:11: too many arguments in call to Countdown
have (*bytes.Buffer)
want ()
編譯器正在告訴你函數的問題,所以更正它
func Countdown(out *bytes.Buffer) {}
countdown_test.go:17: got '' want '3'
這樣結果就完美了!
func Countdown(out *bytes.Buffer) { fmt.Fprint(out, "3") }
我們正在使用 fmt.Fprint
傳入一個 io.Writer
(例如 *bytes.Buffer
)並行送一個 string
。這個測試應該可以通過。
雖然我們都知道 *bytes.Buffer
可以執行,但最好使用通用介面代替。
func Countdown(out io.Writer) { fmt.Fprint(out, "3") }
main中。這樣的話,我們就有了一些可工作的軟體來確保我們的工作正在取得進展。
package main import ( "fmt" "io" "os" ) func Countdown(out io.Writer) { fmt.Fprint(out, "3") } func main() { Countdown(os.Stdout) }
通過花費一些時間讓整個流程正確執行,我們就可以安全且輕鬆的迭代我們的解決方案。我們將不再需要停止並重新執行程式,要對它的工作充滿信心因為所有的邏輯都被測試過了。
func TestCountdown(t *testing.T) { buffer := &bytes.Buffer{} Countdown(buffer) got := buffer.String() want := `3 2 1 Go!` if got != want { t.Errorf("got '%s' want '%s'", got, want) } }
反引號語法是建立 string
的另一種方式,但是允許你放置東西例如放到新的一行,對我們的測試來說是完美的。
countdown_test.go:21: got '3' want '3
2
1
Go!'
func Countdown(out io.Writer) { for i := 3; i > 0; i-- { fmt.Fprintln(out, i) } fmt.Fprint(out, "Go!") }
for
迴圈與 i--
反向計數,並且用 fmt.println
列印我們的數位到 out
,後面跟著一個換行符。最後用 fmt.Fprint
傳送 「Go!」。這裡已經沒有什麼可以重構的了,只需要將變數重構為命名常數
const finalWord = "Go!" const countdownStart = 3 func Countdown(out io.Writer) { for i := countdownStart; i > 0; i-- { fmt.Fprintln(out, i) } fmt.Fprint(out, finalWord) }
如果你現在執行程式,你應該可以獲得想要的輸出,但是向下計數的輸出沒有 1 秒的暫停。
Go 可以通過 time.Sleep
實現這個功能。嘗試將其新增到我們的程式碼中。
func Countdown(out io.Writer) { for i := countdownStart; i > 0; i-- { time.Sleep(1 * time.Second) fmt.Fprintln(out, i) } time.Sleep(1 * time.Second) fmt.Fprint(out, finalWord) }
如果你執行程式,它會以我們期望的方式工作。
測試可以通過,軟體按預期的工作。但是我們有一些問題:
Countdown
測試,我們是否會對被新增到測試執行中 4 秒鐘感到滿意呢?Sleep
ing 的注入,需要抽離出來然後我們才可以在測試中控制它。time.Sleep
,我們可以用 依賴注入 的方式去來代替「真正的」time.Sleep
,然後我們可以使用斷言 監視呼叫讓我們將依賴關係定義為一個介面。這樣我們就可以在 main
使用 真實的 Sleeper
,並且在我們的測試中使用 spy sleeper。通過使用介面,我們的 Countdown
函數忽略了這一點,併為呼叫者增加了一些靈活性。
type Sleeper interface { Sleep() }
Countdown
函數將不會負責 sleep
的時間長度。 這至少簡化了我們的程式碼,也就是說,我們函數的使用者可以根據喜好設定休眠的時長。type SpySleeper struct { Calls int } func (s *SpySleeper) Sleep() { s.Calls++ }
Sleep()
被呼叫了多少次,這樣我們就可以在測試中檢查它。sleep
被呼叫了 4 次。
func TestCountdown(t *testing.T) { buffer := &bytes.Buffer{} spySleeper := &SpySleeper{} Countdown(buffer, spySleeper) got := buffer.String() want := `3 2 1 Go!` if got != want { t.Errorf("got '%s' want '%s'", got, want) } if spySleeper.Calls != 4 { t.Errorf("not enough calls to sleeper, want 4 got %d", spySleeper.Calls) } }
嘗試並執行測試
too many arguments in call to Countdown
have (*bytes.Buffer, Sleeper)
want (io.Writer)
我們需要更新 Countdow
來接受我們的 Sleeper
。
func Countdown(out io.Writer, sleeper Sleeper) { for i := countdownStart; i > 0; i-- { time.Sleep(1 * time.Second) fmt.Fprintln(out, i) } time.Sleep(1 * time.Second) fmt.Fprint(out, finalWord) }
如果您再次嘗試,你的 main
將不會出現相同編譯錯誤的原因
./main.go:26:11: not enough arguments in call to Countdown
have (*os.File)
want (io.Writer, Sleeper)
讓我們建立一個 真正的 sleeper 來實現我們需要的介面
type ConfigurableSleeper struct { duration time.Duration } func (o *ConfigurableSleeper) Sleep() { time.Sleep(o.duration) }
func main() { sleeper := &ConfigurableSleeper{1 * time.Second} Countdown(os.Stdout, sleeper) }
現在測試正在編譯但是沒有通過,因為我們仍然在呼叫 time.Sleep
而不是依賴注入。讓我們解決這個問題。
func Countdown(out io.Writer, sleeper Sleeper) { for i := countdownStart; i > 0; i-- { sleeper.Sleep() fmt.Fprintln(out, i) } sleeper.Sleep() fmt.Fprint(out, finalWord) }
測試應該可以該通過,並且不再需要 4 秒。
Countdown
應該在第一個列印之前 sleep,然後是直到最後一個前的每一個,例如:Sleep
Print N
Sleep
Print N-1
Sleep
sleep
了 4 次,但是那些 sleeps
可能沒按順序發生。func Countdown(out io.Writer, sleeper Sleeper) { for i := countdownStart; i > 0; i-- { sleeper.Sleep() } for i := countdownStart; i > 0; i-- { fmt.Fprintln(out, i) } sleeper.Sleep() fmt.Fprint(out, finalWord) }
type CountdownOperationsSpy struct { Calls []string } func (s *CountdownOperationsSpy) Sleep() { s.Calls = append(s.Calls, sleep) } func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) { s.Calls = append(s.Calls, write) return } const write = "write" const sleep = "sleep"
CountdownOperationsSpy
同時實現了 io.writer
和 Sleeper
,把每一次呼叫記錄到 slice
。在這個測試中,我們只關心操作的順序,所以只需要記錄操作的代名片語成的列表就足夠了。t.Run("sleep after every print", func(t *testing.T) { spySleepPrinter := &CountdownOperationsSpy{} Countdown(spySleepPrinter, spySleepPrinter) want := []string{ sleep, write, sleep, write, sleep, write, sleep, write, } if !reflect.DeepEqual(want, spySleepPrinter.Calls) { t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls) } })
Sleeper
上有兩個測試監視器,所以我們現在可以重構我們的測試,一個測試被列印的內容,另一個是確保我們在列印時間 sleep。最後我們可以刪除第一個監視器,因為它已經不需要了。
func TestCountdown(t *testing.T) { t.Run("prints 3 to Go!", func(t *testing.T) { buffer := &bytes.Buffer{} Countdown(buffer, &CountdownOperationsSpy{}) got := buffer.String() want := `3 2 1 Go!` if got != want { t.Errorf("got '%s' want '%s'", got, want) } }) t.Run("sleep after every print", func(t *testing.T) { spySleepPrinter := &CountdownOperationsSpy{} Countdown(spySleepPrinter, spySleepPrinter) want := []string{ sleep, write, sleep, write, sleep, write, sleep, write, } if !reflect.DeepEqual(want, spySleepPrinter.Calls) { t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls) } }) }
我們現在有了自己的函數,並且它的兩個重要的屬性已經通過合理的測試。
這通常是您測試太多 實現細節 的標誌。盡力克服這個問題,所以你的測試將測試 有用的行為,除非這個實現對於系統執行非常重要。
有時候很難知道到底要測試到 什麼級別,但是這裡有一些我試圖遵循的思維過程和規則。
本文來自部落格園,作者:slowlydance2me,轉載請註明原文連結:https://www.cnblogs.com/slowlydance2me/p/17261292.html