HyperRedStart

Posted on

# 使用 Go 進行單元測試

``````package main
import  "testing"
func  TestHello(t *testing.T) {
got  :=  Hello()
want  :=  "Hello, world"
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
``````
``````package main
import  "fmt"
func  Hello() string {
return  "Hello, world"
}
func  main() {
fmt.Println(Hello())
}
``````
``````go test
PASS
ok      .../Golang_TDD 0.086s
``````

## Array

TDD 模式 先寫測試在寫code

``````// 測試數字相加
func  TestSumAll(t *testing.T) {
got  :=  SumAll([]int{1, 2}, []int{0, 9})
want  := []int{3, 9}
if  !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
// 測試去除第一數陣列後相加
func  TestSumAllTails(t *testing.T) {
got  :=  SumAllTails([]int{1,2}, []int{0,9})
want  := []int{2, 9}
if  !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
``````
``````func  Sum(numbers []int) int {
sum  :=  0
for  _, number := range numbers {
sum += number
}
return sum
}
func  SumAllTails(numbersToSum ...[]int)  []int  {
var sums []int
for  _, numbers := range numbersToSum {
tail := numbers[1:]
sums =  append(sums,  Sum(tail))
}
return sums
}
``````

``````// 使用空陣列讓原有程式造成 panic
t.Run("safely sum empty slices", func(t *testing.T) {
got  :=  SumAllTails([]int{}, []int{3, 4, 5})
want  := []int{0, 9}
if  !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
})
// error logs
panic: runtime error: slice bounds out of range [recovered]
panic: runtime error: slice bounds out of range
``````

``````func  SumAllTails(numbersToSum ...[]int) []int {
var  sums []int
for  _, numbers  :=  range numbersToSum {
// 增加判斷numbers 為空判斷
if  len(numbers) ==  0 {
sums  =  append(sums, 0)
} else {
tail  := numbers[1:]
sums  =  append(sums, Sum(tail))
}
}
return sums
}
``````

``````checkSums  :=  func(t *testing.T, got, want []int) {
// 告知 testing 這是一個 helper function 當發生與預期不符時會指向呼叫 checksums的行數
t.Helper()
if  !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
``````

### 結構與介面

``````func  TestArea(t *testing.T) {
// shape interface
checkArea  :=  func(t *testing.T, shape Shape, want float64) {
t.Helper()
got  := shape.Area()
if got != want {
t.Errorf("got %.2f want %.2f", got, want)
}
}
t.Run("rectangles", func(t *testing.T) {
rectangle  := Rectangle{12, 6}
// Rectangle struct have Area member function
checkArea(t, rectangle, 72.0)
})

t.Run("circles", func(t *testing.T) {
circle  := Circle{10}
// Circle struct have Area member function
checkArea(t, circle, 314.1592653589793)
})
}
``````
``````// 介面
type  Shape  interface {
Area() float64
}
// 結構
type  Rectangle  struct {
Width float64
Height float64
}
// 結構方法
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type  Circle  struct {
}
func (c Circle) Area() float64 {
}
``````

### Table Driven Tests

``````func  TableDrivenTestsArea(t *testing.T) {
// 批次測試
areaTests  := []struct {
shape Shape
want float64
}{
{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
}

for  _, tt  :=  range areaTests {
got  := tt.shape.Area()
if got != tt.want {
t.Errorf("got %.2f want %.2f", got, tt.want)
}
}
}
``````

### 執行特定測試

``````func  TestTableDrivenArea(t *testing.T) {
areaTests  := []struct {
name string
shape Shape
hasArea float64
}{
{name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0},
{name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793},
}
for  _, tt  :=  range areaTests {
t.Run(tt.name, func(t *testing.T) {
got  := tt.shape.Area()
if got != tt.hasArea {
t.Errorf("%#v got %.2f want %.2f", tt.shape, got, tt.hasArea)
}
})
}
}
go test -run TestTableDrivenArea/Rectangle
``````

## Extends繼承

``````package main
import "fmt"

type Person struct {
Id   int
Name string
}
type Tester interface {
Test()
Eat()
}
func (this *Person) Test() {
fmt.Println("\tthis =", &this, "Person.Test")
}
func (this *Person) Eat() {
fmt.Println("\tthis =", &this, "Person.Eat")
}
// Employee從Person繼承，並直接繼承Eat方法，並且將Test方法覆蓋。
type Employee struct {
Person
}
func (this *Employee) Test() {
fmt.Println("\tthis =", &this, "Employee.Test")
this.Person.Test() // 調用父類別方法
}
func main() {
fmt.Println("An Employee instance :")
var nu Employee
nu.Id = 2
nu.Name = "NTom"
nu.Test()
nu.Eat()
fmt.Println()

fmt.Println("A Tester interface to Employee instance :")
var t Tester
t = &nu
t.Test()
t.Eat()
fmt.Println()

fmt.Println("A Tester interface to Person instance :")
t = &nu.Person
t.Test()
t.Eat()
}
``````

## Pointer

``````func  assertNoError(t *testing.T, got error) {
t.Helper()
if got !=  nil {
t.Fatal("got an error but didnt want one")
}
}

func  assertBalance(t *testing.T, wallet Wallet, want Bitcoin) {
t.Helper()
got  := wallet.Balance()
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}

func assertError(t *testing.T, got error, want error) {
t.Helper()
if got ==  nil {
t.Fatal("didn't get an error but wanted one")
}
if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
}

func  TestWallet(t *testing.T) {
t.Run("Deposit", func(t *testing.T) {
wallet  := Wallet{}
// 使用 & 檢查 Wallet 記憶體位置是否一至
fmt.Printf("address of Wallet in test is %p \n", &wallet)
wallet.Deposit(Bitcoin(10))
assertBalance(t, wallet, Bitcoin(10))
})
t.Run("Withdraw", func(t *testing.T) {
wallet  := Wallet{Bitcoin(20)}
err  := wallet.Withdraw(Bitcoin(10))
assertBalance(t, wallet, Bitcoin(10))
assertNoError(t, err)
})
t.Run("Withdraw insufficient funds", func(t *testing.T) {
wallet  := Wallet{Bitcoin(20)}
err  := wallet.Withdraw(Bitcoin(100))
assertBalance(t, wallet, Bitcoin(20))
assertError(t, err, ErrInsufficientFunds)
})
}
``````
``````// 創建一個 error 發生
var  ErrInsufficientFunds  = errors.New("cannot withdraw, insufficient funds")
// 設定新 type 並指定為 int
type  Bitcoin  int
// Bitcoin to string 時回傳 %d BTC 結果
func (b Bitcoin) String() string {
return fmt.Sprintf("%d BTC", b)
}

// 建構 Wallet struct 並擁有型別為 Bitcoin 的成員 balance
type  Wallet  struct {
balance Bitcoin
}
// 建立成員方法 (w *Wallet) *代表使用記憶體位置取得 Wallet struct
func (w *Wallet) Deposit(amount Bitcoin) {
// w *Wallet 的pointer(*) w 已經代表記憶體位置本身，所已不需 &
fmt.Printf("address of Wallet in Deposit is %p \n", w)
w.balance += amount
}

func (w *Wallet) Withdraw(amount Bitcoin) error {
// 檢查所領款的金額是否超過存款金額
if amount > w.balance {
// 如果超過回傳 error
return ErrInsufficientFunds
}
w.balance -= amount
return  nil
}

func (w *Wallet) Balance() Bitcoin {
return w.balance
}
``````

## Map

``````// 建立 map 兩種方法
dictionary =  map[string]string{}
// OR
dictionary =  make(map[string]string)
``````

``````func  TestSearch(t *testing.T) {
dictionary  :=  map[string]string{"test": "this is just a test"}
got  :=  Search(dictionary, "test")
want  :=  "this is just a test"
if got != want {
t.Errorf("got '%s' want '%s' given, '%s'", got, want, "test")
}
}
``````

``````func Search(dictionary map[string]string, word string) string {
return dictionary[word]
}
``````

``````func  TestSearchRefactor(t *testing.T) {
dictionary  := Dictionary{"test": "this is just a test"}
// 檢查 Dictionary記憶體位置是否一至，這邊不需用&取得記憶體位置，Map 本身就是 reference types
fmt.Printf("address of Dictionary in test is %p \n", dictionary)
t.Run("known word", func(t *testing.T) {
got, _  := dictionary.Search("test")
want  :=  "this is just a test"
assertStrings(t, got, want)
})

t.Run("unknown word", func(t *testing.T) {
_, err  := dictionary.Search("unknown")
want  :=  "could not find the word you were looking for"
if err ==  nil {
t.Fatal("expected to get an error.")
}
assertStrings(t, err.Error(), want)
})
}
``````
``````type Dictionary map[string]string
var ErrNotFound = errors.New("could not find the word you were looking for")
// 這邊的 Dictionary 物件成員並沒有使用 * 因為 map 本身就是 reference types 所以不需重新指向記憶體位置
func (d Dictionary) Search(word string) (string, error) {
fmt.Printf("address of Dictionary in Search is %p \n", d)
definition, ok := d[word]
if !ok {
return  "", ErrNotFound
}
return definition, nil
}
``````

``````func  TestAdd(t *testing.T)  {
dictionary := Dictionary{}
dictionary.Add("test",  "this is just a test")
want :=  "this is just a test"
got, err := dictionary.Search("test")
if err !=  nil  {
}
if want != got {
t.Errorf("got '%s' want '%s'", got, want)
}
}
``````

``````func (d Dictionary)  Add(word, definition string)  {
d[word]  = definition
}
``````

``````func assertDefinition(t *testing.T, dictionary Dictionary, word, definition string)  {
t.Helper()
got, err := dictionary.Search(word)
if err !=  nil  {
}
if definition != got {
t.Errorf("got '%s' want '%s'", got, definition)
}
}

t.Run("new word",  func(t *testing.T)  {
dictionary := Dictionary{}
word :=  "test"
definition :=  "this is just a test"
assertError(t, err,  nil)
assertDefinition(t, dictionary, word, definition)
})
t.Run("existing word",  func(t *testing.T)  {
word :=  "test"
definition :=  "this is just a test"
dictionary := Dictionary{word: definition}
assertError(t, err, ErrWordExists)
assertDefinition(t, dictionary, word, definition)
})
}
``````
``````const  (
ErrNotFound =  DictionaryErr("could not find the word you were looking for")
)
type DictionaryErr string
func (e DictionaryErr) Error()  string  {
return  string(e)
}

func (d Dictionary) Add(word, definition string) error {
_, err := d.Search(word)
switch err {
case ErrNotFound:
d[word]  = definition
case  nil:
return ErrWordExists
default:
return err
}
return  nil
}
``````

``````t.Run("existing word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
newDefinition := "new definition"
dictionary := Dictionary{word: definition}
err := dictionary.Update(word, newDefinition)
assertError(t, err, nil)
assertDefinition(t, dictionary, word, newDefinition)
})

t.Run("new word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
dictionary := Dictionary{}
err := dictionary.Update(word, definition)
assertError(t, err, ErrWordDoesNotExist)
})
``````
``````const (
ErrNotFound         = DictionaryErr("could not find the word you were looking for")
// new
ErrWordDoesNotExist = DictionaryErr("cannot update word because it does not exist")
)
func (d Dictionary) Update(word, definition string) error {
_, err := d.Search(word)
switch err {
case ErrNotFound:
return ErrWordDoesNotExist
case nil:
d[word] = definition
default:
return err
}
return nil
}
``````

``````func TestDelete(t *testing.T) {
word := "test"
dictionary := Dictionary{word: "test definition"}
dictionary.Delete(word)
_, err := dictionary.Search(word)
if err != ErrNotFound {
t.Errorf("Expected '%s' to be deleted", word)
}
}
``````
``````func (d Dictionary) Delete(word string) {
// go lang 內建刪除功能
delete(d, word)
}
``````

## Dependency Injection

• 不需要一個特定框架
• 不會將你的code變得更加複雜
• 更有助於程式測試
• 您將會編寫出色的通用功能方法

``````func TestGreet(t *testing.T) {
buffer := bytes.Buffer{}
Greet(&buffer,"Chris")
got := buffer.String()
want := "Hello, Chris"
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
``````
``````func Greet(writer *bytes.Buffer, name string)  {
// fmt.Fprintf 就像是 fmt.Printf 一樣，但他用 writer 取代了預設的 stdout
fmt.Fprintf(writer, "Hello, %s", name)
}
``````

``````// It returns the number of bytes written and any write error encountered.
func  Printf(format string, a ...interface{}) (n int, err error) {
return  Fprintf(os.Stdout, format, a...)
}

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}

type Writer interface {
Write(p []byte)  (n int, err error)
}
``````

``````// 失敗因為 os.Stdout 不是 *bytes.Buffer type
Greet(os.Stdout,  "Elodie")
``````

``````// 我們使用 io.Writer 來解決寫入問題
func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
``````

``````package main

import (
"fmt"
"io"
"net/http"
)

func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}

func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
Greet(w, "world")
}

func main() {
http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))
}
``````

## Mocking

``````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)
}
}
``````
``````const finalWord =  "Go!"
const countdownStart =  3

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)
}
``````

``````func  TestCountdownMock(t *testing.T) {
buffer  :=  &bytes.Buffer{}
// 我們創建了一個假的 sleep 方法
spySleeper  :=  &SpySleeper{}
CountdownMock(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)
}
}
``````
``````type  Sleeper  interface {
Sleep()
}
type  SpySleeper  struct {
Calls int
}
func (s *SpySleeper) Sleep() {
s.Calls++
}

type DefaultSleeper struct {}
func (d *DefaultSleeper) Sleep() {
time.Sleep(1 * time.Second)
}

func CountdownMock(out io.Writer, sleeper Sleeper) {
for  i  := countdownStart; i >  0; i-- {
sleeper.Sleep()
fmt.Fprintln(out, i)
}
sleeper.Sleep()
fmt.Fprint(out, finalWord)
}

func main() {
// 真實 sleep
sleeper := &DefaultSleeper{}
Countdown(os.Stdout, sleeper)
}
``````

``````const write = "write"
const sleep = "sleep"

type CountdownOperationsSpy struct {
// 字串陣列，記錄呼叫的方法
Calls []string
}
// 記錄Sleep
func (s *CountdownOperationsSpy) Sleep() {
s.Calls = append(s.Calls, sleep)
}
// 記錄Write
func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) {
s.Calls = append(s.Calls, write)
return
}
// 監控 Time 時間
type SpyTime struct {
durationSlept time.Duration
}
// 記錄下睡眠時間
func (s *SpyTime) Sleep(duration time.Duration) {
s.durationSlept = duration
}

func TestCountdown(t *testing.T) {
// 測試輸出資料是否正確
t.Run("prints 5 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)
}
})
// 測試執行 function 順序是否正確
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)
}
})
}

func TestConfigurableSleeper(t *testing.T) {
// 測試 ConfigurableSleeper 是否執行正確的睡眠時間
sleepTime := 5 * time.Second
spyTime := &SpyTime{}
sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep}
sleeper.Sleep()
if spyTime.durationSlept != sleepTime {
t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept)
}
}
``````
``````// Sleeper 介面
type Sleeper interface {
Sleep()
}

// ConfigurableSleeper 實做 Sleeper 且定義 delay 時間
type ConfigurableSleeper struct {
duration time.Duration
sleep    func(time.Duration)
}

// Sleep將會依照 duration 時間暫停程式執行
func (c *ConfigurableSleeper) Sleep() {
c.sleep(c.duration)
}

const finalWord = "Go!"
const countdownStart = 3

// Countdown 將根據 delay 時間印出倒數
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)
}

func main() {
sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
Countdown(os.Stdout, sleeper)
}
``````

## Concurrency

``````func  mockWebsiteChecker(url string) bool {
if url ==  "waat://furhurterwe.geds" {
return  false
}
return  true
}
func  TestCheckWebsites(t *testing.T) {
websites  := []string{
"http://blog.gypsydave5.com",
"waat://furhurterwe.geds",
}
want  :=  map[string]bool{
"http://blog.gypsydave5.com": true,
"waat://furhurterwe.geds": false,
}
got := CheckWebsites(mockWebsiteChecker, websites)
if  !reflect.DeepEqual(want, got) {
t.Fatalf("Wanted %v, got %v", want, got)
}
}

func slowStubWebsiteChecker(_ string) bool {
time.Sleep(20 * time.Millisecond)
return true
}

func BenchmarkCheckWebsites(b *testing.B) {
urls := make([]string, 100)
for i := 0; i < len(urls); i++ {
urls[i] = "a url"
}

for i := 0; i < b.N; i++ {
CheckWebsites(slowStubWebsiteChecker, urls)
}
}
``````
``````type WebsiteChecker func(string) bool
type result struct {
string
bool
}

func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
results := make(map[string]bool)
resultChannel := make(chan result)

for _, url := range urls {
go func(u string) {
resultChannel <- result{u, wc(u)}
}(url)
}

for i := 0; i < len(urls); i++ {
result := <-resultChannel
results[result.string] = result.bool
}

return results
}
``````

## Select

``````func TestRacer(t *testing.T) {
t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) {
slowServer := makeDelayedServer(20 * time.Millisecond)
fastServer := makeDelayedServer(0 * time.Millisecond)
// defer 延遲動作，用於當需要執行完function後close物件
defer slowServer.Close()
defer fastServer.Close()

slowURL := slowServer.URL
fastURL := fastServer.URL

want := fastURL
got, err := Racer(slowURL, fastURL)
if err != nil {
t.Fatalf("did not expect an error but got one %v", err)
}
if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
})

t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
server := makeDelayedServer(25 * time.Millisecond)
defer server.Close()
_, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond)
if err == nil {
t.Error("expected an error but didn't get one")
}
})
}

func makeDelayedServer(delay time.Duration) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(delay)
}))
}
``````
``````var tenSecondTimeout = 10 * time.Second
// 測試 a b 網站速度，超過 10s timeout
func Racer(a, b string) (winner string, error error) {
return ConfigurableRacer(a, b, tenSecondTimeout)
}
// 比較 a b 網站 並回傳速度較快的一個
func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) {
// 使用 select 同時並發動作
select {
case <-ping(a):
return a, nil
case <-ping(b):
return b, nil
case <-time.After(timeout):
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
}
}
func ping(url string) chan bool {
ch := make(chan bool)
go func() {
http.Get(url)
ch <- true
}()
return ch
}
``````

## Pointer Panic

``````type  Calc  interface {
increment()
decrement()
}

type  Math struct {
count int
}

func (m *Math) increment() {
println(m)
m.count +=  1
println("increment!", m.count)
}

func (m *Math) decrement() {
println(&m)
m.count -=  1
println("decrement!", m.count)
}

func (m Math) decrement2() {
m.count -=  1
println("decrement!", m.count)
}

func  main() {
// explodes -> math -> nil
var  math  *math =  nil
(*math).increment() // panic: value method main.math.decrement called using nil *math pointer
var  explodes Explodes = math
println(math, explodes) // '0x0 (0x10a7060,0x0)'
if explodes !=  nil {
println("Not nil!") // 'Not nil!'
explodes.increment() // Normal
explodes.decrement2() // panic: value method main.math.decrement called using nil *math pointer
} else {
println("nil!")
}
var  math1 math = math{}
println(&math1)
math1.increment()
println(&math1)
math1.increment()
println(&math1)
math1.increment()
println(math1.count)

var  math2  *math =  &math{}
println(&math2)
math2.increment()
println(&math2)
math2.increment()
println(&math2)
math2.increment()
println(math2.count)
}
``````

https://www.ithome.com.tw/voice/103455

## Panic Recover

``````package main

import (
"fmt"
"os"
)

func  check(err error) {
if err !=  nil {
panic(err)
}
}

func  main() {
f, err  := os.Open("/tmp/dat")
defer  func() {
if  err  :=  recover(); err !=  nil {
fmt.Println(err) // 這已經是頂層的 UI 介面了，想以自己的方式呈現錯誤
}
if f !=  nil {
if  err  := f.Close(); err !=  nil {
panic(err) // 示範再拋出 panic
}
}
}()
check(err)
b1  :=  make([]byte, 5)
check(err)
fmt.Printf("%d bytes: %s\n", n1, string(b1))
}
``````

## Summary

Go 性能明顯優越於近代語言(除了 Rust)

## Reference

Go library
https://github.com/avelino/awesome-go