() translation by (you can also view the original English article)
Ikhtisar
Ini adalah bagian lima dari lima dalam seri tutorial tentang pengujian kode intensif data. Di bagian empat, saya membahas penyimpanan data jarak jauh, menggunakan basis data uji coba, menggunakan snapshot data produksi, dan membuat data uji Anda sendiri. Dalam tutorial ini, saya akan membahas pengujian fuzz, menguji cache Anda, menguji integritas data, menguji idempotency, dan data yang hilang.
Pengujian Fuzz
Gagasan pengujian fuzz adalah membanjiri sistem dengan banyak input acak. Daripada mencoba memikirkan masukan yang akan mencakup semua kasus, yang bisa sulit dan / atau sangat padat karya, Anda membiarkan kesempatan melakukannya untuk Anda. Secara konseptual mirip dengan pembuatan data acak, tetapi tujuan di sini adalah untuk menghasilkan input acak atau semi-acak daripada data persisten.
Kapan Pengujian Fuzz Bermanfaat?
Pengujian fuzz berguna khususnya untuk menemukan masalah keamanan dan kinerja ketika input yang tidak diharapkan menyebabkan crash atau kebocoran memori. Tetapi ini juga dapat membantu memastikan bahwa semua input yang tidak valid terdeteksi lebih awal dan ditolak dengan benar oleh sistem.
Pertimbangkan, misalnya, masukan yang datang dalam bentuk dokumen JSON yang sangat berlapis (sangat umum di API web). Mencoba untuk menghasilkan secara manual daftar lengkap kasus uji coba rawan kesalahan dan banyak pekerjaan. Tapi pengujian fuzz adalah teknik yang sempurna.
Menggunakan Pengujian Fuzz
Ada beberapa perpustakaan yang dapat Anda gunakan untuk pengujian fuzz. Favorit saya adalah gofuzz dari Google. Berikut ini contoh sederhana yang secara otomatis menghasilkan 200 objek unik dari suatu struct dengan beberapa bidang, termasuk struct yang bersarang.
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 |
}
|
29 |
Menguji Cache Anda
Hampir semua sistem kompleks yang berhubungan dengan banyak data memiliki cache, atau lebih mungkin beberapa tingkat cache hierarkis. Seperti kata pepatah, hanya ada dua hal yang sulit dalam ilmu komputer: penamaan sesuatu, pembatalan cache, dan off oleh satu kesalahan.
Lelucon di samping, mengelola strategi dan implementasi caching Anda dapat mempersulit akses data Anda tetapi memiliki dampak yang luar biasa pada biaya akses dan kinerja data Anda. Pengujian cache Anda tidak dapat dilakukan dari luar karena antarmuka Anda menyembunyikan dari mana data berasal, dan mekanisme cache adalah detail implementasi.
Mari kita lihat bagaimana menguji perilaku cache dari layer data hibrida Songify.
Cache Hits dan Misses
Cache hidup dan mati oleh kinerja hit/miss mereka. Fungsionalitas dasar dari sebuah cache adalah bahwa jika data yang diminta tersedia dalam cache (hit) maka itu akan diambil dari cache dan bukan dari penyimpanan data primer. Dalam desain asli HybridDataLayer
, akses cache dilakukan melalui metode pribadi.
Aturan visibilitas membuatnya tidak mungkin untuk memanggil mereka secara langsung atau menggantikannya dari paket lain. Untuk mengaktifkan pengujian cache, saya akan mengubah metode tersebut ke fungsi publik. Ini bagus karena kode aplikasi yang sebenarnya beroperasi melalui antarmuka DataLayer
, yang tidak mengekspos metode tersebut.
Kode pengujian, bagaimanapun, akan dapat menggantikan fungsi publik ini sesuai kebutuhan. Pertama, mari tambahkan metode untuk mendapatkan akses ke klien Redis, sehingga kita dapat memanipulasi cache:
1 |
func (m *HybridDataLayer) GetRedis() *redis.Client { |
2 |
return m.redis |
3 |
}
|
4 |
Selanjutnya saya akan mengubah metode getSongByUser_DB()
ke variabel fungsi publik. Sekarang, dalam pengujian, saya dapat mengganti variabel GetSongsByUser_DB()
dengan fungsi yang melacak berapa kali dipanggil dan kemudian meneruskannya ke fungsi semula. Itu memungkinkan kami untuk memverifikasi jika panggilan ke GetSongsByUser()
mengambil lagu dari cache atau dari DB.
Mari kita hancurkan satu demi satu. Pertama, kita mendapatkan lapisan data (yang juga membersihkan DB dan redis), membuat pengguna, dan menambahkan lagu. Metode AddSong()
juga mengisi 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 |
}
|
25 |
Ini bagian yang keren. Saya menjaga fungsi aslinya dan mendefinisikan fungsi baru yang diinstrumen yang menambah variabel callCount
lokal (semua dalam penutupan) dan memanggil fungsi asli. Kemudian, saya menetapkan fungsi yang diinstrumentasi ke variabel GetSongsByUser_DB
. Mulai sekarang, setiap panggilan oleh lapisan data hibrida ke GetSongsByUser_DB()
akan pergi ke fungsi yang diinstrumentasi.
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 |
11 |
Pada titik ini, kami siap untuk benar-benar menguji operasi cache. Pertama, tes memanggil GetSongsByUser()
dari SongManager
yang meneruskannya ke lapisan data hibrida. Cache seharusnya diisi untuk pengguna ini yang baru saja kami tambahkan. Jadi hasil yang diharapkan adalah bahwa fungsi instrumen kami tidak akan dipanggil, dan callCount
akan tetap nol.
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 |
}
|
12 |
Kasus uji terakhir adalah untuk memastikan bahwa jika data pengguna tidak dalam cache, itu akan diambil dengan benar dari DB. Tes menyelesaikannya dengan menyiram Redis (menghapus semua datanya) dan membuat panggilan lain ke GetSongsByUser()
. Kali ini, fungsi yang diinstrumentasi akan dipanggil, dan tes memverifikasi bahwa callCount
sama dengan 1. Akhirnya, fungsi GetSongsByUser_DB()
yang asli dipulihkan.
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 |
}
|
Cache Invalidasi
Cache kami sangat dasar dan tidak melakukan pembatalan apa pun. Ini berfungsi dengan baik selama semua lagu ditambahkan melalui metode AddSong()
yang menangani pembaruan Redis. Jika kita menambahkan lebih banyak operasi seperti menghapus lagu atau menghapus pengguna, maka operasi ini harus berhati-hati memperbarui Redis.
Cache yang sangat sederhana ini akan berfungsi bahkan jika kita memiliki sistem terdistribusi di mana banyak mesin independen dapat menjalankan layanan Songify kami — selama semua instance bekerja dengan instance DB dan Redis yang sama.
Namun, jika DB dan cache bisa keluar dari sinkronisasi karena operasi pemeliharaan atau alat dan aplikasi lain yang mengubah data kami, maka kami perlu datang dengan kebijakan pembatalan dan penyegaran untuk cache. Ini dapat diuji dengan menggunakan teknik yang sama — menggantikan fungsi target atau langsung mengakses DB dan Redis dalam pengujian Anda untuk memverifikasi keadaan.
LRU Cache
Biasanya, Anda tidak bisa membiarkan cache tumbuh tanpa batas. Skema umum untuk menyimpan data yang paling berguna dalam cache adalah cache LRU (paling jarang digunakan). Data tertua akan terbentur dari cache ketika mencapai kapasitas.
Pengujian ini melibatkan pengaturan kapasitas ke nomor yang relatif kecil selama pengujian, melebihi kapasitas, dan memastikan bahwa data tertua tidak lagi di cache dan mengaksesnya membutuhkan akses DB.
Menguji Integritas Data Anda
Sistem Anda hanya sebaik integritas data Anda. Jika Anda memiliki data yang rusak atau data yang hilang maka Anda dalam kondisi buruk. Dalam sistem dunia nyata, sulit untuk mempertahankan integritas data yang sempurna. Perubahan skema dan format, data dicerna melalui saluran yang mungkin tidak memeriksa semua kendala, bug memasukkan data yang buruk, admin yang mencoba perbaikan manual, pencadangan, dan pemulihan mungkin tidak dapat diandalkan.
Mengingat kenyataan pahit ini, Anda harus menguji integritas data sistem Anda. Pengujian integritas data berbeda dari tes otomatis biasa setelah setiap perubahan kode. Alasannya adalah data bisa menjadi buruk bahkan jika kode tidak berubah. Anda pasti ingin menjalankan pemeriksaan integritas data setelah perubahan kode yang mungkin mengubah penyimpanan atau representasi data, tetapi juga menjalankannya secara berkala.
Batasan Pengujian
Batasan adalah fondasi pemodelan data Anda. Jika Anda menggunakan DB relasional maka Anda dapat menetapkan beberapa kendala pada tingkat SQL dan biarkan DB menegakkannya. Nullness, panjang bidang teks, keunikan dan hubungan 1-N dapat didefinisikan dengan mudah. Tetapi SQL tidak dapat memeriksa semua batasan.
Misalnya, di Desongcious, ada hubungan N-N antara pengguna dan lagu. Setiap lagu harus dikaitkan dengan setidaknya satu pengguna. Tidak ada cara yang baik untuk menerapkan ini di SQL (baik, Anda dapat memiliki kunci asing dari lagu ke pengguna dan memiliki titik lagu ke salah satu pengguna yang terkait dengannya). Kendala lain mungkin adalah bahwa setiap pengguna dapat memiliki paling banyak 500 lagu. Sekali lagi, tidak ada cara untuk mewakilinya dalam SQL. Jika Anda menggunakan penyimpanan data NoSQL maka biasanya bahkan ada lebih sedikit dukungan untuk mendeklarasikan dan memvalidasi batasan di tingkat penyimpanan data.
Itu membuat Anda memiliki beberapa opsi:
- Pastikan bahwa akses ke data hanya melalui antarmuka dan alat yang diperiksa yang menegakkan semua kendala.
- Pindai data Anda secara berkala, perburuan pelanggaran kendala, dan perbaiki.
Pengujian Idempotency
Idempotency berarti melakukan operasi yang sama beberapa kali berturut-turut akan memiliki efek yang sama seperti melakukannya sekali.
Misalnya, pengaturan variabel x ke 5 adalah idempoten. Anda dapat menetapkan x hingga 5 satu kali atau satu juta kali. Ini masih akan 5. Namun, penambahan X oleh 1 tidak idempoten. Setiap kenaikan berurutan mengubah nilainya. Idempotency adalah properti yang sangat diinginkan dalam sistem terdistribusi dengan partisi jaringan sementara dan protokol pemulihan yang mencoba mengirim pesan beberapa kali jika tidak ada tanggapan segera.
Jika Anda mendesain idempotency ke dalam kode akses data Anda, Anda harus mengujinya. Ini biasanya sangat mudah. Untuk setiap operasi idempoten Anda memperpanjang untuk melakukan operasi dua kali atau lebih berturut-turut dan memverifikasi tidak ada kesalahan dan negara tetap sama.
Perhatikan bahwa desain idempoten terkadang dapat menyembunyikan kesalahan. Pertimbangkan untuk menghapus rekaman dari DB. Ini adalah operasi idempoten. Setelah Anda menghapus rekaman, catatan tidak ada lagi di sistem, dan mencoba menghapusnya lagi tidak akan mengembalikannya. Itu berarti bahwa mencoba menghapus catatan yang tidak ada adalah operasi yang valid. Tapi mungkin menutupi fakta bahwa kunci rekaman yang salah dilewatkan oleh si penelepon. Jika Anda mengembalikan pesan kesalahan maka itu tidak idempoten.
Pengujian Migrasi Data
Migrasi data dapat menjadi operasi yang sangat berisiko. Terkadang Anda menjalankan skrip atas semua data atau bagian penting dari data Anda dan melakukan beberapa operasi serius. Anda harus siap dengan rencana B jika terjadi kesalahan (misalnya kembali ke data asli dan mencari tahu apa yang salah).
Dalam banyak kasus, migrasi data dapat menjadi operasi yang lambat dan mahal yang mungkin memerlukan dua sistem berdampingan selama durasi migrasi. Saya berpartisipasi dalam beberapa migrasi data yang memakan waktu beberapa hari atau bahkan berminggu-minggu. Ketika menghadapi migrasi data besar-besaran, ada gunanya menginvestasikan waktu dan menguji migrasi itu sendiri pada sebagian kecil (tapi perwakilan) subset data Anda dan kemudian verifikasi bahwa data yang baru bermigrasi berlaku dan sistem dapat bekerja dengannya.
Menguji Data yang Hilang
Data yang hilang adalah masalah yang menarik. Terkadang data yang hilang akan melanggar integritas data Anda (misalnya Lagu yang penggunanya hilang), dan terkadang hilang (misalnya Seseorang menghapus pengguna dan semua lagu mereka).
Jika data yang hilang menyebabkan masalah integritas data maka Anda akan mendeteksinya dalam tes integritas data Anda. Namun, jika beberapa data hilang maka tidak ada cara mudah untuk mendeteksinya. Jika data tidak pernah masuk ke penyimpanan persisten maka mungkin ada jejak di log atau toko sementara lainnya.
Tergantung berapa banyak data yang hilang, Anda dapat menulis beberapa tes yang dengan sengaja menghapus beberapa data dari sistem Anda dan memverifikasi sistem berperilaku seperti yang diharapkan.
Kesimpulan
Pengujian kode intensif data memerlukan perencanaan yang disengaja dan pemahaman tentang persyaratan kualitas Anda. Anda dapat menguji pada beberapa tingkat abstraksi, dan pilihan Anda akan mempengaruhi seberapa menyeluruh dan komprehensif pengujian Anda, berapa banyak aspek dari lapisan data aktual Anda yang Anda uji, seberapa cepat pengujian Anda berjalan, dan betapa mudahnya memodifikasi pengujian Anda saat perubahan lapisan data.
Tidak ada jawaban yang benar. Anda perlu menemukan sweet spot Anda di sepanjang spektrum dari tes yang super komprehensif, lambat, dan padat karya untuk tes cepat dan ringan.