你據說過測試驅動開發嗎?

TDD 是什麼

根據維基百科的定義,測試驅動開發(Test-driven development 簡稱爲 TDD)是一種軟件開發過程,這種軟件開發過程依賴於對一個很是短的開發循環週期的重複執行。先把需求轉換成很是具體的測試用例,而後對軟件進行編碼讓測試用例經過,最後對軟件進行改進重構,消除代碼的重複,保持代碼整潔。沒有測試驗證的功能,不爲會爲其編寫代碼。html

TDD 是由多個很是短的開發循環週期組成,一個 TDD 開發循環包括以下的3個步驟:前端

  1. 編寫測試,讓測試運行失敗,此時代碼處於紅色狀態。
  2. 編寫生產代碼,讓測試經過,此時代碼處於綠色狀態。
  3. 重構代碼,消除重複,此時代碼處於重構狀態。

這3個步驟就是人們常說的紅、綠、重構循環,這就是一個完整的 TDD 開發循環週期。git

TDD 起源於極限編程中的測試先行編程原則,最先由 Kent Beck 提出。TDD 是一種編程技巧,TDD 的主要目標是讓代碼整潔簡單無 Bug。世界著名的軟件大師 Kent Beck、Martin Fowler、Robert C. Martin 均表示支持 TDD 開發模式,他們甚至和 David Heinemeier Hansson 就 TDD 自己以及 TDD 對軟件設計開發的影響有過深刻的討論:Is TDD Dead?github

TDD 的優勢

TDD 的優勢有不少,下面列出幾點我認爲比較重要的優勢:數據庫

  • 開發人員更瞭解業務,更懂得任務分解:因爲測試用例須要從用戶或者使用者的角度來進行描述,這就要求開發人員能更加充分的瞭解業務,只有更充分的瞭解業務,才能寫好測試用例,並且因爲測試應該儘可能小,這也就會促使咱們把開發任務分解的更小,只有把任務分解的更小,咱們才能達到 TDD 理想的小步快跑的狀態。
  • 代碼測試覆蓋率高,bug 少:因爲先寫測試,而後才能寫生產代碼,只有全部測試經過開發人員才能提交代碼,這就會使得代碼的測試覆蓋率很是高,代碼測試覆蓋率高能代表咱們的代碼是通過充分測試的,這樣生產中會碰到的 bug 就會相對少量多。
  • 更自信的重構:因爲代碼的測試覆蓋率高,每一個功能都有對應的測試代碼,開發人員能夠更大膽進行重構,由於有充分的測試代碼,當咱們重構時,若是破壞了原有的功能,測試就會立刻失敗,這可讓開發人員在開發階段就能發現問題,問題越早發現,修復的成本就越低。開發人員不會由於修改代碼致使其餘功能的損壞卻不能及時發現,引起生產 bug 而變得畏手畏腳,開發人員重構代碼也會變得很是自信。
  • 代碼整潔易擴展:因爲 TDD 開發循環中,咱們在不斷重構代碼,消除代碼的壞味道,這會讓咱們獲得更加整潔的代碼,爲了讓軟件更加容易測試,這會讓咱們更深刻地思考評估咱們的軟件架構,從而改善優化咱們的軟件架構,讓軟件更加的靈活易擴展。
  • 不會出現生產無用的代碼:因爲咱們先把需求轉換成測試用例,而且咱們只爲經過測試來編寫最少的代碼,這樣咱們幾乎不會編寫出生產無用的代碼,咱們全部的代碼都是爲相應的需求來服務的。

TDD 開發循環

一個完整的 TDD 開發循環以下圖所示:express

  1. 編寫測試,測試應該儘可能小。運行測試,測試會失敗,編譯不經過也是一種失敗,若是測試沒有失敗,這代表這個測試沒有任何意義,由於這個測試既沒有幫助咱們實現需求,也沒有幫助咱們修復 bug 完善代碼。這多是以下的緣由致使的:
    • 咱們在上一次 TDD 循環中,生產代碼編寫的太多,已經把此次的測試須要測試的功能實現了。
    • 咱們在以前的測試中忽略了這一次測試中應該測試的部分。
  2. 編寫最少的代碼讓測試經過。爲了儘可能脫離測試沒法經過的狀態中,此步驟中可使用特殊的方法,好比使用僞實現直接返回常量結果值,而後在重構階段逐漸替換常量爲真正的實現。
  3. 重構代碼,減小代碼中的重複代碼,清除代碼中的壞味道。清除生產代碼與測試間的重複設計。這一步驟很是的重要,沒有這一步驟的 TDD 開發是沒有靈魂的 TDD 開發模式,而且可能致使你獲得一個比不使用 TDD 開發模式開發出來的還要糟糕的軟件。
  4. 重複上述步驟。

TDD 開發原則

TDD 三定律編程

  1. 在編寫不能經過的單元測試前,不可編寫生產代碼。這是 TDD 開發最重要的原則,是 TDD 得以實行的重要指導原則,這條原則包含兩層含義:
    • 測試先行,在編寫生產代碼以前要先編寫測試代碼。
    • 只有在編寫的測試失敗的狀況下,才能進行生產代碼的編寫。
  2. 只可編寫恰好沒法經過的單元測試,不能編譯也算是不經過。這條原則指導咱們在編寫測試時,也應該把測試儘可能的拆分的小一些,不要期望一個測試就能完整的測試一整個功能。
  3. 只可編寫恰好足以經過當前失敗測試的生產代碼。這條原則告訴咱們要編寫儘可能少的生產代碼,儘快脫離測試失敗的狀態,這裏的儘可能少的代碼並非表示讓你使用語法糖來達到使用少的代碼行數,處理更多的事情的目標,這裏儘可能少的代碼的意思是,只須要編寫能經過測試的代碼便可,不須要處理全部狀況,好比異常狀況等。這能夠經過後面的測試來驅動咱們來寫這些處理異常狀況的代碼。

TDD 開發策略api

  1. 僞實現,直接返回常量,並在重構階段使用變量逐漸替換常量。
  2. 明顯實現,因爲代碼邏輯簡單,能夠直接寫出代碼實現。
  3. 三角法,經過添加測試使用其失敗,逐漸驅動咱們朝目標前進。

根據錯誤的狀況,僞實現和明顯實現能夠交替進行,當開發進行順暢時,可使用明顯實現,當開發過程當中常常碰到錯誤時,可使用僞實現,慢慢找回自信,而後再使用明顯實現進行開發。當徹底沒有實現思路或者實現思路不清晰時, 可使用三角法來驅動咱們開發,逐漸理清思路。安全

TDD 的難點

  • 任務分解到底須要多細?咱們須要把功能分解成多小的任務才合適呢?而後把測試分解多小才合適呢?這是一個比較難的問題,沒有人能確切給出答案,一切都須要你本身去體會,去練習,去不斷的嘗試,去學習,去積累經驗。
  • 到底要測試什麼?若是咱們測試寫的很差,很容易形成測試代碼須要跟着生產代碼被頻繁的修改,這樣測試不只沒有給咱們的代碼帶來好處,反而給咱們的重構帶來不少的額外的負擔。關於要測試什麼,有一句正確但卻沒法給你具體建議名言:「測試行爲,不要測試實現」,這也是須要長時間的去學習,去練習,去體會的。簡單來講你應該測試全部公開給別人使用的接口,類,函數等,而內部私有的你能夠選擇性的測試,具體的關於應該如何寫測試,能夠觀看以下的關於如何測試的公開演講視頻:

TDD 開發示例

咱們使用 Go 語言來開發一個簡單的 http 服務來演示 TDD 開發模式。服務支持以下的兩種功能:bash

  • GET /users/{name} 會返回用戶使用 POST 方法 調用 API 的次數。
  • POST /users/{name} 會記錄用戶的一次 API 調用,把以前的 API 調用次數加1。

TDD 示例代碼倉庫地址 github.com/mgxian/tdd-…

任務分解

  • 實現 GET 請求
    • 驗證響應碼
    • 驗證返回 API 調用次數
    • 驗證不存在的用戶
  • 實現 POST 請求
    • 驗證響應碼
    • 驗證是否調用了記錄函數
    • 驗證調用記錄是否正確
  • 集成測試
  • 完善主程序

實現 GET 請求

先寫測試

測試獲取 will 的 API 調用次數,並驗證響應碼

func TestGetUsers(t *testing.T) {
	t.Run("return will's api call count", func(t *testing.T) {
		request, _ := http.NewRequest(http.MethodGet, "/users/will", nil)
		response := httptest.NewRecorder()
		UserServer(response, request)
		got := response.Code
		want := http.StatusOK
		if got != want {
			t.Errorf("got %d, want %d", got, want)
		}
	})
}
複製代碼

運行測試你會獲得以下所示的錯誤

.\user_test.go:13:3: undefined: UserServer
複製代碼
編寫最少的代碼讓測試能運行並檢查失敗的測試輸出

如今讓咱們添加對UserServer函數的定義

func UserServer() {}
複製代碼

再次運行測試你會獲得以下的錯誤

.\user_test.go:13:13: too many arguments in call to UserServer
        have (*httptest.ResponseRecorder, *http.Request)
        want ()
複製代碼

如今讓咱們給函數添加相應的參數

func UserServer(w http.ResponseWriter, r *http.Request) {}
複製代碼

再次運行測試,測試經過了。

先寫測試

測試響應數據

func TestGetUsers(t *testing.T) {
	t.Run("return will's api call count", func(t *testing.T) {
		request, _ := http.NewRequest(http.MethodGet, "/users/will", nil)
		response := httptest.NewRecorder()
		UserServer(response, request)
		got := response.Code
		want := http.StatusOK
		if got != want {
			t.Errorf("got %d, want %d", got, want)
		}

		gotCount := response.Body.String()
		wantCount := "6"
		if gotCount != wantCount {
			t.Errorf("got % q, want % q", gotCount, wantCount)
		}
	})
}
複製代碼

運行測試,你會獲得以下的錯誤

user_test.go:23: got "", want "6"
複製代碼
編寫足夠的代碼讓測試經過
func UserServer(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "6")
}
複製代碼

如今測試經過,可是你確定會想罵人了,你這是寫的啥,直接給寫死了返回值?說好的不要寫死呢?先彆着急,因爲咱們沒有存儲數據的地方,如今返回一個固定值讓測試經過,也不能說不是一個好辦法,後面咱們會來解決這個問題的。

完成主程序的結構

咱們儘可能早的把通過驗證的生產代碼,放到主程序中,這樣咱們能夠儘快的獲得一個可運行的軟件,並且後續的程序結構的改動,能夠及時發現。

package main

import (
	"log"
	"net/http"
)

func main() {
	handler := http.HandlerFunc(UserServer)
	if err := http.ListenAndServe(":5000", handler); err != nil {
		log.Fatalf("could not listen on port 5000 %v", err)
	}
}
複製代碼
先寫測試

如今讓咱們再嘗試獲取 mgxian 的 API 調用數據

t.Run("return mgxian's api call count", func(t *testing.T) {
	request, _ := http.NewRequest(http.MethodGet, "/users/mgxian", nil)
	response := httptest.NewRecorder()
	UserServer(response, request)
	got := response.Code
	want := http.StatusOK
	if got != want {
		t.Errorf("got %d, want %d", got, want)
	}

	gotCount := response.Body.String()
	wantCount := "8"
	if gotCount != wantCount {
		t.Errorf("got % q, want % q", gotCount, wantCount)
	}
})
複製代碼

如今運行測試,你會獲得以下的錯誤

user_test.go:40: got "6", want "8"
複製代碼
編寫足夠的代碼讓測試經過

如今讓咱們來修復這個錯誤,爲了能讓咱們能根據 user 的不一樣來響應不一樣的內容,咱們須要從 URL 中獲取到 user ,測試驅動着咱們完成接下來的工做。

func UserServer(w http.ResponseWriter, r *http.Request) {
	user := r.URL.Path[len("/users/"):]
	if user == "will" {
		fmt.Fprint(w, "6")
		return
	}

	if user == "mgxian" {
		fmt.Fprint(w, "8")
		return
	}
}
複製代碼

運行測試經過。

重構

根據 user 來響應不一樣內容的邏輯咱們能夠放在一個單獨的函數中去。

func UserServer(w http.ResponseWriter, r *http.Request) {
	user := r.URL.Path[len("/users/"):]
	apiCallCount := GetUserAPICallCount(user)
	fmt.Fprint(w, apiCallCount)
}

func GetUserAPICallCount(user string) string {
	if user == "will" {
		return "6"
	}

	if user == "mgxian" {
		return "8"
	}

	return ""
}
複製代碼

重構以後,運行測試,測試經過,咱們觀察到咱們的測試程序有部分代碼是重複的,咱們也能夠進行重構,不只生產代碼須要重構,測試代碼也須要重構。

func TestGetUsers(t *testing.T) {
	t.Run("return will's api call count", func(t *testing.T) {
		user := "will"
		request := newGetUserAPICallCountRequest(user)
		response := httptest.NewRecorder()
		UserServer(response, request)

		assertStatus(t, response.Code, http.StatusOK)
		assertCount(t, response.Body.String(), "6")
	})

	t.Run("return mgxian's api call count", func(t *testing.T) {
		user := "mgxian"
		request := newGetUserAPICallCountRequest(user)
		response := httptest.NewRecorder()
		UserServer(response, request)

		assertStatus(t, response.Code, http.StatusOK)
		assertCount(t, response.Body.String(), "8")
	})
}

func newGetUserAPICallCountRequest(user string) *http.Request {
	request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s", user), nil)
	return request
}

func assertStatus(t *testing.T, got, want int) {
	t.Helper()
	if got != want {
		t.Errorf("wrong status code got %d, want %d", got, want)
	}
}

func assertCount(t *testing.T, got, want string) {
	t.Helper()
	if got != want {
		t.Errorf("got % q, want % q", got, want)
	}
}
複製代碼

運行測試,測試經過,測試代碼重構完成。如今讓咱們進一步的思考,咱們的 UserServer 至關於 MVC 模式中的 Controller ,GetUserAPICallCount 至關於 Model ,咱們應該讓它們之間經過 Interface UserStore 來交流,隔離關注點。爲了能讓 UserServer 使用 UserStore 咱們應該把 UserServer 定義爲 struct 類型。

type UserStore interface {
	GetUserAPICallCount(user string) int
}

type UserServer struct {
	store UserStore
}

func (u *UserServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user := r.URL.Path[len("/users/"):]
	apiCallCount := u.store.GetUserAPICallCount(user)
	fmt.Fprint(w, apiCallCount)
}
複製代碼

運行測試你會獲得以下的錯誤

main.go:9:30: type UserServer is not an expression
複製代碼

修改 main 函數新建立的 UserServer

func main() {
	server := &UserServer{}
	if err := http.ListenAndServe(":5000", server); err != nil {
		log.Fatalf("could not listen on port 5000 %v", err)
	}
}
複製代碼

修改測試使用新建立的 UserServer

func TestGetUsers(t *testing.T) {
	server := &UserServer{}
	t.Run("return will's api call count", func(t *testing.T) {
		user := "will"
		request := newGetUserAPICallCountRequest(user)
		response := httptest.NewRecorder()
		server.ServeHTTP(response, request)

		assertStatus(t, response.Code, http.StatusOK)
		assertCount(t, response.Body.String(), "6")
	})

	t.Run("return mgxian's api call count", func(t *testing.T) {
		user := "mgxian"
		request := newGetUserAPICallCountRequest(user)
		response := httptest.NewRecorder()
		server.ServeHTTP(response, request)

		assertStatus(t, response.Code, http.StatusOK)
		assertCount(t, response.Body.String(), "8")
	})
}
複製代碼

再次運行測試你會獲得以下的錯誤,這是因爲咱們並無傳遞 UserStore 給 UserServer 。

panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x18 pc=0x66575f]

複製代碼

編寫一個 stub 類型的 mock 來模擬測試

type StubUserStore struct {
	apiCallCounts map[string]int
}

func (s *StubUserStore) GetUserAPICallCount(user string) int {
	return s.apiCallCounts[user]
}
複製代碼

修改測試使用咱們 mock 出來的 StubUserStore

func TestGetUsers(t *testing.T) {
	store := StubUserStore{
		apiCallCounts: map[string]int{
			"will":   6,
			"mgxian": 8,
		},
	}
	server := &UserServer{&store}
	t.Run("return will's api call count", func(t *testing.T) {
		user := "will"
		request := newGetUserAPICallCountRequest(user)
		response := httptest.NewRecorder()
		server.ServeHTTP(response, request)

		assertStatus(t, response.Code, http.StatusOK)
		assertCount(t, response.Body.String(), "6")
	})

	t.Run("return mgxian's api call count", func(t *testing.T) {
		user := "mgxian"
		request := newGetUserAPICallCountRequest(user)
		response := httptest.NewRecorder()
		server.ServeHTTP(response, request)

		assertStatus(t, response.Code, http.StatusOK)
		assertCount(t, response.Body.String(), "8")
	})
}
複製代碼

再次運行測試,測試所有經過。

爲了使咱們的主程序能正常運行,咱們須要實現一個假的 UserStore

package main

import (
	"log"
	"net/http"
)

type InMemoryUserStore struct{}

func (i *InMemoryUserStore) GetUserAPICallCount(user string) int {
	return 666
}

func main() {
	store := InMemoryUserStore{}
	server := &UserServer{&store}

	if err := http.ListenAndServe(":5000", server); err != nil {
		log.Fatalf("could not listen on port 5000 %v", err)
	}
}
複製代碼
先寫測試

測試一個不存在的用戶

t.Run("return 404 on unknown user", func(t *testing.T) {
	user := "unknown"
	request := newGetUserAPICallCountRequest(user)
	response := httptest.NewRecorder()
	server.ServeHTTP(response, request)

	assertStatus(t, response.Code, http.StatusNotFound)
})
複製代碼

運行測試獲得以下的錯誤

user_test.go:52: wrong status code got 200, want 404
複製代碼
編寫足夠的代碼讓測試經過
func (u *UserServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user := r.URL.Path[len("/users/"):]
	apiCallCount := u.store.GetUserAPICallCount(user)
	if apiCallCount == 0 {
		w.WriteHeader(http.StatusNotFound)
	}
	fmt.Fprint(w, apiCallCount)
}
複製代碼

運行測試,測試經過。

實現 POST 請求

先寫測試

測試記錄 API 調用次數,驗證響應碼

func TestStoreAPICalls(t *testing.T) {
	store := StubUserStore{
		map[string]int{},
	}
	server := &UserServer{&store}

	t.Run("return accepted on POST", func(t *testing.T) {
		request, _ := http.NewRequest(http.MethodPost, "/users/will", nil)
		response := httptest.NewRecorder()

		server.ServeHTTP(response, request)
		assertStatus(t, response.Code, http.StatusAccepted)
	})
}
複製代碼

運行測試,你會獲得以下的錯誤

user_test.go:67: wrong status code got 404, want 202
複製代碼
編寫足夠的代碼讓測試經過
func (u *UserServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	method := r.Method
	if method == http.MethodPost {
		w.WriteHeader(http.StatusAccepted)
		return
	}
	user := r.URL.Path[len("/users/"):]
	apiCallCount := u.store.GetUserAPICallCount(user)
	if apiCallCount == 0 {
		w.WriteHeader(http.StatusNotFound)
	}
	fmt.Fprint(w, apiCallCount)
}
複製代碼

運行測試經過。

重構

把處理 post 和 get 請求的業務邏輯封裝到單獨的函數。

func (u *UserServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		u.showAPICallCount(w, r)
	case http.MethodPost:
		u.processAPICall(w, r)
	}
}

func (u *UserServer) showAPICallCount(w http.ResponseWriter, r *http.Request) {
	user := r.URL.Path[len("/users/"):]
	apiCallCount := u.store.GetUserAPICallCount(user)
	if apiCallCount == 0 {
		w.WriteHeader(http.StatusNotFound)
	}
	fmt.Fprint(w, apiCallCount)
}

func (u *UserServer) processAPICall(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusAccepted)
}
複製代碼

運行測試經過。

先寫測試

驗證當使用 POST 方法時,UserStore 是否被調用記錄 API 請求

給咱們以前實現的 StubUserStore 添加 RecordAPICall 函數,記錄並驗證函數的調用。

type StubUserStore struct {
	apiCallCounts map[string]int
	apiCalls      []string
}

func (s *StubUserStore) GetUserAPICallCount(user string) int {
	return s.apiCallCounts[user]
}

func (s *StubUserStore) RecordAPICall(user string) {
	s.apiCalls = append(s.apiCalls, user)
}
複製代碼

添加測試驗證調用

func TestStoreAPICalls(t *testing.T) {
	store := StubUserStore{
		map[string]int{},
	}
	server := &UserServer{&store}

	t.Run("record api call when POST", func(t *testing.T) {
		request, _ := http.NewRequest(http.MethodPost, "/users/will", nil)
		response := httptest.NewRecorder()

		server.ServeHTTP(response, request)
		assertStatus(t, response.Code, http.StatusAccepted)

		if len(store.apiCalls) != 1 {
			t.Errorf("got %d calls to RecordAPICall want %d", len(store.apiCalls), 1)
		}
	})
}
複製代碼

運行測試,你會獲得以下的錯誤

user_test.go:63:17: too few values in StubUserStore literal
複製代碼
編寫最少的代碼讓測試能運行並檢查失敗的測試輸出
func TestStoreAPICalls(t *testing.T) {
	store := StubUserStore{
		map[string]int{},
		nil,
	}
	server := &UserServer{&store}

	t.Run("record api call when POST", func(t *testing.T) {
		request, _ := http.NewRequest(http.MethodPost, "/users/will", nil)
		response := httptest.NewRecorder()

		server.ServeHTTP(response, request)
		assertStatus(t, response.Code, http.StatusAccepted)

		if len(store.apiCalls) != 1 {
			t.Errorf("got %d calls to RecordAPICall want %d", len(store.apiCalls), 1)
		}
	})
}
複製代碼

運行測試,你會獲得以下的錯誤

user_test.go:76: got 0 calls to RecordAPICall want 1
複製代碼
編寫足夠的代碼讓測試經過

給 UserStore 添加相應的函數

type UserStore interface {
	GetUserAPICallCount(user string) int
	RecordAPICall(user string)
}
複製代碼

因爲編譯器報錯,我須要 InMemoryUserStore 實現相應的函數

func (i *InMemoryUserStore) RecordAPICall(user string) {}
複製代碼

編寫代碼調用 RecordAPICall

func (u *UserServer) processAPICall(w http.ResponseWriter, r *http.Request) {
	u.store.RecordAPICall("bob")
	w.WriteHeader(http.StatusAccepted)
}
複製代碼

運行測試,測試經過。

先寫測試

驗證 API 調用的用戶記錄

func TestStoreAPICalls(t *testing.T) {
	store := StubUserStore{
		map[string]int{},
		nil,
	}
	server := &UserServer{&store}

	t.Run("record api call when POST", func(t *testing.T) {
		user := "will"
		request, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/users/%s", user), nil)
		response := httptest.NewRecorder()

		server.ServeHTTP(response, request)
		assertStatus(t, response.Code, http.StatusAccepted)

		if len(store.apiCalls) != 1 {
			t.Errorf("got %d calls to RecordAPICall want %d", len(store.apiCalls), 1)
		}

		if store.apiCalls[0] != user {
			t.Errorf("did not record correct api call user got %q want %q", store.apiCalls[0], user)
		}
	})
}
複製代碼

運行測試,你會獲得以下的錯誤

user_test.go:81: did not record correct api call user got "bob" want "will"
複製代碼
編寫足夠的代碼讓測試經過
func (u *UserServer) processAPICall(w http.ResponseWriter, r *http.Request) {
	user := r.URL.Path[len("/users/"):]
	u.store.RecordAPICall(user)
	w.WriteHeader(http.StatusAccepted)
}
複製代碼

運行測試經過。

重構

從請求中獲取 user 的代碼重複,提取到調用方,以參數形式傳遞。

func (u *UserServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user := r.URL.Path[len("/users/"):]
	switch r.Method {
	case http.MethodGet:
		u.showAPICallCount(w, user)
	case http.MethodPost:
		u.processAPICall(w, user)
	}
}

func (u *UserServer) showAPICallCount(w http.ResponseWriter, user string) {
	apiCallCount := u.store.GetUserAPICallCount(user)
	if apiCallCount == 0 {
		w.WriteHeader(http.StatusNotFound)
	}
	fmt.Fprint(w, apiCallCount)
}

func (u *UserServer) processAPICall(w http.ResponseWriter, user string) {
	u.store.RecordAPICall(user)
	w.WriteHeader(http.StatusAccepted)
}
複製代碼

運行測試,測試經過,重構完成。

集成測試

兩個功能已經分別開發完成,咱們如今進行集成測試,因爲集成測試不容易寫,出錯後不易查找,而且因爲可能會使用真實的組件如數據庫,因此可能會運行緩慢。所以集成測試應該儘可能少寫。

先寫測試
func TestRecordAPICallsAndGetThem(t *testing.T) {
	store := InMemoryUserStore{}
	server := UserServer{&store}
	user := "will"

	request, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/users/%s", user), nil)
	server.ServeHTTP(httptest.NewRecorder(), request)
	server.ServeHTTP(httptest.NewRecorder(), request)
	server.ServeHTTP(httptest.NewRecorder(), request)

	response := httptest.NewRecorder()
	request = newGetUserAPICallCountRequest(user)
	server.ServeHTTP(response, request)

	assertStatus(t, response.Code, http.StatusOK)
	assertCount(t, response.Body.String(), "3")
}
複製代碼

運行測試,你會獲得以下 的錯誤

server_integration_test.go:25: got "666", want "3"
複製代碼
編寫足夠的代碼讓測試經過

爲 InMemoryUserStore 編寫具體實現

type InMemoryUserStore struct {
	store map[string]int
}

func (i *InMemoryUserStore) GetUserAPICallCount(user string) int {
	return i.store[user]
}

func (i *InMemoryUserStore) RecordAPICall(user string) {
	i.store[user]++
}

func NewInMemoryUserStore() *InMemoryUserStore {
	return &InMemoryUserStore{
		store: make(map[string]int),
	}
}
複製代碼

集成測試使用 InMemoryUserStore

func TestRecordAPICallsAndGetThem(t *testing.T) {
	store := NewInMemoryUserStore()
	server := UserServer{store}
	user := "will"

	request, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/users/%s", user), nil)
	server.ServeHTTP(httptest.NewRecorder(), request)
	server.ServeHTTP(httptest.NewRecorder(), request)
	server.ServeHTTP(httptest.NewRecorder(), request)

	response := httptest.NewRecorder()
	request = newGetUserAPICallCountRequest(user)
	server.ServeHTTP(response, request)

	assertStatus(t, response.Code, http.StatusOK)
	assertCount(t, response.Body.String(), "3")
}
複製代碼

再次運行測試,測試經過。

完善主程序

修改主程序使用 NewInMemoryUserStore 函數。

func main() {
	store := NewInMemoryUserStore()
	server := &UserServer{store}

	if err := http.ListenAndServe(":5000", server); err != nil {
		log.Fatalf("could not listen on port 5000 %v", err)
	}
}
複製代碼

到此一個使用內存來記錄查詢用戶 API 調用次數的程序已經完成,後續步驟你可選擇其餘數據存儲來替換內存存儲進行數據的持久化。只須要實現 UserStore 接口便可。

TDD 總結

當你學習了 TDD 以後,你就學會了這種小步快跑的開發方法,你能夠把它應用在你沒有太大自信的關鍵核心組件的開發中,TDD 能幫助你以小步快跑的方式向目標前進,TDD 只是給了你一種小步快跑的能力,你能夠只在關鍵的時候才使用這種能力。學習 TDD 並非爲了讓你在全部涉及到編碼的地方所有使用 TDD 開發模式。

TDD 的關鍵在於驅動(driven),要讓測試驅動咱們來進行功能開發,每寫一個測試,都驅動咱們寫更多的生產代碼,都在向實現咱們的功能的方向前進。

重構是 TDD 中重要的環節,若是沒有重構,你獲得的可能只是由一堆零亂代碼組合的勉強湊合工做的軟件。只有注重重構才能讓咱們的代碼更整潔,更利於後續 TDD 開發模式的正常執行。

TDD 開發模式減輕人開發人員的心智負擔,經過紅、綠、重構循環,開發人員每個階段都只有一個特定的目標,這使得開發人員每一個階段的關注點只有一個,注意力集中。

TDD 開發模式能讓開發人員更自信,因爲咱們的任務分解的小,開發循環比較短,咱們能夠在很短期內得到測試的反饋,咱們幾乎隨時都有可運行的軟件,這給咱們開發人員帶來很強的安全感,這給了咱們自信心。

TDD 不是銀彈,不是全部項目開發均可以使用 TDD 開發模式來進行開發,在測試成本比較高的狀況下就不太適合使用 TDD 開發模式,好比在前端(Web、iOS、Android)的項目開發中,檢查頁面中的元素的位置及大小等操做比較麻煩,就不太適合使用 TDD 開發模式,可是咱們能夠儘可能減小 UI 部分的業務邏輯,UI 只根據其餘模塊處理後的數據來作簡單直接的展現,把 TDD 應用在其餘爲 UI 提供數據的模塊開發中。

TDD 並不是要求咱們很是嚴格的遵循 TDD 三定律,咱們能夠根據特殊狀況,作適當的小調整,可是總體流程與節奏不能有偏離,TDD 三定律並非爲了給你加上了沒法掙脫的枷鎖,它只是給了咱們一個總體指導原則。

要想流暢的使用 TDD 須要不斷的練習,掌握 TDD 的節奏是流暢使用 TDD 關鍵。想要真正學會使用 TDD ,只能練習、練習、再練習。

後續學習

參考文檔

相關文章
相關標籤/搜索