Golang Unit Test

hyperredstart

HyperRedStart

Posted on March 1, 2022

Golang Unit Test

Golang Unit Test

使用 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)
    }
}
Enter fullscreen mode Exit fullscreen mode
package main
import  "fmt"
func  Hello() string {
    return  "Hello, world"
}
func  main() {
    fmt.Println(Hello())
}
Enter fullscreen mode Exit fullscreen mode
go test
PASS
ok      .../Golang_TDD 0.086s
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

製造錯誤

// 使用空陣列讓原有程式造成 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
Enter fullscreen mode Exit fullscreen mode

修正錯誤

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
}
Enter fullscreen mode Exit fullscreen mode

減少 test 重複 code

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

結構與介面

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)
    })
}
Enter fullscreen mode Exit fullscreen mode
// 介面
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 {
    Radius float64
}
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

執行特定測試

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
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

Pointer

以下範例以帳號內存取現金示範
使用 TDD 開發需先撰寫測試文件

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)
    })
}
Enter fullscreen mode Exit fullscreen mode
// 創建一個 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
}
Enter fullscreen mode Exit fullscreen mode

Map

定義一個 map[KeyType]ValueType 的成員

// 建立 map 兩種方法
dictionary =  map[string]string{}
// OR
dictionary =  make(map[string]string)
Enter fullscreen mode Exit fullscreen mode

撰寫測試文件

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

根據測試文件撰寫符合規格的程式

func Search(dictionary map[string]string, word string) string {
    return dictionary[word]
}
Enter fullscreen mode Exit fullscreen mode

優化重構程式

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)
    })
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

新增新功能 Add

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  {
        t.Fatal("should find added word:", err)
    }
    if want != got {
        t.Errorf("got '%s' want '%s'", got, want)
    }
}
Enter fullscreen mode Exit fullscreen mode

根據測試文件開發功能

func (d Dictionary)  Add(word, definition string)  {
     d[word]  = definition
}
Enter fullscreen mode Exit fullscreen mode

優化重構程式

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

func TestAdd(t *testing.T)  {
    t.Run("new word",  func(t *testing.T)  {
        dictionary := Dictionary{}
        word :=  "test"
        definition :=  "this is just a test"
        err := dictionary.Add(word, definition)
        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}
        err := dictionary.Add(word,  "new test")
        assertError(t, err, ErrWordExists)
        assertDefinition(t, dictionary, word, definition)
    })
}
Enter fullscreen mode Exit fullscreen mode
const  (
 ErrNotFound =  DictionaryErr("could not find the word you were looking for")
 ErrWordExists =  DictionaryErr("cannot add word because it already exists")
)
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
}
Enter fullscreen mode Exit fullscreen mode

新增新功能Update

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)
})
Enter fullscreen mode Exit fullscreen mode
const (
    ErrNotFound         = DictionaryErr("could not find the word you were looking for")
    ErrWordExists       = DictionaryErr("cannot add word because it already exists")
    // 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
}
Enter fullscreen mode Exit fullscreen mode

新增新功能Delete

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
func (d Dictionary) Delete(word string) {
    // go lang 內建刪除功能
    delete(d, word)
}
Enter fullscreen mode Exit fullscreen mode

Dependency Injection

使用依賴性注入將會有已下優點

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

讓我們回到最初的hello方法

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
func Greet(writer *bytes.Buffer, name string)  {
    // fmt.Fprintf 就像是 fmt.Printf 一樣,但他用 writer 取代了預設的 stdout
    fmt.Fprintf(writer, "Hello, %s", name)
}
Enter fullscreen mode Exit fullscreen mode

讓我們看看 go 的 fmt 實現

// 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)
}
Enter fullscreen mode Exit fullscreen mode

我們嘗試使用 Greet function

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

修正錯誤

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

現在已改寫完成了 Greet 方法,我們來嘗試用他來輸出至網頁上

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))
}
Enter fullscreen mode Exit fullscreen mode

Mocking

我們開發了一個程式,必須到數3秒代碼如下:

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
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)
}
Enter fullscreen mode Exit fullscreen mode

不難發現當執行測試時,必須花費超過3秒的時間,想像當我們程式更加複雜後,必須要花多久時間執行測試呢?
所以以下我們改變了寫法 :

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
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)
}
Enter fullscreen mode Exit fullscreen mode

通過以上代碼我們測試時明顯減少了等待時間,但是仍然有些問題我們還尚未測試,他是否是依照 先執行 sleep 後再執行 print 直到最後。

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
// 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)
}
Enter fullscreen mode Exit fullscreen mode

Concurrency

我們以測試網站是否正常為例

func  mockWebsiteChecker(url string) bool {
    if url ==  "waat://furhurterwe.geds" {
        return  false
    }
    return  true
}
func  TestCheckWebsites(t *testing.T) {
    websites  := []string{
        "http://google.com",
        "http://blog.gypsydave5.com",
        "waat://furhurterwe.geds",
    }
    want  :=  map[string]bool{
        "http://google.com": true,
        "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)
    }
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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)
        w.WriteHeader(http.StatusOK)
    }))
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

對於 golang 並無try catch 的解釋
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)
    n1, err  := f.Read(b1)
    check(err)
    fmt.Printf("%d bytes: %s\n", n1, string(b1))
}
Enter fullscreen mode Exit fullscreen mode

Summary

優點
使用 goroutines、channels 快速開發並型程序
Go 性能明顯優越於近代語言(除了 Rust)
標準內建的測試框架
有 Defer功能不會忘記關閉清除物件
缺點
沒有泛型
沒有 enum
數據結構只有 map,slice
擁有指標特性可能使開發者混亂

Reference

TDD 開發模式
CI/CD 使用
https://github.com/drone/drone
ORM Library 測試
https://github.com/mattn/go-oci8/tree/master/_example
https://www.jianshu.com/p/add47894c446
https://gocodecloud.com/blog/2016/08/09/accessing-an-oracle-db-in-go/
https://blog.csdn.net/notbaron/article/details/78168026
https://o7planning.org/en/10467/installing-c-cpp-compiler-mingw
https://github.com/wendal/go-oci8
https://www.oracle.com/technetwork/topics/winx64soft-089540.html
https://www.144d.com/post-490.html
https://www.cnblogs.com/ghj1976/p/3540257.html
https://medium.com/@kfoss/oracle-and-go-on-windows-64-bit-2b37496fe179
gRPC 使用
https://grpc.io/docs/quickstart/go.html#prerequisites
MicroServices 架構開發
https://yq.aliyun.com/articles/2764
https://lostechies.com/andrewsiemer/2016/01/11/testing-microservices/
https://blog.csdn.net/tengxing007/article/details/78289645
https://juejin.im/post/5a3257496fb9a044fc44c3d3
https://medium.com/seek-blog/microservices-in-go-2fc1570f6800
https://outcrawl.com/go-microservices-cqrs-docker
https://dzone.com/articles/go-microservices-blog-series-part-1
https://en.wikipedia.org/wiki/Software_architecture

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

💖 💪 🙅 🚩
hyperredstart
HyperRedStart

Posted on March 1, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Golang Unit Test
tutorial Golang Unit Test

March 1, 2022