Viết Go Unit test cho db CRUD với random data


Khi phát triển các ứng dụng Go tương tác với cơ sở dữ liệu, việc đảm bảo tính chính xác của các thao tác CRUD (Create, Read, Update, Delete) là vô cùng quan trọng. Unit test với dữ liệu ngẫu nhiên giúp kiểm tra tính ổn định của hệ thống dưới nhiều điều kiện đầu vào khác nhau, phát hiện các lỗi tiềm ẩn và đảm bảo code hoạt động đúng như mong đợi trong mọi tình huống.
Bài viết này sẽ hướng dẫn cách viết các unit test toàn diện cho các thao tác CRUD trong Go, sử dụng dữ liệu ngẫu nhiên để mô phỏng các tình huống thực tế. Chúng ta sẽ tìm hiểu cách thiết lập môi trường test, tạo dữ liệu mẫu ngẫu nhiên và viết các test case hiệu quả để đảm bảo chức năng của ứng dụng.
Trong bài giảng trước, chúng ta đã tìm hiểu cách tạo code Golang Crud để giao tiếp với cơ sở dữ liệu. Hôm nay chúng ta sẽ học cách viết unit test cho các hoạt động CRUD đó.
Setup main test
Mọi hàm unit test trong Go phải bắt đầu với tiền tố Test
(chữ T viết hoa) và nhận một đối tượng testing.T
làm tham số. Đối tượng T
này được sử dụng để quản lý trạng thái của test.
Tôi sẽ khai báo một hàm đặc biệt có tên TestMain()
, nhận một đối tượng testing.M
làm tham số.
Theo convention, TestMain()
là entry point chính của tất cả unit test trong một package Go cụ thể, trong trường hợp này là package db
.
Lưu ý rằng unit test trong Golang được chạy riêng biệt cho từng package. Vì vậy, nếu dự án có nhiều package, bạn có thể có nhiều file main_test.go
, mỗi file chứa một TestMain()
riêng biệt.
Dưới đây là file main_test.go
package db
import (
"context"
"log"
"os"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
)
const (
dbSource = "postgresql://postgres:Abc@12345678@localhost:54322/simple_bank?sslmode=disable"
)
var testQueries *Queries
func TestMain(m *testing.M) {
connPool, err := pgxpool.New(context.Background(), dbSource)
if err != nil {
log.Fatal("cannot connect to db:", err)
}
defer connPool.Close()
testQueries = New(connPool)
os.Exit(m.Run())
}
Import: Import các package cần thiết (
pgxpool
để kết nối DB,testing
để chạy test).Khai báo biến toàn cục:
testQueries
để dùng chung trong các test case.Hàm
TestMain
:Tải config từ file cấu hình.
Tạo connection pool để kết nối database.
Gán
testStore
để sử dụng trong test.Chạy tất cả các test case với
m.Run
()
.
Chạy lệnh sau để kiểm tra:
go test -v ./db/sqlc
Kết quả sẽ trông như thế này:
Test Create Account
Hãy bắt đầu với hàm CreateAccount()
. Tôi sẽ tạo một tệp mới có tên account_test.go
bên trong thư mục db/sqlc
.
Trong Golang, có một quy ước là đặt tên file test trong cùng thư mục với mã nguồn, và tên của file test nên kết thúc bằng hậu tố _test
.
Tên package của file test này sẽ là db
, giống với package chứa mã CRUD của chúng ta. Bây giờ, hãy định nghĩa hàm TestCreateAccount()
.
package db
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func createRandomAccount(t *testing.T) Account {
arg := CreateAccountParams{
Owner: "test_user",
Balance: 100,
Currency: "USD",
}
account, err := testQueries.CreateAccount(context.Background(), arg)
require.NoError(t, err)
require.NotEmpty(t, account)
require.Equal(t, arg.Owner, account.Owner)
require.Equal(t, arg.Balance, account.Balance)
require.Equal(t, arg.Currency, account.Currency)
require.NotZero(t, account.ID)
require.NotZero(t, account.CreatedAt)
return account
}
func TestCreateAccount(t *testing.T) {
createRandomAccount(t)
}
func TestGetAccount(t *testing.T) {
// Create account first
account1 := createRandomAccount(t)
// Get the created account
account2, err := testQueries.GetAccount(context.Background(), account1.ID)
require.NoError(t, err)
require.NotEmpty(t, account2)
require.Equal(t, account1.ID, account2.ID)
require.Equal(t, account1.Owner, account2.Owner)
require.Equal(t, account1.Balance, account2.Balance)
require.Equal(t, account1.Currency, account2.Currency)
require.WithinDuration(t, account1.CreatedAt.Time, account2.CreatedAt.Time, time.Second)
}
func TestUpdateAccount(t *testing.T) {
account1 := createRandomAccount(t)
arg := UpdateAccountParams{
ID: account1.ID,
Balance: 200,
}
account2, err := testQueries.UpdateAccount(context.Background(), arg)
require.NoError(t, err)
require.NotEmpty(t, account2)
require.Equal(t, account1.ID, account2.ID)
require.Equal(t, account1.Owner, account2.Owner)
require.Equal(t, arg.Balance, account2.Balance)
require.Equal(t, account1.Currency, account2.Currency)
require.WithinDuration(t, account1.CreatedAt.Time, account2.CreatedAt.Time, time.Second)
}
func TestDeleteAccount(t *testing.T) {
account1 := createRandomAccount(t)
err := testQueries.DeleteAccount(context.Background(), account1.ID)
require.NoError(t, err)
account2, err := testQueries.GetAccount(context.Background(), account1.ID)
require.Error(t, err)
require.Empty(t, account2)
}
func TestListAccounts(t *testing.T) {
for i := 0; i < 10; i++ {
createRandomAccount(t)
}
arg := ListAccountsParams{
Limit: 5,
Offset: 5,
}
accounts, err := testQueries.ListAccounts(context.Background(), arg)
require.NoError(t, err)
require.Len(t, accounts, 5)
for _, account := range accounts {
require.NotEmpty(t, account)
}
}
File test này bao gồm các test case cho các chức năng CRUD cơ bản của Account:
createRandomAccount
: Helper function để tạo account ngẫu nhiên cho mục đích testTestCreateAccount
: Test việc tạo account mớiTestGetAccount
: Test việc lấy thông tin một accountTestUpdateAccount
: Test việc cập nhật thông tin accountTestDeleteAccount
: Test việc xóa accountTestListAccounts
: Test việc lấy danh sách accounts với phân trang
Mỗi test case đều tuân theo cấu trúc:
Arrange
: Chuẩn bị dữ liệu testAct
: Thực hiện hành động cần testAssert
: Kiểm tra kết quả với các assertions
Các test sử dụng thư viện testify/require để viết các assertions rõ ràng và dễ đọc. Mỗi test case đều kiểm tra đầy đủ các trường hợp thành công và thất bại cần thiết.
Chạy lệnh go test -v ./db/sqlc
để chạy test
Mở database để kiểm tra dữ liệu được tạo
Có một số bản ghi đã được tạo ra, tuy nhiên có một vấn đề là dữ liệu đang bị hard code nên đang không đúng với logic lắm. Chúng ta tiến hành sửa lại file test để tạo random data.
Generate random data
Bằng cách tạo dữ liệu ngẫu nhiên, ta tiết kiệm được rất nhiều thời gian trong việc chọn giá trị phù hợp, đồng thời code sẽ gọn hơn và dễ hiểu hơn.
Hơn nữa, vì dữ liệu là ngẫu nhiên, ta cũng tránh được xung đột giữa các unit test, đặc biệt quan trọng khi có cột bị ràng buộc unique trong database.
Giờ tạo thư mục util
, thêm file random.go
và currency.go
vào đó. Tên package là util
, trùng với tên thư mục. Hai file này mục đích để tạo dữ liệu random.
Đây là file currency.go
package util
// Constants for all supported currencies
const (
USD = "USD"
EUR = "EUR"
CAD = "CAD"
)
// IsSupportedCurrency checks if the currency is supported
func IsSupportedCurrency(currency string) bool {
switch currency {
case USD, EUR, CAD:
return true
}
return false
}
Định nghĩa danh sách các loại tiền tệ được hỗ trợ (
USD
,EUR
,CAD
) dưới dạng hằng số.Hàm
IsSupportedCurrency(currency string) bool
kiểm tra xem một loại tiền tệ có nằm trong danh sách hỗ trợ hay không, trả vềtrue
nếu có, ngược lại trả vềfalse
.
Đây là file random.go
package util
import (
"fmt"
"math/rand"
"strings"
)
const alphabet = "abcdefghijklmnopqrstuvwxyz"
// RandomInt generates a random integer between min and max
func RandomInt(min, max int64) int64 {
return min + rand.Int63n(max-min+1)
}
// RandomString generates a random string of length n
func RandomString(n int) string {
var sb strings.Builder
k := len(alphabet)
for i := 0; i < n; i++ {
c := alphabet[rand.Intn(k)]
sb.WriteByte(c)
}
return sb.String()
}
// RandomOwner generates a random owner name
func RandomOwner() string {
return RandomString(6)
}
// RandomMoney generates a random amount of money
func RandomMoney() int64 {
return RandomInt(0, 1000)
}
// RandomCurrency generates a random currency code
func RandomCurrency() string {
currencies := []string{USD, EUR, CAD}
n := len(currencies)
return currencies[rand.Intn(n)]
}
// RandomEmail generates a random email
func RandomEmail() string {
return fmt.Sprintf("%s@email.com", RandomString(6))
}
Khai báo package
util
và import các thư viện cần thiết (fmt
,math/rand
,strings
).Định nghĩa bảng chữ cái (
alphabet
) để tạo chuỗi ngẫu nhiên.RandomInt(min, max int64) int64
: Tạo số nguyên ngẫu nhiên trong khoảng[min, max]
.RandomString(n int) string
: Tạo chuỗi ngẫu nhiên có độ dàin
từ bảng chữ cái.RandomOwner() string
: Tạo tên chủ sở hữu ngẫu nhiên (chuỗi 6 ký tự).RandomMoney() int64
: Tạo số tiền ngẫu nhiên từ 0 đến 1000.RandomCurrency() string
: Chọn ngẫu nhiên một loại tiền tệ từ danh sáchUSD, EUR, CAD
.RandomEmail() string
: Tạo email ngẫu nhiên theo định dạngxxxxxx@email.com
.
Okay, tiếp theo sửa lại file account_test.go
sử dụng data random
Chạy lệnh go test -v -cover ./db/sqlc
để chạy lại test
go test
: Chạy các bài test trong Go.-v
(verbose): Hiển thị chi tiết kết quả từng test case.-cover
: Bật chế độ đo độ bao phủ (coverage) của các bài test../db/sqlc
: Chỉ định thư mục chứa mã nguồn cần test (trong trường hợp này là thư mụcdb/sqlc
Kết quả như sau
=== RUN TestCreateAccount
--- PASS: TestCreateAccount (0.03s)
=== RUN TestGetAccount
--- PASS: TestGetAccount (0.02s)
=== RUN TestUpdateAccount
--- PASS: TestUpdateAccount (0.04s)
=== RUN TestDeleteAccount
--- PASS: TestDeleteAccount (0.06s)
=== RUN TestListAccounts
--- PASS: TestListAccounts (0.22s)
PASS
coverage: 35.2% of statements
ok github.com/eminel9311/simplebank/db/sqlc 0.379s coverage: 35.2% of statements
Coverage: 35.2% of statements nghĩa là chỉ 35.2% số dòng lệnh trong mã nguồn được thực thi khi chạy các bài test. Còn các Unit test cho các file khác như entry.sql.go
transfer.sql.go
… Các bạn xem tại Git của mình nhé.
Cuối cùng chúng ta kiểm tra lại database xem dữ liệu được tạo như thế nào nhé
Viết unit test cho các thao tác CRUD trong Go với dữ liệu ngẫu nhiên giúp đảm bảo tính đúng đắn của code, đồng thời giảm thiểu rủi ro xung đột giữa các test case. Việc sử dụng dữ liệu ngẫu nhiên còn giúp kiểm tra ứng dụng trong nhiều tình huống khác nhau, phát hiện lỗi tiềm ẩn hiệu quả hơn.
Bằng cách áp dụng phương pháp này, bạn có thể xây dựng các bài test linh hoạt, dễ bảo trì và phản ánh sát thực tế hơn. Hãy tiếp tục tối ưu test coverage để nâng cao chất lượng hệ thống!
Xin chào và hẹn gặp lại các bạn trong các bài viết lần sau. Link tài liệu tham khảo: https://dev.to/techschoolguru/write-go-unit-tests-for-db-crud-with-random-data-53no
Subscribe to my newsletter
Read articles from Eminel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Eminel
Eminel
Hello, my name is Eminel a software engineer specializing in scalable software architecture, microservices, and AI-powered platforms. Passionate about mentoring, performance optimization, and building solutions that drive business success.