<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(&user)
if len(user.Username) < 2 || len(user.Username) > 30 {
return StatusError{400, errors.New("usernames need to be more than 2 characters and less than 30 characters")}
}
emailRe := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
if !emailRe.MatchString(user.Email) {
return StatusError{400, errors.New("invalid email address")}
}
if len(user.Password) < 8 {
return StatusError{400, errors.New("passwords need to be more at least 8 characters")}
}
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("mp").C("users")
err := conn.Insert(&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'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're not really testing the db insertion, and there's not much point in your tests anyway. All InsertUser does is call the db, so why bother to test if you'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'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'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'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'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'm developing. My application is set up so that in debug/test mode it'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's important that that logic isn'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("IS_TESTING") {
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'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 == "already@exists.com" {
return errors.New("doesn't matter")
}
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
}{
{"user1", "password", "new@user.io", 200, true},
{"user2", password", "already@exists.com", 422 true},
{"u", "password", "new@user.io", 400, false}, // username too short
{"uer1", "pass", "not an email", 400, false}, // not an email
{"uer1", "pass", "new@user.io", 400, false}, // password too short
}
for _, tc := range tests {
// call your PostUser function either directly or via a httptest.NewServer()
res := testRequest(t, ts, "POST", url, data)
if res.StatusCode != tc.statusCode {
t.Errorf("got http status %d, want: %d", res.StatusCode, tc.statusCode)
}
if !tc.invokeInsertUsers && datastore.InsertUsersInvoked {
t.Errorf("datastore.InsertUsers() invoked, expected: %v", 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
- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码`
- 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传