TDD測試驅動開發:Hello, YOU -- 透過簡單的 hello_test.go 程式學會編寫測試,學習使用常量來最佳化並根據需要重構

slowlydance2me發表於2023-03-16

測試驅動開發,英文全稱Test-Driven Development,簡稱TDD,是一種不同於傳統軟體開發流程的新型的開發方法。 它要求在編寫某個功能的程式碼之前先編寫測試程式碼,然後只編寫使測試透過的功能程式碼,透過測試來推動整個開發的進行。 這有助於編寫簡潔可用和高質量的程式碼,並加速開發過程。

Hello, YOU

書接上文?上篇文章中,我們嘗試編寫了hello word 函式以及第一個測試 hello_test

package main

import "fmt"

func Hello() string {
    return "Hello, world"
}

func main() {
    fmt.Println(Hello())
}

package main

import "testing"

func TestHello(t *testing.T) {
    got := Hello()
    want := "Hello, world"

    if got != want {
        t.Errorf("got '%q' want '%q'", got, want)
    }
}

現在有了測試,就可以安全地迭代我們的軟體了。
在上一個示例中,我們在寫好程式碼 之後 編寫了測試,以便讓你學會如何編寫測試和宣告函式。
從此刻起,我們將 首先編寫測試
我們的下一個需求是指定 hello 問候的接受者。
讓我們從在測試中捕獲這些需求開始。這是基本的測試驅動開發,可以確保我們的測試用例 真正 在測試我們想要的功能。
當你回顧編寫的測試時,存在一個風險:即使程式碼沒有按照預期工作,測試也可能繼續透過。
package main

import "testing"

func TestHello(t *testing.T) {
    got := Hello("Chris")
    want := "Hello, Chris"

    if got != want {
        t.Errorf("got '%q' want '%q'", got, want)
    }
}

這時執行 go test,你應該會獲得一個編譯錯誤

 

 

 

當使用像 Go 這樣的靜態型別語言時,聆聽編譯器 是很重要的。
編譯器理解你的程式碼應該如何拼接到一起工作,所以你就不必關心這些了。
在這種情況下,編譯器告訴你需要怎麼做才能繼續。我們必須修改函式 Hello 來接受一個引數。
修改 Hello
func Hello(name string) string {
    return "Hello, world"
}

如果你嘗試再次執行測試,main.go 會編譯失敗,因為你沒有傳遞引數。傳入引數「world」讓它透過。

func main() {
    fmt.Println(Hello("world"))
}

現在,當你執行測試時,你應該看到類似的內容

我們最終得到了一個可編譯的程式,但是根據測試它並沒有達到我們的要求。
為了使測試透過,我們使用 name 引數並用 Hello,字串連線它

 

func Hello(name string) string {
    return "Hello, " + name
}

現在再執行測試應該就透過了。

 

接下來我們可以介紹一下另一種語言特性 常量

通常我們這樣定義一個常量
 
const englishHelloPrefix = "Hello, "

現在我們可以重構程式碼

const englishHelloPrefix = "Hello, "

func Hello(name string) string {
    return englishHelloPrefix + name
}
重構之後,重新測試以確保程式無誤。
常量應該可以提高應用程式的效能,它避免了每次使用 Hello 時建立 "Hello, " 字串例項。
顯然,對於這個例子來說,效能提升是微不足道的!但是建立常量的價值是可以快速理解值的含義,有時還可以幫助提高效能。

再次回到 Hello, world

下一個需求是當我們的函式用空字串呼叫時,它預設為列印 "Hello, World" 而不是 "Hello, "
首先編寫一個新的失敗測試
func TestHello(t *testing.T) {

    t.Run("saying hello to people", func(t *testing.T) {
        got := Hello("Chris")
        want := "Hello, Chris"

        if got != want {
            t.Errorf("got '%q' want '%q'", got, want)
        }
    })

    t.Run("say hello world when an empty string is supplied", func(t *testing.T) {
        got := Hello("")
        want := "Hello, World"

        if got != want {
            t.Errorf("got '%q' want '%q'", got, want)
        }
    })

}
這裡我們將介紹測試庫中的另一個工具 -- 子測試。有時,對一個「事情」進行分組測試,然後再對不同場景進行子測試非常有效。
這種方法的好處是,你可以建立在其他測試中也能夠使用的共享程式碼。
當我們檢查資訊是否符合預期時,會有重複的程式碼。
重構不 僅僅 是針對程式的程式碼!
重要的是,你的測試 清楚地說明 了程式碼需要做什麼。
 
func TestHello(t *testing.T) {

    assertCorrectMessage := func(t *testing.T, got, want string) {
        t.Helper()
        if got != want {
            t.Errorf("got '%q' want '%q'", got, want)
        }
    }

    t.Run("saying hello to people", func(t *testing.T) {
        got := Hello("Chris")
        want := "Hello, Chris"
        assertCorrectMessage(t, got, want)
    })

    t.Run("empty string defaults to 'world'", func(t *testing.T) {
        got := Hello("")
        want := "Hello, World"
        assertCorrectMessage(t, got, want)
    })

}
使用 go test 測試

 

 

我們在這裡做了什麼?
我們將斷言重構為函式。這減少了重複並且提高了測試的可讀性。
在 Go 中,你可以在其他函式中宣告函式並將它們分配給變數。
你可以像呼叫普通函式一樣呼叫它們。我們需要傳入 t *testing.T,這樣我們就可以在需要的時候令測試程式碼失敗。
t.Helper() 需要告訴測試套件這個方法是輔助函式(helper)。
透過這樣做,當測試失敗時所報告的行號將在函式呼叫中而不是在輔助函式內部。這將幫助其他開發人員更容易地跟蹤問題。
如果你仍然不理解,請註釋掉它,使測試失敗並觀察測試輸出。
(你會發現錯誤程式碼報告是在第10行)

 

 

現在我們有了一個很好的失敗測試,所以讓我們使用 if 修復程式碼。
const englishHelloPrefix = "Hello, "

func Hello(name string) string {
    if name == "" {
        name = "World"
    }
    return englishHelloPrefix + name
}

如果我們執行測試,應該看到它滿足了新的要求,並且我們沒有意外地破壞其他功能。

 

先來一個小總結

讓我們再次回顧一下這個週期
  •  
    編寫一個測試
  •  
    讓編譯透過
  •  
    執行測試,檢視失敗原因並檢查錯誤訊息是很有意義的
  •  
    編寫足夠的程式碼以使測試透過
  •  
    重構
從表面上看可能很乏味,但堅持這種反饋迴圈非常重要。
它不僅確保你有 相關的測試,還可以確保你透過重構測試的安全性來 設計優秀的軟體
檢視測試失敗是一個重要的檢查手段,因為它還可以讓你看到錯誤資訊。作為一名開發人員,如果測試失敗時不能清楚地說明問題所在,那麼使用這個程式碼庫可能會非常困難。
透過確保你的測試的效率 ,並設定你的工具,可以使執行測試足夠簡單,你在編寫程式碼時就可以進入流暢的狀態。
 

繼續前進!更多需求

西班牙語

天吶,我們有更多的需求了。現在需要支援第二個引數,指定問候的語言。如果一種不能識別的語言被傳進來,就預設為英語。
透過 重構 輕鬆實現這一功能,我們是有信心的!
 
    t.Run("in Spanish", func(t *testing.T) {
        got := Hello("Elodie", "Spanish")
        want := "Hola, Elodie"
        assertCorrectMessage(t, got, want)
    })

先編寫測試。當你嘗試執行測試時,編譯器 應該 會出錯,因為你用兩個引數而不是一個來呼叫 Hello

 
./hello_test.go:27:19: too many arguments in call to Hello
    have (string, string)
    want (string)

透過向 Hello 新增另一個字串引數來解決編譯問題

func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }
    return englishHelloPrefix + name
}

當你嘗試再次執行測試時,它會報錯在其他測試和 hello.go 中沒有傳遞足夠的引數給 Hello 函式

./hello.go:15:19: not enough arguments in call to Hello
    have (string)
    want (string, string)

透過傳遞空字串來解決它們。現在,除了我們的新場景外,你的所有測試都應該編譯並透過

hello_test.go:29: got 'Hello, Elodie' want 'Hola, Elodie'

這裡我們可以使用 if 檢查語言是否是「西班牙語」,如果是就修改資訊

func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }

    if language == "Spanish" {
        return "Hola, " + name
    }

    return englishHelloPrefix + name
}
測試現在應該透過了。
現在是 重構 的時候了。你應該在程式碼中看出了一些問題,其中有一些重複的「魔術」字串。自己嘗試重構它,每次更改都要重新執行測試,以確保重構不會破壞任何內容。
const spanish = "Spanish"
const helloPrefix = "Hello, "
const spanishHelloPrefix = "Hola, "

func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }

    if language == spanish {
        return spanishHelloPrefix + name
    }

    return englishHelloPrefix + name
}

法語

  • 編寫一個測試,斷言如果你傳遞 "French" 你會得到 "Bonjour, "
  •  
    看到它失敗,檢查錯誤資訊是否容易理解
 
在程式碼中進行最小的合理更改
 
你可能寫了一些看起來大致如此的東西
func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }

    if language == spanish {
        return spanishHelloPrefix + name
    }

    if language == french {
        return frenchHelloPrefix + name
    }

    return englishHelloPrefix + name
}

switch

於是我們自然而然想到了使用 switch

當你有很多 if 語句檢查一個特定的值時,通常使用 switch 語句來代替。如果我們希望稍後新增更多的語言支援,我們可以使用 switch 來重構程式碼,使程式碼更易於閱讀和擴充套件。

func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }

    prefix := englishHelloPrefix

    switch language {
    case french:
        prefix = frenchHelloPrefix
    case spanish:
        prefix = spanishHelloPrefix
    }

    return prefix + name
}

編寫一個測試,新增用你選擇的語言寫的hello,你應該可以看到擴充套件這個 神奇 的函式是多麼簡單。

 

最後一次重構

你可能會抱怨說也許我們的函式正在變得很臃腫。對此最簡單的重構是將一些功能提取到另一個函式中。

func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }

    return greetingPrefix(language) + name
}

func greetingPrefix(language string) (prefix string) {
    switch language {
    case french:
        prefix = frenchHelloPrefix
    case spanish:
        prefix = spanishHelloPrefix
    default:
        prefix = englishHelloPrefix
    }
    return
}
一些新的概念:
  •  
    在我們的函式簽名中,我們使用了 命名返回值prefix string)。
  •  
    這將在你的函式中建立一個名為 prefix 的變數。
     
  •  
    它將被分配「零」值。這取決於型別,例如 int 是 0,對於字串它是 ""
     
  •  
    你只需呼叫 return 而不是 return prefix 即可返回所設定的值。
  •  
    這將顯示在 Go Doc 中,所以它使你的程式碼更加清晰。
  •  
    如果沒有其他 case 語句匹配,將會執行 default 分支。

相關文章