Tôi nhớ 2 năm đầu tiên đi làm, tôi không hề được viết tí test nào. Team chúng tôi thường rất tí tởn khi hoàn thành chức năng đúng (thậm chí là trước) deadline. Nhưng mọi thứ trở nên thật hỗn loạn khi client (luôn là tester/QA hoặc PM) thực hiện thao tác trên giao diện.
Vì không có unit testing hay bất kỳ thể loại test nào nên sự thành công của mỗi lần merge code phụ thuộc vào 2 yếu tố: trí nhớ siêu đẳng của tech lead và "tổ tiên phù hộ".
Năm thứ 4 đi làm quả là trải nghiệm tuyệt vời khi code của tôi rụng 1 loạt vì unit testing của chính mình.
1. Data racing trong testing
Khi viết test, một sai lầm thường mắc phải là xài chung một database cho tất cả các test case.
Một kịch bản tôi thường thấy là:
- Test case 1: thực hiện CREATE resource
- Test case 2: thực hiện UPDATE resource
- Test case 3: thực hiện DELETE resource
Mọi thứ chỉ chạy êm đẹp khi các test case được chạy đúng thứ tự từ 1 tới 3. Tuy nhiên, việc chạy tuần tự như vậy sẽ rất chậm. Nhưng nếu để các test chạy concurrent thì nến test case 3 chạy trước 2 thì sẽ gặp lỗi "not found". Do test case 3 đã xoá resource trong database.
Hiện tượng này gọi là data racing, còn được gọi là race condition, xảy ra khi hai hay nhiều tiến trình truy cập đồng thời và cập nhật cùng một dữ liệu mà không có cơ chế đồng bộ.
Để khắc phục tình trạng này, chúng ta cần tạo cho mỗi test case một database riêng để đảm bảo dữ liệu của chúng chạy độc lập.
2. Lựa chọn database phù hợp
Trong Go, việc tạo ra các process chạy conccurrent rất dễ dàng với go routine. Đây là một thế mạnh của Go. Tuy nhiên, nếu dùng nhiều go routine để đọc/ghi vào database, bạn có thể sẽ gặp phải lỗi như "bad connection", "connection busy", "database locks", ...
Đọc thêm về lỗi database locks ở SQLite3: SQLite Database is Locked: How to Resolve
Mỗi go routine sẽ tạo một connection tới database và khi số connection vượt quá giới hạn của database cho phép hoặc khi bạn ghi đồng thời vào database, các lỗi trên sẽ xảy ra. Trong trường hợp này, những database lightweight như SQLite3 không phải là sự lựa chọn hợp lý. Bạn nên cân nhắc sử dụng các database mạnh hơn như PostgreSQL hoặc MySQL.
3. Khi nào nên dùng database thật để test?
Khi viết test, có những trường hợp bạn cần dựng một hoặc nhiều database có chứa dữ liệu. Bạn có thể tạo data bằng 2 cách:
- Cách 1: Dùng code để INSERT/UPDATE vào database
Ví dụ, package dưới làm nhiệm vụ tạo từ 3 tới 6 user ngẫu nhiên trong database.
package seeding
import (
"context"
"fmt"
"math/rand"
"os"
"time"
"my-app/ent"
lorem "github.com/drhodes/golorem"
)
var randGen = rand.New(rand.NewSource(time.Now().UnixNano()))
func CreateUsers(tx *ent.Tx) ([]*ent.User, error) {
var bulk []*ent.UserCreate
fakeDataCount := randGen.Intn(3) + 3 // min 3, max 6
ctx := context.TODO()
for i := 1; i <= fakeDataCount; i++ {
bulk = append(bulk, tx.ShopMeta.Create().SetInput(ent.CreateShopMetaInput{
FirstName: lorem.Word(5, 9),
LastName : lorem.Word(5, 9),
}))
}
return tx.User.CreateBulk(bulk...).SaveX(ctx), nil
}
- Cách 2: Viết 1 file chứa các câu SQL và chạy file đó trc khi chạy test.
Ví dụ, câu query dưới đây tạo bảng users
và thêm các record ngẫu nhiên cho bảng này.
Cách 1 giúp bạn dễ dàng tạo record ngẫu nhiên và từng bảng. Tuy nhiên, vì phải tương tác với database bằng code nên chạy code tạo data test sẽ chậm hơn cách 2. Cách 1 phù hợp với dự án có lượng dữ liệu test ít.
Cách 2 đòi hỏi người viết phải có cái nhìn tổng thể về hệ thống để biết được mối quan hệ của dữ liệu và phải "cứng tay" để sửa và update dữ liệu nếu có yêu cầu sau này. Cách 2 phù hợp với dự án có lượng dữ liệu test nhiều.
4. Test builder
Khi tạo bộ dữ liệu test, người sử dụng rất có thể chỉnh sửa logic, mặc dù pass được test của anh ta nhưng lại ảnh hưởng tới những test case khác. Để đảm bảo lỗi này không phát sinh, nơi khởi tạo dữ liệu test chỉ cần biết gọi hàm nào để tạo dữ liệu.
Ta có thể áp dụng design pattern factory method để khắc phục lỗi trên.
5. Khi khoá ngoài là không cần thiết
Mặc dù, khoá ngoài là cần thiết để đảm bảo tính toàn vẹn của dữ liệu khi test, nhưng trong một vài trường hợp, tắt khoá ngoài là sự lựa chọn phù hợp.
Các trường hợp này bao gồm:
- Khi bạn đang kiểm tra các chức năng cụ thể không cần tới foreign key: Điều này cho phép bạn tách biệt chức năng đang được thử nghiệm mà không phải lo lắng về việc thiết lập tập data test bao gồm đầy đủ các dữ liệu liên quan.
- Khi bạn đang làm việc với legacy code mà các test trước đó có thể chưa tuân thủ đầy đủ ràng buộc foreign key: Vô hiệu hoá khoá ngoài sẽ giúp bạn refactor đoạn test cũ nhanh chóng.
6. Tổng kết
Có nhiều lý do để nói rằng dự án của bạn không cần test như deadline, khách hàng không yêu cầu, bản chất dự án chỉ cần code trong ngắn hạn, không cần phải maintain.
Tuy nhiên, việc không có test dễ khiến cho code phát sinh các bug ẩn mà trong lúc code bạn không biết, đặc biệt trong trường hợp hệ thống của bạn có nhiều logic và nhiều dev cùng code chung.
Hi vọng bài viết cung cấp cho bạn một vài bí quyết để viết test hiệu quả hơn trong Go.
Đọc thêm: Golang là gì? Backend có nên học Golang?, 23 Classic Pattern trong Golang, Golang Channel là gì? v.v