使用Go测试数据密集型代码,第2部分

总览

这是测试数据密集型代码的教程系列中的五分之二。 在第一部分中,我介绍了支持适当测试的抽象数据层的设计,如何处理数据层中的错误,如何模拟数据访问代码以及如何针对抽象数据层进行测试。 在本教程中,我将针对基于流行的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_songuser_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_songuser_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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值