Cách viết unit test cho REST API trong Golang
I. Mô tả bài toán
Giả sử ta đã có 1 REST API và API này có các endpoint là GetGetEntries
, GetEntryByID
, CreateEntry
, UpdateEntry
và DeleteEntry
. Tương ứng với chúng là các đường dẫn như bên dưới:
GetEntries -> "/entries" -> Method GET
GetEntryByID -> "/entry?id=1234" -> Method GET
CreateEntry -> "/entry" -> Method POST
UpdateEntry -> "/entry" -> Method PUT
DeleteEntry -> "/entry" -> Method DELETE
Những endpoint này sẽ giúp ta tương tác với 1 struct có tên là entry
type entry struct {
ID int `json:"id,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
EmailAddress string `json:"email_address,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
}
Vậy thì trong trường hợp sử dụng ngôn ngữ Golang, làm thế nào để biết Unit test cho API này ?
II. Giải quyết bài toán
Để giải quyết bài toán này ta sẽ sử dụng 2 package sau:
testing
: là 1 built-in package được dùng để implement và run test. Ta sẽ sử dụng câu lệnhgo test
kết hợp với 1 số tham số tùy chọn để chạy test.net/http/httptest
: cũng là 1 built-in package khác. Dùng để test những thứ liên quan đến HTTP.
File test có thể cùng hoặc khác package với file cần test. Tiếp theo là quy tắc đặt tên hết sức quan trọng. Chỉ khi ta đặt đúng theo quy tắc thì thì package testing
mới hiểu được đâu là file unit test mà nó cần phải test:
- Tên file test phải kết thúc bằng
_test
. Ví dụ ta cần test fileendpoints.go
thì file test của chúng ta sẽ tên làendpoints_test.go
- Tên của phương thức test phải bắt đầu bằng
Test
. Ví dụ ta cần test phương thứcGetEntries
thì phương thức test của chúng ta sẽ tên làTestGetEntries
package example
import (
"test"
"net/http/httptest"
)
func TestGetEntries(t *testing.T) {
}
func TestGetEntryByID(t *testing.T) {
}
func TestGetEntryByIDNotFound(t *testing.T) {
}
func TestCreateEntry(t *testing.T) {
}
func TestEditEntry(t *testing.T) {
}
func TestDeleteEntry(t *testing.T) {
}
Phương thức test trong Golang sẽ nhận vào tham số và có thể sẽ sử dụng 1 số phương thức như bên dưới:
- Tham số đầu tiên và duy nhất phải là
t *testing.T
t.Log
có thể được dùng nếu ta có nhu cầu in ra thông tin, sau đó tiếp tục chạy tiếpt.Fail
được dùng để đánh dấu là hàm test đã bị fail nhưng vẫn tiếp tục chạy tiếp.t.Error
là cách viết gọn, nó tương đương với việc gọit.Log
, sau đó gọi thêmt.Fail
. Nếu hàm này được dùng khi ta muốn chạy tiếp để debug thêm thông tin.t.Errorf
cũng tương tự nhưt.Error
nhưng được hỗ trợ thêm fomart khi log thông tin.t.Fatal
được dùng khi log thông tin ra, đánh dấu là test đã bị fail và dừng luôn tại đó vì nếu có chạy tiếp thì không có gì đáng quan tâm cả.
1. Testcase cho GetEntries
func TestGetEntries(t *testing.T) {
req, err := http.NewRequest("GET", "/entries", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(GetEntries)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := `[{"id":1,"first_name":"Krish","last_name":"Bhanushali","email_address":"krishsb@g.com","phone_number":"0987654321"},{"id":2,"first_name":"xyz","last_name":"pqr","email_address":"xyz@pqr.com","phone_number":"1234567890"},{"id":6,"first_name":"FirstNameSample","last_name":"LastNameSample","email_address":"lr@gmail.com","phone_number":"1111111111"}]`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
- Khởi tạo 1 request mới sử dụng phương thức GET và đường dẫn là
/entries
- Kiểm tra xem việc đó lỗi xảy ra hay không? Nếu có thì gọi
t.Fatal
vì ngay cả việc new request còn bị lỗi thì những thông tin phía sau không có gì đáng quan tâm. - Gọi phương thức cần test. Lúc này sẽ có thể phát sinh lỗi do nhiều nguyên nhân khác nhau và ta quan tâm nguyên nhân đó là gì nên sử dụng
Errorf
. - So sánh status thực tế nhận được và status mong muốn. Nếu khác nhau thì gọi
Errorf
- So sánh response thực tế và response mong muốn. Nếu khác nhau thì gọi
Errorf
2. Testcase cho GetEntryByID
func TestGetEntryByID(t *testing.T) {
req, err := http.NewRequest("GET", "/entry", nil)
if err != nil {
t.Fatal(err)
}
q := req.URL.Query()
q.Add("id", "1")
req.URL.RawQuery = q.Encode()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(GetEntryByID)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := `{"id":1,"first_name":"Krish","last_name":"Bhanushali","email_address":"krishsb2405@gmail.com","phone_number":"0987654321"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
2. Testcase cho GetEntryByIDNotFound
func TestGetEntryByIDNotFound(t *testing.T) {
req, err := http.NewRequest("GET", "/entry", nil)
if err != nil {
t.Fatal(err)
}
q := req.URL.Query()
q.Add("id", "123")
req.URL.RawQuery = q.Encode()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(GetEntryByID)
handler.ServeHTTP(rr, req)
if status := rr.Code; status == http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusBadRequest)
}
}
3. Testcase cho CreateEntry
func TestCreateEntry(t *testing.T) {
var jsonStr = []byte(`{"id":4,"first_name":"xyz","last_name":"pqr","email_address":"xyz@pqr.com","phone_number":"1234567890"}`)
req, err := http.NewRequest("POST", "/entry", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(CreateEntry)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := `{"id":4,"first_name":"xyz","last_name":"pqr","email_address":"xyz@pqr.com","phone_number":"1234567890"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
jsonStr
là data mà ta cần post đi.- Khởi tạo 1 request mới để post đi dữ liệu là
jsonStr
. Nếu có lỗi gọiFatal
- Sau đó sau khi thực hiện request đó. Tương tự, từ giờ có nhiều nguyên nhân gây ra lỗi khác nhau và ta quan tâm là có thể gặp phải những lỗi gì nên sẽ dùng
Errorf
. - Nếu status khác với mong đợi. In ra lỗi bằng
Errorf
. - Nếu body khác với mong đợi. In ra lỗi bằng
Errorf
.
4. Testcase cho EditEntry
func TestEditEntry(t *testing.T) {
var jsonStr = []byte(`{"id":4,"first_name":"xyz change","last_name":"pqr","email_address":"xyz@pqr.com","phone_number":"1234567890"}`)
req, err := http.NewRequest("PUT", "/entry", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(UpdateEntry)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := `{"id":4,"first_name":"xyz change","last_name":"pqr","email_address":"xyz@pqr.com","phone_number":"1234567890"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
Logic test tương tự như CreateEntry nhưng lần này phương thức là PUT
5. Testcase cho DeleteEntry
func TestDeleteEntry(t *testing.T) {
req, err := http.NewRequest("DELETE", "/entry", nil)
if err != nil {
t.Fatal(err)
}
q := req.URL.Query()
q.Add("id", "4")
req.URL.RawQuery = q.Encode()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(DeleteEntry)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := `{"id":4,"first_name":"xyz change","last_name":"pqr","email_address":"xyz@pqr.com","phone_number":"1234567890"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
Logic test tương tự như GetEntryByID nhưng lần này method là DELETE
6. Thực thi các testcase vừa viết
Ta thực hiện lệnh go test -v
để chạy toàn bộ các testcase.
Hoặc sử dụng lệnh go test -v -run <Test Function Name>
để chạy 1 testcase cụ thể
III. Nguồn tham khảo
https://golang.org/pkg/testing/
https://golang.org/pkg/net/http/httptest/
https://blog.alexellis.io/golang-writing-unit-tests/
https://codeburst.io/unit-testing-for-rest-apis-in-go-86c70dada52d