How to Write Database-Agnostic Functions in GoLang to Make Them Easier to Unit-Test?

blov · 2017-10-23 02:00:08 · 654 次点击    
这是一个分享于 2017-10-23 02:00:08 的资源,其中的信息可能已经有所发展或是发生改变。

TLDR: What's the best way of unit-testing a function that uses a database as its datasource but that the datasource does not have to be a database (that is, the function isn't aware of where the data is coming from. e.g. it can be an object, etc.)?

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

I'm trying to find out what's the best practice for unit-testing functions that use a database as its datasource - which is passed through its parameter - but that the datasource does not have to be a database (e.g. it can be an object, etc.). The reason I add the clause, "that the datasource does not have to be a database," is because I don't want to use a database during unit-tests. So, how do I write a function that is database-agnostic?

One possible approach is to provide an "env" parameter that holds the datasource, like so (GoLang):

type Env struct {
    DataSource  interface{}
}

func FunctionToTest(env Env) {
    switch et := (env.DataSource).(type) {
    case UserDatasource:
        userSource := (env.DataSource).(UserDatasource)
        user := userSource.getUser()
    default:
        // Throw error
    }
}

func FunctionToTest2(env Env) {
    switch et := (env.DataSource).(type) {
    case CredentialsDatasource:
        credentialSource := (env.DataSource).(CredentialsDatasource)
        password := credentialSource.getPassword()
    default:
        // Throw error
    }
}

The issue with this is that it seems "hacky" and it feels like there exists a better solution. The reason I want to learn how to do this is so that I can write unit-tests that mock the database.

I appreciate any and all input.

Thanks in advance!


评论:

jasrags:

Why not just write an actual interface for your db functions then you can pass in a mock/dummy version in your tests?

softwaregav:

You would want to use an interface here. A common pattern used to abstract away where the data comes from is the repository pattern. Here is an article where you can read about the repository pattern and other tips for developing well-written Go applications.

package user
type User struct {
    ID int64
    Name string
}
type Repository interface {
    Find(id int64) (*User, error)
    Store(u *User) error
}
func ChangeUserName(id int64, name string, r Repository) error {
    u, err := r.Find(id)
    if err != nil {
        return err
    }
    u.Name = name
    err = r.Store(u)
    return err
}

Now you are able to pass in any struct with the methods matching your user.Repository interface. For example:

package mysql
type DB struct {
    *sql.DB
}
func New(db *sql.DB) *DB {
    return DB{db}
}
func (d *DB) Find(id int64) (*user.User, error) {
    // mysql stuff to find user
}
func (d *DB) Store(u *user.User) error {
    // mysql stuff to store user
}

then ...

package main
func main() {
    // ...
    // conn := code to open *sql.DB connection
    db := mysql.New(conn)
    err := user.ChangeUserName(1, 'bob', db)
    // ...
}

You can now also use a mock to test your function.

package mocks
type UserRepository struct {
    Users []*user.User
    ShouldError bool
}
func (r *UserRepository) Find(id int64) (*user.User, error) {
    if r.ShouldError {
        return nil, errors.New("")
    }
    for _, u := range r.Users {
        if u.ID == id {
            return u
        }
    }
    return errors.New("user not found")
}
func (r *UserRepository) Store(u *user.User) error {
    if r.ShouldError {
        return errors.New("")
    }
    r.Users = append(r.Users, u)
    return nil
}

then to test...

func Test_ChangeUserName_Stores_Changed_User(t *testing.T) {
    u := &user.User{ID: 1, Name: 'Bob'}
    r := mocks.UserRepository{Users: []*user.User{u}}
    err := user.ChangeUserName(1, 'Fred', r)
    // ...
}
ChristophBerger:

The blog post you referenced to actually inspired me to do a simplified writeup of the basics of using dependency injection in Go a while ago. It is basically what you laid out in the code above - an interface to define operations, a „real“ package and a „mock“ package, and the required injection happening in main() or in the test function, respectively. Bam, decoupling done.


入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

654 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传