Viết Unit Test trong project với Golang
Bài đăng này đã không được cập nhật trong 4 năm
Ở các phần trước, chúng ta đã hoàn thành các chức năng cơ bản của 1 webserver golang như Login, Register, CRUD. Mục đích bài viết nhằm giới thiệu một cách cơ bản cách triển khai, viết unit test với Golang. Cách viết các testcase phức tạp, đòi hỏi nhiều công phu hơn sẽ chưa được giới thiệu trong bài viết này. Mong là sẽ sớm được giới thiệu với các bạn trong một bài viết không xa.
1. Giới thiệu
Go cung cấp 2 thư viện tiêu chuẩn dùng để viết test:
- testing: Cung cấp các chức năng kiểm thử tự động cơ bản
- httptest: Cung cấp các chức năng để kiểm thử http
Ngoài ra còn khá nhiều thư viện, framework test do bên thứ 3 phát triển như gocheck, ginkgo, v.v
Các file test khi dùng package testing
có phần đuôi ở tên là _test.go
. Ví dụ ta có file hello.go
thì file test sẽ tên là hello_test.go
Thêm nữa, tên hàm test sẽ có dạng
// Xxx không được bắt đầu bằng chữ thường
func TestXxx(*testing.T)
Hàm Test truyền vào 1 tham số kiểu con trỏ kiểu T
của thư viện testing. kiểu T
chứa các phương thức hỗ trợ việc quản lý trạng thái của testcase cũng như hỗ trợ việc format logs in ra của testcase thông qua các phương thức Error
, Errorf
, Fail
, Log
, v.v. Chi tiết tất cả phương thức của kiểu T
và cách sử dụng các bạn có thể xem chi tiết ở đây.
type T struct {
common
isParallel bool
context *testContext // For running tests and subtests.
}
2. Viết testcase với thư viện testing
Chúng ta có ví dụ đơn giản như sau:
// hello.go
package main
import "fmt"
func hello(name string) string {
if name == "" {
return fmt.Sprintf("What is your name ?")
} else {
return fmt.Sprintf("Hello %s", name)
}
}
// hello_test.go
package main
import "testing"
func TestHello(t *testing.T) {
emptyNameResult := hello("")
if emptyNameResult != "What is your name ?" {
t.Errorf("Output expect What is your name ? instead of %v", emptyNameResult)
}
result := hello("Gopher")
if result != "Hello Gopher" {
t.Errorf("Output expect Hello Gopher instead of %v", result)
}
}
- Hàm
hello
thực hiện chức năng cơ bản là trả vềHello
+tham số name
truyền vào. Nếuname
rỗng thì trả về chuỗiWhat is your name ?
. - Hàm
TestHello
kiểm tra 2 khả năng đầu ra của hàmhello
. Nếu giá trị trả về khác với đầu ra kỳ vọng thì test sẽ failed. - Phương thức
t.Errorf
cũng nhưt.Error
có chức năng đánh dấu các testfailed
nếu đầu ra không đúng kỳ vọng. Sau khi thực thi xong, code trong hàmTestHello
vẫn tiếp tục được thực thi.t.Errorf
kháct.Error
giống nhưfmt.Printlnf
khácfmt.Println
.
Chúng ta chạy thử test, test case pass vì giá trị trả về khớp với kỳ vọng.
Chúng ta thử sửa hàm hello
một chút và giữ nguyên hàm TestHello
func hello(name string) string {
if name == "" {
return fmt.Sprintf("You do not have name !")
} else {
return fmt.Sprintf("Hello %s", name)
}
}
Run test case:
Output thực tế là You do not have name !
thay vì What is your name ?
như kỳ vọng.
Kiểm tra test coverage
coverage 100%, tuyệt vời
3. Viết testcase với thư viện httptest
// server.go
package main
import (
"fmt"
_ "encoding/json"
"net/http"
)
func welcome(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, "Welcome to our website")
return
}
func main() {
http.HandleFunc("/", welcome)
http.ListenAndServe(":3000", nil)
}
// server_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestWelcome(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/", welcome)
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
}
- Hàm
Welcome
đơn giản thực hiện trả vềclient
thông điệpWelcome to our website
khi người dùng truy cập vàohttp//localhost:3000
. - Hàm
TestWelcome
sẽ cần tạo một webserver riêng (với multiplexer mặc định của thư viện net/http). Kết quả status code trả về được kiểm tra, nếu khác200
thì test sẽ failed.
3. Viết testcase bằng thư viện của bên thứ 3
Các thư viện được hỗ trợ chính thức bởi Go Team như testing
hay httptesting
đơn giản, dễ học, dễ dùng. Tuy nhiên, với các ứng dụng trung bình đến lớn, lượng testcase cần viết nhiều hay sử dụng các phương pháp viết test tối tân hơn như double test thì chúng lại cho thấy sự hạn chế nhất định. Đó là lúc cần đến những công cụ mạnh mẽ hơn từ bên thứ 3.
Gocheck
Cài đặt:
go get -u gopkg.in/check.v1
// person_test.go
package demo_test
import (
"testing"
. "gopkg.in/check.v1"
)
// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }
type Person struct{
name string
age uint64
country string
}
func (p *Person) IsAdult() bool {
if p.age < 18 {
return false
}
return true
}
func (p *Person) IsVietNamese() bool {
if p.country == "VietNam" || p.country == "VN" {
return true
}
return false
}
var _ = Suite(&Person{})
func (s *Person) TestIsAdult(c *C) {
conglt := Person{"conglt", 20, "VietNam"}
c.Assert(conglt.IsAdult(), Equals, true)
}
func (s *Person) TestIsVietNamese(c *C) {
conglt := Person{"conglt", 20, "DaiViet"}
c.Assert(conglt.IsVietNamese(), Equals, false)
}
func Suite(suite interface{}) interface{}
- Hàm
Suite
có nhiệm vụ đăng ký đối tượng cần kiểm thử, ở đoạn code trên ngụ ý rằng ta cần kiểm thử các phương thức của structPerson
. Bất kỳ phương thức nào bắt đầu bằng tiền tố Test sẽ được coi là phương thức kiểm thử. - Hàm
func TestingT(testingT *testing.T)
sẽ có nhiệm vụ chạy các testcase ứng với đối tượng được đăng ký trong hàmSuite
. - Giả sử ta tạo mới một struct là
Personal
và thay thế câu lệnhvar _ = Suite(&Person{})
bằngvar _ = Suite(&Personal{})
Chạy go test lên:
Như chúng ta thấy, không có testcase nào được chạy do chúng ta chẳng viết bất cứ hàm nào để test Personal
cả. Đổi lại như cũ và chạy go test
, ta sẽ có 2 testcase của Person
như sau.
Ginkgo
Ginkgo là một framework hỗ trợ việc kiểm thử trong Go. So với Gocheck, Ginkgo mạnh mẽ, cũng như hỗ trợ nhiều tính năng hơn. Nếu các bạn đã từng viết test cho Javascript thì hẳn sẽ thấy quen thuộc khi làm quen với Ginkgo.
Cài đặt:
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...
Đi kèm với Ginko, chúng ta cài đặt thêm Gomega là một matcher/assertion library, tương tự như thư viện Chai bên Javascript. Nó giúp kiểm tra các giá trị trả về có đúng với kỳ vọng hay không ?
Demo
Ở ví dụ này chúng ta định nghĩa struct
Book gồm có 3 thông tin và method CategoryByLength
để phân loại sách theo số trang. Lớn hơn bằng 300 trang thì thuộc dạng NOVEL
, bé hơn 300 trang thì trả về SHORT STORY
.
mkdir ginkgo
cd ginkgo
go mod int github.com/conglt10/test-ginkgo # Tên package bạn có thể đặt tên bất kỳ
// book.go
package book
type Book struct {
Title string
Author string
Pages uint64
}
func (b *Book) CategoryByLength() string {
if (b.Pages >= 300) {
return "NOVEL"
} else {
return "SHORT STORY"
}
}
// book_test.go
package book_test
import (
. "github.com/conglt10/test-ginkgo"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Book", func() {
var (
longBook Book
shortBook Book
)
BeforeEach(func() {
longBook = Book{
Title: "Les Miserables",
Author: "Victor Hugo",
Pages: 1488,
}
shortBook = Book{
Title: "Fox In Socks",
Author: "Dr. Seuss",
Pages: 24,
}
})
Describe("Categorizing book length", func() {
Context("With more than 300 pages", func() {
It("should be a novel", func() {
Expect(longBook.CategoryByLength()).To(Equal("NOVEL"))
})
})
Context("With fewer than 300 pages", func() {
It("should be a short story", func() {
Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY"))
})
})
})
})
- File
book_test.go
ta viết test để test methodCategoryByLength
- Ta fake 2 biến dữ liệu đại dại cho 2 loại sách
- Khi gọi phương thức
CategoryByLength
với biếnlongBook
, ta dùng các hàm của thư viện Gomega để kiểm tra giá trị trả về có như mong đợi làNOVEL
hay không ? Tương tự với biếnshortBook
.
Chạy test thôi
Tài liệu tham khảo
https://www.manning.com/books/go-web-programming
https://medium.com/rungo/unit-testing-made-easy-in-go-25077669318
All rights reserved