Tips for develoepr-friendly testing #golangtokyo

Tips for develoepr-friendly testing #golangtokyo

9eed44f137609e6ce3b6f1e14f80b9e1?s=128

Masayuki Izumi

August 21, 2018
Tweet

Transcript

  1. Tips for developer friendly testing izumin5210 - golang.tokyo #17

  2. izumin5210 Engineer at Wantedly, Inc. Wantedly People ‣ Web Application

    Engineer - Server-side : Golang, Ruby, etc. - Web Frontend ‣ Interests in developer productivity on microservices
  3. developer-friendly testing? ‣ ςετ͸ͳͥॻ͘ʁ  ҆શɾ҆৺ͳػೳ௥ՃɾϦϑΝΫλϦϯά յΕͨ͜ͱʹ͙͢ؾ͚ͮͯศར ‣ EFWFMPQFSGSJFOEMZ 

    ςετ͕خ͍͠ͷ͸ɼམͪͨͱ͖ མͪͨͱ͖ʹ௚͠΍͍͢ςετʹ͍ͨ͠ʂ
  4. https://speakerdeck.com/mitchellh/advanced-testing-with-go What / How about Test methodology and Testablecode?

  5. Table Driven Tests also called "Parameterized Tests" in other platforms

  6. func TestCreatePost(t *testing.T) { cases := []struct { body string

    }{ { body: "awesome post", }, { body: "", }, } user := &User{ID: 1} for _, c := range cases { post, _ := user.CreatePost(c.body) if got, want := post.UserID, user.ID; got != want { t.Errorf("Created post has userID %d, want %d", got, want) } } }
  7. func TestCreatePost(t *testing.T) { cases := []struct { body string

    }{ { body: "awesome post", }, { body: "", }, } user := &User{ID: 1} for _, c := range cases { post, _ := user.CreatePost(c.body) if got, want := post.UserID, user.ID; got != want { t.Errorf("Created post has userID %d, want %d", got, want) } } } === RUN TestCreatePost --- FAIL: TestCreatePost (0.00s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0x106522] goroutine 5 [running]: testing.tRunner.func1(0x1055c0a0, 0xf) /usr/local/go/src/testing/testing.go:742 +0x380 panic(0x124240, 0x1cd288) /usr/local/go/src/runtime/panic.go:502 +0x2c0 main.TestCreatePost(0x1055c0a0, 0xbab699fc) /tmp/sandbox824737978/main.go:41 +0x82 testing.tRunner(0x1055c0a0, 0x1551cc) /usr/local/go/src/testing/testing.go:777 +0x160 created by testing.(*T).Run /usr/local/go/src/testing/testing.go:824 +0x320
  8. for _, c := range cases { - post, _

    := user.CreatePost(c.body) + post, err := user.CreatePost(c.body) + + if err != nil { + t.Fatalf("CreatePost should not return errors: %v", err) + } if got, want := post.UserID, user.ID; got != want { t.Errorf("Created post has userID %d, want %d", got, want) } }
  9. for _, c := range cases { - post, _

    := user.CreatePost(c.body) + post, err := user.CreatePost(c.body) + + if err != nil { + t.Fatalf("CreatePost should not return errors: %v", err) + } if got, want := post.UserID, user.ID; got != want { t.Errorf("Created post has userID %d, want %d", got, want) } } === RUN TestCreatePost --- FAIL: TestCreatePost (0.00s) main.go:42: CreatePost should not return errors: body shoud not be empty FAIL
  10. ‣ ׬૸ͤͯ͋͛͞Δ  A'BUBMA΍ADPOUJOVFAͳͲΛ͔͍ͭ QBOJDΛճආ͢Δ QBOJDʹͳΔͱͲ͜Ͱςετ͕མ͔ͪͨ΋ Θ͔ΓͮΒ͍ for _, c

    := range cases { - post, _ := user.CreatePost(c.body) + post, err := user.CreatePost(c.body) + + if err != nil { + t.Fatalf("CreatePost should not return error + } if got, want := post.UserID, user.ID; got != w t.Errorf("Created post has userID %d, want % } } ˞ Ұൠతʹɼςετέʔε಺ͷϧʔϓ͸ྑ͘ͳ͍ͱ͞ΕΔ (PͰڐ͞Ε͍ͯΔͷ͸ɼAU&SSPSA͕ςετΛࢭΊͳ͍͔Β<ཁग़య>
  11. ͲͷέʔεͰམͪͨʁ === RUN TestCreatePost --- FAIL: TestCreatePost (0.00s) main.go:42: CreatePost

    should not return errors: body shoud not be empty FAIL
  12. cases := []struct { + test string body string }{

    { + test: "with oneline body", body: "awesome post", }, { + test: "with empty body", body: "", }, } user := &User{ID: 1} for _, c := range cases { post, err := user.CreatePost(c.body) if err != nil { - t.Fatalf("CreatePost should not return errors: %v", err) + t.Fatalf("%s: CreatePost should not return errors: %v", c.test, err) } if got, want := post.UserID, user.ID; got != want { - t.Errorf("Created post has userID %d, want %d", got, want) + t.Errorf("%s: Created post has userID %d, want %d", c.test, got, want) } }
  13. cases := []struct { + test string body string }{

    { + test: "with oneline body", body: "awesome post", }, { + test: "with empty body", body: "", }, } user := &User{ID: 1} for _, c := range cases { post, err := user.CreatePost(c.body) if err != nil { - t.Fatalf("CreatePost should not return errors: %v", err) + t.Fatalf("%s: CreatePost should not return errors: %v", c.test, err) } if got, want := post.UserID, user.ID; got != want { - t.Errorf("Created post has userID %d, want %d", got, want) + t.Errorf("%s: Created post has userID %d, want %d", c.test, got, want) } } === RUN TestCreatePost --- FAIL: TestCreatePost (0.00s) main.go:42: with empty body: CreatePost should not return errors: body shoud not be empty FAIL
  14. ‣ ໊લΛ͚ͭΑ͏  Ͳͷέʔε͕མ͔ͪͨΛ໌֬ʹ AU&SSPSGAʹؚΊΔ͜ͱ΋Մೳ͚ͩͲɼ ͔ͳΓΊΜͲ͍͘͞ cases := []struct {

    + test string body string }{ { + test: "with oneline body", body: "awesome post", }, { + test: "with empty body", body: "", }, } user := &User{ID: 1} for _, c := range cases { post, err := user.CreatePost(c.body) if err != nil { - t.Fatalf("CreatePost should not return errors: %v", err) + t.Fatalf("%s: CreatePost should not return errors: %v", c.t } if got, want := post.UserID, user.ID; got != want { - t.Errorf("Created post has userID %d, want %d", got, want) + t.Errorf("%s: Created post has userID %d, want %d", c.test, } }
  15. TVCUFTU + t.Run(c.test, func(t *testing.T) { post, err := user.CreatePost(c.body)

    if err != nil { - t.Fatalf("%s: CreatePost should not return errors: %v", c.test, err) + t.Fatalf("CreatePost should not return errors: %v", err) } if got, want := post.UserID, user.ID; got != want { - t.Errorf("%s: Created post has userID %d, want %d", c.test, got, want) + t.Errorf("Created post has userID %d, want %d", got, want) } + })
  16. + t.Run(c.test, func(t *testing.T) { post, err := user.CreatePost(c.body) if

    err != nil { - t.Fatalf("%s: CreatePost should not return errors: %v", c.test, err) + t.Fatalf("CreatePost should not return errors: %v", err) } if got, want := post.UserID, user.ID; got != want { - t.Errorf("%s: Created post has userID %d, want %d", c.test, got, want) + t.Errorf("Created post has userID %d, want %d", got, want) } + }) === RUN TestCreatePost === RUN TestCreatePost/with_oneline_body === RUN TestCreatePost/with_empty_body --- FAIL: TestCreatePost (0.00s) --- PASS: TestCreatePost/with_oneline_body --- FAIL: TestCreatePost/with_empty_body ( main.go:46: with empty body: CreatePost s FAIL TVCUFTU
  17. ‣ TVCUFTUʹΘ͚Α͏ʢʁʣ  Ͳͷέʔε͕௨ͬͯΔ͔·Ͱݟ΍͍͢ ଟগఔ౓Φʔόʔϔου͕͋Δ<ཁग़య>ͷͰɼ ͓޷ΈͰ + t.Run(c.test, func(t *testing.T)

    { post, err := user.CreatePost(c.body) if err != nil { - t.Fatalf("%s: CreatePost should not return + t.Fatalf("CreatePost should not return erro } if got, want := post.UserID, user.ID; got != - t.Errorf("%s: Created post has userID %d, w + t.Errorf("Created post has userID %d, want } + })
  18. Complare large object / complex object

  19. func TestJSON(t *testing.T) { var got, want map[string]interface{} _ =

    json.Unmarshal([]byte(gotJSON), &got) _ = json.Unmarshal([]byte(wantJSON), &want) if !reflect.DeepEqual(got, want) { t.Errorf("Returned object is %#v, want %#v", got, want) } }
  20. func TestJSON(t *testing.T) { var got, want map[string]interface{} _ =

    json.Unmarshal([]byte(gotJSON), &got) _ = json.Unmarshal([]byte(wantJSON), &want) if !reflect.DeepEqual(got, want) { t.Errorf("Returned object is %#v, want %#v", got, want) } } --- FAIL: TestJSON (0.00s) main_test.go:17: Returned object is map[string]interface {}{"git_refs_url":"http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", "keys_url":"http://api.github.com/repos/ octocat/Hello-World/keys{/key_id}", "milestones_url":"http://api.github.com/repos/octocat/Hello-World/milestones{/number}", "notifications_url":"http://api.github.com/repos/octocat/Hello-World/ notifications{?since,all,participating}", "releases_url":"http://api.github.com/repos/octocat/Hello-World/releases{/id}", "comments_url":"http://api.github.com/repos/octocat/Hello-World/ comments{/number}", "commits_url":"http://api.github.com/repos/octocat/Hello-World/commits{/sha}", "contents_url":"http://api.github.com/repos/octocat/Hello-World/contents/{+path}", "stargazers_url":"http://api.github.com/repos/octocat/Hello-World/stargazers", "trees_url":"http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", "forks_url":"http://api.github.com/ repos/octocat/Hello-World/forks", "pulls_url":"http://api.github.com/repos/octocat/Hello-World/pulls{/number}", "git_commits_url":"http://api.github.com/repos/octocat/Hello-World/git/commits{/ sha}", "git_tags_url":"http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", "git_url":"git:github.com/octocat/Hello-World.git", "labels_url":"http://api.github.com/repos/octocat/ Hello-World/labels{/name}", "languages_url":"http://api.github.com/repos/octocat/Hello-World/languages", "archive_url":"http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", "collaborators_url":"http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", "events_url":"http://api.github.com/repos/octocat/Hello-World/events", "tags_url":"http:// api.github.com/repos/octocat/Hello-World/tags", "issue_events_url":"http://api.github.com/repos/octocat/Hello-World/issues/events{/number}", "issues_url":"http://api.github.com/repos/octocat/ Hello-World/issues{/number}", "id":1.296269e+06, "html_url":"https://github.com/octocat/Hello-World", "compare_url":"http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", "deployments_url":"http://api.github.com/repos/octocat/Hello-World/deployments", "issue_comment_url":"http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", "node_id":"MDEwOlJlcG9zaXRvcnkxMjk2MjY5", "name":"Hello-World", "assignees_url":"http://api.github.com/repos/octocat/Hello-World/assignees{/user}", "merges_url":"http://api.github.com/repos/ octocat/Hello-World/merges", "ssh_url":"git@github.com:octocat/Hello-World.git", "owner":map[string]interface {}{"gravatar_id":"", "repos_url":"https://api.github.com/users/octocat/repos", "site_admin":false, "node_id":"MDQ6VXNlcjE=", "avatar_url":"https://github.com/images/error/octocat_happy.gif", "following_url":"https://api.github.com/users/octocat/following{/other_user}", "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", "type":"User", "login":"octocat", "id":1, "organizations_url":"https://api.github.com/users/octocat/orgs", "events_url":"https://api.github.com/users/octocat/events{/privacy}", "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", "received_events_url":"https://api.github.com/ users/octocat/received_events", "url":"https://api.github.com/users/octocat", "html_url":"https://github.com/octocat", "followers_url":"https://api.github.com/users/octocat/followers", "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}"}, "private":true, "blobs_url":"http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", "subscribers_url":"http:// api.github.com/repos/octocat/Hello-World/subscribers", "subscription_url":"http://api.github.com/repos/octocat/Hello-World/subscription", "teams_url":"http://api.github.com/repos/octocat/Hello- World/teams", "fork":false, "url":"https://api.github.com/repos/octocat/Hello-World", "contributors_url":"http://api.github.com/repos/octocat/Hello-World/contributors", "downloads_url":"http:// api.github.com/repos/octocat/Hello-World/downloads", "statuses_url":"http://api.github.com/repos/octocat/Hello-World/statuses/{sha}", "full_name":"octocat/Hello-World", "description":"This your first repo!", "branches_url":"http://api.github.com/repos/octocat/Hello-World/branches{/branch}"}, want map[string]interface {}{"private":false, "description":"This your first repo!", "archive_url":"http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", "branches_url":"http://api.github.com/repos/octocat/Hello-World/branches{/branch}", "forks_url":"http:// api.github.com/repos/octocat/Hello-World/forks", "issue_comment_url":"http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", "releases_url":"http://api.github.com/repos/ octocat/Hello-World/releases{/id}", "full_name":"octocat/Hello-World", "subscribers_url":"http://api.github.com/repos/octocat/Hello-World/subscribers", "teams_url":"http://api.github.com/repos/ octocat/Hello-World/teams", "stargazers_url":"http://api.github.com/repos/octocat/Hello-World/stargazers", "languages_url":"http://api.github.com/repos/octocat/Hello-World/languages", "notifications_url":"http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", "compare_url":"http://api.github.com/repos/octocat/Hello-World/compare/{base}... {head}", "collaborators_url":"http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", "labels_url":"http://api.github.com/repos/octocat/Hello-World/labels{/name}", "milestones_url":"http://api.github.com/repos/octocat/Hello-World/milestones{/number}", "owner":map[string]interface {}{"avatar_url":"https://github.com/images/error/octocat_happy.gif", "gravatar_id":"", "followers_url":"https://api.github.com/users/octocat/followers", "id":1, "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", "received_events_url":"https://api.github.com/users/octocat/received_events", "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", "starred_url":"https://api.github.com/users/ octocat/starred{/owner}{/repo}", "type":"User", "login":"octocat", "node_id":"MDQ6VXNlcjE=", "url":"https://api.github.com/users/octocat", "repos_url":"https://api.github.com/users/octocat/ repos", "events_url":"https://api.github.com/users/octocat/events{/privacy}", "site_admin":false, "html_url":"https://github.com/octocat", "following_url":"https://api.github.com/users/octocat/ following{/other_user}", "organizations_url":"https://api.github.com/users/octocat/orgs"}, "html_url":"https://github.com/octocat/Hello-World", "fork":false, "url":"https://api.github.com/repos/ octocat/Hello-World", "downloads_url":"http://api.github.com/repos/octocat/Hello-World/downloads", "issue_events_url":"http://api.github.com/repos/octocat/Hello-World/issues/events{/number}", "pulls_url":"http://api.github.com/repos/octocat/Hello-World/pulls{/number}", "id":1.296269e+06, "contents_url":"http://api.github.com/repos/octocat/Hello-World/contents/{+path}", "git_refs_url":"http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", "merges_url":"http://api.github.com/repos/octocat/Hello-World/merges", "statuses_url":"http://api.github.com/ repos/octocat/Hello-World/statuses/{sha}", "subscription_url":"http://api.github.com/repos/octocat/Hello-World/subscription", "name":"Hello-World", "contributors_url":"http://api.github.com/ repos/octocat/Hello-World/contributors", "events_url":"http://api.github.com/repos/octocat/Hello-World/events", "trees_url":"http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", "comments_url":"http://api.github.com/repos/octocat/Hello-World/comments{/number}", "git_commits_url":"http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", "git_url":"git:github.com/octocat/Hello-World.git", "tags_url":"http://api.github.com/repos/octocat/Hello-World/tags", "deployments_url":"http://api.github.com/repos/octocat/Hello-World/ deployments", "assignees_url":"http://api.github.com/repos/octocat/Hello-World/assignees{/user}", "blobs_url":"http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", "commits_url":"http://api.github.com/repos/octocat/Hello-World/commits{/sha}", "git_tags_url":"http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", "issues_url":"http:// api.github.com/repos/octocat/Hello-World/issues{/number}", "keys_url":"http://api.github.com/repos/octocat/Hello-World/keys{/key_id}", "ssh_url":"git@github.com:octocat/Hello-World.git", "node_id":"MDEwOlJlcG9zaXRvcnkxMjk2MjY5"} FAIL exit status 1 FAIL github.com/wantedly/yashima-rails 0.691s
  21. github.com/google/go-cmp/cmp func TestJSONWithDiff(t *testing.T) { var got, want map[string]interface{} _

    = json.Unmarshal([]byte(gotJSON), &got) _ = json.Unmarshal([]byte(wantJSON), &want) if diff := cmp.Diff(got, want); diff != "" { t.Errorf("response json differs: (-got +want)\n%s", diff) } }
  22. github.com/google/go-cmp/cmp func TestJSONWithDiff(t *testing.T) { var got, want map[string]interface{} _

    = json.Unmarshal([]byte(gotJSON), &got) _ = json.Unmarshal([]byte(wantJSON), &want) if diff := cmp.Diff(got, want); diff != "" { t.Errorf("response json differs: (-got +want)\n%s", diff) } } --- FAIL: TestJSONWithDiff (0.00s) main_test.go:27: response json differs: (-got +want) root["private"]: -: true +: false FAIL exit status 1 FAIL github.com/wantedly/yashima-rails 0.691s
  23. ‣ HJUIVCDPNHPPHMFHPDNQ  Ͱ͔͍ΦϒδΣΫτͰ΋EJGG͚ͩݟΕΔ EFFQͳൺֱ΋ͯ͘͠ΕΔ ASFqFDU%FFQ&RVBMAΑΓTBGFΒ͍͠ func TestJSONWithDiff(t *testing.T) {

    var got, want map[string]interface{} _ = json.Unmarshal([]byte(gotJSON), &got) _ = json.Unmarshal([]byte(wantJSON), &want) if diff := cmp.Diff(got, want); diff != "" { t.Errorf("response json differs: (-got +want)\n%s", di } } --- FAIL: TestJSONWithDiff (0.00s) main_test.go:27: response json diff root["private"]: -: true +: false FAIL exit status 1 FAIL github.com/wantedly/yashima-rails
  24. Golden Files Testing also called "Snapshot Testing" in other platforms

  25. ‣ (PMEFOpMFTUFTUJOH  ςετ͍ͨ͠จࣈྻΛϑΝΠϧʹॻ͖ग़͢ ςετର৅ͱϑΝΠϧ͔ΒಡΈग़ͨ͠΋ͷͷ ൺֱΛߦ͏ ίʔυੜ੒πʔϧͭ͘Δͱ͖ʹศར

  26. ‣ DVQBMPZ4OBQTIPU5  HJUIVCDPNCSBEMFZKLFNQDVQBMPZ  ςετର৅ΛΘ͚ͨͩ͢ EJGG΋ग़ͯศར HJUIVCDPNJ[VNJOHSBQJͷ ίʔυੜ੒ςετʹར༻͍ͯ͠Δ ---

    FAIL: Test_ServiceGenerator/foo/Generate (0.00s) --- FAIL: Test_ServiceGenerator/foo/Generate/api/protos/foo.proto (0 service_test.go:236: snapshot not equal: --- Previous +++ Current @@ -1,3 +1,3 @@ syntax = "proto3"; -option go_package = "api_pb"; +option go_package = "testapp/api;api_pb"; package testapp.api;
  27. HJUEJGGͰHPMEFOpMFΛݟΕ͹ɼมߋ͕Θ͔Δ

  28. ‣ ςετͰ͸ʮյΕͨՕॴͷݟ͚ͭ΍͢͞ʯ΋ॏཁʂ ‣ (PͰ͸ɼʮςετΛ׬૸ͤ͞Δʯ͜ͱΛҙࣝ͢Δ ‣ ͍ΖΜͳख๏͕͋ΔͷͰɼQSPTDPOTཧղͯ͠ਖ਼͘͠࢖͍෼͚Α͏ʂ  5BCMF%SJWFO5FTUJOHͰ͸ɼͲͷ૊Έ߹ΘͤͰ໰୊͕ى͖ͨͷ͔Λݟ΍͘͢ʂ  Ͱ͔͍ΦϒδΣΫτͷൺֱ͸HPDNQͳͲΛར༻ͯ݁͠ՌΛݟ΍͘͢ʂ

     ίʔυੜ੒ܥͷπʔϧͰ͸(PMEFO'JMF5FTUTͰϨϏϡΞʔʹ΋༏͘͠ʂ
  29. ίʔυॻ͘ͷ͸౰ͨΓલɻߏ଄ͷ੔ཧ੔಴΍Ϟσϧઃܭ͕޷͖ͳਓ8BOUFE We are hiring ! https://www.wantedly.com/projects/223823