Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

izumin5210 Engineer at Wantedly, Inc. Wantedly People ‣ Web Application Engineer - Server-side : Golang, Ruby, etc. - Web Frontend ‣ Interests in developer productivity on microservices

Slide 3

Slide 3 text

developer-friendly testing? ‣ ςετ͸ͳͥॻ͘ʁ ҆શɾ҆৺ͳػೳ௥ՃɾϦϑΝΫλϦϯά յΕͨ͜ͱʹ͙͢ؾ͚ͮͯศར ‣ EFWFMPQFSGSJFOEMZ ςετ͕خ͍͠ͷ͸ɼམͪͨͱ͖ མͪͨͱ͖ʹ௚͠΍͍͢ςετʹ͍ͨ͠ʂ

Slide 4

Slide 4 text

https://speakerdeck.com/mitchellh/advanced-testing-with-go What / How about Test methodology and Testablecode?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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) } } }

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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) } }

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

‣ ׬૸ͤͯ͋͛͞Δ 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͕ςετΛࢭΊͳ͍͔Β<ཁग़య>

Slide 11

Slide 11 text

ͲͷέʔεͰམͪͨʁ === RUN TestCreatePost --- FAIL: TestCreatePost (0.00s) main.go:42: CreatePost should not return errors: body shoud not be empty FAIL

Slide 12

Slide 12 text

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) } }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

‣ ໊લΛ͚ͭΑ͏ Ͳͷέʔε͕མ͔ͪͨΛ໌֬ʹ 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, } }

Slide 15

Slide 15 text

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) } + })

Slide 16

Slide 16 text

+ 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

Slide 17

Slide 17 text

‣ 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 } + })

Slide 18

Slide 18 text

Complare large object / complex object

Slide 19

Slide 19 text

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) } }

Slide 20

Slide 20 text

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":"[email protected]: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":"[email protected]:octocat/Hello-World.git", "node_id":"MDEwOlJlcG9zaXRvcnkxMjk2MjY5"} FAIL exit status 1 FAIL github.com/wantedly/yashima-rails 0.691s

Slide 21

Slide 21 text

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) } }

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

‣ 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

Slide 24

Slide 24 text

Golden Files Testing also called "Snapshot Testing" in other platforms

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

‣ 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;

Slide 27

Slide 27 text

HJUEJGGͰHPMEFOpMFΛݟΕ͹ɼมߋ͕Θ͔Δ

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

ίʔυॻ͘ͷ͸౰ͨΓલɻߏ଄ͷ੔ཧ੔಴΍Ϟσϧઃܭ͕޷͖ͳਓ8BOUFE We are hiring ! https://www.wantedly.com/projects/223823