Need help with testing handler that requires a database

xuanbao · · 543 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>Hi all,</p> <p>I have a function that I want to test that looks like this (error handling has been omitted here):</p> <p>func PostUser(env *Env, w http.ResponseWriter, req *http.Request) error {</p> <pre><code>decoder := json.NewDecoder(req.Body) // reads in request body var user model.User decoder.Decode(&amp;user) if len(user.Username) &lt; 2 || len(user.Username) &gt; 30 { return StatusError{400, errors.New(&#34;usernames need to be more than 2 characters and less than 30 characters&#34;)} } emailRe := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) if !emailRe.MatchString(user.Email) { return StatusError{400, errors.New(&#34;invalid email address&#34;)} } if len(user.Password) &lt; 8 { return StatusError{400, errors.New(&#34;passwords need to be more at least 8 characters&#34;)} } hashedPassword,_ := bcrypt.GenerateFromPassword([]byte(user.Password), 12) env.DB.InsertUser(user.Username, hashedPassword, user.Email) userData,_ := json.Marshal(user) defer req.Body.Close() w.Write(userData) return nil } </code></pre> <p>My db.go looks like this:</p> <pre><code>type Datastore interface { InsertUsers() error } type DB struct { Session *mgo.Session } </code></pre> <p>My env.go looks like this:</p> <pre><code> type Env struct { DB *db.DB } </code></pre> <p>And my InsertUser func looks like this:</p> <pre><code>func (d *DB) InsertUser(username string, password []byte, email string) error { conn := d.Session.DB(&#34;mp&#34;).C(&#34;users&#34;) err := conn.Insert(&amp;model.User{username, password, email}) if err != nil { log.Print(err) return err } return nil } </code></pre> <p>So my question is, should I test PostUser and InsertUser separately? Also, what is the best way to mock out the database? Any example of how to write a test for these 2 functions would be great.</p> <hr/>**评论:**<br/><br/>very-little-gravitas: <pre><p>You don&#39;t have to mock out the db if you use a test db for tests ;) If you do so your integration tests would be closer to the real environment, which is a win, at the cost of slightly slower tests, otherwise you&#39;re not really testing the db insertion, and there&#39;s not much point in your tests anyway. All InsertUser does is call the db, so why bother to test if you&#39;re not using a real db?</p></pre>freetoplay123: <pre><p>Thanks for the tip. But what if I want to unit test the PostUser handler? Would I have to use some sort of mock database (assuming if I won&#39;t be using the real database), or can I somehow avoid calling the InsertUser function within the PostUser during the test?</p></pre>nkumar15: <pre><p>Yes if you are really interested in testing handler, then just mock the db, say a mockdb struct which defined all methods specficied in datastore interface. But instead of producing results from database it will produce/inserting result from/in an array of your data.</p></pre>very-little-gravitas: <pre><p>In this instance, if you mock the db in the function given, the test would only test that your mock was written correctly, as <em>every</em> line in the function is related to db access. </p> <p>The point of a unit test is to test data in against data out so it&#39;s suitable for functions without side effects, for example a function to transform a value (e.g. calculate tax), a method on a struct etc. You can&#39;t meaningfully unit test a function which relies almost entirely on external state. </p> <p>Handlers are better tested with integration tests which are testing the entire flow of your application (including authentication, db access, rendering templates). That way you&#39;re actually testing what will happen for end users when they hit that handler, not testing how good your mocks are. </p></pre>metamatic: <pre><p>I have a local database running on my machine when I&#39;m developing. My application is set up so that in debug/test mode it&#39;ll clear the database and load a known set of test data into the local database. I run the tests against that.</p> <p>I do this because the database itself enforces referential integrity and various other conditions, so it&#39;s important that that logic isn&#39;t ignored during testing.</p></pre>dlsniper: <pre><p>The best way for getting maximum unit test coverage is to have an if condition there:</p> <p><code> if !os.Getenv(&#34;IS_TESTING&#34;) { env.DB.InsertUser(user.Username, hashedPassword, user.Email); } </code></p> <p>The impact there is minimum, you only lose a line. If you have a lot of lines of code in your file, then it&#39;s perfect.</p></pre>brlag: <pre><p>Sometimes you have to have a certain percentage of unit test coverage no matter what your code covers and have to unit test without a test DB. In this case he would be unit testing that his code builds the proper insert for the InsertUser</p></pre>try2think1st: <pre><p>You could mock your database in a way that makes it possible to define the result of the InsertUsers() call for each test like this:</p> <pre><code>type Datastore struct { InsertUsersFn func(u *model.User) error InsertUsersInvoked bool } func (s *Datastore) InsertUsers(u *models.User) error { InsertUsersInvoked = true return s.InsertUsersFn(u) } </code></pre> <p>Now in your test you can define InsertUsersFn() and a table test containing all relevant cases that your handler should test for before actually invoking InsertUsers() or returning an error.</p> <pre><code>func TestPostUser(t *testing.T) { datastore.InsertUsersFn = func(u *model.User) error { if u.Email == &#34;already@exists.com&#34; { return errors.New(&#34;doesn&#39;t matter&#34;) } u.ID = 1 return nil } tests := []struct { username string password string email string statusCode int invokeInsertUsers bool // add more fields to check for more if needed }{ {&#34;user1&#34;, &#34;password&#34;, &#34;new@user.io&#34;, 200, true}, {&#34;user2&#34;, password&#34;, &#34;already@exists.com&#34;, 422 true}, {&#34;u&#34;, &#34;password&#34;, &#34;new@user.io&#34;, 400, false}, // username too short {&#34;uer1&#34;, &#34;pass&#34;, &#34;not an email&#34;, 400, false}, // not an email {&#34;uer1&#34;, &#34;pass&#34;, &#34;new@user.io&#34;, 400, false}, // password too short } for _, tc := range tests { // call your PostUser function either directly or via a httptest.NewServer() res := testRequest(t, ts, &#34;POST&#34;, url, data) if res.StatusCode != tc.statusCode { t.Errorf(&#34;got http status %d, want: %d&#34;, res.StatusCode, tc.statusCode) } if !tc.invokeInsertUsers &amp;&amp; datastore.InsertUsersInvoked { t.Errorf(&#34;datastore.InsertUsers() invoked, expected: %v&#34;, tc.invokeInsertUsers) } datastore.InsertUsersInvoked = false } } </code></pre> <p>This way you only test if your handler is behaving correctly. You can have another integration test with build tag integration to only test your structs against a real database when your schema changes.</p></pre>

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

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