1. 程式人生 > 程式設計 >你聽說過測試驅動開發嗎?

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

TDD 是什麼

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

TDD 是由多個非常短的開發迴圈週期組成,一個 TDD 開發迴圈包括如下的3個步驟:

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

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

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

TDD 的優點

TDD 的優點有很多,下面列出幾點我認為比較重要的優點:

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

TDD 開發迴圈

一個完整的 TDD 開發迴圈如下圖所示:

  1. 編寫測試,測試應該儘量小。執行測試,測試會失敗,編譯不通過也是一種失敗,如果測試沒有失敗,這表明這個測試沒有任何意義,因為這個測試既沒有幫助我們實現需求,也沒有幫助我們修復 bug 完善程式碼。這可能是如下的原因導致的:
    • 我們在上一次 TDD 迴圈中,生產程式碼編寫的太多,已經把這次的測試需要測試的功能實現了。
    • 我們在之前的測試中忽略了這一次測試中應該測試的部分。
  2. 編寫最少的程式碼讓測試通過。為了儘量脫離測試無法通過的狀態中,此步驟中可以使用特殊的方法,比如使用偽實現直接返回常量結果值,然後在重構階段逐漸替換常量為真正的實現。
  3. 重構程式碼,減少程式碼中的重複程式碼,清除程式碼中的壞味道。清除生產程式碼與測試間的重複設計。這一步驟非常的重要,沒有這一步驟的 TDD 開發是沒有靈魂的 TDD 開發模式,並且可能導致你得到一個比不使用 TDD 開發模式開發出來的還要糟糕的軟體。
  4. 重複上述步驟。

TDD 開發原則

TDD 三定律

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

TDD 開發策略

  1. 偽實現,直接返回常量,並在重構階段使用變數逐漸替換常量。
  2. 明顯實現,由於程式碼邏輯簡單,可以直接寫出程式碼實現。
  3. 三角法,通過新增測試使用其失敗,逐漸驅動我們朝目標前進。

根據錯誤的情況,偽實現和明顯實現可以交替進行,當開發進行順暢時,可以使用明顯實現,當開發過程中經常碰到錯誤時,可以使用偽實現,慢慢找回自信,然後再使用明顯實現進行開發。當完全沒有實現思路或者實現思路不清晰時, 可以使用三角法來驅動我們開發,逐漸理清思路。

TDD 的難點

  • 任務分解到底需要多細?我們需要把功能分解成多小的任務才合適呢?然後把測試分解多小才合適呢?這是一個比較難的問題,沒有人能確切給出答案,一切都需要你自己去體會,去練習,去不斷的嘗試,去學習,去積累經驗。
  • 到底要測試什麼?如果我們測試寫的不好,很容易造成測試程式碼需要跟著生產程式碼被頻繁的修改,這樣測試不僅沒有給我們的程式碼帶來好處,反而給我們的重構帶來很多的額外的負擔。關於要測試什麼,有一句正確但卻無法給你具體建議名言:“測試行為,不要測試實現”,這也是需要長時間的去學習,去練習,去體會的。簡單來說你應該測試所有公開給別人使用的介面,類,函式等,而內部私有的你可以選擇性的測試,具體的關於應該如何寫測試,可以觀看如下的關於如何測試的公開演講視訊:

TDD 開發示例

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

  • 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",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,"/users/mgxian",nil)
	response := httptest.NewRecorder()
	UserServer(response,request)
	got := response.Code
	want := http.StatusOK
	if got != want {
		t.Errorf("got %d,want)
	}

	gotCount := response.Body.String()
	wantCount := "8"
	if gotCount != wantCount {
		t.Errorf("got % q,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,"8")
	})
}

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

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

func assertCount(t *testing.T,want string) {
	t.Helper()
	if got != want {
		t.Errorf("got % q,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,func(t *testing.T) {
		user := "mgxian"
		request := newGetUserAPICallCountRequest(user)
		response := httptest.NewRecorder()
		server.ServeHTTP(response,"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","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",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,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",_ := http.NewRequest(http.MethodPost,nil)
		response := httptest.NewRecorder()

		server.ServeHTTP(response,request)
		assertStatus(t,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,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",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,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{},func(t *testing.T) {
		user := "will"
		request,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,user string) {
	u.store.RecordAPICall(user)
	w.WriteHeader(http.StatusAccepted)
}
複製程式碼

執行測試,測試通過,重構完成。

整合測試

兩個功能已經分別開發完成,我們現在進行整合測試,由於整合測試不容易寫,出錯後不易查詢,並且由於可能會使用真實的元件如資料庫,所以可能會執行緩慢。因此整合測試應該儘量少寫。

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

	request,nil)
	server.ServeHTTP(httptest.NewRecorder(),request)
	server.ServeHTTP(httptest.NewRecorder(),request)

	response := httptest.NewRecorder()
	request = newGetUserAPICallCountRequest(user)
	server.ServeHTTP(response,http.StatusOK)
	assertCount(t,"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,"3")
}
複製程式碼

再次執行測試,測試通過。

完善主程式

修改主程式使用 NewInMemoryUserStore 函式。

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

	if err := http.ListenAndServe(":5000",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 ,只能練習、練習、再練習。

後續學習

參考檔案