Skip to content

Commit 8ec06df

Browse files
author
badboyd
committed
golang-unit-test: my slide
1 parent 1aab2ac commit 8ec06df

14 files changed

+483
-0
lines changed

count.png

58.6 KB
Loading

cover.png

70.4 KB
Loading

coverage.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
% go test -cover
2+
PASS
3+
coverage: 92.9% of statements
4+
ok size 0.026s
5+
%

coverage0.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
% go test -cover
2+
PASS
3+
coverage: 92.9% of statements
4+
ok size 0.026s
5+
%

golang-unit-test.slide

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
Testing in Go
2+
2 Mar 2020
3+
4+
badboyd
5+
https://github.com/badboyd
6+
7+
* Purpose of this talk
8+
- Write unit test in golang
9+
10+
* Why unit test?
11+
- Get computers to check our work
12+
- Run them automatically (every time we change something)
13+
- End up with a suite of tests describing the functionality
14+
- Bold and robust refactoring
15+
- We know if we break something by mistake
16+
- We know what we've broken
17+
- We all do manual testing - takes time and effort >> make less effort
18+
19+
* Test
20+
21+
.play -edit test1.go /START OMIT/,/END OMIT/
22+
23+
* Table tests
24+
25+
.play -edit test2.go /START OMIT/,/END OMIT/
26+
27+
* Go test
28+
29+
- go test - runs tests and gives you output
30+
- go test ./... - run tests for multi-package projects
31+
- go test -cover
32+
33+
* T
34+
35+
The `*testing.T` argument is used for error reporting:
36+
37+
t.Errorf("got bar = %v, want %v", got, want)
38+
t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)
39+
t.Logf("iteration %v", i)
40+
41+
And enabling parallel tests:
42+
43+
t.Parallel()
44+
45+
And controlling whether a test runs at all:
46+
47+
if runtime.GOARCH == "arm" {
48+
t.Skip("this doesn't work on ARM")
49+
}
50+
51+
52+
* Test coverage
53+
54+
The `go` tool can report test coverage statistics.
55+
56+
$ go test -cover
57+
PASS
58+
coverage: 96.4% of statements
59+
ok strings 0.692s
60+
61+
The `go` tool can generate coverage profiles that may be intepreted by the `cover` tool.
62+
63+
$ go test -coverprofile=cover.out
64+
$ go tool cover -func=cover.out
65+
strings/reader.go: Len 66.7%
66+
strings/strings.go: TrimSuffix 100.0%
67+
... many lines omitted ...
68+
strings/strings.go: Replace 100.0%
69+
strings/strings.go: EqualFold 100.0%
70+
total: (statements) 96.4%
71+
72+
* Coverage visualization
73+
74+
$ go tool cover -html=cover.out
75+
76+
.image cover.png
77+
78+
* Setup and teardown
79+
.play -edit setupteardown.go
80+
81+
82+
* Test Main
83+
.play -edit testmain.go
84+
85+
86+
* Testing HTTP clients and servers
87+
88+
The `net/http/httptest` package provides helpers for testing code that makes or serves HTTP requests.
89+
90+
91+
* httptest.Server
92+
93+
An `httptest.Server` listens on a system-chosen port on the local loopback interface, for use in end-to-end HTTP tests.
94+
95+
type Server struct {
96+
URL string // base URL of form http://ipaddr:port with no trailing slash
97+
Listener net.Listener
98+
99+
// TLS is the optional TLS configuration, populated with a new config
100+
// after TLS is started. If set on an unstarted server before StartTLS
101+
// is called, existing fields are copied into the new config.
102+
TLS *tls.Config
103+
104+
// Config may be changed after calling NewUnstartedServer and
105+
// before Start or StartTLS.
106+
Config *http.Server
107+
}
108+
109+
func NewServer(handler http.Handler) *Server
110+
111+
func (*Server) Close() error
112+
113+
* httptest.Server in action
114+
115+
This code sets up a temporary HTTP server that serves a simple "Hello" response.
116+
117+
.play httpserver.go /START OMIT/,/STOP OMIT/
118+
119+
120+
* httptest.ResponseRecorder
121+
122+
`httptest.ResponseRecorder` is an implementation of `http.ResponseWriter` that records its mutations for later inspection in tests.
123+
124+
type ResponseRecorder struct {
125+
Code int // the HTTP response code from WriteHeader
126+
HeaderMap http.Header // the HTTP response headers
127+
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
128+
Flushed bool
129+
}
130+
131+
* httptest.ResponseRecorder in action
132+
133+
By passing a `ResponseRecorder` into an HTTP handler we can inspect the generated response.
134+
135+
.play httprecorder.go /START OMIT/,/STOP OMIT/
136+
137+
* FE
138+
.play table_test2.go /START OMIT/,/END OMIT/
139+
140+
* Race Detection
141+
142+
A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write.
143+
144+
To help diagnose such bugs, Go includes a built-in data race detector.
145+
146+
Pass the `-race` flag to the go tool to enable the race detector:
147+
148+
$ go test -race mypkg // to test the package
149+
$ go run -race mysrc.go // to run the source file
150+
$ go build -race mycmd // to build the command
151+
$ go install -race mypkg // to install the package

httprecorder.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"net/http/httptest"
8+
)
9+
10+
func main() {
11+
// START OMIT
12+
handler := func(w http.ResponseWriter, r *http.Request) {
13+
http.Error(w, "something failed", http.StatusInternalServerError)
14+
}
15+
16+
req, err := http.NewRequest("GET", "http://example.com/foo", nil)
17+
if err != nil {
18+
log.Fatal(err)
19+
}
20+
21+
w := httptest.NewRecorder()
22+
handler(w, req)
23+
24+
fmt.Printf("%d - %s", w.Code, w.Body.String())
25+
// STOP OMIT
26+
}

httpserver.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"log"
7+
"net/http"
8+
"net/http/httptest"
9+
)
10+
11+
func main() {
12+
// START OMIT
13+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
fmt.Fprintln(w, "Hello, client")
15+
}))
16+
defer ts.Close()
17+
18+
res, err := http.Get(ts.URL)
19+
if err != nil {
20+
log.Fatal(err)
21+
}
22+
23+
greeting, err := ioutil.ReadAll(res.Body)
24+
res.Body.Close()
25+
if err != nil {
26+
log.Fatal(err)
27+
}
28+
29+
fmt.Printf("%s", greeting)
30+
// STOP OMIT
31+
}

setupteardown.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import "testing"
4+
import "fmt"
5+
6+
func setup() {
7+
8+
}
9+
func teardown() {
10+
11+
}
12+
13+
func TestX(t *testing.T) {
14+
setup()
15+
defer teardown()
16+
17+
}
18+
19+
func main() {
20+
fmt.Println("man")
21+
}

table_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"io/ioutil"
6+
"net/http"
7+
"net/http/httptest"
8+
"strings"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
// START OMIT
15+
var tests = []struct {
16+
Method string
17+
Path string
18+
Body io.Reader
19+
BodyContains string
20+
Status int
21+
}{{
22+
Method: "GET",
23+
Path: "/things",
24+
BodyContains: "Hello Golang UK Conference",
25+
Status: http.StatusOK,
26+
}, {
27+
Method: "POST",
28+
Path: "/things",
29+
Body: strings.NewReader(`{"name":"Golang UK Conference"}`),
30+
BodyContains: "Hello Golang UK Conference",
31+
Status: http.StatusCreated,
32+
}}
33+
34+
// END OMIT
35+
func TestAll(t *testing.T) {
36+
assert := assert.New(t)
37+
server := httptest.NewServer(&myhandler{}) // HL
38+
defer server.Close() // HL
39+
for _, test := range tests {
40+
r, err := http.NewRequest(test.Method, server.URL+test.Path, test.Body) // HL
41+
assert.NoError(err)
42+
// call handler
43+
response, err := http.DefaultClient.Do(r) // HL
44+
assert.NoError(err)
45+
actualBody, err := ioutil.ReadAll(response.Body)
46+
assert.NoError(err)
47+
assert.NoError(response.Body.Close())
48+
// make assertions
49+
assert.Contains(actualBody, test.BodyContains)
50+
if test.Status > 0 {
51+
assert.Equal(test.Status, response.StatusCode, "status code")
52+
}
53+
}
54+
}
55+
56+
type myhandler struct{}
57+
58+
func (h *myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
59+
60+
}
61+
62+
func main() {
63+
var tests []testing.InternalTest
64+
tests = append(tests, testing.InternalTest{Name: "TestAll", F: TestAll})
65+
testing.Main(func(pat, str string) (bool, error) { return true, nil }, tests, nil, nil)
66+
}

table_test2.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"io/ioutil"
6+
"net/http"
7+
"net/http/httptest"
8+
"strings"
9+
"testing"
10+
11+
"github.com/cheekybits/is"
12+
)
13+
14+
var tests = []struct {
15+
Method string
16+
Path string
17+
Body io.Reader
18+
BodyContains string
19+
Status int
20+
}{{
21+
Method: "GET",
22+
Path: "/things",
23+
BodyContains: "Hello Gophers",
24+
Status: http.StatusOK,
25+
}, {
26+
Method: "POST",
27+
Path: "/things",
28+
Body: strings.NewReader(`{"name":"Gophers"}`),
29+
BodyContains: "Hello Gophers",
30+
Status: http.StatusCreated,
31+
}}
32+
33+
// START OMIT
34+
func TestAll(t *testing.T) {
35+
server := httptest.NewServer(&myhandler{}) // HL
36+
defer server.Close() // HL
37+
for _, test := range tests {
38+
t.Run(test.Method+" "+test.Path, func(t *testing.T) {
39+
is := is.New(t)
40+
r, err := http.NewRequest(test.Method, server.URL+test.Path, test.Body) // HL
41+
is.NoErr(err)
42+
// call handler
43+
response, err := http.DefaultClient.Do(r) // HL
44+
is.NoErr(err)
45+
actualBody, err := ioutil.ReadAll(response.Body)
46+
is.NoErr(err)
47+
// make assertions
48+
is.True(strings.Contains(string(actualBody), test.BodyContains)) // HL
49+
is.Equal(test.Status, response.StatusCode) // HL
50+
})
51+
}
52+
}
53+
54+
// END OMIT
55+
type myhandler struct{}
56+
57+
func (h *myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
58+
59+
}
60+
61+
func main() {
62+
var tests []testing.InternalTest
63+
tests = append(tests, testing.InternalTest{Name: "TestAll", F: TestAll})
64+
testing.Main(func(pat, str string) (bool, error) { return true, nil }, tests, nil, nil)
65+
}

0 commit comments

Comments
 (0)