Advertisement
  1. Code
  2. Go

Pengujian kode Data-intensif dengan Go, Bagian 2

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

() 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.

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.