1. Giới thiệu về Kiểm thử Đơn vị
Kiểm thử đơn vị ám chỉ việc kiểm tra và xác nhận đơn vị kiểm thử nhỏ nhất trong một chương trình, như một hàm hoặc phương thức trong ngôn ngữ Go. Kiểm thử đơn vị đảm bảo rằng mã hoạt động như mong đợi và cho phép các nhà phát triển thay đổi mã mà không vô tình làm hỏng chức năng hiện có.
Trong dự án Golang, tầm quan trọng của kiểm thử đơn vị không cần phải nói. Đầu tiên, nó có thể cải thiện chất lượng mã, mang lại sự tự tin hơn cho nhà phát triển khi thực hiện thay đổi mã. Thứ hai, kiểm thử đơn vị có thể phục vụ như tài liệu cho mã, giải thích hành vi mong đợi của nó. Ngoài ra, việc chạy kiểm thử đơn vị tự động trong môi trường tích hợp liên tục có thể phát hiện nhanh chóng lỗi mới được đưa vào, từ đó cải thiện tính ổn định của phần mềm.
2. Thực hiện Kiểm thử Cơ bản bằng Gói testing
Thư viện chuẩn của ngôn ngữ Go bao gồm gói testing
, cung cấp công cụ và chức năng để viết và chạy kiểm thử.
2.1 Tạo Trường Hợp Kiểm thử Đầu Tiên Của Bạn
Để viết một hàm kiểm thử, bạn cần tạo một tệp có hậu tố _test.go
. Ví dụ, nếu tệp mã nguồn của bạn có tên là calculator.go
, tệp kiểm thử của bạn nên có tên là calculator_test.go
.
Tiếp theo, là lúc để tạo hàm kiểm thử. Một hàm kiểm thử cần phải import gói testing
và tuân theo một định dạng cụ thể. Dưới đây là một ví dụ đơn giản:
// calculator_test.go
package calculator
import (
"testing"
"fmt"
)
// Kiểm thử hàm cộng
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Mong đợi %v, nhưng nhận %v", expected, result)
}
}
Trong ví dụ này, TestAdd
là một hàm kiểm thử kiểm thử một hàm Add
ảo. Nếu kết quả của hàm Add
khớp với kết quả mong đợi, thì kiểm thử sẽ thành công; nếu không, t.Errorf
sẽ được gọi để ghi lại thông tin về việc kiểm thử thất bại.
2.2 Hiểu Quy tắc Đặt tên và Đặc điểm của Hàm Kiểm thử
Hàm kiểm thử phải bắt đầu bằng Test
, tiếp theo là một chuỗi không viết thường, và tham số duy nhất của chúng phải là một con trỏ tới testing.T
. Như ví dụ đã cho, TestAdd
tuân theo quy tắc đặt tên và đặc điểm chính xác.
2.3 Chạy Trường Hợp Kiểm thử
Bạn có thể chạy các trường hợp kiểm thử của mình bằng cách sử dụng công cụ dòng lệnh. Đối với một trường hợp kiểm thử cụ thể, hãy chạy lệnh sau:
go test -v // Chạy kiểm thử trong thư mục hiện tại và hiển thị đầu ra chi tiết
Nếu bạn muốn chạy một trường hợp kiểm thử cụ thể, bạn có thể sử dụng cờ -run
tiếp theo là biểu thức chính quy:
go test -v -run TestAdd // Chạy chỉ hàm kiểm thử TestAdd
Lệnh go test
sẽ tự động tìm tất cả các tệp _test.go
và thực thi mọi hàm kiểm thử thỏa mãn tiêu chí. Nếu tất cả các kiểm thử đều thành công, bạn sẽ thấy một thông báo tương tự như PASS
trên dòng lệnh; nếu bất kỳ kiểm thử nào thất bại, bạn sẽ thấy FAIL
cùng với thông báo lỗi tương ứng.
3. Viết Trường Hợp Kiểm thử
3.1 Báo cáo Lỗi bằng t.Errorf
và t.Fatalf
Trong ngôn ngữ Go, framework kiểm thử cung cấp các phương thức khác nhau để báo cáo lỗi. Hai phương thức phổ biến nhất là Errorf
và Fatalf
, cả hai đều là phương thức của đối tượng testing.T
. Errorf
được sử dụng để báo cáo lỗi trong quá trình kiểm thử nhưng không dừng trường hợp kiểm thử hiện tại, trong khi Fatalf
dừng ngay lập tức trường hợp kiểm thử sau khi báo cáo lỗi. Điều quan trọng là chọn phương thức phù hợp dựa trên yêu cầu kiểm thử.
Ví dụ sử dụng Errorf
:
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if got != want {
t.Errorf("Add(1, 2) = %d; mong đợi %d", got, want)
}
}
Nếu bạn muốn dừng ngay lập tức khi phát hiện lỗi, bạn có thể sử dụng Fatalf
:
func TestSubtract(t *testing.T) {
got := Subtract(5, 3)
if got != 2 {
t.Fatalf("Subtract(5, 3) = %d; mong đợi 2", got)
}
}
Nói chung, nếu lỗi sẽ làm cho mã sau không thực thi đúng hoặc việc thất bại kiểm thử có thể được xác nhận trước, tốt nhất là sử dụng Fatalf
. Nếu không, nên sử dụng Errorf
để có kết quả kiểm thử toàn diện hơn.
3.2 Tổ chức Subtests và Chạy Subtests
Trong Go, chúng ta có thể sử dụng t.Run
để tổ chức các subtest, giúp chúng ta viết mã kiểm thử một cách có cấu trúc hơn. Các subtest có thể có Setup
và Teardown
riêng và có thể chạy một cách độc lập, cung cấp tính linh hoạt tuyệt vời. Điều này đặc biệt hữu ích khi thực hiện các kiểm thử phức tạp hoặc kiểm thử có tham số.
Ví dụ về việc sử dụng subtest t.Run
:
func TestMultiply(t *testing.T) {
testcases := []struct {
name string
a, b, expected int
}{
{"2x3", 2, 3, 6},
{"-1x-1", -1, -1, 1},
{"0x4", 0, 4, 0},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if got := Multiply(tc.a, tc.b); got != tc.expected {
t.Errorf("Multiply(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
Nếu chúng ta muốn chạy subtest có tên "2x3" một cách độc lập, chúng ta có thể chạy lệnh sau trong dòng lệnh:
go test -run TestMultiply/2x3
Vui lòng lưu ý rằng tên subtest phân biệt chữ hoa chữ thường.
4. Công Việc Chuẩn Bị Trước và Sau Khi Kiểm Thử
4.1 Setup và Teardown
Khi thực hiện kiểm thử, chúng ta thường cần chuẩn bị một số trạng thái ban đầu cho các kiểm thử (như kết nối cơ sở dữ liệu, tạo tệp, v.v.), và tương tự, chúng ta cần thực hiện một số công việc dọn dẹp sau khi kiểm thử hoàn thành. Trong Go, chúng ta thường thực hiện Setup
và Teardown
trực tiếp trong các hàm kiểm thử, và hàm t.Cleanup
cung cấp khả năng đăng ký các hàm gọi lại dọn dẹp.
Dưới đây là một ví dụ đơn giản:
func TestDatabase(t *testing.T) {
db, err := SetupDatabase()
if err != nil {
t.Fatalf("setup failed: %v", err)
}
// Đăng ký một hàm gọi lại dọn dẹp để đảm bảo rằng kết nối cơ sở dữ liệu được đóng khi kiểm thử hoàn thành
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("failed to close database: %v", err)
}
})
// Thực hiện kiểm thử...
}
Trong hàm TestDatabase
, chúng ta trước tiên gọi hàm SetupDatabase
để thiết lập môi trường kiểm thử. Sau đó, chúng ta sử dụng t.Cleanup()
để đăng ký một hàm sẽ được gọi sau khi kiểm thử hoàn thành để thực hiện công việc dọn dẹp, trong ví dụ này là đóng kết nối cơ sở dữ liệu. Như vậy, chúng ta có thể đảm bảo rằng tài nguyên được giải phóng đúng cách bất kể kiểm thử thành công hay thất bại.
5. Cải Thiện Hiệu Suất Kiểm Thử
Cải thiện hiệu suất kiểm thử có thể giúp chúng ta phát triển nhanh hơn, nhanh chóng phát hiện vấn đề, và đảm bảo chất lượng mã. Dưới đây, chúng ta sẽ thảo luận về bao phủ kiểm thử, kiểm thử được điều khiển bằng bảng, và việc sử dụng mocks để cải thiện hiệu suất kiểm thử.
5.1 Bao Phủ Kiểm Thử và Công Cụ Liên Quan
Công cụ go test
cung cấp tính năng bao phủ kiểm thử rất hữu ích, giúp chúng ta hiểu được các phần của mã được kiểm thử ra sao, từ đó phát hiện ra những phần của mã không được kiểm thử.
Sử dụng lệnh go test -cover
, bạn có thể xem phần trăm bao phủ kiểm thử hiện tại:
go test -cover
Nếu bạn muốn hiểu rõ hơn về những dòng mã đã được thực thi và những dòng không, bạn có thể sử dụng tham số -coverprofile
, tạo ra một tập tin dữ liệu bao phủ. Sau đó, bạn có thể sử dụng lệnh go tool cover
để tạo ra bản báo cáo bao phủ kiểm thử chi tiết.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
Lệnh trên sẽ mở một báo cáo web, hiển thị một cách trực quan những dòng mã được kiểm thử và những dòng không được. Màu xanh biểu thị mã đã được kiểm thử, trong khi màu đỏ biểu thị những dòng mã chưa được kiểm thử.
5.2 Sử dụng Mock
Trong quá trình kiểm thử, chúng ta thường gặp các tình huống cần mô phỏng các phụ thuộc bên ngoài. Mock có thể giúp chúng ta mô phỏng các phụ thuộc này, loại bỏ việc phụ thuộc vào dịch vụ hoặc tài nguyên bên ngoài cụ thể trong môi trường kiểm thử.
Có nhiều công cụ mock trong cộng đồng Go, như testify/mock
và gomock
. Những công cụ này thường cung cấp một loạt các API để tạo và sử dụng đối tượng mock.
Dưới đây là một ví dụ cơ bản về việc sử dụng testify/mock
. Điều đầu tiên cần làm là định nghĩa một interface và phiên bản mock của nó:
type DataService interface {
FetchData() (int, error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchData() (int, error) {
args := m.Called()
return args.Int(0), args.Error(1)
}
Trong quá trình kiểm thử, chúng ta có thể sử dụng MockDataService
để thay thế dịch vụ dữ liệu thực tế:
func TestSomething(t *testing.T) {
mockDataSvc := new(MockDataService)
mockDataSvc.On("FetchData").Return(42, nil) // Cấu hình hành vi dự kiến
result, err := mockDataSvc.FetchData() // Sử dụng đối tượng mock
assert.NoError(t, err)
assert.Equal(t, 42, result)
mockDataSvc.AssertExpectations(t) // Xác nhận liệu hành vi dự kiến có xảy ra không
}
Qua cách tiếp cận trên, chúng ta có thể tránh phụ thuộc vào hoặc cô lập các dịch vụ bên ngoài, các cuộc gọi cơ sở dữ liệu, v.v. trong quá trình kiểm thử. Điều này có thể tăng tốc độ thực thi kiểm thử và làm cho các kiểm thử của chúng ta ổn định và đáng tin cậy hơn.
6. Các Kỹ Thuật Kiểm Thử Nâng Cao
Sau khi nắm vững cơ bản về kiểm thử đơn vị trong Go, chúng ta có thể tiếp tục khám phá một số kỹ thuật kiểm thử nâng cao hơn, giúp xây dựng phần mềm mạnh mẽ hơn và cải thiện hiệu quả kiểm thử.
6.1 Kiểm Thử Các Hàm Riêng tư
Trong Golang, các hàm riêng tư thường ám chỉ các hàm không công bố, tức là các hàm có tên bắt đầu bằng chữ cái viết thường. Thông thường, chúng ta thích kiểm thử các giao diện công cộng, vì chúng phản ánh tính khả dụng của mã. Tuy nhiên, có những trường hợp khi kiểm thử trực tiếp các hàm riêng tư cũng hợp lý, như khi hàm riêng tư có logic phức tạp và được gọi bởi nhiều hàm công cộng.
Kiểm thử các hàm riêng tư khác biệt so với kiểm thử các hàm công cộng vì chúng không thể được truy cập từ bên ngoài gói. Một kỹ thuật phổ biến là viết mã kiểm thử trong cùng gói, cho phép truy cập vào hàm riêng tư.
Dưới đây là một ví dụ đơn giản:
// calculator.go
package calculator
func add(a, b int) int {
return a + b
}
Tập tin kiểm thử tương ứng như sau:
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
expected := 4
actual := add(2, 2)
if actual != expected {
t.Errorf("expected %d, got %d", expected, actual)
}
}
Bằng cách đặt tập tin kiểm thử trong cùng gói, chúng ta có thể kiểm thử trực tiếp hàm add
.
6.2 Các Mẫu và Thực Tiễn Kiểm Thử Phổ Biến
Kiểm thử đơn vị trong Golang có một số mẫu và thực tiễn phổ biến giúp làm việc kiểm thử dễ dàng hơn và giữ mã nguồn rõ ràng và dễ bảo trì.
-
Kiểm Thử Dựa trên Bảng (Table-Driven Tests)
Kiểm thử dựa trên bảng là một phương pháp tổ chức các đầu vào kiểm thử và kết quả mong đợi. Bằng cách xác định một bộ các trường hợp kiểm thử và sau đó lặp qua chúng để kiểm thử, phương pháp này rất dễ thêm các trường hợp kiểm thử mới và cũng làm cho mã nguồn dễ đọc và bảo trì.
// calculator_test.go
package calculator
import "testing"
func TestAddTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{1, 2, 3},
{2, 2, 4},
{5, -1, 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := add(tt.a, tt.b)
if tt.want != ans {
t.Errorf("got %d, want %d", ans, tt.want)
}
})
}
}
-
Sử Dụng Mock để Kiểm Thử
Mocking là một kỹ thuật kiểm thử liên quan đến việc thay thế các phụ thuộc để kiểm thử các phần của chức năng. Trong Golang, giao diện là cách chính để thực hiện mock. Bằng cách sử dụng giao diện, một bản triển khai mock có thể được tạo ra và sau đó sử dụng trong quá trình kiểm thử.