总览
这是测试数据密集型代码的教程系列中的五分之二。 在第一部分中,我介绍了支持适当测试的抽象数据层的设计,如何处理数据层中的错误,如何模拟数据访问代码以及如何针对抽象数据层进行测试。 在本教程中,我将针对基于流行的SQLite的真实内存数据层进行测试。
针对内存中数据存储进行测试
针对抽象数据层进行测试非常适合某些需要高度精确性的用例,您可以准确了解要针对数据层进行测试的代码调用,并且可以准备模拟响应。
有时候,这并不容易。 对数据层的一系列调用可能很难确定,或者需要花费大量精力来准备有效的适当罐头响应。 在这些情况下,您可能需要处理内存中的数据存储。
内存中数据存储的好处是:
- 非常快。
- 您在实际的数据存储上工作。
- 您通常可以使用文件或代码从头开始填充它。
特别是如果您的数据存储是关系数据库,则SQLite是一个不错的选择。 只需记住,SQLite与其他流行的关系数据库(例如MySQL和PostgreSQL)之间存在差异。
确保在测试中考虑到这一点。 请注意,您仍然可以通过抽象数据层访问数据,但是现在测试过程中的后备存储是内存中数据存储。 您的测试将以不同的方式填充测试数据,但是被测试的代码很高兴不知道发生了什么。
使用SQLite
SQLite是一个嵌入式数据库(与您的应用程序链接)。 没有单独的数据库服务器在运行。 它通常将数据存储在文件中,但也可以选择内存中的后备存储。
这是InMemoryDataStore
结构。 它也是concrete_data_layer
程序包的一部分,并且导入了go-sqlite3第三方程序包,该程序包实现了标准Golang“数据库/ sql”接口。
package concrete_data_layer
import (
"database/sql"
. "abstract_data_layer"
_ "github.com/mattn/go-sqlite3"
"time"
"fmt"
)
type InMemoryDataLayer struct {
db *sql.DB
}
构造内存数据层
NewInMemoryDataLayer()
构造函数创建内存中的sqlite DB并返回指向InMemoryDataLayer
的指针。
func NewInMemoryDataLayer() (*InMemoryDataLayer, error) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
return nil, err
}
err = createSqliteSchema(db)
return &InMemoryDataLayer{db}, nil
}
请注意,每次打开新的“:memory:”数据库时,都是从头开始的。 如果要在对NewInMemoryDataLayer()
多次调用中NewInMemoryDataLayer()
持久性,则应使用file::memory:?cache=shared
。 有关更多详细信息,请参见此GitHub讨论线程 。
InMemoryDataLayer
实现DataLayer
接口,并将具有正确关系的数据实际存储在其sqlite数据库中。 为此,我们首先需要创建一个适当的模式,这恰好是构造函数中createSqliteSchema()
函数的工作。 它创建了三个数据表(song,user和label)以及两个交叉引用表label_song
和user_song
。
它添加了一些约束,索引和外键以将表彼此关联。 我不会详细说明具体细节。 其要旨是将整个模式DDL声明为单个字符串(由多个DDL语句组成),然后使用db.Exec()
方法执行该db.Exec()
,如果出现任何错误,则返回错误。
func createSqliteSchema(db *sql.DB) error {
schema := `
CREATE TABLE IF NOT EXISTS song (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT UNIQUE,
name TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT UNIQUE,
registered_at TIMESTAMP,
last_login TIMESTAMP
);
CREATE INDEX user_email_idx ON user(email);
CREATE TABLE IF NOT EXISTS label (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE
);
CREATE INDEX label_name_idx ON label(name);
CREATE TABLE IF NOT EXISTS label_song (
label_id INTEGER NOT NULL REFERENCES label(id),
song_id INTEGER NOT NULL REFERENCES song(id),
PRIMARY KEY (label_id, song_id)
);
CREATE TABLE IF NOT EXISTS user_song (
user_id INTEGER NOT NULL REFERENCES user(id),
song_id INTEGER NOT NULL REFERENCES song(id),
PRIMARY KEY(user_id, song_id)
);`
_, err := db.Exec(schema)
return err
}
重要的是要认识到,尽管SQL是标准的,但每个数据库管理系统(DBMS)都有其自己的风格,确切的模式定义不一定会像其他DB那样起作用。
实施内存中数据层
为了让您了解内存数据层的实现工作,这里有两种方法: AddSong()
和GetSongsByUser()
。
AddSong()
方法完成了大量工作。 它将一条记录插入到song
表以及每个参考表中: label_song
和user_song
。 在任何时候,如果任何操作失败,它只会返回一个错误。 我不使用任何事务,因为它仅用于测试目的,我也不担心数据库中的部分数据。
func (m *InMemoryDataLayer) AddSong(user User,
song Song,
labels []Label) error {
s := `INSERT INTO song(url, name, description)
values(?, ?, ?)`
statement, err := m.db.Prepare(s)
if err != nil {
return err
}
result, err := statement.Exec(song.Url,
song.Name,
song.Description)
if err != nil {
return err
}
songId, err := result.LastInsertId()
if err != nil {
return err
}
s = "SELECT id FROM user where email = ?"
rows, err := m.db.Query(s, user.Email)
if err != nil {
return err
}
var userId int
for rows.Next() {
err = rows.Scan(&userId)
if err != nil {
return err
}
}
s = `INSERT INTO user_song(user_id, song_id)
values(?, ?)`
statement, err = m.db.Prepare(s)
if err != nil {
return err
}
_, err = statement.Exec(userId, songId)
if err != nil {
return err
}
var labelId int64
s := "INSERT INTO label(name) values(?)"
label_ins, err := m.db.Prepare(s)
if err != nil {
return err
}
s = `INSERT INTO label_song(label_id, song_id)
values(?, ?)`
label_song_ins, err := m.db.Prepare(s)
if err != nil {
return err
}
for _, t := range labels {
s = "SELECT id FROM label where name = ?"
rows, err := m.db.Query(s, t.Name)
if err != nil {
return err
}
labelId = -1
for rows.Next() {
err = rows.Scan(&labelId)
if err != nil {
return err
}
}
if labelId == -1 {
result, err = label_ins.Exec(t.Name)
if err != nil {
return err
}
labelId, err = result.LastInsertId()
if err != nil {
return err
}
}
result, err = label_song_ins.Exec(labelId, songId)
if err != nil {
return err
}
}
return nil
}
GetSongsByUser()
使用user_song
交叉引用中的join +子选择来返回特定用户的歌曲。 它使用Query()
方法,然后在以后扫描每一行以从域对象模型填充Song
结构,并返回一首歌曲。 作为关系数据库的低层实现被安全地隐藏了。
func (m *InMemoryDataLayer) GetSongsByUser(u User) ([]Song,
error) {
s := `SELECT url, title, description FROM song L
INNER JOIN user_song UL ON UL.song_id = L.id
WHERE UL.user_id = (SELECT id from user
WHERE email = ?)`
rows, err := m.db.Query(s, u.Email)
if err != nil {
return nil, err
}
for rows.Next() {
var song Song
err = rows.Scan(&song.Url,
&song.Title,
&song.Description)
if err != nil {
return nil, err
}
songs = append(songs, song)
}
return songs, nil
}
这是一个很好的例子,利用像sqlite这样的真实关系数据库来实现内存中的数据存储,而不是滚动我们自己的关系数据库,这需要保留地图并确保所有簿记都是正确的。
针对SQLite运行测试
现在我们有了适当的内存数据层,下面让我们看一下测试。 我将这些测试放在一个单独的名为sqlite_test
包中,然后在本地导入抽象数据层(域模型),具体数据层(以创建内存数据层)和歌曲管理器(被测试的代码) 。 我还准备了两首激动人心的巴拿马艺术家El Chombo演唱的歌曲 !
package sqlite_test
import (
"testing"
. "abstract_data_layer"
. "concrete_data_layer"
. "song_manager"
)
const (
url1 = "https://www.youtube.com/watch?v=MlW7T0SUH0E"
url2 = "https://www.youtube.com/watch?v=cVFDlg4pbwM"
)
var testSong = Song{Url: url1, Name: "Chacaron"}
var testSong2 = Song{Url: url2, Name: "El Gato Volador"}
测试方法从头开始创建一个新的内存数据层,现在可以调用数据层上的方法以准备测试环境。 一切设置完成后,他们可以调用歌曲管理器方法,然后验证数据层是否包含预期状态。
例如, AddSong_Success()
测试方法创建一个用户,使用歌曲管理器的AddSong()
方法添加一首歌曲,并验证稍后调用GetSongsByUser()
返回所添加的歌曲。 然后添加另一首歌曲并再次验证。
func TestAddSong_Success(t *testing.T) {
u := User{Name: "Gigi", Email: "gg@gg.com"}
dl, err := NewInMemoryDataLayer()
if err != nil {
t.Error("Failed to create in-memory data layer")
}
err = dl.CreateUser(u)
if err != nil {
t.Error("Failed to create user")
}
lm, err := NewSongManager(u, dl)
if err != nil {
t.Error("NewSongManager() returned 'nil'")
}
err = lm.AddSong(testSong, nil)
if err != nil {
t.Error("AddSong() failed")
}
songs, err := dl.GetSongsByUser(u)
if err != nil {
t.Error("GetSongsByUser() failed")
}
if len(songs) != 1 {
t.Error(`GetSongsByUser() didn't return
one song as expected`)
}
if songs[0] != testSong {
t.Error("Added song doesn't match input song")
}
// Add another song
err = lm.AddSong(testSong2, nil)
if err != nil {
t.Error("AddSong() failed")
}
songs, err = dl.GetSongsByUser(u)
if err != nil {
t.Error("GetSongsByUser() failed")
}
if len(songs) != 2 {
t.Error(`GetSongsByUser() didn't return
two songs as expected`)
}
if songs[0] != testSong {
t.Error("Added song doesn't match input song")
}
if songs[1] != testSong2 {
t.Error("Added song doesn't match input song")
}
}
TestAddSong_Duplicate()
测试方法类似,但是它没有添加第二首歌曲,而是添加了同一首歌曲,这会导致重复的歌曲错误:
u := User{Name: "Gigi", Email: "gg@gg.com"}
dl, err := NewInMemoryDataLayer()
if err != nil {
t.Error("Failed to create in-memory data layer")
}
err = dl.CreateUser(u)
if err != nil {
t.Error("Failed to create user")
}
lm, err := NewSongManager(u, dl)
if err != nil {
t.Error("NewSongManager() returned 'nil'")
}
err = lm.AddSong(testSong, nil)
if err != nil {
t.Error("AddSong() failed")
}
songs, err := dl.GetSongsByUser(u)
if err != nil {
t.Error("GetSongsByUser() failed")
}
if len(songs) != 1 {
t.Error(`GetSongsByUser() didn't return
one song as expected`)
}
if songs[0] != testSong {
t.Error("Added song doesn't match input song")
}
// Add the same song again
err = lm.AddSong(testSong, nil)
if err == nil {
t.Error(`AddSong() should have failed for
a duplicate song`)
}
expectedErrorMsg := "Duplicate song"
errorMsg := err.Error()
if errorMsg != expectedErrorMsg {
t.Error(`AddSong() returned wrong error
message for duplicate song`)
}
}
结论
在本教程中,我们实现了一个基于SQLite的内存数据层,在内存SQLite数据库中填充了测试数据,并利用内存数据层对应用程序进行了测试。
在第三部分中,我们将重点针对包含多个数据存储(关系数据库和Redis缓存)的本地复杂数据层进行测试。 敬请关注。
翻译自: https://code.tutsplus.com/tutorials/testing-data-intensive-code-with-go-part-2--cms-29848