Advertisement
  1. Code
  2. Go

Pengujian Kode Data-Intensif Dengan Go, Bagian 1

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

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

Ikhtisar

Banyak sistem non-trivial juga bersifat data-intensif atau data-driven. Menguji bagian-bagian sistem yang bersifat data-intensif sangat berbeda dari pengujian sistem-sistem intensif-kode. Pertama, mungkin ada banyak kecanggihan di lapisan data itu sendiri, seperti penyimpanan data hibrid, caching, backup, dan redundansi.

Semua mesin ini tidak ada hubungannya dengan aplikasi itu sendiri, tetapi harus diuji. Kedua, kode mungkin sangat generik, dan untuk mengujinya, Anda perlu menghasilkan data yang terstruktur dengan cara tertentu. Dalam rangkaian lima tutorial ini, saya akan membahas semua aspek ini, menjelajahi beberapa strategi untuk merancang sistem data-intensif yang dapat diuji dengan Go, dan menyelami contoh-contoh spesifik.

Pada bagian pertama, saya akan membahas desain lapisan data abstrak yang memungkinkan pengujian yang tepat, bagaimana melakukan penanganan kesalahan pada lapisan data, cara meniru kode akses data, dan cara menguji terhadap lapisan data abstrak.

Pengujian Terhadap Lapisan Data

Berurusan dengan penyimpanan data nyata dan seluk-beluknya rumit dan tidak terkait dengan logika bisnis. Konsep lapisan data memungkinkan Anda untuk mengekspos antarmuka yang rapi ke data Anda dan menyembunyikan detail besar tentang bagaimana data disimpan dan bagaimana cara mengaksesnya. Saya akan menggunakan aplikasi sampel yang disebut "Songify" untuk manajemen musik pribadi untuk mengilustrasikan konsep dengan kode nyata.

Merancang Layer Data Abstrak

Mari tinjau domain pengelolaan musik pribadi—pengguna dapat menambahkan lagu dan memberi label mereka—dan mempertimbangkan data apa yang perlu kami simpan dan cara mengaksesnya. Objek di domain kami adalah pengguna, lagu, dan label. Ada dua kategori operasi yang ingin Anda lakukan pada data apa pun: kueri (read-only) dan perubahan status (create, update, delete). Berikut ini adalah antarmuka dasar untuk lapisan data:

1
package abstract_data_layer
2
3
import "time"
4
5
type Song struct {
6
    Url         string
7
  Name        string
8
	Description string
9
}
10
11
type Label struct {
12
	Name string
13
}
14
15
type User struct {
16
	Name         string
17
	Email        string
18
	RegisteredAt time.Time
19
	LastLogin    time.Time
20
}
21
22
type DataLayer interface {
23
	// Queries (read-only)

24
	GetUsers() ([]User, error)
25
	GetUserByEmail(email string) (User, error)
26
	GetLabels() ([]Label, error)
27
	GetSongs() ([]Song, error)
28
	GetSongsByUser(user User) ([]Song, error)
29
	GetSongsByLabel(label string) ([]Song, error)
30
31
	// State changing operations

32
	CreateUser(user User) error
33
	ChangeUserName(user User, name string) error
34
	AddLabel(label string) error
35
	AddSong(user User, song Song, labels []Label) error
36
}

Perhatikan bahwa tujuan dari model domain ini adalah menyajikan lapisan data sederhana namun tidak sepenuhnya sepele untuk menunjukkan aspek pengujian. Tentunya, dalam aplikasi nyata akan ada lebih banyak objek seperti album, genre, artis, dan banyak lagi informasi tentang setiap lagu. Jika dorongan datang untuk mendorong, Anda selalu dapat menyimpan informasi acak tentang lagu dalam deskripsinya, serta melampirkan sebanyak mungkin label yang Anda inginkan.

Dalam praktiknya, Anda mungkin ingin membagi lapisan data Anda menjadi beberapa antarmuka. Beberapa struct mungkin memiliki lebih banyak atribut, dan metode mungkin memerlukan lebih banyak argumen (misal. Semua metode GetXXX() mungkin akan memerlukan beberapa argumen paging). Anda mungkin memerlukan antarmuka dan metode akses data lainnya untuk operasi pemeliharaan seperti pemuatan massal, pencadangan, dan migrasi. Terkadang masuk akal untuk mengekspos antarmuka akses data asynchronous sebagai gantinya atau di samping antarmuka sinkron.

Apa yang kami peroleh dari lapisan data abstrak ini?

  • One-stop shop untuk operasi akses data.
  • Pandangan yang jelas tentang persyaratan manajemen data aplikasi kami dalam hal domain.
  • Kemampuan untuk mengubah implementasi lapisan data konkret sesuka hati.
  • Kemampuan untuk mengembangkan domain/logika bisnis lapisan awal terhadap antarmuka sebelum lapisan data konkret selesai atau stabil.
  • Terakhir tapi bukan yang akhir, kemampuan untuk meniru lapisan data untuk pengujian yang cepat dan fleksibel dari domain/logika bisnis.

Error dan Penanganan Error di Lapisan Data

Data dapat disimpan di beberapa toko data terdistribusi, di beberapa kluster di berbagai lokasi geografis dalam kombinasi pusat data on-premise dan cloud.

Akan ada kegagalan, dan kegagalan itu perlu ditangani. Idealnya, kesalahan penanganan logika (retries, timeout, pemberitahuan kegagalan bencana) dapat ditangani oleh lapisan data konkret. Kode logika domain hanya akan mendapatkan kembali data atau kesalahan umum ketika data tidak dapat dijangkau.

Dalam beberapa kasus, logika domain mungkin menginginkan akses yang lebih terperinci ke data dan memilih strategi penggantian dalam situasi tertentu (misalnya hanya sebagian data yang tersedia karena bagian dari kluster tidak dapat diakses, atau data sudah basi karena cache tidak disegarkan). Aspek-aspek tersebut memiliki implikasi untuk desain lapisan data Anda dan untuk pengujiannya.

Sejauh pengujian berjalan, Anda harus mengembalikan kesalahan Anda sendiri didefinisikan dalam lapisan data abstrak dan memetakan semua pesan kesalahan konkret ke jenis kesalahan Anda sendiri atau bergantung pada pesan kesalahan yang sangat generik.

Tiruan Kode Akses Data

Mari kita tiru lapisan data kita. Tujuan dari tiruan ini adalah untuk menggantikan lapisan data nyata selama tes. Yang membutuhkan lapisan data tiruan untuk mengekspos antarmuka yang sama dan untuk dapat menanggapi setiap urutan metode dengan respons yang direkam (atau dihitung).

Selain itu, berguna untuk melacak berapa kali setiap metode dipanggil. Saya tidak akan menunjukkannya di sini, tetapi bahkan dimungkinkan untuk melacak urutan panggilan ke metode yang berbeda dan argumen mana yang dilewatkan ke masing-masing metode untuk memastikan rantai panggilan tertentu.

Berikut adalah struktur lapisan data tiruan.

1
package concrete_data_layer
2
3
import (
4
    . "abstract_data_layer"
5
)
6
7
8
const (
9
	GET_USERS = iota
10
	GET_USER_BY_EMAIL 
11
	GET_LABELS          
12
	GET_SONGS
13
	GET_SONGS_BY_USER
14
	GET_SONG_BY_LABEL
15
	ERRORS            
16
)
17
18
type MockDataLayer struct {
19
	Errors                  []error
20
	GetUsersResponses       [][]User
21
	GetUserByEmailResponses []User
22
	GetLabelsResponses      [][]Label
23
	GetSongsResponses       [][]Song
24
	GetSongsByUserResponses [][]Song
25
	GetSongsByLabelResponses[][]Song
26
	Indices                 []int
27
}
28
29
func NewMockDataLayer() MockDataLayer {
30
	return MockDataLayer{Indices: []int{0, 0, 0, 0, 0, 0, 0, 0}}
31
}

Pernyataan const mencantumkan semua operasi yang didukung dan kesalahannya. Setiap operasi memiliki indeks tersendiri dalam irisan Indices. Indeks untuk setiap operasi mewakili berapa kali metode yang bersangkutan dipanggil serta apa tanggapan dan kesalahan berikutnya seharusnya.

Untuk setiap metode yang memiliki nilai kembalian selain kesalahan, ada potongan tanggapan. Ketika metode tiruan dipanggil, respons dan kesalahan yang sesuai (berdasarkan indeks untuk metode ini) dikembalikan. Untuk metode yang tidak memiliki nilai balik kecuali kesalahan, Anda tidak perlu menentukan potongan XXXResponses.

Perhatikan bahwa Kesalahan dibagi oleh semua metode. Itu berarti bahwa jika Anda ingin menguji serangkaian panggilan, Anda harus menyuntikkan jumlah kesalahan yang benar dalam urutan yang benar. Desain alternatif akan digunakan untuk setiap respons pasangan yang terdiri dari nilai dan kesalahan kembali. The NewMockDataLayer() mengembalikan fungsi struktur layer data tiruan baru dengan semua indeks diinisialisasi ke nol.

Berikut adalah implementasi metode GetUsers(), yang menggambarkan konsep-konsep ini.

1
func(m *MockDataLayer) GetUsers() (users []User, err error) {
2
    i := m.Indices[GET_USERS]
3
	users = m.GetUsersResponses[i]
4
	if len(m.Errors) > 0 {
5
		err = m.Errors[m.Indices[ERRORS]]
6
		m.Indices[ERRORS]++
7
	}
8
	m.Indices[GET_USERS]++
9
	return
10
}

Baris pertama mendapat indeks saat ini dari operasi GET_USERS (akan menjadi 0 awalnya).

Baris kedua mendapat respons untuk indeks saat ini.

Baris ketiga hingga kelima menugaskan kesalahan indeks saat ini jika bidang Errors diisi dan menaikkan indeks kesalahan. Saat menguji jalan yang bahagia, kesalahannya akan nihil. Untuk membuatnya lebih mudah digunakan, Anda dapat menghindari menginisialisasi bidang Errors dan kemudian setiap metode akan mengembalikan nil untuk kesalahan.

Baris berikutnya akan menambahkan indeks, sehingga panggilan berikutnya akan mendapatkan respons yang tepat.

Baris terakhir baru kembali. Nilai pengembalian bernama untuk pengguna dan kesalahan sudah diisi (atau nol secara default untuk kesalahan).

Berikut ini metode lain, GetLabels(), yang mengikuti pola yang sama. Satu-satunya perbedaan adalah indeks mana yang digunakan dan koleksi tanggapan rekaman apa yang digunakan.

1
func(m *MockDataLayer) GetLabels() (labels []Label, err error) {
2
    i := m.Indices[GET_LABELS]
3
	labels = m.GetLabelsResponses[i]
4
	if len(m.Errors) > 0 {
5
		err = m.Errors[m.Indices[ERRORS]]
6
		m.Indices[ERRORS]++
7
	}
8
	m.Indices[GET_LABELS]++
9
	return
10
}

Ini adalah contoh utama dari kasus penggunaan di mana generik dapat menyimpan banyak kode boiler. Mungkin untuk memanfaatkan refleksi untuk efek yang sama, tetapi di luar lingkup tutorial ini. Yang utama diambil di sini adalah bahwa lapisan data tiruan dapat mengikuti pola tujuan umum dan mendukung setiap skenario pengujian, seperti yang akan Anda lihat segera.

Bagaimana dengan beberapa metode yang hanya mengembalikan kesalahan? Lihat metode CreateUser(). Ini lebih sederhana karena hanya menangani kesalahan dan tidak perlu mengelola respons terekam.

1
func(m *MockDataLayer) CreateUser(user User) (err error) {
2
    if len(m.Errors) > 0 {
3
		i := m.Indices[CREATE_USER]
4
		err = m.Errors[m.Indices[ERRORS]]
5
		m.Indices[ERRORS]++
6
	}
7
	return
8
}

Lapisan data tiruan ini hanyalah contoh dari apa yang diperlukan untuk meniru antarmuka dan menyediakan beberapa layanan yang berguna untuk diuji. Anda dapat datang dengan implementasi tiruan Anda sendiri atau menggunakan perpustakaan tiruan yang tersedia. Bahkan ada framework GoMock standar.

Saya pribadi menemukan kerangka kerja tiruan yang mudah diimplementasikan dan lebih memilih untuk menggulung sendiri (sering menghasilkannya secara otomatis) karena saya menghabiskan sebagian besar waktu pengembangan saya menulis tes dan meniru dependensi. YMMV.

Pengujian Terhadap Lapisan Data Abstrak

Sekarang kita memiliki lapisan data tiruan, mari tulis beberapa tes terhadapnya. Sangat penting untuk menyadari bahwa di sini kita tidak menguji lapisan data itu sendiri. Kami akan menguji lapisan data itu sendiri dengan metode lain nantinya dalam seri ini. Tujuannya di sini adalah untuk menguji logika kode yang bergantung pada lapisan data abstrak.

Misalnya, anggap pengguna ingin menambahkan lagu, tetapi kami memiliki kuota 100 lagu per pengguna. Perilaku yang diharapkan adalah jika pengguna memiliki kurang dari 100 lagu dan lagu yang ditambahkan adalah baru, itu akan ditambahkan. Jika lagu sudah ada kemudian kembali kesalahan "Duplikasi lagu". Jika pengguna telah memiliki 100 lagu kemudian kembali kesalahan "Melebihi kuota lagu".

Mari menulis tes untuk kasus uji ini menggunakan lapisan data tiruan kami. Ini adalah tes kotak putih, yang berarti Anda perlu mengetahui metode mana dari lapisan data yang sedang diuji kode yang akan dipanggil dan di mana urutan sehingga Anda dapat mengisi tanggapan tiruan dan kesalahan dengan benar. Jadi pendekatan tes pertama tidak ideal di sini. Mari kita menulis kode pertama.

Berikut ini adalah SongManager struct. Itu tergantung hanya pada lapisan data abstrak. Yang akan memungkinkan Anda untuk lulus pelaksanaan lapisan data nyata dalam produksi, tetapi lapisan data tiruan selama pengujian.

SongManager sendiri benar-benar agnostik terhadap implementasi konkrit dari antarmuka DataLayerSongManager struct juga menerima pengguna, yang disimpannya. Agaknya, setiap pengguna aktif memiliki instance SongManager sendiri, dan pengguna hanya dapat menambahkan lagu untuk mereka sendiri. Fungsi NewSongManager() memastikan antarmuka DataLayer input tidak nol.

1
package song_manager
2
3
import (
4
    "errors"
5
	. "abstract_data_layer"
6
)
7
8
9
const (
10
	MAX_SONGS_PER_USER = 100
11
)
12
13
14
type SongManager struct {
15
    user User
16
	dal DataLayer
17
}
18
19
func NewSongManager(user User, 
20
                    dal DataLayer) (*SongManager, error) {
21
	if dal == nil {
22
		return nil, errors.New("DataLayer can't be nil")
23
	}
24
	return &SongManager{user, dal}, nil
25
}

Mari kita menerapkan metode AddSong(). Metode ini memanggil getSongsByUser() data layer pertama, dan kemudian melewati beberapa pemeriksaan. Jika semuanya OK, itu memanggil metode AddSong() data layer dan mengembalikan hasilnya.

1
func(lm *SongManager) AddSong(newSong Song, 
2
                              labels []Label) error {
3
    songs, err := lm.dal.GetSongsByUser(lm.user)
4
	if err != nil {
5
		return nil
6
	}
7
8
	// Check if song is a duplicate

9
	for _, song := range songs {
10
		if song.Url == newSong.Url {
11
			return errors.New("Duplicate song")
12
		}
13
	}
14
15
	// Check if user has max number of songs

16
	if len(songs) == MAX_SONGS_PER_USER {
17
		return errors.New("Song quota exceeded")
18
	}
19
20
	return lm.dal.AddSong(user, newSong, labels)
21
}

Melihat kode ini, Anda dapat melihat bahwa ada dua kasus uji lain yang kami abaikan: panggilan ke metode lapisan data GetSongByUser() dan AddSong() mungkin gagal karena alasan lain. Sekarang, dengan penerapan SongManager.AddSong() di depan kami, kami dapat menulis tes komprehensif yang mencakup semua kasus penggunaan. Mari kita mulai dengan jalan yang menyenangkan. Metode TestAddSong_Success() membuat pengguna bernama Gigi dan lapisan data tiruan.

Ini mengisi bidang GetSongsByUserResponses dengan irisan yang berisi irisan kosong, yang akan menghasilkan irisan kosong ketika SongManager memanggil GetSongsByUser() pada lapisan data tiruan tanpa kesalahan. Tidak perlu melakukan apa pun untuk panggilan ke metode AddSong() data tiruan, yang akan mengembalikan kesalahan nil secara default. Tes hanya memverifikasi bahwa memang tidak ada kesalahan yang dikembalikan dari panggilan parent ke metode SongManager AddSong().

1
package song_manager
2
3
import (
4
    "testing"
5
	. "abstract_data_layer"
6
	. "concrete_data_layer"
7
)
8
9
func TestAddSong_Success(t *testing.T) {
10
	u := User{Name:"Gigi", Email: "gg@gg.com"}
11
	mock := NewMockDataLayer()
12
	// Prepare mock responses

13
	mock.GetSongsByUserResponses = [][]Song{{}}
14
15
	lm, err := NewSongManager(u, &mock)
16
	if err != nil {
17
		t.Error("NewSongManager() returned 'nil'")
18
	}
19
    url := https://www.youtube.com/watch?v=MlW7T0SUH0E"

20
	err = lm.AddSong(Song{Url: url", Name: "Chacarron"}, nil)

21
	if err != nil {
22
		t.Error("AddSong() failed")
23
	}
24
}
25
26
$ go test
27
PASS
28
ok  	song_manager	0.006s

Kondisi kesalahan pengujian juga sangat mudah. Anda memiliki kontrol penuh pada apa yang mengembalikan lapisan data dari panggilan ke GetSongsByUser() dan AddSong(). Berikut ini adalah tes untuk memverifikasi bahwa ketika menambahkan duplikat lagu Anda mendapatkan pesan kesalahan yang benar kembali.

1
func TestAddSong_Duplicate(t *testing.T) {
2
    u := User{Name:"Gigi", Email: "gg@gg.com"}
3
4
	mock := NewMockDataLayer()
5
	// Prepare mock responses

6
	mock.GetSongsByUserResponses = [][]Song{{testSong}}
7
8
	lm, err := NewSongManager(u, &mock)
9
	if err != nil {
10
		t.Error("NewSongManager() returned 'nil'")
11
	}
12
13
	err = lm.AddSong(testSong, nil)
14
	if err == nil {
15
		t.Error("AddSong() should have failed")
16
	}
17
18
	if err.Error() != "Duplicate song" {
19
		t.Error("AddSong() wrong error: " + err.Error())
20
	}
21
}

Dua uji kasus berikut menguji bahwa pesan kesalahan yang benar dikembalikan ketika lapisan data itu sendiri gagal. Dalam kasus pertama, getSongsByUser() layer data mengembalikan error.

1
func TestAddSong_DataLayerFailure_1(t *testing.T) {
2
    u := User{Name:"Gigi", Email: "gg@gg.com"}
3
4
	mock := NewMockDataLayer()
5
	// Prepare mock responses
6
	mock.GetSongsByUserResponses = [][]Song{{}}
7
	e := errors.New("GetSongsByUser() failure")
8
	mock.Errors = []error{e}
9
10
	lm, err := NewSongManager(u, &mock)
11
	if err != nil {
12
		t.Error("NewSongManager() returned 'nil'")
13
	}
14
15
	err = lm.AddSong(testSong, nil)
16
	if err == nil {
17
		t.Error("AddSong() should have failed")
18
	}
19
20
	if err.Error() != "GetSongsByUser() failure" {
21
		t.Error("AddSong() wrong error: " + err.Error())
22
	}
23
}

Dalam kasus kedua, metode AddSong() layer data mengembalikan kesalahan. Karena panggilan pertama ke GetSongsByUser() harus berhasil, potongan mock.Errors berisi dua item: nil untuk panggilan pertama dan kesalahan untuk panggilan kedua.

1
func TestAddSong_DataLayerFailure_2(t *testing.T) {
2
    u := User{Name:"Gigi", Email: "gg@gg.com"}
3
4
	mock := NewMockDataLayer()
5
	// Prepare mock responses

6
	mock.GetSongsByUserResponses = [][]Song{{}}
7
	e := errors.New("AddSong() failure")
8
	mock.Errors = []error{nil, e}
9
10
	lm, err := NewSongManager(u, &mock)
11
	if err != nil {
12
		t.Error("NewSongManager() returned 'nil'")
13
	}
14
15
	err = lm.AddSong(testSong, nil)
16
	if err == nil {
17
		t.Error("AddSong() should have failed")
18
	}
19
20
	if err.Error() != "AddSong() failure" {
21
		t.Error("AddSong() wrong error: " + err.Error())
22
	}
23
}

Kesimpulan

Dalam tutorial ini, kami memperkenalkan konsep lapisan data abstrak. Kemudian, menggunakan domain pengelolaan musik pribadi, kami menunjukkan cara mendesain lapisan data, membuat lapisan data tiruan, dan menggunakan lapisan data tiruan untuk menguji aplikasi.

Di bagian dua, kami akan fokus pada pengujian menggunakan lapisan data nyata di-memori. Nantikan.

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.