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

总览

这是关于使用Go测试数据密集型代码的教程系列中的五分之三。 在第二部分中,我介绍了基于流行的SQLite针对真实内存数据层进行的测试。 在本教程中,我将针对包含关系数据库和Redis缓存的本地复杂数据层进行测试。

针对本地数据层进行测试

针对内存数据层进行的测试非常棒。 测试速度很快,您可以完全控制。 但是有时您需要更接近生产数据层的实际配置。 以下是一些可能的原因:

  • 您使用要测试的关系数据库的特定详细信息。
  • 您的数据层由几个交互的数据存储组成。
  • 被测试的代码由访问同一数据层的多个进程组成。
  • 您想使用标准工具准备或观察测试数据。
  • 如果您的数据层不断变化,则您不想实现专用的内存中数据层。
  • 您只想知道您正在针对实际数据层进行测试。
  • 您需要使用内存中无法容纳的大量数据进行测试。

我敢肯定还有其他原因,但是您可以看到为什么在许多情况下仅使用内存数据层进行测试可能还不够的原因。

好。 因此,我们想测试一个实际的数据层。 但是我们仍然希望尽可能地轻量和敏捷。 这意味着本地数据层。 好处如下:

  • 无需在数据中心或云中配置和配置任何内容。
  • 无需担心我们的测试会意外破坏生产数据。
  • 无需在共享测试环境中与其他开发人员进行协调。
  • 网络通话不会慢下来。
  • 完全控制数据层的内容,并可以随时从头开始。

在本教程中,我们将进行事前准备。 我们将(部分实现)一个混合数据层,该数据层由MariaDB关系数据库和Redis服务器组成。 然后,我们将使用Docker来建立可在测试中使用的本地数据层。

使用Docker避免安装麻烦

首先,您当然需要Docker。 如果您不熟悉Docker,请查阅文档 。 下一步是获取我们的数据存储区的图像: MariaDBRedis 。 无需赘述,MariaDB是与MySQL兼容的出色关系数据库,而Redis是出色的内存键值存储(以及更多)。

> docker pull mariadb
...

> docker pull redis
...

> docker images
REPOSITORY      TAG      IMAGE ID      CREATED      SIZE
mariadb         latest   51d6a5e69fa7  2 weeks ago  402MB
redis           latest   b6dddb991dfa  2 weeks ago  107MB

现在,我们已经安装了Docker并具有用于MariaDB和Redis的映像,我们可以编写一个docker-compose.yml文件,该文件将用于启动数据存储。 让我们将数据库称为“ songify”。

mariadb-songify:
  image: mariadb:latest
  command: >
      --general-log 
      --general-log-file=/var/log/mysql/query.log
  expose:
    - "3306"
  ports:
    - "3306:3306"
  environment:
    MYSQL_DATABASE: "songify"
    MYSQL_ALLOW_EMPTY_PASSWORD: "true"
  volumes_from:
    - mariadb-data
mariadb-data:
  image: mariadb:latest
  volumes:
    - /var/lib/mysql
  entrypoint: /bin/bash

redis:
  image: redis
  expose:
    - "6379"
  ports:
    - "6379:6379"

您可以使用docker-compose up命令启动数据存储(类似于vagrant up )。 输出应如下所示:

> docker-compose up
Starting hybridtest_redis_1 ...
Starting hybridtest_mariadb-data_1 ...
Starting hybridtest_redis_1
Starting hybridtest_mariadb-data_1 ... done
Starting hybridtest_mariadb-songify_1 ...
Starting hybridtest_mariadb-songify_1 ... done
Attaching to hybridtest_mariadb-data_1, 
             hybridtest_redis_1, 
             hybridtest_mariadb-songify_1
.
.
.
redis_1  | * DB loaded from disk: 0.002 seconds
redis_1  | * Ready to accept connections
.
.
.
mariadb-songify_1  | [Note] mysqld: ready for connections.
.
.
.

此时,您有一个成熟的MariaDB服务器在端口3306上监听,而Redis服务器在端口6379(这两个都是标准端口)上监听。

混合数据层

让我们利用这些强大的数据存储,并将我们的数据层升级到一个混合数据层,该数据层在Redis中缓存每个用户的歌曲。 当GetSongsByUser() 在调用时,数据层将首先检查Redis是否已经为用户存储了歌曲。 如果这样做,则仅从Redis返回歌曲,但如果不这样做(缓存未命中),则它将从MariaDB提取歌曲并填充Redis缓存,因此可以为下一次准备。

这是结构和构造函数定义。 该结构保持像以前一样的数据库句柄以及一个redis客户端。 构造函数连接到关系数据库以及Redis。 它创建模式并仅在相应参数为true时刷新redis,这仅是测试所必需的。 在生产中,您只需创建一次架构(忽略架构迁移)。

type HybridDataLayer struct {
    db *sql.DB
	redis *redis.Client
}

func NewHybridDataLayer(dbHost string, 
                        dbPort int, 
                        redisHost string, 
                        createSchema bool, 
                        clearRedis bool) (*HybridDataLayer, 
                                          error) {
	dsn := fmt.Sprintf("root@tcp(%s:%d)/", dbHost, dbPort)
	if createSchema {
		err := createMariaDBSchema(dsn)
		if err != nil {
			return nil, err
		}
	}

	db, err := sql.Open("mysql", 
                         dsn+"desongcious?parseTime=true")
	if err != nil {
		return nil, err
	}

	redisClient := redis.NewClient(&redis.Options{
		Addr:     redisHost + ":6379",
		Password: "",
		DB:       0,
	})

	_, err = redisClient.Ping().Result()
	if err != nil {
		return nil, err
	}

	if clearRedis {
		redisClient.FlushDB()
	}

	return &HybridDataLayer{db, redisClient}, nil
}

使用MariaDB

就DDL而言,MariaDB和SQLite有所不同。 差异很小,但很重要。 Go没有像Python出色的SQLAlchemy那样成熟的跨数据库工具包,因此您必须自己进行管理(不,Gorm不算在内)。 主要区别在于:

  • SQL驱动程序是“ github.com/go-sql-driver/mysql”。
  • 数据库不存在于内存中,因此每次都将其重新创建(删除并创建)。
  • 模式必须是独立DDL语句的一部分,而不是所有语句的一个字符串。
  • 自动递增的主键由AUTO_INCREMENT标记。
  • VARCHAR而不是TEXT

这是代码:

func createMariaDBSchema(dsn string) error {
    db, err := sql.Open("mysql", dsn)
	if err != nil {
		return err
	}

	// Recreate DB
	commands := []string{
		"DROP DATABASE songify;",
		"CREATE DATABASE songify;",
	}
	for _, s := range (commands) {
		_, err = db.Exec(s)
		if err != nil {
			return err
		}
	}

	// Create schema
	db, err = sql.Open("mysql", dsn+"songify?parseTime=true")
	if err != nil {
		return err
	}

	schema := []string{
		`CREATE TABLE IF NOT EXISTS song (
		  id          INTEGER PRIMARY KEY AUTO_INCREMENT,
		  url         VARCHAR(2088) UNIQUE,
		  title       VARCHAR(100),
		  description VARCHAR(500)
		);`,
		`CREATE TABLE IF NOT EXISTS user (
		  id            INTEGER PRIMARY KEY AUTO_INCREMENT,
		  name          VARCHAR(100),
		  email         VARCHAR(100) 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 AUTO_INCREMENT,
		  name VARCHAR(100) 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)
		);`,
	}

	for _, s := range (schema) {
		_, err = db.Exec(s)
		if err != nil {
			return err
		}
	}
	return nil
}

使用Redis

从Go开始,Redis非常易于使用。 “ github.com/go-redis/redis”客户端库非常直观,忠实地遵循Redis命令。 例如,要测试某个密钥是否存在,您只需使用redis客户端的Exits()方法即可,该方法接受一个或多个密钥并返回存在的密钥数量。

在这种情况下,我只检查一个键:

count, err := m.redis.Exists(email).Result()
	if err != nil {
		return err
	}

测试对多个数据存储的访问

测试实际上是相同的。 界面未更改,行为也未更改。 唯一的变化是,实现现在在Redis中保留了一个缓存。 现在, GetSongsByEmail()方法仅调用refreshUser_Redis()

func (m *HybridDataLayer) GetSongsByUser(u User) (songs []Song, 
                                                  err error) {
    err = m.refreshUser_Redis(u.Email, &songs)
	return
}

refreshUser_Redis()方法从Redis返回用户歌曲(如果存在的话),否则从MariaDB获取它们。

type Songs *[]Song

func (m *HybridDataLayer) refreshUser_Redis(email string, 
                                            out Songs) error {
    count, err := m.redis.Exists(email).Result()
	if err != nil {
		return err
	}

	if count == 0 {
		err = m.getSongsByUser_DB(email, out)
		if err != nil {
			return err
		}

		for _, song := range *out {
			s, err := serializeSong(song)
			if err != nil {
				return err
			}

			_, err = m.redis.SAdd(email, s).Result()
			if err != nil {
				return err
			}
		}
		return
	}

	members, err := m.redis.SMembers(email).Result()
	for _, member := range members {
		song, err := deserializeSong([]byte(member))
		if err != nil {
			return err
		}
		*out = append(*out, song)
	}

	return out, nil
}

从测试方法论的角度来看,这里存在一个小问题。 当我们通过抽象数据层接口进行测试时,我们看不到数据层的实现。

例如,可能存在一个很大的缺陷,即数据层完全跳过缓存并始终从数据库中获取数据。 测试将通过,但我们无法从缓存中受益。 我将在第五部分中谈论测试缓存,这非常重要。

结论

在本教程中,我们介绍了对由多个数据存储(关系数据库和Redis缓存)组成的本地复杂数据层的测试。 我们还利用Docker轻松部署了多个数据存储以进行测试。

在第四部分中,我们将集中精力针对远程数据存储进行测试,使用生产数据快照进行测试,并生成我们自己的测试数据。 敬请关注!

翻译自: https://code.tutsplus.com/tutorials/testing-data-intensive-code-with-go-part-3--cms-29850

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值