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

blov · · 514 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p><strong>TLDR</strong>: What&#39;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&#39;t aware of where the data is coming from. e.g. it can be an object, etc.)?</p> <p>=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=</p> <p>I&#39;m trying to find out what&#39;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, &#34;that the datasource does not have to be a database,&#34; is because I don&#39;t want to use a database during unit-tests. So, how do I write a function that is database-agnostic?</p> <p>One possible approach is to provide an &#34;env&#34; parameter that holds the datasource, like so (GoLang):</p> <pre><code>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 } } </code></pre> <p>The issue with this is that it seems &#34;hacky&#34; 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.</p> <p>I appreciate any and all input.</p> <p>Thanks in advance!</p> <hr/>**评论:**<br/><br/>jasrags: <pre><p>Why not just write an actual interface for your db functions then you can pass in a mock/dummy version in your tests?</p></pre>softwaregav: <pre><p>You would want to use an interface here. A common pattern used to abstract away where the data comes from is the repository pattern. <a href="http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/" rel="nofollow">Here</a> is an article where you can read about the repository pattern and other tips for developing well-written Go applications.</p> <pre><code>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 } </code></pre> <p>Now you are able to pass in any struct with the methods matching your <code>user.Repository</code> interface. For example:</p> <pre><code>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 } </code></pre> <p>then ...</p> <pre><code>package main func main() { // ... // conn := code to open *sql.DB connection db := mysql.New(conn) err := user.ChangeUserName(1, &#39;bob&#39;, db) // ... } </code></pre> <p>You can now also use a mock to test your function.</p> <pre><code>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(&#34;&#34;) } for _, u := range r.Users { if u.ID == id { return u } } return errors.New(&#34;user not found&#34;) } func (r *UserRepository) Store(u *user.User) error { if r.ShouldError { return errors.New(&#34;&#34;) } r.Users = append(r.Users, u) return nil } </code></pre> <p>then to test...</p> <pre><code>func Test_ChangeUserName_Stores_Changed_User(t *testing.T) { u := &amp;user.User{ID: 1, Name: &#39;Bob&#39;} r := mocks.UserRepository{Users: []*user.User{u}} err := user.ChangeUserName(1, &#39;Fred&#39;, r) // ... } </code></pre></pre>ChristophBerger: <pre><p>The blog post you referenced to actually inspired me to do a simplified writeup of the basics of <a href="https://appliedgo.net/di" rel="nofollow">using dependency injection in Go</a> 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.</p></pre>

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

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