1 回答

TA貢獻1826條經驗 獲得超6個贊
在繼續測試之前,你的代碼有一個嚴重的流程,如果你在未來的編程任務中不關心它,這將成為一個問題。
https://pkg.go.dev/net/http請參閱第二個示例
客戶端必須在完成響應正文后將其關閉
現在讓我們解決這個問題(我們稍后將不得不回到這個主題),兩種可能性。
1/ 在 中,使用延遲在耗盡該資源后將其關閉;main
func main() {
resp := sendRequest()
defer body.Close()
body := readBody(resp)
id := unmarshallData(body)
fmt.Println(id)
}
2/ 在readBody
func readBody(resp *http.Response) []byte {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}
return body
}
使用延遲是關閉資源的預期方式。它有助于讀者識別資源的生存期并提高可讀性。
注意:我不會使用太多的表測試驅動模式,但你應該,就像你在你的OP中所做的那樣。
轉到測試部分。
測試可以寫在同一個包或其同一版本下,并帶有尾隨,例如 。這在兩個方面具有影響。_test
[package target]_test
使用單獨的包,它們將在最終版本中被忽略。這將有助于生成更小的二進制文件。
使用單獨的包,以黑盒方式測試API,只能訪問它顯式公開的標識符。
您當前的測試是白盒的,這意味著您可以訪問 的任何聲明,無論是否公開。main
關于 ,圍繞這一點編寫測試并不是很有趣,因為它做得太少,并且您的測試不應該編寫來測試std庫。sendRequest
但是,為了演示,并且出于充分的理由,我們可能不希望依賴外部資源來執行我們的測試。
為了實現這一點,我們必須使其中消耗的全局依賴項成為注入的依賴項。因此,以后可以替換它所依賴的一個反應物,即http。獲取方法。
func sendRequest(client interface{Get() (*http.Response, error)}) *http.Response {
resp, err := client.Get("https://gorest.co.in/public/v1/users/1841")
if err != nil {
log.Fatalln(err)
}
return resp
}
在這里,我使用內聯接口聲明。interface{Get() (*http.Response, error)}
現在,我們可以添加一個新的測試,該測試注入一段代碼,該代碼段將準確返回將觸發我們要在代碼中測試的行為的值。
type fakeGetter struct {
resp *http.Response
err error
}
func (f fakeGetter) Get(u string) (*http.Response, error) {
return f.resp, f.err
}
func TestSendRequestReturnsNilResponseOnError(t *testing.T) {
c := fakeGetter{
err: fmt.Errorf("whatever error will do"),
}
resp := sendRequest(c)
if resp != nil {
t.Fatal("it should return a nil response when an error arises")
}
}
現在運行此測試并查看結果。它不是決定性的,因為您的函數包含對 log 的調用。致命,它依次執行操作系統。退出;我們無法對此進行檢驗。
如果我們試圖改變這一點,我們可能會認為我們可能會呼吁恐慌,因為我們可以恢復。
我不建議這樣做,在我看來,這是臭的和壞的,但它存在,所以我們可能會考慮。這也是對函數簽名的最小可能更改。返回錯誤會破壞更多的當前簽名。我想在那個演示中盡量減少這一點。但是,根據經驗,請返回錯誤并始終檢查它們。
在函數中,將此調用替換為 并更新測試以捕獲死機。sendRequestlog.Fatalln(err)panic(err)
func TestSendRequestReturnsNilResponseOnError(t *testing.T) {
var hasPanicked bool
defer func() {
_ = recover() // if you capture the output value or recover, you get the error gave to the panic call. We have no use of it.
hasPanicked = true
}()
c := fakeGetter{
err: fmt.Errorf("whatever error will do"),
}
resp := sendRequest(c)
if resp != nil {
t.Fatal("it should return a nil response when an error arises")
}
if !hasPanicked {
t.Fatal("it should have panicked")
}
}
現在,我們可以轉到另一個執行路徑,即非錯誤返回。
為此,我們鍛造了所需的*http。我們想要傳遞到函數中的響應實例,然后我們將檢查其屬性,以確定函數執行的操作是否與我們預期的內聯。
我們將考慮要確保返回時未修改: /
下面的測試只設置兩個屬性,我將使用它來演示如何使用 NopCloser 和字符串設置 Body。新閱讀器,因為以后使用Go語言時經常需要它;
我也使用反射。DeepEqual作為蠻力相等檢查器,通常你可以更細粒度,得到更好的測試。 在這種情況下,它做了這項工作,但它引入了復雜性,不能證明系統地使用它是合理的。DeepEqual
func TestSendRequestReturnsUnmodifiedResponse(t *testing.T) {
c := fakeGetter{
err: nil,
resp: &http.Response{
Status: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader("some text")),
},
}
resp := sendRequest(c)
if !reflect.DeepEqual(resp, c.resp) {
t.Fatal("the response should not have been modified")
}
}
在這一點上,你可能已經認為這個小功能不好,如果你不這樣做,我向你保證它不是。它做的太少,它只是包裝了方法,它的測試對業務邏輯的生存沒有多大興趣。sendRequest
http.Get
繼續運作。readBody
所有申請的評論也適用于此。sendRequest
它做得太少
它是
os.Exit
有一件事不適用。由于對 的調用不依賴于外部資源,因此嘗試注入該依賴項毫無意義。我們可以四處測試。ioutil.ReadAll
但是,為了演示,現在是時候討論缺少的呼叫了。defer resp.Body.Close()
讓我們假設我們去介紹中提出的第二個命題并對此進行測試。
該結構充分地將其接收方公開為接口。http.Response
Body
為了確保代碼調用'關閉,我們可以為它編寫一個存根。該存根將記錄是否進行了該調用,然后測試可以檢查該調用,如果不是,則觸發錯誤。
type closeCallRecorder struct {
hasClosed bool
}
func (c *closeCallRecorder) Close() error {
c.hasClosed = true
return nil
}
func (c *closeCallRecorder) Read(p []byte) (int, error) {
return 0, nil
}
func TestReadBodyCallsClose(t *testing.T) {
body := &closeCallRecorder{}
res := &http.Response{
Body: body,
}
_ = readBody(res)
if !body.hasClosed {
t.Fatal("the response body was not closed")
}
}
同樣,為了便于演示,我們可能希望測試該函數是否調用了 。Read
type readCallRecorder struct {
hasRead bool
}
func (c *readCallRecorder) Read(p []byte) (int, error) {
c.hasRead = true
return 0, nil
}
func TestReadBodyHasReadAnything(t *testing.T) {
body := &readCallRecorder{}
res := &http.Response{
Body: ioutil.NopCloser(body),
}
_ = readBody(res)
if !body.hasRead {
t.Fatal("the response body was not read")
}
}
我們還驗證了身體之間沒有修改,
func TestReadBodyDidNotModifyTheResponse(t *testing.T) {
want := "this"
res := &http.Response{
Body: ioutil.NopCloser(strings.NewReader(want)),
}
resp := readBody(res)
if got := string(resp); want != got {
t.Fatal("invalid response, wanted=%q got %q", want, got)
}
}
我們差不多完成了,讓我們把一個移到函數上。unmarshallData
你已經寫了一個關于它的測試。不過,這是可以的,我會這樣寫,以使其更精簡:
type UserData struct {
Meta interface{} `json:"meta"`
Data Data `json:"data"`
}
type Data struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Gender string `json:"gender"`
Status string `json:"status"`
}
func Test_unmarshallData(t *testing.T) {
type args struct {
body []byte
}
tests := []UserData{
UserData{Data: Data{ID: 1841}},
}
for _, u := range tests {
want := u.ID
b, _ := json.Marshal(u)
t.Run("Unmarshal", func(t *testing.T) {
if got := unmarshallData(b); got != want {
t.Errorf("unmarshallData() = %v, want %v", got, want)
}
})
}
}
然后,通常適用:
不記錄。致命
你在測試什么?編組者 ?
最后,現在我們已經收集了所有這些部分,我們可以重構以編寫更合理的函數,并重新使用所有這些部分來幫助我們測試此類代碼。
我不會這樣做,但這是一個啟動器,它仍然令人恐慌,我仍然不推薦,但是前面的演示已經顯示了測試返回錯誤的版本所需的一切。
type userFetcher struct {
Requester interface {
Get(u string) (*http.Response, error)
}
}
func (u userFetcher) Fetch() int {
resp, err := u.Requester.Get("https://gorest.co.in/public/v1/users/1841") // it does not really matter that this string is static, using the requester we can mock the response, its body and the error.
if err != nil {
panic(err)
}
defer resp.Body.Close() //always.
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var userData UserData
err = json.Unmarshal(body, &userData)
if err != nil {
panic(err)
}
return userData.Data.ID
}
- 1 回答
- 0 關注
- 99 瀏覽
添加回答
舉報