Advertisement
  1. Code
  2. Go

Kiểm tra code chuyên sâu dữ liệu cùng Go, Phần 5

Scroll to top
Read Time: 15 min
This post is part of a series called Testing Data-Intensive Code with Go.
Testing Data-Intensive Code With Go, Part 4

() translation by (you can also view the original English article)

Tổng quan

Đây là phần 5 trong loạt bài hướng dẫn gồm 5 phần về test cho code chuyên sâu về dữ liệu. Trong phần 4, tôi đã trình bày các data store từ xa, sử dụng database test được chia sẻ, sử dụng ảnh chụp nhanh dữ liệu sản xuất và tạo dữ liệu test của riêng bạn. Trong hướng dẫn này, tôi sẽ tiến hành kiểm tra fuzz, kiểm tra bộ nhớ đệm của bạn, kiểm tra tính toàn vẹn dữ liệu, kiểm tra tính ổn định và dữ liệu bị thiếu.

Kiểm tra Fuzz

Ý tưởng của test fuzz là tràn ngập hệ thống với rất nhiều dữ liệu nhập vào ngẫu nhiên. Thay vì cố gắng nghĩ về dữ liệu nhập vào sẽ gồm tất cả các trường hợp, có thể khó khăn và (hoặc) rất tốn công, bạn hãy để cơ hội làm điều đó cho bạn. Kiểm tra này là khái niệm tương tự như việc tạo dữ liệu ngẫu nhiên, nhưng ý định ở đây là tạo ra các dữ liệu nhập vào ngẫu nhiên hoặc bán ngẫu nhiên thay vì dữ liệu bền vững.

Việc kiểm tra Fuzz hữu ích cho điều gì?

Kiểm tra Fuzz đặc biệt hữu ích trong việc tìm kiếm các vấn đề về bảo mật và hiệu suất khi các dữ liệu nhập vào không mong muốn gây ra sự cố hoặc rò rỉ bộ nhớ. Nhưng nó cũng có thể giúp đảm bảo rằng tất cả các dữ liệu nhập vào không hợp lệ được phát hiện sớm và bị hệ thống từ chối phù hợp.

Ví dụ, hãy cân nhắc dữ liệu nhập vào dưới dạng các tài liệu JSON được lồng sâu vào (rất phổ biến trong các web API). Việc cố gắng tự tạo ra danh sách toàn diện của các trường hợp test dễ bị lỗi và rất nhiều công việc. Nhưng test fuzz là kỹ thuật hoàn hảo.

Sử dụng test Fuzz

Có một số thư viện bạn có thể sử dụng để kiểm tra fuzz. Thư viện yêu thích của tôi là gofuzz của Google. Dưới đây là một ví dụ đơn giản tự động tạo 200 đối tượng độc nhất của một struct với một số field, bao gồm cả một struct lồng nhau.

1
import (
2
    "fmt"
3
  "github.com/google/gofuzz"
4
)
5
6
func SimpleFuzzing() {
7
	type SomeType struct {
8
		A string
9
		B string
10
		C int
11
		D struct {
12
			E float64
13
		}
14
	}
15
16
	f := fuzz.New()
17
	uniqueObject := SomeType{}
18
19
	uniqueObjects := map[SomeType]int{}
20
21
	for i := 0; i < 200; i++ {
22
		f.Fuzz(&object)
23
		uniqueObjects[object]++
24
	}
25
	fmt.Printf("Got %v unique objects.\n", len(uniqueObjects))
26
	// Output:

27
	// Got 200 unique objects.

28
}

Kiểm tra bộ nhớ đệm của bạn

Khá nhiều hệ thống phức tạp liên quan đến nhiều dữ liệu đều có bộ nhớ đệm, hoặc nhiều khả năng giống như một vài lưu trữ phân cấp. Như đã nói, chỉ có hai khó khăn trong khoa học máy tính: việc đặt tin, vô hiệu hóa bộ nhớ đệm và tắt đi do lỗi.

Sau tất cả việc quản lý chiến lược và triển khai bộ nhớ đệm của bạn có thể làm phức tạp việc truy cập dữ liệu của bạn nhưng có tác động to lớn đến chi phí và hiệu suất truy cập dữ liệu của bạn. Việc kiểm tra bộ nhớ đệm của bạn không thể được thực hiện từ bên ngoài vì giao diện của bạn ẩn đi nguồn gốc dữ liệ  và cơ chế bộ nhớ đệm là một chi tiết của việc triển khai.

Hãy xem cách kiểm tra hành vi bộ nhớ đệm của data layer hỗn hợp Songify.

Hit và miss của bộ nhớ đếm

Bộ nhớ đệm tồn tại và mất đi bởi hiệu suất hit/miss của họ. Chức năng cơ bản của bộ nhớ đệm là nếu dữ liệu được yêu cầu có sẵn trong bộ nhớ đệm (một hit) thì nó sẽ được tìm nạp từ bộ nhớ đệm chứ không phải từ data store chính. Trong thiết kế ban đầu của HybridDataLayer, việc truy cập bộ nhớ đệm được thực hiện thông qua các phương thức private.

Các quy tắc minh bạch khiến cho việc gọi chúng trực tiếp hoặc thay chúng từ một package khác là bất khả thi. Để kích hoạt bộ nhớ đệm, tôi sẽ thay đổi các phương thức đó thành các hàm public. Điều này tốt vì code thực tế của ứng dụng hoạt động thông qua giao diện DataLayer, không hiển thị các phương thức đó.

Tuy nhiên test code sẽ có thể thay thế các hàm public này khi cần thiết. Trước tiên, hãy bổ sung một phương thức để có quyền truy xuất vào máy khách Redis, để chúng tôi có thể thao tác bộ nhớ đệm:

1
func (m *HybridDataLayer) GetRedis() *redis.Client {
2
    return m.redis
3
}

Tiếp theo tôi sẽ thay đổi các phương thức getSongByUser_DB() thành một biến số hàm public. Bây giờ, trong test, tôi có thể thay thế biến GetSongsByUser_DB() bằng một hàm theo dõi số lần nó được gọi và sau đó chuyển tiếp nó đến hàm nguyên bản. Điều đó cho phép chúng tôi xác minh nếu gọi đến hàm GetSongsByUser() đã tải các bài hát từ bộ nhớ đệm hoặc từ DB.

Hãy chia nhỏ từng phần một. Đầu tiên, chúng ta có được data layer (nó cũng xóa DB và redis), tạo người dùng mới và thêm một bài hát. Phương thức AddSong() cũng đưa vào redis.

1
func TestGetSongsByUser_Cache(t *testing.T) {
2
    now := time.Now()
3
	u := User{Name: "Gigi", 
4
             Email: "gg@gg.com", 
5
             RegisteredAt: now, LastLogin: now}
6
	dl, err := getDataLayer()
7
	if err != nil {
8
		t.Error("Failed to create hybrid data layer")
9
	}
10
11
	err = dl.CreateUser(u)
12
	if err != nil {
13
		t.Error("Failed to create user")
14
	}
15
16
	lm, err := NewSongManager(u, dl)
17
	if err != nil {
18
		t.Error("NewSongManager() returned 'nil'")
19
	}
20
21
	err = lm.AddSong(testSong, nil)
22
	if err != nil {
23
		t.Error("AddSong() failed")
24
	}

Đây là phần hay ho. Tôi duy trì hàm nguyên bản và định nghĩa một hàm mới làm tăng biến callCount cục bộ (tất cả trong một closure) và gọi hàm nguyên gốc. Sau đó, tôi gán hàm instrumented cho biến GetSongsByUser_DB. Từ giờ trở đi, mỗi lần data layer gọi đến GetSongsByUser_DB() sẽ chuyển đến hàm được ghi.

1
    callCount := 0
2
	originalFunc := GetSongsByUser_DB
3
	instrumentedFunc := func(m *HybridDataLayer, 
4
	                         email string, 
5
                             songs *[]Song) (err error) {
6
		callCount += 1
7
		return originalFunc(m, email, songs)
8
	}
9
10
	GetSongsByUser_DB = instrumentedFunc

Tại thời điểm này, chúng tôi đã sẵn sàng để thực sự kiểm tra hoạt động của bộ nhớ đệm. Đầu tiên, test gọi GetSongsByUser() của SongManager để chuyển tiếp nó đến data layer. Bộ nhớ đệm lẽ ra được đưa đến cho người dùng mà chúng tôi vừa bổ sung. Vì vậy, kết quả dự kiến là hàm instrumented của chúng tôi sẽ không được gọi và callCount sẽ duy trì ở mức 0.

1
    _, err = lm.GetSongsByUser(u)
2
	if err != nil {
3
		t.Error("GetSongsByUser() failed")
4
	}
5
6
	// Verify the DB wasn't accessed because cache should be

7
    // populated by AddSong()

8
	if callCount > 0 {
9
		t.Error(`GetSongsByUser_DB() called when it
10
                 shouldn't have`)
11
	}

Trường hợp test cuối cùng là để đảm bảo rằng nếu dữ liệu của người dùng không có trong bộ nhớ đệm, nó sẽ được tìm nạp chính xác từ DB. Test hoàn thành nó bằng cách xóa Redis (xóa tất cả dữ liệu của nó) và thực hiện một lần gọi khác đến GetSongsByUser(). Lần này, hàm instrumented sẽ được gọi và kiểm tra xác minh rằng callCount bằng 1. Cuối cùng, hàm GetSongsByUser_DB() ban đầu được khôi phục.

1
    // Clear the cache

2
	dl.GetRedis().FlushDB()
3
4
	// Get the songs again, now it's should go to the DB

5
    // because the cache is empty

6
	_, err = lm.GetSongsByUser(u)
7
	if err != nil {
8
		t.Error("GetSongsByUser() failed")
9
	}
10
11
	// Verify the DB was accessed because the cache is empty

12
	if callCount != 1 {
13
		t.Error(`GetSongsByUser_DB() wasn't called once 
14
                 as it should have`)
15
	}
16
17
	GetSongsByUser_DB = originalFunc
18
}

Vô hiệu hóa bộ nhớ đệm

Bộ nhớ đệm của chúng tôi rất cơ bản và không làm mất hiệu lực. Điều này hoạt động khá tốt miễn là tất cả các bài hát được thêm vào thông qua phương thức AddSong() xử lý cập nhật Redis. Nếu chúng tôi bổ sung nhiều hoạt động như xóa bài hát hoặc xóa người dùng thì các thao tác này sẽ đảm bảo cập nhật Redis tương ứng.

Bộ nhớ đệm rất đơn giản này sẽ hoạt động ngay cả khi chúng ta có một hệ thống phân tán, nơi nhiều máy độc lập có thể chạy dịch vụ Songify của chúng tôi miễn là tất cả các phiên bản đều hoạt động với cùng giá trị DB và Redis.

Tuy nhiên, nếu DB và bộ nhớ đệm có thể không đồng bộ do các hoạt động bảo trì hoặc các công cụ và ứng dụng khác thay đổi dữ liệu của chúng tôi thì chúng tôi cần đưa ra chính sách làm mất hiệu lực và làm mới cho bộ nhớ đệm. Nó có thể được kiểm tra bằng cách sử dụng các kỹ thuật tương tự - thay thế các hàm mục tiêu hoặc truy xuất trực tiếp vào DB và Redis trong test của bạn để xác minh trạng thái.

Bộ nhớ đệm LRU

Thông thường, bạn không thể để bộ nhớ cache phát triển vô hạn. Một lược đồ phổ biến để duy trì dữ liệu hữu ích nhất trong bộ nhớ đệm là bộ nhớ đệm LRU (gần đây ít nhất được sử dụng). Dữ liệu cũ nhất bị lỗi từ bộ nhớ đệm khi nó chạm đến giới hạn dụng lượng.

Việc test bao gồm thiết lập dung lượng thành con số tương đối nhỏ trong quá trình kiểm tra, vượt quá dung lượng và đảm bảo rằng dữ liệu cũ nhất không còn trong bộ nhớ đệm nữa và việc truy xuất nó yêu cầu truy xuất DB.

Kiểm tra tính toàn vẹn dữ liệu của bạn

Hệ thống của bạn chỉ tốt như tính toàn vẹn dữ liệu của bạn. Nếu bạn có dữ liệu bị hỏng hoặc thiếu dữ liệu thì bạn đang trong tình trạng xấu. Trong các hệ thống trong thế giới thực, thật khó để duy trì tính toàn vẹn dữ liệu hoàn hảo. Lược đồ và định dạng thay đổi, dữ liệu được nhập qua các kênh có thể không kiểm tra tất cả các ràng buộc, lỗi cho phép dữ liệu xấu, các quản trị viên cố gắng sửa lỗi thủ công, sao lưu và khôi phục có thể không đáng tin cậy.

Với thực tế khắc nghiệt này, bạn nên kiểm tra tính toàn vẹn dữ liệu của hệ thống. Kiểm tra tính toàn vẹn dữ liệu khác với kiểm tra tự động thông thường sau mỗi lần thay đổi code. Lý do là dữ liệu có thể bị hỏng ngay cả khi code không thay đổi. Bạn chắc chắn muốn chạy kiểm tra tính toàn vẹn dữ liệu sau khi thay đổi code; điều này có thể thay đổi lưu trữ hoặc đại diện dữ liệu, nhưng cũng chạy chúng định kỳ.

Các ràng buộc cho test

Các ràng buộc là nền tảng của việc mô hình dữ liệu của bạn. Nếu bạn sử dụng relational DB thì bạn có thể xác định một số ràng buộc ở cấp SQL và để DB thực thi chúng. Độ rỗng (null), độ dài của text field, tính duy nhất và mối quan hệ 1-N có thể được định nghĩa dễ dàng. Nhưng SQL không thể kiểm tra tất cả các ràng buộc.

Ví dụ, trong Desongcons, có mối quan hệ N-N giữa người dùng và bài hát. Mỗi bài hát phải được liên kết với ít nhất một người dùng. Không có cách nào tốt để thực thi điều này trong SQL (bạn có thể có foreign key từ bài hát đến người dùng và có bài hát trỏ đến một trong những người dùng được liên kết với nó). Một ràng buộc khác có thể là mỗi người dùng có thể có tối đa 500 bài hát. Một lần nữa, không có cách nào để biểu diễn nó trong SQL. Nếu bạn sử dụng data store là NoQuery thì thường sẽ nhận ít hỗ trợ hơn cho việc khai báo và xác thực các ràng buộc ở cấp độ data storage.

Điều đó mang đến cho bạn một vài lựa chọn:

  • Đảm bảo rằng quyền truy xuất vào dữ liệu chỉ đi qua các giao diện và công cụ được hiệu chỉnh thực thi tất cả các ràng buộc.
  • Định kỳ quét dữ liệu của bạn, tìm kiếm vi phạm ràng buộc và sửa chúng.

Kiểm tra idempotency

Idempotency (tính lũy đẳng) có nghĩa là thực hiện cùng một phép toán nhiều lần liên tiếp sẽ có hiệu quả tương tự như thực hiện nó một lần.

Ví dụ: xét biến x thành 5 là idempotent. Bạn có thể xét x thành 5 một lần hoặc một triệu lần. Nó vẫn sẽ là 5. Tuy nhiên, tăng X lên 1 thì không phải là idempotent. Mỗi lần tăng liên tiếp thay đổi giá trị của nó. Idempotency là một thuộc tính rất được mong muốn trong các hệ thống phân tán với các phân vùng mạng tạm thời và các giao thức khôi phục nhằm thử lại việc gửi tin nhắn nhiều lần nếu không có phản hồi tức thì.

Nếu bạn thiết kế idempotency vào code truy cập dữ liệu của mình thì bạn nên kiểm tra nó. Việc này thường rất dễ. Đối với mỗi phép toàn tạm thời, bạn liên tiếp mở rộng để thực hiện phép toán hai lần trở lên và xác minh không có lỗi và vẫn giữ nguyên trạng thái.

Lưu ý rằng thiết kế idempotent đôi khi có thể ẩn lỗi. Hãy cân xóa một bản ghi từ một DB. Đây là một hoạt động bình thường. Sau khi bạn xóa một bản ghi, bản ghi đó không còn tồn tại trong hệ thống nữa và cố gắng xóa nó một lần nữa sẽ không mang nó trở lại. Điều đó nghĩa là cố gắng xóa một bản ghi không tồn tại là một hoạt động hợp lệ. Nhưng điều này có thể che giấu thực tế là bản ghi sai đã được truyền đến người gọi. Nếu bạn trả về một thông báo lỗi thì đó không phải là idempotent.

Kiểm tra việc di chuyển dữ liệu

Việc di chuyển dữ liệu có thể mang rủi ro. Đôi khi bạn chạy một script trên tất cả dữ liệu hoặc các phần quan trọng của dữ liệu của bạn và thực hiện một số giải phẫu nghiêm trọng. Bạn nên sẵn sàng với kế hoạch B trong trường hợp có sự cố xảy ra (ví dụ: quay lại dữ liệu nguyên bản và tìm ra sự cố xảy ra).

Trong nhiều trường hợp, di chuyển dữ liệu có thể là một hoạt động chậm và tốn kém, có thể yêu cầu hai hệ thống cạnh nhau trong suốt thời gian di chuyển. Tôi đã tham gia vào một số lần di chuyển dữ liệu mất vài ngày hoặc thậm chí vài tuần. Khi phải đối mặt với việc di chuyển dữ liệu lớn, thì nó đáng để đầu tư thời gian và kiểm tra chính việc di chuyển trên một tập hợp con nhỏ (nhưng có tính đại diện) cho dữ liệu của bạn và sau đó xác minh rằng dữ liệu vừa được di chuyển là hợp lệ và hệ thống có thể hoạt động với nó.

Kiểm tra dữ liệu bị thiếu

Việc thiếu dữ liệu là một vấn đề thú vị. Đôi khi dữ liệu bị thiếu sẽ vi phạm tính toàn vẹn dữ liệu của bạn (ví dụ: một bài hát mà người dùng đang thiếu) và đôi khi nó bị thiếu (ví dụ: ai đó xóa người dùng và tất cả các bài hát của họ).

Nếu dữ liệu bị thiếu gây ra sự cố toàn vẹn dữ liệu thì bạn sẽ phát hiện ra nó trong các kiểm tra tính toàn vẹn dữ liệu của bạn. Tuy nhiên, nếu một số dữ liệu bị thiếu thì không có cách nào dễ dàng phát hiện ra. Nếu dữ liệu không bao giờ được lưu vào bộ lưu trữ liên tục thì có thể có dấu vết trong log hoặc các store tạm thời khác.

Tùy thuộc vào mức độ rủi ro của dữ liệu bị thiếu, bạn có thể viết một số test xóa một số dữ liệu có chủ ý khỏi hệ thống của bạn và xác minh hệ thống hoạt động như mong đợi.

Tổng kết

Kiểm tra code chuyên sâu dữ liệu đòi hỏi việc lập kế hoạch cẩn thận và độ hiểu biết về yêu cầu chất lượng từ bạn. Bạn có thể test ở nhiều mức độ trừu tượng và các lựa chọn của bạn sẽ ảnh hưởng đến mức độ toàn diện của các test của bạn, bao nhiêu khía cạnh của lớp dữ liệu thực tế mà bạn kiểm tra, kiểm tra tốc độ test của bạn nhanh đến thế nào và mức độ dễ dàng để sửa đổi các test khi thay đổi layer dữ liệu.

Không có câu trả lời đúng. Bạn cần tìm ra điểm ngọt của mình dọc theo quang phổ từ các test siêu toàn diện, chậm và tốn nhiều công sức đến các test nhanh, gọn nhẹ.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.