├── example4 ├── .server.go.swp ├── .server_test.go.swp ├── server.go └── server_test.go ├── example1 ├── hello.go └── hello_test.go ├── example2 ├── gh_test.go └── gh.go └── example3 ├── job.go └── job_test.go /example4/.server.go.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanleclaire/testing-article/HEAD/example4/.server.go.swp -------------------------------------------------------------------------------- /example4/.server_test.go.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanleclaire/testing-article/HEAD/example4/.server_test.go.swp -------------------------------------------------------------------------------- /example1/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func hello() string { 8 | return "Hello, Testing!" 9 | } 10 | 11 | func main() { 12 | fmt.Println(hello()) 13 | } 14 | -------------------------------------------------------------------------------- /example1/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHello(t *testing.T) { 8 | expectedStr := "Hello, Testing!" 9 | result := hello() 10 | if result != expectedStr { 11 | t.Fatalf("Expected %s, got %s", expectedStr, result) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example4/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func mainHandler(w http.ResponseWriter, r *http.Request) { 10 | token := r.Header.Get("x-Access-Token") 11 | if token == "magic" { 12 | fmt.Fprintf(w, "You have some magic in you\n") 13 | log.Println("Allowed an access attempt") 14 | } else { 15 | http.Error(w, "You don't have enough magic in you", http.StatusForbidden) 16 | log.Println("Denied an access attempt") 17 | } 18 | } 19 | 20 | func main() { 21 | http.HandleFunc("/", mainHandler) 22 | log.Fatal(http.ListenAndServe(":8080", nil)) 23 | } 24 | -------------------------------------------------------------------------------- /example2/gh_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | type FakeReleaseInfoer struct { 6 | Tag string 7 | Err error 8 | } 9 | 10 | func (f FakeReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) { 11 | if f.Err != nil { 12 | return "", f.Err 13 | } 14 | 15 | return f.Tag, nil 16 | } 17 | 18 | func TestGetReleaseTagMessage(t *testing.T) { 19 | f := FakeReleaseInfoer{ 20 | Tag: "v0.1.0", 21 | Err: nil, 22 | } 23 | 24 | expectedMsg := "The latest release is v0.1.0" 25 | msg, err := getReleaseTagMessage(f, "dev/null") 26 | if err != nil { 27 | t.Fatalf("Expected err to be nil but it was %s", err) 28 | } 29 | 30 | if expectedMsg != msg { 31 | t.Fatalf("Expected %s but got %s", expectedMsg, msg) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example2/gh.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | type ReleasesInfo struct { 12 | Id uint `json:"id"` 13 | TagName string `json:"tag_name"` 14 | } 15 | 16 | type ReleaseInfoer interface { 17 | GetLatestReleaseTag(string) (string, error) 18 | } 19 | 20 | type GithubReleaseInfoer struct{} 21 | 22 | func (gh GithubReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) { 23 | apiUrl := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo) 24 | response, err := http.Get(apiUrl) 25 | if err != nil { 26 | return "", err 27 | } 28 | 29 | defer response.Body.Close() 30 | 31 | body, err := ioutil.ReadAll(response.Body) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | releases := []ReleasesInfo{} 37 | 38 | if err := json.Unmarshal(body, &releases); err != nil { 39 | return "", err 40 | } 41 | 42 | tag := releases[0].TagName 43 | 44 | return tag, nil 45 | } 46 | 47 | // Function to get the message to display to the end user. 48 | func getReleaseTagMessage(ri ReleaseInfoer, repo string) (string, error) { 49 | tag, err := ri.GetLatestReleaseTag(repo) 50 | if err != nil { 51 | return "", fmt.Errorf("Error querying GitHub API: %s", err) 52 | } 53 | 54 | return fmt.Sprintf("The latest release is %s", tag), nil 55 | } 56 | 57 | func main() { 58 | gh := GithubReleaseInfoer{} 59 | msg, err := getReleaseTagMessage(gh, "docker/machine") 60 | if err != nil { 61 | fmt.Fprintln(os.Stderr, err) 62 | os.Exit(1) 63 | } 64 | 65 | fmt.Println(msg) 66 | } 67 | -------------------------------------------------------------------------------- /example4/server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestMainHandler(t *testing.T) { 15 | rootRequest, err := http.NewRequest("GET", "/", nil) 16 | if err != nil { 17 | t.Fatal("Root request error: %s", err) 18 | } 19 | 20 | cases := []struct { 21 | w *httptest.ResponseRecorder 22 | r *http.Request 23 | accessTokenHeader string 24 | expectedResponseCode int 25 | expectedResponseBody []byte 26 | expectedLogs []string 27 | }{ 28 | { 29 | w: httptest.NewRecorder(), 30 | r: rootRequest, 31 | accessTokenHeader: "magic", 32 | expectedResponseCode: http.StatusOK, 33 | expectedResponseBody: []byte("You have some magic in you\n"), 34 | expectedLogs: []string{ 35 | "Allowed an access attempt\n", 36 | }, 37 | }, 38 | { 39 | w: httptest.NewRecorder(), 40 | r: rootRequest, 41 | accessTokenHeader: "", 42 | expectedResponseCode: http.StatusForbidden, 43 | expectedResponseBody: []byte("You don't have enough magic in you\n"), 44 | expectedLogs: []string{ 45 | "Denied an access attempt\n", 46 | }, 47 | }, 48 | } 49 | 50 | for _, c := range cases { 51 | logReader, logWriter := io.Pipe() 52 | bufLogReader := bufio.NewReader(logReader) 53 | log.SetOutput(logWriter) 54 | 55 | c.r.Header.Set("X-Access-Token", c.accessTokenHeader) 56 | 57 | go func() { 58 | for _, expectedLine := range c.expectedLogs { 59 | msg, err := bufLogReader.ReadString('\n') 60 | if err != nil { 61 | t.Errorf("Expected to be able to read from log but got error: %s", err) 62 | } 63 | if !strings.HasSuffix(msg, expectedLine) { 64 | t.Errorf("Log line didn't match suffix:\n\t%q\n\t%q", expectedLine, msg) 65 | } 66 | } 67 | }() 68 | 69 | mainHandler(c.w, c.r) 70 | 71 | if c.expectedResponseCode != c.w.Code { 72 | t.Errorf("Status Code didn't match:\n\t%q\n\t%q", c.expectedResponseCode, c.w.Code) 73 | } 74 | 75 | if !bytes.Equal(c.expectedResponseBody, c.w.Body.Bytes()) { 76 | t.Errorf("Body didn't match:\n\t%q\n\t%q", string(c.expectedResponseBody), c.w.Body.String()) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example3/job.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type Logger interface { 11 | Log(...interface{}) 12 | } 13 | 14 | type SuspendResumer interface { 15 | Suspend() error 16 | Resume() error 17 | } 18 | 19 | type Job interface { 20 | Logger 21 | SuspendResumer 22 | Run() error 23 | } 24 | 25 | type ServerPoller interface { 26 | PollServer() (string, error) 27 | } 28 | 29 | type PollerLogger struct{} 30 | 31 | type URLServerPoller struct { 32 | resourceUrl string 33 | } 34 | 35 | type PollSuspendResumer struct { 36 | SuspendCh chan bool 37 | ResumeCh chan bool 38 | } 39 | 40 | type PollerJob struct { 41 | WaitDuration time.Duration 42 | ServerPoller 43 | Logger 44 | *PollSuspendResumer 45 | } 46 | 47 | func NewPollerJob(resourceUrl string, waitDuration time.Duration) PollerJob { 48 | return PollerJob{ 49 | WaitDuration: waitDuration, 50 | Logger: &PollerLogger{}, 51 | ServerPoller: &URLServerPoller{ 52 | resourceUrl: resourceUrl, 53 | }, 54 | PollSuspendResumer: &PollSuspendResumer{ 55 | SuspendCh: make(chan bool), 56 | ResumeCh: make(chan bool), 57 | }, 58 | } 59 | } 60 | 61 | func (l *PollerLogger) Log(args ...interface{}) { 62 | log.Println(args...) 63 | } 64 | 65 | func (usp *URLServerPoller) PollServer() (string, error) { 66 | resp, err := http.Get(usp.resourceUrl) 67 | if err != nil { 68 | return "", err 69 | } 70 | 71 | return fmt.Sprint(usp.resourceUrl, " -- ", resp.Status), nil 72 | } 73 | 74 | func (ssr *PollSuspendResumer) Suspend() error { 75 | ssr.SuspendCh <- true 76 | return nil 77 | } 78 | 79 | func (ssr *PollSuspendResumer) Resume() error { 80 | ssr.ResumeCh <- true 81 | return nil 82 | } 83 | 84 | func (p PollerJob) Run() error { 85 | for { 86 | select { 87 | case <-p.PollSuspendResumer.SuspendCh: 88 | <-p.PollSuspendResumer.ResumeCh 89 | default: 90 | state, err := p.PollServer() 91 | if err != nil { 92 | p.Log("Error trying to get state: ", err) 93 | } else { 94 | p.Log(state) 95 | } 96 | 97 | time.Sleep(p.WaitDuration) 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func main() { 105 | var j Job 106 | j = NewPollerJob("http://nathanleclaire.com", 1*time.Second) 107 | go j.Run() 108 | time.Sleep(5 * time.Second) 109 | 110 | j.Log("Suspending monitoring of server for 5 seconds...") 111 | j.Suspend() 112 | time.Sleep(5 * time.Second) 113 | 114 | j.Log("Resuming job...") 115 | j.Resume() 116 | 117 | // Wait for a bit before exiting 118 | time.Sleep(5 * time.Second) 119 | } 120 | -------------------------------------------------------------------------------- /example3/job_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type ReadableLogger interface { 11 | Logger 12 | Read() string 13 | } 14 | 15 | type MessageReader struct { 16 | Msg string 17 | } 18 | 19 | func (mr *MessageReader) Read() string { 20 | return mr.Msg 21 | } 22 | 23 | type LastEntryLogger struct { 24 | *MessageReader 25 | } 26 | 27 | func (lel *LastEntryLogger) Log(args ...interface{}) { 28 | lel.Msg = fmt.Sprint(args...) 29 | } 30 | 31 | type DiscardFirstWriteLogger struct { 32 | *MessageReader 33 | writtenBefore bool 34 | } 35 | 36 | func (dfwl *DiscardFirstWriteLogger) Log(args ...interface{}) { 37 | if dfwl.writtenBefore { 38 | dfwl.Msg = fmt.Sprint(args...) 39 | } 40 | dfwl.writtenBefore = true 41 | } 42 | 43 | type FakeServerPoller struct { 44 | result string 45 | err error 46 | } 47 | 48 | func (fsp FakeServerPoller) PollServer() (string, error) { 49 | return fsp.result, fsp.err 50 | } 51 | 52 | func TestPollerJobRunLog(t *testing.T) { 53 | waitBeforeReading := 100 * time.Millisecond 54 | shortInterval := 20 * time.Millisecond 55 | longInterval := 200 * time.Millisecond 56 | 57 | testCases := []struct { 58 | p PollerJob 59 | logger ReadableLogger 60 | sp ServerPoller 61 | expectedMsg string 62 | }{ 63 | { 64 | p: NewPollerJob("madeup.website", shortInterval), 65 | logger: &LastEntryLogger{&MessageReader{}}, 66 | sp: FakeServerPoller{"200 OK", nil}, 67 | expectedMsg: "200 OK", 68 | }, 69 | { 70 | p: NewPollerJob("down.website", shortInterval), 71 | logger: &LastEntryLogger{&MessageReader{}}, 72 | sp: FakeServerPoller{"500 SERVER ERROR", nil}, 73 | expectedMsg: "500 SERVER ERROR", 74 | }, 75 | { 76 | p: NewPollerJob("error.website", shortInterval), 77 | logger: &LastEntryLogger{&MessageReader{}}, 78 | sp: FakeServerPoller{"", errors.New("DNS probe failed")}, 79 | expectedMsg: "Error trying to get state: DNS probe failed", 80 | }, 81 | { 82 | p: NewPollerJob("some.website", longInterval), 83 | 84 | // Discard first write since we want to verify that no 85 | // additional logs get made after the first one (time 86 | // out) 87 | logger: &DiscardFirstWriteLogger{MessageReader: &MessageReader{}}, 88 | 89 | sp: FakeServerPoller{"200 OK", nil}, 90 | expectedMsg: "", 91 | }, 92 | } 93 | 94 | for _, c := range testCases { 95 | c.p.Logger = c.logger 96 | c.p.ServerPoller = c.sp 97 | 98 | go c.p.Run() 99 | 100 | time.Sleep(waitBeforeReading) 101 | 102 | if c.logger.Read() != c.expectedMsg { 103 | t.Errorf("Expected message did not align with what was written:\n\texpected: %q\n\tactual: %q", c.expectedMsg, c.logger.Read()) 104 | } 105 | } 106 | } 107 | 108 | func TestPollerJobSuspendResume(t *testing.T) { 109 | p := NewPollerJob("foobar.com", 20*time.Millisecond) 110 | waitBeforeReading := 100 * time.Millisecond 111 | expectedLogLine := "200 OK" 112 | normalServerPoller := &FakeServerPoller{expectedLogLine, nil} 113 | 114 | logger := &LastEntryLogger{&MessageReader{}} 115 | p.Logger = logger 116 | p.ServerPoller = normalServerPoller 117 | 118 | // First start the job / polling 119 | go p.Run() 120 | 121 | time.Sleep(waitBeforeReading) 122 | 123 | if logger.Read() != expectedLogLine { 124 | t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", expectedLogLine, logger.Read()) 125 | } 126 | 127 | // Then suspend the job 128 | if err := p.Suspend(); err != nil { 129 | t.Errorf("Expected suspend error to be nil but got %q", err) 130 | } 131 | 132 | // Fake the log line to detect if poller is still running 133 | newExpectedLogLine := "500 Internal Server Error" 134 | logger.MessageReader.Msg = newExpectedLogLine 135 | 136 | // Give it a second to poll if it's going to poll 137 | time.Sleep(waitBeforeReading) 138 | 139 | // If this log writes, we know we are polling the server when we're not 140 | // supposed to (job should be suspended). 141 | if logger.Read() != newExpectedLogLine { 142 | t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", newExpectedLogLine, logger.Read()) 143 | } 144 | 145 | if err := p.Resume(); err != nil { 146 | t.Errorf("Expected resume error to be nil but got %q", err) 147 | } 148 | 149 | // Give it a second to poll if it's going to poll 150 | time.Sleep(waitBeforeReading) 151 | 152 | if logger.Read() != expectedLogLine { 153 | t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", expectedLogLine, logger.Read()) 154 | } 155 | } 156 | --------------------------------------------------------------------------------