How can i mock database calls without a library?

huangapple go评论86阅读模式
英文:

How can i mock database calls without a library?

问题

我一直在努力理解单元测试、依赖注入、TDD等等的内容,但是我一直在测试那些涉及数据库调用的函数上卡住了。

假设你有一个接受Database接口的PostgresStore结构体,该接口具有Query()方法。

type PostgresStore struct {
	db Database
}

type Database interface {
	Query(query string, args ...interface{}) (*sql.Rows, error)
}

而你的PostgresStore有一个GetPatients方法,该方法调用了数据库查询。

func (p *PostgresStore) GetPatients() ([]Patient, error) {
	rows, err := p.db.Query("SELECT id, name, age, insurance FROM patients")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Patient{}
	for rows.Next() {
		var i Patient
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.Surname,
			&i.Age,
			&i.InsuranceCompany,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

在实际实现中,你只需将*sql.DB作为Database参数传递,但是你们如何使用一个虚假的数据库结构编写单元测试呢?

英文:

i've been trying to wrap my head around unit testing, dependency injection, tdd and all that stuff and i've been stuck on testing functions that make database calls, for example.

Let's say you have a PostgresStore struct that takes in a Database interface, which has a Query() method.

type PostgresStore struct {
	db Database
}

type Database interface {
	Query(query string, args ...interface{}) (*sql.Rows, error)
}

And your PostgresStore has a GetPatients method, which calls database query.

func (p *PostgresStore) GetPatients() ([]Patient, error) {
	rows, err := p.db.Query("SELECT id, name, age, insurance FROM patients")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Patient{}
	for rows.Next() {
		var i Patient
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.Surname,
			&i.Age,
			&i.InsuranceCompany,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

In the real implementation, you would just pass a *sql.DB as Database argument, but how would you guys write a unit test with a fake database struct?

答案1

得分: 2

让我尝试澄清一些你的疑问。首先,我将分享一个工作示例,以更好地理解正在发生的情况。然后,我将提到所有相关的方面。

repo/db.go

package repo

import "database/sql"

type Patient struct {
	ID               int
	Name             string
	Surname          string
	Age              int
	InsuranceCompany string
}

type PostgresStore struct {
	// 依赖于 Go 标准库中的 "database/sql" 包提供的通用 DB
	db *sql.DB
}

func (p *PostgresStore) GetPatient(id int) ([]Patient, error) {
	rows, err := p.db.Query("SELECT id, name, age, insurance FROM patients")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Patient{}
	for rows.Next() {
		var i Patient
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.Surname,
			&i.Age,
			&i.InsuranceCompany,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

在这里,唯一相关的更改是如何定义 PostgresStore 结构体。作为 db 字段,你应该依赖于 Go 标准库中的 database/sql 包提供的通用 DB。由于这一点,可以轻松地将其实现与虚拟的实现进行交换,正如我们将在后面看到的那样。

请注意,在 GetPatient 方法中,你接受一个 id 参数,但你没有使用它。你的查询更适合于像 GetAllPatients 这样的方法。请确保相应地进行修正。

repo/db_test.go

package repo

import (
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
)

func TestGetPatient(t *testing.T) {
	// 1. 设置虚拟数据库和模拟对象
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("不应出现错误: %v", err)
	}

	// 2. 配置模拟对象。我们期望的是什么(查询或命令)?结果是什么(错误或无错误)。
	rows := sqlmock.NewRows([]string{"id", "name", "surname", "age", "insurance"}).AddRow(1, "john", "doe", 23, "insurance-test")
	mock.ExpectQuery("SELECT id, name, age, insurance FROM patients").WillReturnRows(rows)

	// 3. 使用虚拟数据库实例化 PostgresStore
	sut := &PostgresStore{
		db: db,
	}

	// 4. 调用要测试的操作
	got, err := sut.GetPatient(1)

	// 5. 断言结果
	assert.Nil(t, err)
	assert.Contains(t, got, Patient{1, "john", "doe", 23, "insurance-test"})
}

在这里,有很多内容需要涵盖。首先,你可以查看代码中的注释,以更好地了解每个步骤。在代码中,我们依赖于 github.com/DATA-DOG/go-sqlmock 包,它允许我们轻松地模拟数据库客户端。

显然,这段代码的目的是给出一个实现你需求的一般思路。它可以以更好的方式编写,但对于在这种情况下编写测试来说,它是一个很好的起点。

如果有帮助,请告诉我,谢谢!

英文:

let me try to clarify some of your doubts. First of all, I'm gonna share a working example to better understand what's going on. Then, I'm gonna mention all of the relevant aspects.

repo/db.go

package repo

import "database/sql"

type Patient struct {
	ID               int
	Name             string
	Surname          string
	Age              int
	InsuranceCompany string
}

type PostgresStore struct {
	// rely on the generic DB provided by the "sql" package
	db *sql.DB
}

func (p *PostgresStore) GetPatient(id int) ([]Patient, error) {
	rows, err := p.db.Query("SELECT id, name, age, insurance FROM patients")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Patient{}
	for rows.Next() {
		var i Patient
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.Surname,
			&i.Age,
			&i.InsuranceCompany,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

Here, the only relevant change is how you define the PostgresStore struct. As the db field, you should rely on the generic DB provided by the database/sql package of the Go Standard Library. Thanks to this, it's trivial to swap its implementation with a fake one, as we're gonna see later.
> Please note that in the GetPatient method you're accepting an id parameter but you're not using it. Your query is more suitable to a method like GetAllPatients or something like that. Be sure to fix it accordingly.

repo/db_test.go

package repo

import (
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
)

func TestGetPatient(t *testing.T) {
	// 1. set up fake db and mock
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("err not expected: %v", err)
	}

	// 2. configure the mock. What we expect (query or command)? The outcome (error vs no error).
	rows := sqlmock.NewRows([]string{"id", "name", "surname", "age", "insurance"}).AddRow(1, "john", "doe", 23, "insurance-test")
	mock.ExpectQuery("SELECT id, name, age, insurance FROM patients").WillReturnRows(rows)

	// 3. instantiate the PostgresStore with the fake db
	sut := &PostgresStore{
		db: db,
	}

	// 4. invoke the action we've to test
	got, err := sut.GetPatient(1)

	// 5. assert the result
	assert.Nil(t, err)
	assert.Contains(t, got, Patient{1, "john", "doe", 23, "insurance-test"})
}

Here, there are a lot to cover. First, you can check the comments within the code that give you a better idea of each step. In the code, we're relying on the package github.com/DATA-DOG/go-sqlmock that allows us to easily mock a database client.

Obviously, the purpose of this code is to give a general idea on how to implement your needs. It can be written in a better way but it can be a good starting point for writing tests in this scenario.

Let me know if this helps, thanks!

huangapple
  • 本文由 发表于 2023年2月7日 20:17:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/75373245.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定