() translation by (you can also view the original English article)
Peninjauan
Ini adalah bagian dua dari lima seri tutorial pada pengujian kode data-intensif. Pada bagian satu, aku menutupi desain lapisan data abstrak yang memungkinkan pengujian yang tepat, bagaimana menangani kesalahan dalam lapisan data, bagaimana untuk mengolok-olok kode akses data, dan bagaimana untuk menguji melawan lapisan data abstrak. Dalam tutorial ini, aku akan pergi selama pengujian terhadap lapisan nyata di memori data berdasarkan SQLite populer.
Pengujian terhadap Penyimpanan Data di memori
Pengujian terhadap lapisan data abstrak besar untuk beberapa kasus digunakan di mana Anda perlu banyak presisi, Anda memahami persis apa panggilan kode di bawah ujian akan membuat terhadap lapisan data, dan Anda OK dengan mempersiapkan tanggapan tiruan.
Kadang-kadang, hal ini tidak semudah itu. Serangkaian panggilan ke lapisan data mungkin sulit untuk gambar, atau dibutuhkan banyak usaha untuk mempersiapkan tanggapan kalengan yang tepat yang berlaku. Dalam kasus ini, Anda mungkin perlu untuk bekerja melawan sebuah toko di memori data.
Manfaat dari sebuah toko di memori data adalah:
- Ini sangat cepat.
- Anda bekerja melawan sebuah toko data aktual.
- Anda sering dapat mengisinya dari awal menggunakan file atau kode.
Khususnya jika penyimpanan data Anda adalah DB relasional, maka SQLite adalah opsi yang fantastis. Hanya ingat bahwa ada perbedaan antara SQLite dan DB relasional populer lainnya seperti MySQL dan PostgreSQL.
Pastikan Anda memperhitungkan hal itu dalam tes Anda. Perhatikan bahwa Anda masih mengakses data Anda melalui lapisan data abstrak, tetapi sekarang backing store selama pengujian adalah penyimpanan data di memori. Tes Anda akan mengisi data uji secara berbeda, tetapi kode yang sedang diuji tidak menyadari apa yang sedang terjadi.
Menggunakan SQLite
SQLite adalah DB yang disematkan (terkait dengan aplikasi Anda). Tidak ada server DB terpisah yang berjalan. Ini biasanya menyimpan data dalam file, tetapi juga memiliki opsi dari backing store di memori.
Berikut adalah struct InMemoryDataStore
. Ini juga merupakan bagian dari paket concrete_data_layer
, dan itu mengimpor paket pihak ketiga go-sqlite3 yang mengimplementasikan antarmuka "database / sql" Golang standar.
1 |
package concrete_data_layer |
2 |
|
3 |
import ( |
4 |
"database/sql"
|
5 |
. "abstract_data_layer" |
6 |
_ "github.com/mattn/go-sqlite3" |
7 |
"time"
|
8 |
"fmt"
|
9 |
)
|
10 |
|
11 |
|
12 |
type InMemoryDataLayer struct { |
13 |
db *sql.DB |
14 |
}
|
Membangun Lapisan Data Dalam Memori
NewInMemoryDataLayer()
fungsi konstruktor menciptakan DB sqlite di memori dan mengembalikan pointer ke InMemoryDataLayer
.
1 |
func NewInMemoryDataLayer() (*InMemoryDataLayer, error) { |
2 |
db, err := sql.Open("sqlite3", ":memory:") |
3 |
if err != nil { |
4 |
return nil, err |
5 |
}
|
6 |
|
7 |
err = createSqliteSchema(db) |
8 |
|
9 |
return &InMemoryDataLayer{db}, nil |
10 |
}
|
Catatan bahwa setiap kali Anda membuka baru ": memori:" DB, Anda mulai dari awal. Jika Anda ingin ketekunan melintasi beberapa panggilan untuk NewInMemoryDataLayer()
, Anda harus menggunakan file::memory:?cache=shared
. Melihat thread diskusi GitHub untuk rincian lebih lanjut.
InMemoryDataLayer
mengimplementasikan antarmuka DataLayer
dan benar-benar menyimpan data dengan hubungan yang benar dalam sqlite database. Untuk melakukannya, pertama kita perlu membuat skema yang tepat, yang sebenarnya pekerjaan createSqliteSchema()
fungsi dalam konstruktor. Ini menciptakan tiga tabel data — lagu, pengguna, dan label — dan lintas-referensi dua tabel, label_song
dan user_song
.
Ia menambahkan beberapa kendala, indeks, dan kunci asing berhubungan tabel dengan satu sama lain. Aku tidak akan diam pada rincian tertentu. Inti dari itu adalah bahwa skema seluruh DDL dinyatakan sebagai string tunggal (terdiri dari beberapa pernyataan DDL) yang kemudian dijalankan menggunakan db. Metode db.Exec()
, dan jika ada yang tidak beres, mengembalikan kesalahan.
1 |
func createSqliteSchema(db *sql.DB) error { |
2 |
schema := ` |
3 |
CREATE TABLE IF NOT EXISTS song ( |
4 |
id INTEGER PRIMARY KEY AUTOINCREMENT, |
5 |
url TEXT UNIQUE, |
6 |
name TEXT, |
7 |
description TEXT |
8 |
);
|
9 |
CREATE TABLE IF NOT EXISTS user ( |
10 |
id INTEGER PRIMARY KEY AUTOINCREMENT, |
11 |
name TEXT, |
12 |
email TEXT UNIQUE, |
13 |
registered_at TIMESTAMP, |
14 |
last_login TIMESTAMP |
15 |
);
|
16 |
CREATE INDEX user_email_idx ON user(email); |
17 |
CREATE TABLE IF NOT EXISTS label ( |
18 |
id INTEGER PRIMARY KEY AUTOINCREMENT, |
19 |
name TEXT UNIQUE |
20 |
);
|
21 |
CREATE INDEX label_name_idx ON label(name); |
22 |
CREATE TABLE IF NOT EXISTS label_song ( |
23 |
label_id INTEGER NOT NULL REFERENCES label(id), |
24 |
song_id INTEGER NOT NULL REFERENCES song(id), |
25 |
PRIMARY KEY (label_id, song_id) |
26 |
);
|
27 |
CREATE TABLE IF NOT EXISTS user_song ( |
28 |
user_id INTEGER NOT NULL REFERENCES user(id), |
29 |
song_id INTEGER NOT NULL REFERENCES song(id), |
30 |
PRIMARY KEY(user_id, song_id) |
31 |
);` |
32 |
|
33 |
_, err := db.Exec(schema) |
34 |
return err |
35 |
}
|
Penting untuk menyadari bahwa sementara SQL standar, setiap sistem manajemen database (DBMS) memiliki citarasa tersendiri, dan tepat skema definisi tentu tidak akan bekerja untuk lain DB.
Melaksanakan Layer Data di memori
Untuk memberikan rasa upaya pelaksanaan lapisan di memori data, berikut adalah beberapa metode: AddSong()
dan GetSongsByUser()
.
Metode AddSong()
melakukan banyak pekerjaan. Itu memasukkan data ke dalam tabel lagu
serta ke masing-masing Tabel referensi: label_song
dan user_song
. Pada setiap titik, jika operasi gagal, hanya mengembalikan kesalahan. Saya tidak menggunakan transaksi karena itu dirancang untuk tujuan pengujian, dan saya tidak khawatir tentang sebagian data dalam DB.
1 |
func (m *InMemoryDataLayer) AddSong(user User, |
2 |
song Song, |
3 |
labels []Label) error { |
4 |
s := `INSERT INTO song(url, name, description) |
5 |
values(?, ?, ?)` |
6 |
statement, err := m.db.Prepare(s) |
7 |
if err != nil { |
8 |
return err |
9 |
}
|
10 |
|
11 |
result, err := statement.Exec(song.Url, |
12 |
song.Name, |
13 |
song.Description) |
14 |
if err != nil { |
15 |
return err |
16 |
}
|
17 |
|
18 |
songId, err := result.LastInsertId() |
19 |
if err != nil { |
20 |
return err |
21 |
}
|
22 |
|
23 |
s = "SELECT id FROM user where email = ?" |
24 |
rows, err := m.db.Query(s, user.Email) |
25 |
if err != nil { |
26 |
return err |
27 |
}
|
28 |
|
29 |
var userId int |
30 |
for rows.Next() { |
31 |
err = rows.Scan(&userId) |
32 |
if err != nil { |
33 |
return err |
34 |
}
|
35 |
}
|
36 |
|
37 |
s = `INSERT INTO user_song(user_id, song_id) |
38 |
values(?, ?)` |
39 |
statement, err = m.db.Prepare(s) |
40 |
if err != nil { |
41 |
return err |
42 |
}
|
43 |
|
44 |
_, err = statement.Exec(userId, songId) |
45 |
if err != nil { |
46 |
return err |
47 |
}
|
48 |
|
49 |
var labelId int64 |
50 |
s := "INSERT INTO label(name) values(?)" |
51 |
label_ins, err := m.db.Prepare(s) |
52 |
if err != nil { |
53 |
return err |
54 |
}
|
55 |
s = `INSERT INTO label_song(label_id, song_id) |
56 |
values(?, ?)` |
57 |
label_song_ins, err := m.db.Prepare(s) |
58 |
if err != nil { |
59 |
return err |
60 |
}
|
61 |
for _, t := range labels { |
62 |
s = "SELECT id FROM label where name = ?" |
63 |
rows, err := m.db.Query(s, t.Name) |
64 |
if err != nil { |
65 |
return err |
66 |
}
|
67 |
|
68 |
labelId = -1 |
69 |
for rows.Next() { |
70 |
err = rows.Scan(&labelId) |
71 |
if err != nil { |
72 |
return err |
73 |
}
|
74 |
}
|
75 |
|
76 |
if labelId == -1 { |
77 |
result, err = label_ins.Exec(t.Name) |
78 |
if err != nil { |
79 |
return err |
80 |
}
|
81 |
|
82 |
labelId, err = result.LastInsertId() |
83 |
if err != nil { |
84 |
return err |
85 |
}
|
86 |
}
|
87 |
|
88 |
result, err = label_song_ins.Exec(labelId, songId) |
89 |
if err != nil { |
90 |
return err |
91 |
}
|
92 |
}
|
93 |
|
94 |
return nil |
95 |
}
|
GetSongsByUser()
menggunakan join + Pilih sub dari referensi silang user_song
kembali lagu untuk pengguna tertentu. Menggunakan metode Query()
dan kemudian kemudian scan setiap baris untuk mengisi struct lagu
dari domain object model dan kembali sepotong lagu. Implementasi rendah sebagai DB relasional yang tersembunyi dengan aman.
1 |
func (m *InMemoryDataLayer) GetSongsByUser(u User) ([]Song, |
2 |
error) { |
3 |
s := `SELECT url, title, description FROM song L |
4 |
INNER JOIN user_song UL ON UL.song_id = L.id |
5 |
WHERE UL.user_id = (SELECT id from user |
6 |
WHERE email = ?)` |
7 |
rows, err := m.db.Query(s, u.Email) |
8 |
if err != nil { |
9 |
return nil, err |
10 |
} |
11 |
|
12 |
for rows.Next() { |
13 |
var song Song |
14 |
err = rows.Scan(&song.Url, |
15 |
&song.Title, |
16 |
&song.Description) |
17 |
if err != nil { |
18 |
return nil, err |
19 |
} |
20 |
|
21 |
songs = append(songs, song) |
22 |
} |
23 |
return songs, nil |
24 |
} |
Ini adalah contoh yang bagus dari memanfaatkan DB nyata relasional seperti sqlite untuk mengimplementasikan Toko di memori data vs bergulir kita sendiri, yang akan memerlukan menjaga maps dan memastikan semua pembukuan benar.
Menjalankan tes terhadap SQLite
Sekarang bahwa kita memiliki lapisan data di memori yang tepat, mari kita lihat di tes. Aku meletakkan tes ini dalam paket terpisah disebut sqlite_test
, dan mengimpor lokal lapisan data abstrak (domain model), lapisan data beton (untuk membuat lapisan data di memori), dan lagu manager (kode di bawah ujian). Saya juga menyiapkan dua lagu untuk tes dari artis Panama sensasional El Chombo!
1 |
package sqlite_test |
2 |
|
3 |
import ( |
4 |
"testing"
|
5 |
. "abstract_data_layer" |
6 |
. "concrete_data_layer" |
7 |
. "song_manager" |
8 |
)
|
9 |
|
10 |
const ( |
11 |
url1 = "https://www.youtube.com/watch?v=MlW7T0SUH0E" |
12 |
url2 = "https://www.youtube.com/watch?v=cVFDlg4pbwM" |
13 |
)
|
14 |
var testSong = Song{Url: url1, Name: "Chacaron"} |
15 |
var testSong2 = Song{Url: url2, Name: "El Gato Volador"} |
Metode uji membuat lapisan di memori data baru untuk memulai dari nol dan sekarang dapat memanggil metode lapisan data untuk mempersiapkan lingkungan pengujian. Ketika semuanya sudah diatur, mereka dapat memanggil metode manajer lagu dan kemudian memverifikasi bahwa lapisan data berisi negara diharapkan.
Sebagai contoh, metode pengujian AddSong_Success()
menciptakan pengguna, menambahkan lagu menggunakan Pengelola lagu AddSong()
metode, dan memverifikasi bahwa kemudian memanggil GetSongsByUser()
kembali lagu ditambahkan. Kemudian menambahkan lagu lain dan memverifikasi lagi.
1 |
func TestAddSong_Success(t *testing.T) { |
2 |
u := User{Name: "Gigi", Email: "gg@gg.com"} |
3 |
|
4 |
dl, err := NewInMemoryDataLayer() |
5 |
if err != nil { |
6 |
t.Error("Failed to create in-memory data layer") |
7 |
}
|
8 |
|
9 |
err = dl.CreateUser(u) |
10 |
if err != nil { |
11 |
t.Error("Failed to create user") |
12 |
}
|
13 |
|
14 |
lm, err := NewSongManager(u, dl) |
15 |
if err != nil { |
16 |
t.Error("NewSongManager() returned 'nil'") |
17 |
}
|
18 |
|
19 |
err = lm.AddSong(testSong, nil) |
20 |
if err != nil { |
21 |
t.Error("AddSong() failed") |
22 |
}
|
23 |
|
24 |
songs, err := dl.GetSongsByUser(u) |
25 |
if err != nil { |
26 |
t.Error("GetSongsByUser() failed") |
27 |
}
|
28 |
|
29 |
if len(songs) != 1 { |
30 |
t.Error(`GetSongsByUser() didn't return |
31 |
one song as expected`) |
32 |
}
|
33 |
|
34 |
if songs[0] != testSong { |
35 |
t.Error("Added song doesn't match input song") |
36 |
}
|
37 |
|
38 |
// Add another song
|
39 |
err = lm.AddSong(testSong2, nil) |
40 |
if err != nil { |
41 |
t.Error("AddSong() failed") |
42 |
}
|
43 |
|
44 |
songs, err = dl.GetSongsByUser(u) |
45 |
if err != nil { |
46 |
t.Error("GetSongsByUser() failed") |
47 |
}
|
48 |
|
49 |
if len(songs) != 2 { |
50 |
t.Error(`GetSongsByUser() didn't return |
51 |
two songs as expected`) |
52 |
}
|
53 |
|
54 |
if songs[0] != testSong { |
55 |
t.Error("Added song doesn't match input song") |
56 |
}
|
57 |
if songs[1] != testSong2 { |
58 |
t.Error("Added song doesn't match input song") |
59 |
}
|
60 |
}
|
Metode pengujian TestAddSong_Duplicate()
serupa, tetapi bukan menambahkan lagu baru untuk kedua kalinya, ia menambahkan lagu yang sama, yang mengakibatkan kesalahan duplikat lagu:
1 |
u := User{Name: "Gigi", Email: "gg@gg.com"} |
2 |
|
3 |
dl, err := NewInMemoryDataLayer() |
4 |
if err != nil { |
5 |
t.Error("Failed to create in-memory data layer") |
6 |
}
|
7 |
|
8 |
err = dl.CreateUser(u) |
9 |
if err != nil { |
10 |
t.Error("Failed to create user") |
11 |
}
|
12 |
|
13 |
lm, err := NewSongManager(u, dl) |
14 |
if err != nil { |
15 |
t.Error("NewSongManager() returned 'nil'") |
16 |
}
|
17 |
|
18 |
err = lm.AddSong(testSong, nil) |
19 |
if err != nil { |
20 |
t.Error("AddSong() failed") |
21 |
}
|
22 |
|
23 |
songs, err := dl.GetSongsByUser(u) |
24 |
if err != nil { |
25 |
t.Error("GetSongsByUser() failed") |
26 |
}
|
27 |
|
28 |
if len(songs) != 1 { |
29 |
t.Error(`GetSongsByUser() didn't return |
30 |
one song as expected`) |
31 |
}
|
32 |
|
33 |
if songs[0] != testSong { |
34 |
t.Error("Added song doesn't match input song") |
35 |
}
|
36 |
|
37 |
// Add the same song again
|
38 |
err = lm.AddSong(testSong, nil) |
39 |
if err == nil { |
40 |
t.Error(`AddSong() should have failed for |
41 |
a duplicate song`) |
42 |
}
|
43 |
|
44 |
expectedErrorMsg := "Duplicate song" |
45 |
errorMsg := err.Error() |
46 |
if errorMsg != expectedErrorMsg { |
47 |
t.Error(`AddSong() returned wrong error |
48 |
message for duplicate song`) |
49 |
}
|
50 |
}
|
Kesimpulan
Dalam tutorial ini, kami menerapkan lapisan di memori data berdasarkan SQLite, penduduknya database SQLite di memori dengan data uji, dan dimanfaatkan lapisan di memori data untuk menguji aplikasi.
Dalam bagian tiga, kita akan fokus pada pengujian terhadap lapisan lokal data yang kompleks yang terdiri dari beberapa data toko (DB relasional dan Redis cache). Menantikan.