mirror of
https://github.com/quii/learn-go-with-tests.git
synced 2026-02-01 17:47:03 +00:00
go fmt code block to reduce formating errors (#350)
I used gofmtmd package: ```sh ls -1 *.md | xargs -I % sh -c 'echo %; gofmtmd % -r' ``` There is some errors, in file that could not be parsed
This commit is contained in:
parent
a6fcd41941
commit
d2e1cf2ef6
@ -22,14 +22,14 @@ import "testing"
|
||||
|
||||
func TestSum(t *testing.T) {
|
||||
|
||||
numbers := [5]int{1, 2, 3, 4, 5}
|
||||
numbers := [5]int{1, 2, 3, 4, 5}
|
||||
|
||||
got := Sum(numbers)
|
||||
want := 15
|
||||
got := Sum(numbers)
|
||||
want := 15
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -57,7 +57,7 @@ In `sum.go`
|
||||
package main
|
||||
|
||||
func Sum(numbers [5]int) int {
|
||||
return 0
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
@ -69,11 +69,11 @@ Your test should now fail with _a clear error message_
|
||||
|
||||
```go
|
||||
func Sum(numbers [5]int) int {
|
||||
sum := 0
|
||||
for i := 0; i < 5; i++ {
|
||||
sum += numbers[i]
|
||||
}
|
||||
return sum
|
||||
sum := 0
|
||||
for i := 0; i < 5; i++ {
|
||||
sum += numbers[i]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
```
|
||||
|
||||
@ -87,11 +87,11 @@ Let's introduce [`range`](https://gobyexample.com/range) to help clean up our co
|
||||
|
||||
```go
|
||||
func Sum(numbers [5]int) int {
|
||||
sum := 0
|
||||
for _, number := range numbers {
|
||||
sum += number
|
||||
}
|
||||
return sum
|
||||
sum := 0
|
||||
for _, number := range numbers {
|
||||
sum += number
|
||||
}
|
||||
return sum
|
||||
}
|
||||
```
|
||||
|
||||
@ -125,27 +125,27 @@ declaring them
|
||||
```go
|
||||
func TestSum(t *testing.T) {
|
||||
|
||||
t.Run("collection of 5 numbers", func(t *testing.T) {
|
||||
numbers := [5]int{1, 2, 3, 4, 5}
|
||||
t.Run("collection of 5 numbers", func(t *testing.T) {
|
||||
numbers := [5]int{1, 2, 3, 4, 5}
|
||||
|
||||
got := Sum(numbers)
|
||||
want := 15
|
||||
got := Sum(numbers)
|
||||
want := 15
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("collection of any size", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3}
|
||||
t.Run("collection of any size", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3}
|
||||
|
||||
got := Sum(numbers)
|
||||
want := 6
|
||||
got := Sum(numbers)
|
||||
want := 6
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
@ -169,11 +169,11 @@ In our case, no-one else is using our function so rather than having two functio
|
||||
|
||||
```go
|
||||
func Sum(numbers []int) int {
|
||||
sum := 0
|
||||
for _, number := range numbers {
|
||||
sum += number
|
||||
}
|
||||
return sum
|
||||
sum := 0
|
||||
for _, number := range numbers {
|
||||
sum += number
|
||||
}
|
||||
return sum
|
||||
}
|
||||
```
|
||||
|
||||
@ -190,27 +190,27 @@ We had already refactored `Sum` and all we've done is changing from arrays to sl
|
||||
```go
|
||||
func TestSum(t *testing.T) {
|
||||
|
||||
t.Run("collection of 5 numbers", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
t.Run("collection of 5 numbers", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
|
||||
got := Sum(numbers)
|
||||
want := 15
|
||||
got := Sum(numbers)
|
||||
want := 15
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("collection of any size", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3}
|
||||
t.Run("collection of any size", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3}
|
||||
|
||||
got := Sum(numbers)
|
||||
want := 6
|
||||
got := Sum(numbers)
|
||||
want := 6
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %d want %d given, %v", got, want, numbers)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
@ -263,12 +263,12 @@ or
|
||||
```go
|
||||
func TestSumAll(t *testing.T) {
|
||||
|
||||
got := SumAll([]int{1,2}, []int{0,9})
|
||||
want := []int{3, 9}
|
||||
got := SumAll([]int{1, 2}, []int{0, 9})
|
||||
want := []int{3, 9}
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -284,7 +284,7 @@ Go can let you write [_variadic functions_](https://gobyexample.com/variadic-fun
|
||||
|
||||
```go
|
||||
func SumAll(numbersToSum ...[]int) (sums []int) {
|
||||
return
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
@ -300,12 +300,12 @@ useful for seeing if _any_ two variables are the same.
|
||||
```go
|
||||
func TestSumAll(t *testing.T) {
|
||||
|
||||
got := SumAll([]int{1,2}, []int{0,9})
|
||||
want := []int{3, 9}
|
||||
got := SumAll([]int{1, 2}, []int{0, 9})
|
||||
want := []int{3, 9}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -318,12 +318,12 @@ temporarily change the test to:
|
||||
```go
|
||||
func TestSumAll(t *testing.T) {
|
||||
|
||||
got := SumAll([]int{1,2}, []int{0,9})
|
||||
want := "bob"
|
||||
got := SumAll([]int{1, 2}, []int{0, 9})
|
||||
want := "bob"
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -343,14 +343,14 @@ What we need to do is iterate over the varargs, calculate the sum using our
|
||||
|
||||
```go
|
||||
func SumAll(numbersToSum ...[]int) []int {
|
||||
lengthOfNumbers := len(numbersToSum)
|
||||
sums := make([]int, lengthOfNumbers)
|
||||
lengthOfNumbers := len(numbersToSum)
|
||||
sums := make([]int, lengthOfNumbers)
|
||||
|
||||
for i, numbers := range numbersToSum {
|
||||
sums[i] = Sum(numbers)
|
||||
}
|
||||
for i, numbers := range numbersToSum {
|
||||
sums[i] = Sum(numbers)
|
||||
}
|
||||
|
||||
return sums
|
||||
return sums
|
||||
}
|
||||
```
|
||||
|
||||
@ -374,12 +374,12 @@ returning a new slice with all the items in it.
|
||||
|
||||
```go
|
||||
func SumAll(numbersToSum ...[]int) []int {
|
||||
var sums []int
|
||||
for _, numbers := range numbersToSum {
|
||||
sums = append(sums, Sum(numbers))
|
||||
}
|
||||
var sums []int
|
||||
for _, numbers := range numbersToSum {
|
||||
sums = append(sums, Sum(numbers))
|
||||
}
|
||||
|
||||
return sums
|
||||
return sums
|
||||
}
|
||||
```
|
||||
|
||||
@ -394,12 +394,12 @@ all the items apart from the first one \(the "head"\)
|
||||
|
||||
```go
|
||||
func TestSumAllTails(t *testing.T) {
|
||||
got := SumAllTails([]int{1,2}, []int{0,9})
|
||||
want := []int{2, 9}
|
||||
got := SumAllTails([]int{1, 2}, []int{0, 9})
|
||||
want := []int{2, 9}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -417,13 +417,13 @@ Rename the function to `SumAllTails` and re-run the test
|
||||
|
||||
```go
|
||||
func SumAllTails(numbersToSum ...[]int) []int {
|
||||
var sums []int
|
||||
for _, numbers := range numbersToSum {
|
||||
tail := numbers[1:]
|
||||
sums = append(sums, Sum(tail))
|
||||
}
|
||||
var sums []int
|
||||
for _, numbers := range numbersToSum {
|
||||
tail := numbers[1:]
|
||||
sums = append(sums, Sum(tail))
|
||||
}
|
||||
|
||||
return sums
|
||||
return sums
|
||||
}
|
||||
```
|
||||
|
||||
@ -446,23 +446,23 @@ capture all elements from `myEmptySlice[1:]`?
|
||||
```go
|
||||
func TestSumAllTails(t *testing.T) {
|
||||
|
||||
t.Run("make the sums of some slices", func(t *testing.T) {
|
||||
got := SumAllTails([]int{1,2}, []int{0,9})
|
||||
want := []int{2, 9}
|
||||
t.Run("make the sums of some slices", func(t *testing.T) {
|
||||
got := SumAllTails([]int{1, 2}, []int{0, 9})
|
||||
want := []int{2, 9}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
})
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("safely sum empty slices", func(t *testing.T) {
|
||||
got := SumAllTails([]int{}, []int{3, 4, 5})
|
||||
want := []int{0, 9}
|
||||
t.Run("safely sum empty slices", func(t *testing.T) {
|
||||
got := SumAllTails([]int{}, []int{3, 4, 5})
|
||||
want := []int{0, 9}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
})
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
@ -482,17 +482,17 @@ works, runtime errors are our enemies because they affect our users.
|
||||
|
||||
```go
|
||||
func SumAllTails(numbersToSum ...[]int) []int {
|
||||
var sums []int
|
||||
for _, numbers := range numbersToSum {
|
||||
if len(numbers) == 0 {
|
||||
sums = append(sums, 0)
|
||||
} else {
|
||||
tail := numbers[1:]
|
||||
sums = append(sums, Sum(tail))
|
||||
}
|
||||
}
|
||||
var sums []int
|
||||
for _, numbers := range numbersToSum {
|
||||
if len(numbers) == 0 {
|
||||
sums = append(sums, 0)
|
||||
} else {
|
||||
tail := numbers[1:]
|
||||
sums = append(sums, Sum(tail))
|
||||
}
|
||||
}
|
||||
|
||||
return sums
|
||||
return sums
|
||||
}
|
||||
```
|
||||
|
||||
@ -503,24 +503,24 @@ Our tests have some repeated code around assertion again, let's extract that int
|
||||
```go
|
||||
func TestSumAllTails(t *testing.T) {
|
||||
|
||||
checkSums := func(t *testing.T, got, want []int) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
checkSums := func(t *testing.T, got, want []int) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("make the sums of tails of", func(t *testing.T) {
|
||||
got := SumAllTails([]int{1, 2}, []int{0, 9})
|
||||
want := []int{2, 9}
|
||||
checkSums(t, got, want)
|
||||
})
|
||||
t.Run("make the sums of tails of", func(t *testing.T) {
|
||||
got := SumAllTails([]int{1, 2}, []int{0, 9})
|
||||
want := []int{2, 9}
|
||||
checkSums(t, got, want)
|
||||
})
|
||||
|
||||
t.Run("safely sum empty slices", func(t *testing.T) {
|
||||
got := SumAllTails([]int{}, []int{3, 4, 5})
|
||||
want := []int{0, 9}
|
||||
checkSums(t, got, want)
|
||||
})
|
||||
t.Run("safely sum empty slices", func(t *testing.T) {
|
||||
got := SumAllTails([]int{}, []int{3, 4, 5})
|
||||
want := []int{0, 9}
|
||||
checkSums(t, got, want)
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
282
command-line.md
282
command-line.md
@ -14,9 +14,9 @@ We have an application with a `main.go` file that launches an HTTP server. The H
|
||||
|
||||
```go
|
||||
type PlayerStore interface {
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
GetLeague() League
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
GetLeague() League
|
||||
}
|
||||
```
|
||||
|
||||
@ -71,32 +71,32 @@ The paths will be different on your computer, but it should be similar to this:
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"github.com/quii/learn-go-with-tests/command-line/v1"
|
||||
"github.com/quii/learn-go-with-tests/command-line/v1"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
const dbFileName = "game.db.json"
|
||||
|
||||
func main() {
|
||||
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
|
||||
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("problem opening %s %v", dbFileName, err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("problem opening %s %v", dbFileName, err)
|
||||
}
|
||||
|
||||
store, err := poker.NewFileSystemPlayerStore(db)
|
||||
store, err := poker.NewFileSystemPlayerStore(db)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("problem creating file system player store, %v ", err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("problem creating file system player store, %v ", err)
|
||||
}
|
||||
|
||||
server := poker.NewPlayerServer(store)
|
||||
server := poker.NewPlayerServer(store)
|
||||
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -122,7 +122,7 @@ package main
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Let's play poker")
|
||||
fmt.Println("Let's play poker")
|
||||
}
|
||||
```
|
||||
|
||||
@ -138,13 +138,13 @@ Inside `CLI_test.go` (in the root of the project, not inside `cmd`)
|
||||
|
||||
```go
|
||||
func TestCLI(t *testing.T) {
|
||||
playerStore := &StubPlayerStore{}
|
||||
cli := &CLI{playerStore}
|
||||
cli.PlayPoker()
|
||||
playerStore := &StubPlayerStore{}
|
||||
cli := &CLI{playerStore}
|
||||
cli.PlayPoker()
|
||||
|
||||
if len(playerStore.winCalls) !=1 {
|
||||
t.Fatal("expected a win call but didn't get any")
|
||||
}
|
||||
if len(playerStore.winCalls) != 1 {
|
||||
t.Fatal("expected a win call but didn't get any")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -168,7 +168,7 @@ You should end up with code like this
|
||||
|
||||
```go
|
||||
type CLI struct {
|
||||
playerStore PlayerStore
|
||||
playerStore PlayerStore
|
||||
}
|
||||
|
||||
func (cli *CLI) PlayPoker() {}
|
||||
@ -186,7 +186,7 @@ FAIL
|
||||
|
||||
```go
|
||||
func (cli *CLI) PlayPoker() {
|
||||
cli.playerStore.RecordWin("Cleo")
|
||||
cli.playerStore.RecordWin("Cleo")
|
||||
}
|
||||
```
|
||||
|
||||
@ -200,22 +200,22 @@ Let's extend our test to exercise this.
|
||||
|
||||
```go
|
||||
func TestCLI(t *testing.T) {
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
|
||||
if len(playerStore.winCalls) < 1 {
|
||||
t.Fatal("expected a win call but didn't get any")
|
||||
}
|
||||
if len(playerStore.winCalls) < 1 {
|
||||
t.Fatal("expected a win call but didn't get any")
|
||||
}
|
||||
|
||||
got := playerStore.winCalls[0]
|
||||
want := "Chris"
|
||||
got := playerStore.winCalls[0]
|
||||
want := "Chris"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("didn't record correct winner, got %q, want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("didn't record correct winner, got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -233,8 +233,8 @@ We need to add our new dependency into `CLI`.
|
||||
|
||||
```go
|
||||
type CLI struct {
|
||||
playerStore PlayerStore
|
||||
in io.Reader
|
||||
playerStore PlayerStore
|
||||
in io.Reader
|
||||
}
|
||||
```
|
||||
|
||||
@ -250,7 +250,7 @@ Remember to do the strictly easiest thing first
|
||||
|
||||
```go
|
||||
func (cli *CLI) PlayPoker() {
|
||||
cli.playerStore.RecordWin("Chris")
|
||||
cli.playerStore.RecordWin("Chris")
|
||||
}
|
||||
```
|
||||
|
||||
@ -262,15 +262,15 @@ In `server_test` we earlier did checks to see if wins are recorded as we have he
|
||||
|
||||
```go
|
||||
func assertPlayerWin(t *testing.T, store *StubPlayerStore, winner string) {
|
||||
t.Helper()
|
||||
t.Helper()
|
||||
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
|
||||
if store.winCalls[0] != winner {
|
||||
t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], winner)
|
||||
}
|
||||
if store.winCalls[0] != winner {
|
||||
t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], winner)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -280,13 +280,13 @@ The test should now read like so
|
||||
|
||||
```go
|
||||
func TestCLI(t *testing.T) {
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
|
||||
assertPlayerWin(t, playerStore, "Chris")
|
||||
assertPlayerWin(t, playerStore, "Chris")
|
||||
}
|
||||
```
|
||||
|
||||
@ -297,25 +297,25 @@ Now let's write _another_ test with different user input to force us into actual
|
||||
```go
|
||||
func TestCLI(t *testing.T) {
|
||||
|
||||
t.Run("record chris win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
t.Run("record chris win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
|
||||
assertPlayerWin(t, playerStore, "Chris")
|
||||
})
|
||||
assertPlayerWin(t, playerStore, "Chris")
|
||||
})
|
||||
|
||||
t.Run("record cleo win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Cleo wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
t.Run("record cleo win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Cleo wins\n")
|
||||
playerStore := &StubPlayerStore{}
|
||||
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
cli := &CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
|
||||
assertPlayerWin(t, playerStore, "Cleo")
|
||||
})
|
||||
assertPlayerWin(t, playerStore, "Cleo")
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
@ -343,18 +343,18 @@ Update the code to the following
|
||||
|
||||
```go
|
||||
type CLI struct {
|
||||
playerStore PlayerStore
|
||||
in io.Reader
|
||||
playerStore PlayerStore
|
||||
in io.Reader
|
||||
}
|
||||
|
||||
func (cli *CLI) PlayPoker() {
|
||||
reader := bufio.NewScanner(cli.in)
|
||||
reader.Scan()
|
||||
cli.playerStore.RecordWin(extractWinner(reader.Text()))
|
||||
cli.playerStore.RecordWin(extractWinner(reader.Text()))
|
||||
}
|
||||
|
||||
func extractWinner(userInput string) string {
|
||||
return strings.Replace(userInput, " wins", "", 1)
|
||||
return strings.Replace(userInput, " wins", "", 1)
|
||||
}
|
||||
```
|
||||
|
||||
@ -371,32 +371,32 @@ In `main.go` add the following and run it. (you may have to adjust the path of t
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/quii/learn-go-with-tests/command-line/v3"
|
||||
"log"
|
||||
"os"
|
||||
"fmt"
|
||||
"github.com/quii/learn-go-with-tests/command-line/v3"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const dbFileName = "game.db.json"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Let's play poker")
|
||||
fmt.Println("Type {Name} wins to record a win")
|
||||
fmt.Println("Let's play poker")
|
||||
fmt.Println("Type {Name} wins to record a win")
|
||||
|
||||
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
|
||||
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("problem opening %s %v", dbFileName, err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("problem opening %s %v", dbFileName, err)
|
||||
}
|
||||
|
||||
store, err := poker.NewFileSystemPlayerStore(db)
|
||||
store, err := poker.NewFileSystemPlayerStore(db)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("problem creating file system player store, %v ", err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("problem creating file system player store, %v ", err)
|
||||
}
|
||||
|
||||
game := poker.CLI{store, os.Stdin}
|
||||
game.PlayPoker()
|
||||
game := poker.CLI{store, os.Stdin}
|
||||
game.PlayPoker()
|
||||
}
|
||||
```
|
||||
|
||||
@ -454,34 +454,34 @@ package poker
|
||||
import "testing"
|
||||
|
||||
type StubPlayerStore struct {
|
||||
scores map[string]int
|
||||
winCalls []string
|
||||
league []Player
|
||||
scores map[string]int
|
||||
winCalls []string
|
||||
league []Player
|
||||
}
|
||||
|
||||
func (s *StubPlayerStore) GetPlayerScore(name string) int {
|
||||
score := s.scores[name]
|
||||
return score
|
||||
score := s.scores[name]
|
||||
return score
|
||||
}
|
||||
|
||||
func (s *StubPlayerStore) RecordWin(name string) {
|
||||
s.winCalls = append(s.winCalls, name)
|
||||
s.winCalls = append(s.winCalls, name)
|
||||
}
|
||||
|
||||
func (s *StubPlayerStore) GetLeague() League {
|
||||
return s.league
|
||||
return s.league
|
||||
}
|
||||
|
||||
func AssertPlayerWin(t *testing.T, store *StubPlayerStore, winner string) {
|
||||
t.Helper()
|
||||
t.Helper()
|
||||
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
|
||||
if store.winCalls[0] != winner {
|
||||
t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], winner)
|
||||
}
|
||||
if store.winCalls[0] != winner {
|
||||
t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], winner)
|
||||
}
|
||||
}
|
||||
|
||||
// todo for you - the rest of the helpers
|
||||
@ -494,25 +494,25 @@ In our `CLI` test you'll need to call the code as if you were using it within a
|
||||
```go
|
||||
func TestCLI(t *testing.T) {
|
||||
|
||||
t.Run("record chris win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &poker.StubPlayerStore{}
|
||||
t.Run("record chris win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Chris wins\n")
|
||||
playerStore := &poker.StubPlayerStore{}
|
||||
|
||||
cli := &poker.CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
cli := &poker.CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
|
||||
poker.AssertPlayerWin(t, playerStore, "Chris")
|
||||
})
|
||||
poker.AssertPlayerWin(t, playerStore, "Chris")
|
||||
})
|
||||
|
||||
t.Run("record cleo win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Cleo wins\n")
|
||||
playerStore := &poker.StubPlayerStore{}
|
||||
t.Run("record cleo win from user input", func(t *testing.T) {
|
||||
in := strings.NewReader("Cleo wins\n")
|
||||
playerStore := &poker.StubPlayerStore{}
|
||||
|
||||
cli := &poker.CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
cli := &poker.CLI{playerStore, in}
|
||||
cli.PlayPoker()
|
||||
|
||||
poker.AssertPlayerWin(t, playerStore, "Cleo")
|
||||
})
|
||||
poker.AssertPlayerWin(t, playerStore, "Cleo")
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
@ -604,25 +604,25 @@ Now refactor both of our applications to use this function to create the store.
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/quii/learn-go-with-tests/command-line/v3"
|
||||
"log"
|
||||
"os"
|
||||
"fmt"
|
||||
"fmt"
|
||||
"github.com/quii/learn-go-with-tests/command-line/v3"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const dbFileName = "game.db.json"
|
||||
|
||||
func main() {
|
||||
store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName)
|
||||
store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer close()
|
||||
|
||||
fmt.Println("Let's play poker")
|
||||
fmt.Println("Type {Name} wins to record a win")
|
||||
poker.NewCLI(store, os.Stdin).PlayPoker()
|
||||
fmt.Println("Let's play poker")
|
||||
fmt.Println("Type {Name} wins to record a win")
|
||||
poker.NewCLI(store, os.Stdin).PlayPoker()
|
||||
}
|
||||
```
|
||||
|
||||
@ -632,26 +632,26 @@ func main() {
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/quii/learn-go-with-tests/command-line/v3"
|
||||
"log"
|
||||
"net/http"
|
||||
"github.com/quii/learn-go-with-tests/command-line/v3"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const dbFileName = "game.db.json"
|
||||
|
||||
func main() {
|
||||
store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName)
|
||||
store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer close()
|
||||
|
||||
server := poker.NewPlayerServer(store)
|
||||
server := poker.NewPlayerServer(store)
|
||||
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
148
concurrency.md
148
concurrency.md
@ -11,13 +11,13 @@ package concurrency
|
||||
type WebsiteChecker func(string) bool
|
||||
|
||||
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
|
||||
results := make(map[string]bool)
|
||||
results := make(map[string]bool)
|
||||
|
||||
for _, url := range urls {
|
||||
results[url] = wc(url)
|
||||
}
|
||||
for _, url := range urls {
|
||||
results[url] = wc(url)
|
||||
}
|
||||
|
||||
return results
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
@ -36,35 +36,35 @@ Here's the test they've written:
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mockWebsiteChecker(url string) bool {
|
||||
if url == "waat://furhurterwe.geds" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
if url == "waat://furhurterwe.geds" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestCheckWebsites(t *testing.T) {
|
||||
websites := []string{
|
||||
"http://google.com",
|
||||
"http://blog.gypsydave5.com",
|
||||
"waat://furhurterwe.geds",
|
||||
}
|
||||
websites := []string{
|
||||
"http://google.com",
|
||||
"http://blog.gypsydave5.com",
|
||||
"waat://furhurterwe.geds",
|
||||
}
|
||||
|
||||
want := map[string]bool{
|
||||
"http://google.com": true,
|
||||
"http://blog.gypsydave5.com": true,
|
||||
"waat://furhurterwe.geds": false,
|
||||
}
|
||||
want := map[string]bool{
|
||||
"http://google.com": true,
|
||||
"http://blog.gypsydave5.com": true,
|
||||
"waat://furhurterwe.geds": false,
|
||||
}
|
||||
|
||||
got := CheckWebsites(mockWebsiteChecker, websites)
|
||||
got := CheckWebsites(mockWebsiteChecker, websites)
|
||||
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("Wanted %v, got %v", want, got)
|
||||
}
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("Wanted %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -81,24 +81,24 @@ effect of our changes.
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func slowStubWebsiteChecker(_ string) bool {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
return true
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
return true
|
||||
}
|
||||
|
||||
func BenchmarkCheckWebsites(b *testing.B) {
|
||||
urls := make([]string, 100)
|
||||
for i := 0; i < len(urls); i++ {
|
||||
urls[i] = "a url"
|
||||
}
|
||||
urls := make([]string, 100)
|
||||
for i := 0; i < len(urls); i++ {
|
||||
urls[i] = "a url"
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
CheckWebsites(slowStubWebsiteChecker, urls)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
CheckWebsites(slowStubWebsiteChecker, urls)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -158,15 +158,15 @@ package concurrency
|
||||
type WebsiteChecker func(string) bool
|
||||
|
||||
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
|
||||
results := make(map[string]bool)
|
||||
results := make(map[string]bool)
|
||||
|
||||
for _, url := range urls {
|
||||
go func() {
|
||||
results[url] = wc(url)
|
||||
}()
|
||||
}
|
||||
for _, url := range urls {
|
||||
go func() {
|
||||
results[url] = wc(url)
|
||||
}()
|
||||
}
|
||||
|
||||
return results
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
@ -228,17 +228,17 @@ import "time"
|
||||
type WebsiteChecker func(string) bool
|
||||
|
||||
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
|
||||
results := make(map[string]bool)
|
||||
results := make(map[string]bool)
|
||||
|
||||
for _, url := range urls {
|
||||
go func() {
|
||||
results[url] = wc(url)
|
||||
}()
|
||||
}
|
||||
for _, url := range urls {
|
||||
go func() {
|
||||
results[url] = wc(url)
|
||||
}()
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
return results
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
@ -266,23 +266,23 @@ To fix this:
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"time"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WebsiteChecker func(string) bool
|
||||
|
||||
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
|
||||
results := make(map[string]bool)
|
||||
results := make(map[string]bool)
|
||||
|
||||
for _, url := range urls {
|
||||
go func(u string) {
|
||||
results[u] = wc(u)
|
||||
}(url)
|
||||
}
|
||||
for _, url := range urls {
|
||||
go func(u string) {
|
||||
results[u] = wc(u)
|
||||
}(url)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
return results
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
@ -405,26 +405,26 @@ package concurrency
|
||||
|
||||
type WebsiteChecker func(string) bool
|
||||
type result struct {
|
||||
string
|
||||
bool
|
||||
string
|
||||
bool
|
||||
}
|
||||
|
||||
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
|
||||
results := make(map[string]bool)
|
||||
resultChannel := make(chan result)
|
||||
results := make(map[string]bool)
|
||||
resultChannel := make(chan result)
|
||||
|
||||
for _, url := range urls {
|
||||
go func(u string) {
|
||||
resultChannel <- result{u, wc(u)}
|
||||
}(url)
|
||||
}
|
||||
for _, url := range urls {
|
||||
go func(u string) {
|
||||
resultChannel <- result{u, wc(u)}
|
||||
}(url)
|
||||
}
|
||||
|
||||
for i := 0; i < len(urls); i++ {
|
||||
result := <-resultChannel
|
||||
results[result.string] = result.bool
|
||||
}
|
||||
for i := 0; i < len(urls); i++ {
|
||||
result := <-resultChannel
|
||||
results[result.string] = result.bool
|
||||
}
|
||||
|
||||
return results
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ Just to recap, here is what that function could look like
|
||||
|
||||
```go
|
||||
func Greet(name string) {
|
||||
fmt.Printf("Hello, %s", name)
|
||||
fmt.Printf("Hello, %s", name)
|
||||
}
|
||||
```
|
||||
|
||||
@ -34,7 +34,7 @@ If you look at the source code of `fmt.Printf` you can see a way for us to hook
|
||||
```go
|
||||
// It returns the number of bytes written and any write error encountered.
|
||||
func Printf(format string, a ...interface{}) (n int, err error) {
|
||||
return Fprintf(os.Stdout, format, a...)
|
||||
return Fprintf(os.Stdout, format, a...)
|
||||
}
|
||||
```
|
||||
|
||||
@ -44,11 +44,11 @@ What exactly _is_ an `os.Stdout`? What does `Fprintf` expect to get passed to it
|
||||
|
||||
```go
|
||||
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||
p := newPrinter()
|
||||
p.doPrintf(format, a)
|
||||
n, err = w.Write(p.buf)
|
||||
p.free()
|
||||
return
|
||||
p := newPrinter()
|
||||
p.doPrintf(format, a)
|
||||
n, err = w.Write(p.buf)
|
||||
p.free()
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
@ -56,7 +56,7 @@ An `io.Writer`
|
||||
|
||||
```go
|
||||
type Writer interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
```
|
||||
|
||||
@ -68,15 +68,15 @@ So we know under the covers we're ultimately using `Writer` to send our greeting
|
||||
|
||||
```go
|
||||
func TestGreet(t *testing.T) {
|
||||
buffer := bytes.Buffer{}
|
||||
Greet(&buffer,"Chris")
|
||||
buffer := bytes.Buffer{}
|
||||
Greet(&buffer, "Chris")
|
||||
|
||||
got := buffer.String()
|
||||
want := "Hello, Chris"
|
||||
got := buffer.String()
|
||||
want := "Hello, Chris"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -100,7 +100,7 @@ _Listen to the compiler_ and fix the problem.
|
||||
|
||||
```go
|
||||
func Greet(writer *bytes.Buffer, name string) {
|
||||
fmt.Printf("Hello, %s", name)
|
||||
fmt.Printf("Hello, %s", name)
|
||||
}
|
||||
```
|
||||
|
||||
@ -114,7 +114,7 @@ Use the writer to send the greeting to the buffer in our test. Remember `fmt.Fpr
|
||||
|
||||
```go
|
||||
func Greet(writer *bytes.Buffer, name string) {
|
||||
fmt.Fprintf(writer, "Hello, %s", name)
|
||||
fmt.Fprintf(writer, "Hello, %s", name)
|
||||
}
|
||||
```
|
||||
|
||||
@ -128,7 +128,7 @@ To demonstrate this, try wiring up the `Greet` function into a Go application wh
|
||||
|
||||
```go
|
||||
func main() {
|
||||
Greet(os.Stdout, "Elodie")
|
||||
Greet(os.Stdout, "Elodie")
|
||||
}
|
||||
```
|
||||
|
||||
@ -152,7 +152,7 @@ func Greet(writer io.Writer, name string) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
Greet(os.Stdout, "Elodie")
|
||||
Greet(os.Stdout, "Elodie")
|
||||
}
|
||||
```
|
||||
|
||||
@ -168,21 +168,21 @@ Run the following
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Greet(writer io.Writer, name string) {
|
||||
fmt.Fprintf(writer, "Hello, %s", name)
|
||||
fmt.Fprintf(writer, "Hello, %s", name)
|
||||
}
|
||||
|
||||
func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Greet(w, "world")
|
||||
Greet(w, "world")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))
|
||||
http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -37,23 +37,23 @@ As Pedro says, we _could_ write a test for the status error like so.
|
||||
```go
|
||||
t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {
|
||||
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
defer svr.Close()
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
defer svr.Close()
|
||||
|
||||
_, err := DumbGetter(svr.URL)
|
||||
_, err := DumbGetter(svr.URL)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
|
||||
want := fmt.Sprintf("did not get 200 from %s, got %d", svr.URL, http.StatusTeapot)
|
||||
got := err.Error()
|
||||
want := fmt.Sprintf("did not get 200 from %s, got %d", svr.URL, http.StatusTeapot)
|
||||
got := err.Error()
|
||||
|
||||
if got != want {
|
||||
t.Errorf(`got "%v", want "%v"`, got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf(`got "%v", want "%v"`, got, want)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@ -95,28 +95,28 @@ Let's change our existing test to reflect this need
|
||||
```go
|
||||
t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {
|
||||
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
defer svr.Close()
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
defer svr.Close()
|
||||
|
||||
_, err := DumbGetter(svr.URL)
|
||||
_, err := DumbGetter(svr.URL)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
|
||||
got, isStatusErr := err.(BadStatusError)
|
||||
got, isStatusErr := err.(BadStatusError)
|
||||
|
||||
if !isStatusErr {
|
||||
t.Fatalf("was not a BadStatusError, got %T", err)
|
||||
}
|
||||
if !isStatusErr {
|
||||
t.Fatalf("was not a BadStatusError, got %T", err)
|
||||
}
|
||||
|
||||
want := BadStatusError{URL:svr.URL, Status:http.StatusTeapot}
|
||||
want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@ -144,7 +144,7 @@ Let's fix `DumbGetter` by updating our error handling code to use our type
|
||||
|
||||
```go
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return "", BadStatusError{URL: url, Status: res.StatusCode}
|
||||
return "", BadStatusError{URL: url, Status: res.StatusCode}
|
||||
}
|
||||
```
|
||||
|
||||
@ -169,28 +169,28 @@ As of Go 1.13 there are new ways to work with errors in the standard library whi
|
||||
```go
|
||||
t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {
|
||||
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
defer svr.Close()
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
defer svr.Close()
|
||||
|
||||
_, err := DumbGetter(svr.URL)
|
||||
_, err := DumbGetter(svr.URL)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
|
||||
var got BadStatusError
|
||||
isBadStatusError := errors.As(err, &got)
|
||||
want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}
|
||||
var got BadStatusError
|
||||
isBadStatusError := errors.As(err, &got)
|
||||
want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}
|
||||
|
||||
if !isBadStatusError {
|
||||
t.Fatalf("was not a BadStatusError, got %T", err)
|
||||
}
|
||||
if !isBadStatusError {
|
||||
t.Fatalf("was not a BadStatusError, got %T", err)
|
||||
}
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
216
hello-world.md
216
hello-world.md
@ -20,7 +20,7 @@ package main
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello, world")
|
||||
fmt.Println("Hello, world")
|
||||
}
|
||||
```
|
||||
|
||||
@ -44,11 +44,11 @@ package main
|
||||
import "fmt"
|
||||
|
||||
func Hello() string {
|
||||
return "Hello, world"
|
||||
return "Hello, world"
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println(Hello())
|
||||
fmt.Println(Hello())
|
||||
}
|
||||
```
|
||||
|
||||
@ -62,12 +62,12 @@ package main
|
||||
import "testing"
|
||||
|
||||
func TestHello(t *testing.T) {
|
||||
got := Hello()
|
||||
want := "Hello, world"
|
||||
got := Hello()
|
||||
want := "Hello, world"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -126,12 +126,12 @@ package main
|
||||
import "testing"
|
||||
|
||||
func TestHello(t *testing.T) {
|
||||
got := Hello("Chris")
|
||||
want := "Hello, Chris"
|
||||
got := Hello("Chris")
|
||||
want := "Hello, Chris"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -151,7 +151,7 @@ Edit the `Hello` function to accept an argument of type string
|
||||
|
||||
```go
|
||||
func Hello(name string) string {
|
||||
return "Hello, world"
|
||||
return "Hello, world"
|
||||
}
|
||||
```
|
||||
|
||||
@ -159,7 +159,7 @@ If you try and run your tests again your `main.go` will fail to compile because
|
||||
|
||||
```go
|
||||
func main() {
|
||||
fmt.Println(Hello("world"))
|
||||
fmt.Println(Hello("world"))
|
||||
}
|
||||
```
|
||||
|
||||
@ -175,7 +175,7 @@ Let's make the test pass by using the name argument and concatenate it with `Hel
|
||||
|
||||
```go
|
||||
func Hello(name string) string {
|
||||
return "Hello, " + name
|
||||
return "Hello, " + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -205,7 +205,7 @@ We can now refactor our code
|
||||
const englishHelloPrefix = "Hello, "
|
||||
|
||||
func Hello(name string) string {
|
||||
return englishHelloPrefix + name
|
||||
return englishHelloPrefix + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -224,23 +224,23 @@ Start by writing a new failing test
|
||||
```go
|
||||
func TestHello(t *testing.T) {
|
||||
|
||||
t.Run("saying hello to people", func(t *testing.T) {
|
||||
got := Hello("Chris")
|
||||
want := "Hello, Chris"
|
||||
t.Run("saying hello to people", func(t *testing.T) {
|
||||
got := Hello("Chris")
|
||||
want := "Hello, Chris"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("say 'Hello, World' when an empty string is supplied", func(t *testing.T) {
|
||||
got := Hello("")
|
||||
want := "Hello, World"
|
||||
t.Run("say 'Hello, World' when an empty string is supplied", func(t *testing.T) {
|
||||
got := Hello("")
|
||||
want := "Hello, World"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
@ -260,24 +260,24 @@ We can and should refactor our tests.
|
||||
```go
|
||||
func TestHello(t *testing.T) {
|
||||
|
||||
assertCorrectMessage := func(t *testing.T, got, want string) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
assertCorrectMessage := func(t *testing.T, got, want string) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("saying hello to people", func(t *testing.T) {
|
||||
got := Hello("Chris")
|
||||
want := "Hello, Chris"
|
||||
assertCorrectMessage(t, got, want)
|
||||
})
|
||||
t.Run("saying hello to people", func(t *testing.T) {
|
||||
got := Hello("Chris")
|
||||
want := "Hello, Chris"
|
||||
assertCorrectMessage(t, got, want)
|
||||
})
|
||||
|
||||
t.Run("empty string defaults to 'World'", func(t *testing.T) {
|
||||
got := Hello("")
|
||||
want := "Hello, World"
|
||||
assertCorrectMessage(t, got, want)
|
||||
})
|
||||
t.Run("empty string defaults to 'World'", func(t *testing.T) {
|
||||
got := Hello("")
|
||||
want := "Hello, World"
|
||||
assertCorrectMessage(t, got, want)
|
||||
})
|
||||
|
||||
}
|
||||
```
|
||||
@ -294,10 +294,10 @@ Now that we have a well-written failing test, let's fix the code, using an `if`.
|
||||
const englishHelloPrefix = "Hello, "
|
||||
|
||||
func Hello(name string) string {
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
return englishHelloPrefix + name
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
return englishHelloPrefix + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -337,11 +337,11 @@ We should be confident that we can use TDD to flesh out this functionality easil
|
||||
Write a test for a user passing in Spanish. Add it to the existing suite.
|
||||
|
||||
```go
|
||||
t.Run("in Spanish", func(t *testing.T) {
|
||||
got := Hello("Elodie", "Spanish")
|
||||
want := "Hola, Elodie"
|
||||
assertCorrectMessage(t, got, want)
|
||||
})
|
||||
t.Run("in Spanish", func(t *testing.T) {
|
||||
got := Hello("Elodie", "Spanish")
|
||||
want := "Hola, Elodie"
|
||||
assertCorrectMessage(t, got, want)
|
||||
})
|
||||
```
|
||||
|
||||
Remember not to cheat! _Test first_. When you try and run the test, the compiler _should_ complain because you are calling `Hello` with two arguments rather than one.
|
||||
@ -356,10 +356,10 @@ Fix the compilation problems by adding another string argument to `Hello`
|
||||
|
||||
```go
|
||||
func Hello(name string, language string) string {
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
return englishHelloPrefix + name
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
return englishHelloPrefix + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -381,15 +381,15 @@ We can use `if` here to check the language is equal to "Spanish" and if so chang
|
||||
|
||||
```go
|
||||
func Hello(name string, language string) string {
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
|
||||
if language == "Spanish" {
|
||||
return "Hola, " + name
|
||||
}
|
||||
if language == "Spanish" {
|
||||
return "Hola, " + name
|
||||
}
|
||||
|
||||
return englishHelloPrefix + name
|
||||
return englishHelloPrefix + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -403,15 +403,15 @@ const englishHelloPrefix = "Hello, "
|
||||
const spanishHelloPrefix = "Hola, "
|
||||
|
||||
func Hello(name string, language string) string {
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
|
||||
if language == spanish {
|
||||
return spanishHelloPrefix + name
|
||||
}
|
||||
if language == spanish {
|
||||
return spanishHelloPrefix + name
|
||||
}
|
||||
|
||||
return englishHelloPrefix + name
|
||||
return englishHelloPrefix + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -425,19 +425,19 @@ You may have written something that looks roughly like this
|
||||
|
||||
```go
|
||||
func Hello(name string, language string) string {
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
|
||||
if language == spanish {
|
||||
return spanishHelloPrefix + name
|
||||
}
|
||||
if language == spanish {
|
||||
return spanishHelloPrefix + name
|
||||
}
|
||||
|
||||
if language == french {
|
||||
return frenchHelloPrefix + name
|
||||
}
|
||||
if language == french {
|
||||
return frenchHelloPrefix + name
|
||||
}
|
||||
|
||||
return englishHelloPrefix + name
|
||||
return englishHelloPrefix + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -447,20 +447,20 @@ When you have lots of `if` statements checking a particular value it is common t
|
||||
|
||||
```go
|
||||
func Hello(name string, language string) string {
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
|
||||
prefix := englishHelloPrefix
|
||||
prefix := englishHelloPrefix
|
||||
|
||||
switch language {
|
||||
case french:
|
||||
prefix = frenchHelloPrefix
|
||||
case spanish:
|
||||
prefix = spanishHelloPrefix
|
||||
}
|
||||
switch language {
|
||||
case french:
|
||||
prefix = frenchHelloPrefix
|
||||
case spanish:
|
||||
prefix = spanishHelloPrefix
|
||||
}
|
||||
|
||||
return prefix + name
|
||||
return prefix + name
|
||||
}
|
||||
```
|
||||
|
||||
@ -472,23 +472,23 @@ You could argue that maybe our function is getting a little big. The simplest re
|
||||
|
||||
```go
|
||||
func Hello(name string, language string) string {
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
|
||||
return greetingPrefix(language) + name
|
||||
return greetingPrefix(language) + name
|
||||
}
|
||||
|
||||
func greetingPrefix(language string) (prefix string) {
|
||||
switch language {
|
||||
case french:
|
||||
prefix = frenchHelloPrefix
|
||||
case spanish:
|
||||
prefix = spanishHelloPrefix
|
||||
default:
|
||||
prefix = englishHelloPrefix
|
||||
}
|
||||
return
|
||||
switch language {
|
||||
case french:
|
||||
prefix = frenchHelloPrefix
|
||||
case spanish:
|
||||
prefix = spanishHelloPrefix
|
||||
default:
|
||||
prefix = englishHelloPrefix
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -210,10 +210,10 @@ func NewUserServer(service UserService) *UserServer {
|
||||
return &UserServer{service: service}
|
||||
}
|
||||
|
||||
func (u *UserServer) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
||||
func (u *UserServer) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
// request parsing and validation
|
||||
// request parsing and validation
|
||||
var newUser User
|
||||
err := json.NewDecoder(r.Body).Decode(&newUser)
|
||||
|
||||
@ -222,10 +222,10 @@ func (u *UserServer) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// call a service thing to take care of the hard work
|
||||
// call a service thing to take care of the hard work
|
||||
insertedID, err := u.service.Register(newUser)
|
||||
|
||||
// depending on what we get back, respond accordingly
|
||||
// depending on what we get back, respond accordingly
|
||||
if err != nil {
|
||||
//todo: handle different kinds of errors differently
|
||||
http.Error(w, fmt.Sprintf("problem registering new user: %v", err), http.StatusInternalServerError)
|
||||
|
||||
552
http-server.md
552
http-server.md
@ -57,7 +57,7 @@ This will start a web server listening on a port, creating a goroutine for every
|
||||
|
||||
```go
|
||||
type Handler interface {
|
||||
ServeHTTP(ResponseWriter, *Request)
|
||||
ServeHTTP(ResponseWriter, *Request)
|
||||
}
|
||||
```
|
||||
|
||||
@ -67,19 +67,19 @@ Let's write a test for a function `PlayerServer` that takes in those two argumen
|
||||
|
||||
```go
|
||||
func TestGETPlayers(t *testing.T) {
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodGet, "/players/Pepper", nil)
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodGet, "/players/Pepper", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
PlayerServer(response, request)
|
||||
PlayerServer(response, request)
|
||||
|
||||
got := response.Body.String()
|
||||
want := "20"
|
||||
got := response.Body.String()
|
||||
want := "20"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -134,7 +134,7 @@ From the DI chapter, we touched on HTTP servers with a `Greet` function. We lear
|
||||
|
||||
```go
|
||||
func PlayerServer(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "20")
|
||||
fmt.Fprint(w, "20")
|
||||
}
|
||||
```
|
||||
|
||||
@ -153,15 +153,15 @@ Create a new file for our application and put this code in.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
handler := http.HandlerFunc(PlayerServer)
|
||||
if err := http.ListenAndServe(":5000", handler); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
handler := http.HandlerFunc(PlayerServer)
|
||||
if err := http.ListenAndServe(":5000", handler); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -196,17 +196,17 @@ We'll add another subtest to our suite which tries to get the score of a differe
|
||||
|
||||
```go
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodGet, "/players/Floyd", nil)
|
||||
response := httptest.NewRecorder()
|
||||
request, _ := http.NewRequest(http.MethodGet, "/players/Floyd", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
PlayerServer(response, request)
|
||||
PlayerServer(response, request)
|
||||
|
||||
got := response.Body.String()
|
||||
want := "10"
|
||||
got := response.Body.String()
|
||||
want := "10"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@ -230,17 +230,17 @@ Remember we are just trying to take as small as steps as reasonably possible, so
|
||||
|
||||
```go
|
||||
func PlayerServer(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
|
||||
if player == "Pepper" {
|
||||
fmt.Fprint(w, "20")
|
||||
return
|
||||
}
|
||||
if player == "Pepper" {
|
||||
fmt.Fprint(w, "20")
|
||||
return
|
||||
}
|
||||
|
||||
if player == "Floyd" {
|
||||
fmt.Fprint(w, "10")
|
||||
return
|
||||
}
|
||||
if player == "Floyd" {
|
||||
fmt.Fprint(w, "10")
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -258,21 +258,21 @@ We can simplify the `PlayerServer` by separating out the score retrieval into a
|
||||
|
||||
```go
|
||||
func PlayerServer(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
|
||||
fmt.Fprint(w, GetPlayerScore(player))
|
||||
fmt.Fprint(w, GetPlayerScore(player))
|
||||
}
|
||||
|
||||
func GetPlayerScore(name string) string {
|
||||
if name == "Pepper" {
|
||||
return "20"
|
||||
}
|
||||
if name == "Pepper" {
|
||||
return "20"
|
||||
}
|
||||
|
||||
if name == "Floyd" {
|
||||
return "10"
|
||||
}
|
||||
if name == "Floyd" {
|
||||
return "10"
|
||||
}
|
||||
|
||||
return ""
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
@ -280,35 +280,35 @@ And we can DRY up some of the code in the tests by making some helpers
|
||||
|
||||
```go
|
||||
func TestGETPlayers(t *testing.T) {
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
PlayerServer(response, request)
|
||||
PlayerServer(response, request)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
PlayerServer(response, request)
|
||||
PlayerServer(response, request)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
}
|
||||
|
||||
func newGetScoreRequest(name string) *http.Request {
|
||||
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil)
|
||||
return req
|
||||
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
func assertResponseBody(t *testing.T, got, want string) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("response body is wrong, got %q want %q", got, want)
|
||||
}
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("response body is wrong, got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -322,7 +322,7 @@ Let's move our function we re-factored to be an interface instead
|
||||
|
||||
```go
|
||||
type PlayerStore interface {
|
||||
GetPlayerScore(name string) int
|
||||
GetPlayerScore(name string) int
|
||||
}
|
||||
```
|
||||
|
||||
@ -330,7 +330,7 @@ For our `PlayerServer` to be able to use a `PlayerStore`, it will need a referen
|
||||
|
||||
```go
|
||||
type PlayerServer struct {
|
||||
store PlayerStore
|
||||
store PlayerStore
|
||||
}
|
||||
```
|
||||
|
||||
@ -338,8 +338,8 @@ Finally, we will now implement the `Handler` interface by adding a method to our
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
fmt.Fprint(w, p.store.GetPlayerScore(player))
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
fmt.Fprint(w, p.store.GetPlayerScore(player))
|
||||
}
|
||||
```
|
||||
|
||||
@ -349,16 +349,16 @@ Here is the full code listing of our server
|
||||
|
||||
```go
|
||||
type PlayerStore interface {
|
||||
GetPlayerScore(name string) int
|
||||
GetPlayerScore(name string) int
|
||||
}
|
||||
|
||||
type PlayerServer struct {
|
||||
store PlayerStore
|
||||
store PlayerStore
|
||||
}
|
||||
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
fmt.Fprint(w, p.store.GetPlayerScore(player))
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
fmt.Fprint(w, p.store.GetPlayerScore(player))
|
||||
}
|
||||
```
|
||||
|
||||
@ -372,25 +372,25 @@ We need to change our tests to instead create a new instance of our `PlayerServe
|
||||
|
||||
```go
|
||||
func TestGETPlayers(t *testing.T) {
|
||||
server := &PlayerServer{}
|
||||
server := &PlayerServer{}
|
||||
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -404,11 +404,11 @@ Now `main.go` won't compile for the same reason.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
server := &PlayerServer{}
|
||||
server := &PlayerServer{}
|
||||
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -424,12 +424,12 @@ This is because we have not passed in a `PlayerStore` in our tests. We'll need t
|
||||
|
||||
```go
|
||||
type StubPlayerStore struct {
|
||||
scores map[string]int
|
||||
scores map[string]int
|
||||
}
|
||||
|
||||
func (s *StubPlayerStore) GetPlayerScore(name string) int {
|
||||
score := s.scores[name]
|
||||
return score
|
||||
score := s.scores[name]
|
||||
return score
|
||||
}
|
||||
```
|
||||
|
||||
@ -437,31 +437,31 @@ A `map` is a quick and easy way of making a stub key/value store for our tests.
|
||||
|
||||
```go
|
||||
func TestGETPlayers(t *testing.T) {
|
||||
store := StubPlayerStore{
|
||||
map[string]int{
|
||||
"Pepper": 20,
|
||||
"Floyd": 10,
|
||||
},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
store := StubPlayerStore{
|
||||
map[string]int{
|
||||
"Pepper": 20,
|
||||
"Floyd": 10,
|
||||
},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -479,15 +479,15 @@ We'll need to make an implementation of one, but that's difficult right now as w
|
||||
type InMemoryPlayerStore struct{}
|
||||
|
||||
func (i *InMemoryPlayerStore) GetPlayerScore(name string) int {
|
||||
return 123
|
||||
return 123
|
||||
}
|
||||
|
||||
func main() {
|
||||
server := &PlayerServer{&InMemoryPlayerStore{}}
|
||||
server := &PlayerServer{&InMemoryPlayerStore{}}
|
||||
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -507,17 +507,17 @@ Add a missing player scenario to our existing suite
|
||||
|
||||
```go
|
||||
t.Run("returns 404 on missing players", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Apollo")
|
||||
response := httptest.NewRecorder()
|
||||
request := newGetScoreRequest("Apollo")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
got := response.Code
|
||||
want := http.StatusNotFound
|
||||
got := response.Code
|
||||
want := http.StatusNotFound
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got status %d want %d", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got status %d want %d", got, want)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@ -533,11 +533,11 @@ t.Run("returns 404 on missing players", func(t *testing.T) {
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
fmt.Fprint(w, p.store.GetPlayerScore(player))
|
||||
fmt.Fprint(w, p.store.GetPlayerScore(player))
|
||||
}
|
||||
```
|
||||
|
||||
@ -553,61 +553,61 @@ Here are the new tests
|
||||
|
||||
```go
|
||||
func TestGETPlayers(t *testing.T) {
|
||||
store := StubPlayerStore{
|
||||
map[string]int{
|
||||
"Pepper": 20,
|
||||
"Floyd": 10,
|
||||
},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
store := StubPlayerStore{
|
||||
map[string]int{
|
||||
"Pepper": 20,
|
||||
"Floyd": 10,
|
||||
},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Pepper's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
assertResponseBody(t, response.Body.String(), "20")
|
||||
})
|
||||
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns Floyd's score", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Floyd")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
assertResponseBody(t, response.Body.String(), "10")
|
||||
})
|
||||
|
||||
t.Run("returns 404 on missing players", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Apollo")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("returns 404 on missing players", func(t *testing.T) {
|
||||
request := newGetScoreRequest("Apollo")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertStatus(t, response.Code, http.StatusNotFound)
|
||||
})
|
||||
assertStatus(t, response.Code, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func assertStatus(t *testing.T, got, want int) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("did not get correct status, got %d, want %d", got, want)
|
||||
}
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("did not get correct status, got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func newGetScoreRequest(name string) *http.Request {
|
||||
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil)
|
||||
return req
|
||||
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
func assertResponseBody(t *testing.T, got, want string) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("response body is wrong, got %q want %q", got, want)
|
||||
}
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("response body is wrong, got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -617,15 +617,15 @@ Now our first two tests fail because of the 404 instead of 200, so we can fix `P
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
|
||||
score := p.store.GetPlayerScore(player)
|
||||
score := p.store.GetPlayerScore(player)
|
||||
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, score)
|
||||
fmt.Fprint(w, score)
|
||||
}
|
||||
```
|
||||
|
||||
@ -637,19 +637,19 @@ Now that we can retrieve scores from a store it now makes sense to be able to st
|
||||
|
||||
```go
|
||||
func TestStoreWins(t *testing.T) {
|
||||
store := StubPlayerStore{
|
||||
map[string]int{},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
store := StubPlayerStore{
|
||||
map[string]int{},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
|
||||
t.Run("it returns accepted on POST", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodPost, "/players/Pepper", nil)
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("it returns accepted on POST", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodPost, "/players/Pepper", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertStatus(t, response.Code, http.StatusAccepted)
|
||||
})
|
||||
assertStatus(t, response.Code, http.StatusAccepted)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -670,20 +670,20 @@ Remember we are deliberately committing sins, so an `if` statement based on the
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
|
||||
score := p.store.GetPlayerScore(player)
|
||||
score := p.store.GetPlayerScore(player)
|
||||
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, score)
|
||||
fmt.Fprint(w, score)
|
||||
}
|
||||
```
|
||||
|
||||
@ -694,29 +694,29 @@ The handler is looking a bit muddled now. Let's break the code up to make it eas
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, r)
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, r)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *PlayerServer) showScore(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
|
||||
score := p.store.GetPlayerScore(player)
|
||||
score := p.store.GetPlayerScore(player)
|
||||
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, score)
|
||||
fmt.Fprint(w, score)
|
||||
}
|
||||
|
||||
func (p *PlayerServer) processWin(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
```
|
||||
|
||||
@ -730,17 +730,17 @@ We can accomplish this by extending our `StubPlayerStore` with a new `RecordWin`
|
||||
|
||||
```go
|
||||
type StubPlayerStore struct {
|
||||
scores map[string]int
|
||||
winCalls []string
|
||||
scores map[string]int
|
||||
winCalls []string
|
||||
}
|
||||
|
||||
func (s *StubPlayerStore) GetPlayerScore(name string) int {
|
||||
score := s.scores[name]
|
||||
return score
|
||||
score := s.scores[name]
|
||||
return score
|
||||
}
|
||||
|
||||
func (s *StubPlayerStore) RecordWin(name string) {
|
||||
s.winCalls = append(s.winCalls, name)
|
||||
s.winCalls = append(s.winCalls, name)
|
||||
}
|
||||
```
|
||||
|
||||
@ -748,28 +748,28 @@ Now extend our test to check the number of invocations for a start
|
||||
|
||||
```go
|
||||
func TestStoreWins(t *testing.T) {
|
||||
store := StubPlayerStore{
|
||||
map[string]int{},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
store := StubPlayerStore{
|
||||
map[string]int{},
|
||||
}
|
||||
server := &PlayerServer{&store}
|
||||
|
||||
t.Run("it records wins when POST", func(t *testing.T) {
|
||||
request := newPostWinRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("it records wins when POST", func(t *testing.T) {
|
||||
request := newPostWinRequest("Pepper")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertStatus(t, response.Code, http.StatusAccepted)
|
||||
assertStatus(t, response.Code, http.StatusAccepted)
|
||||
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Errorf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
})
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Errorf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newPostWinRequest(name string) *http.Request {
|
||||
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/players/%s", name), nil)
|
||||
return req
|
||||
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/players/%s", name), nil)
|
||||
return req
|
||||
}
|
||||
```
|
||||
|
||||
@ -786,8 +786,8 @@ We need to update our code where we create a `StubPlayerStore` as we've added a
|
||||
|
||||
```go
|
||||
store := StubPlayerStore{
|
||||
map[string]int{},
|
||||
nil,
|
||||
map[string]int{},
|
||||
nil,
|
||||
}
|
||||
```
|
||||
|
||||
@ -805,8 +805,8 @@ We need to update `PlayerServer`'s idea of what a `PlayerStore` is by changing t
|
||||
|
||||
```go
|
||||
type PlayerStore interface {
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
}
|
||||
```
|
||||
|
||||
@ -831,8 +831,8 @@ Now that `PlayerStore` has `RecordWin` we can call it within our `PlayerServer`
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) processWin(w http.ResponseWriter) {
|
||||
p.store.RecordWin("Bob")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
p.store.RecordWin("Bob")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
```
|
||||
|
||||
@ -842,22 +842,22 @@ Run the tests and it should be passing! Obviously `"Bob"` isn't exactly what we
|
||||
|
||||
```go
|
||||
t.Run("it records wins on POST", func(t *testing.T) {
|
||||
player := "Pepper"
|
||||
player := "Pepper"
|
||||
|
||||
request := newPostWinRequest(player)
|
||||
response := httptest.NewRecorder()
|
||||
request := newPostWinRequest(player)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertStatus(t, response.Code, http.StatusAccepted)
|
||||
assertStatus(t, response.Code, http.StatusAccepted)
|
||||
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
if len(store.winCalls) != 1 {
|
||||
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
|
||||
}
|
||||
|
||||
if store.winCalls[0] != player {
|
||||
t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], player)
|
||||
}
|
||||
if store.winCalls[0] != player {
|
||||
t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], player)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@ -875,9 +875,9 @@ Now that we know there is one element in our `winCalls` slice we can safely refe
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) processWin(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
p.store.RecordWin(player)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
p.store.RecordWin(player)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
```
|
||||
|
||||
@ -889,29 +889,29 @@ We can DRY up this code a bit as we're extracting the player name the same way i
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
player := strings.TrimPrefix(r.URL.Path, "/players/")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlayerServer) showScore(w http.ResponseWriter, player string) {
|
||||
score := p.store.GetPlayerScore(player)
|
||||
score := p.store.GetPlayerScore(player)
|
||||
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, score)
|
||||
fmt.Fprint(w, score)
|
||||
}
|
||||
|
||||
func (p *PlayerServer) processWin(w http.ResponseWriter, player string) {
|
||||
p.store.RecordWin(player)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
p.store.RecordWin(player)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
```
|
||||
|
||||
@ -937,19 +937,19 @@ In the interest of brevity, I am going to show you the final refactored integrat
|
||||
|
||||
```go
|
||||
func TestRecordingWinsAndRetrievingThem(t *testing.T) {
|
||||
store := InMemoryPlayerStore{}
|
||||
server := PlayerServer{&store}
|
||||
player := "Pepper"
|
||||
store := InMemoryPlayerStore{}
|
||||
server := PlayerServer{&store}
|
||||
player := "Pepper"
|
||||
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
server.ServeHTTP(response, newGetScoreRequest(player))
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
response := httptest.NewRecorder()
|
||||
server.ServeHTTP(response, newGetScoreRequest(player))
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "3")
|
||||
assertResponseBody(t, response.Body.String(), "3")
|
||||
}
|
||||
```
|
||||
|
||||
@ -974,19 +974,19 @@ If I were to get stuck in this scenario, I would revert my changes back to the f
|
||||
|
||||
```go
|
||||
func NewInMemoryPlayerStore() *InMemoryPlayerStore {
|
||||
return &InMemoryPlayerStore{map[string]int{}}
|
||||
return &InMemoryPlayerStore{map[string]int{}}
|
||||
}
|
||||
|
||||
type InMemoryPlayerStore struct{
|
||||
store map[string]int
|
||||
type InMemoryPlayerStore struct {
|
||||
store map[string]int
|
||||
}
|
||||
|
||||
func (i *InMemoryPlayerStore) RecordWin(name string) {
|
||||
i.store[name]++
|
||||
i.store[name]++
|
||||
}
|
||||
|
||||
func (i *InMemoryPlayerStore) GetPlayerScore(name string) int {
|
||||
return i.store[name]
|
||||
return i.store[name]
|
||||
}
|
||||
```
|
||||
|
||||
@ -1000,16 +1000,16 @@ The integration test passes, now we just need to change `main` to use `NewInMemo
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := &PlayerServer{NewInMemoryPlayerStore()}
|
||||
server := &PlayerServer{NewInMemoryPlayerStore()}
|
||||
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
24
integers.md
24
integers.md
@ -14,12 +14,12 @@ package integers
|
||||
import "testing"
|
||||
|
||||
func TestAdder(t *testing.T) {
|
||||
sum := Add(2, 2)
|
||||
expected := 4
|
||||
sum := Add(2, 2)
|
||||
expected := 4
|
||||
|
||||
if sum != expected {
|
||||
t.Errorf("expected '%d' but got '%d'", expected, sum)
|
||||
}
|
||||
if sum != expected {
|
||||
t.Errorf("expected '%d' but got '%d'", expected, sum)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -43,7 +43,7 @@ Write enough code to satisfy the compiler _and that's all_ - remember we want to
|
||||
package integers
|
||||
|
||||
func Add(x, y int) int {
|
||||
return 0
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
@ -61,7 +61,7 @@ In the strictest sense of TDD we should now write the _minimal amount of code to
|
||||
|
||||
```go
|
||||
func Add(x, y int) int {
|
||||
return 4
|
||||
return 4
|
||||
}
|
||||
```
|
||||
|
||||
@ -75,7 +75,7 @@ For now, let's fix it properly
|
||||
|
||||
```go
|
||||
func Add(x, y int) int {
|
||||
return x + y
|
||||
return x + y
|
||||
}
|
||||
```
|
||||
|
||||
@ -94,7 +94,7 @@ You can add documentation to functions with comments, and these will appear in G
|
||||
```go
|
||||
// Add takes two integers and returns the sum of them.
|
||||
func Add(x, y int) int {
|
||||
return x + y
|
||||
return x + y
|
||||
}
|
||||
```
|
||||
|
||||
@ -112,9 +112,9 @@ As with typical tests, examples are functions that reside in a package's `_test.
|
||||
|
||||
```go
|
||||
func ExampleAdd() {
|
||||
sum := Add(1, 5)
|
||||
fmt.Println(sum)
|
||||
// Output: 6
|
||||
sum := Add(1, 5)
|
||||
fmt.Println(sum)
|
||||
// Output: 6
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
32
iteration.md
32
iteration.md
@ -16,12 +16,12 @@ package iteration
|
||||
import "testing"
|
||||
|
||||
func TestRepeat(t *testing.T) {
|
||||
repeated := Repeat("a")
|
||||
expected := "aaaaa"
|
||||
repeated := Repeat("a")
|
||||
expected := "aaaaa"
|
||||
|
||||
if repeated != expected {
|
||||
t.Errorf("expected %q but got %q", expected, repeated)
|
||||
}
|
||||
if repeated != expected {
|
||||
t.Errorf("expected %q but got %q", expected, repeated)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -39,7 +39,7 @@ All you need to do right now is enough to make it compile so you can check your
|
||||
package iteration
|
||||
|
||||
func Repeat(character string) string {
|
||||
return ""
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
@ -53,18 +53,18 @@ The `for` syntax is very unremarkable and follows most C-like languages.
|
||||
|
||||
```go
|
||||
func Repeat(character string) string {
|
||||
var repeated string
|
||||
for i := 0; i < 5; i++ {
|
||||
repeated = repeated + character
|
||||
}
|
||||
return repeated
|
||||
var repeated string
|
||||
for i := 0; i < 5; i++ {
|
||||
repeated = repeated + character
|
||||
}
|
||||
return repeated
|
||||
}
|
||||
```
|
||||
|
||||
Unlike other languages like C, Java, or JavaScript there are no parentheses surrounding the three components of the for statement and the braces `{ }` are always required. You might wonder what is happening in the row
|
||||
|
||||
```go
|
||||
var repeated string
|
||||
var repeated string
|
||||
```
|
||||
|
||||
as we've been using `:=` so far to declare and initializing variables. However, `:=` is simply [short hand for both steps](https://gobyexample.com/variables). Here we are declaring a `string` variable only. Hence, the explicit version. We can also use `var` to declare functions, as we'll see later on.
|
||||
@ -81,7 +81,7 @@ Now it's time to refactor and introduce another construct `+=` assignment operat
|
||||
const repeatCount = 5
|
||||
|
||||
func Repeat(character string) string {
|
||||
var repeated string
|
||||
var repeated string
|
||||
for i := 0; i < repeatCount; i++ {
|
||||
repeated += character
|
||||
}
|
||||
@ -97,9 +97,9 @@ Writing [benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks) in Go is an
|
||||
|
||||
```go
|
||||
func BenchmarkRepeat(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Repeat("a")
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
Repeat("a")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
402
json.md
402
json.md
@ -13,43 +13,43 @@ Our product owner has a new requirement; to have a new endpoint called `/league`
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type PlayerStore interface {
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
}
|
||||
|
||||
type PlayerServer struct {
|
||||
store PlayerStore
|
||||
store PlayerStore
|
||||
}
|
||||
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
player := r.URL.Path[len("/players/"):]
|
||||
player := r.URL.Path[len("/players/"):]
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlayerServer) showScore(w http.ResponseWriter, player string) {
|
||||
score := p.store.GetPlayerScore(player)
|
||||
score := p.store.GetPlayerScore(player)
|
||||
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
if score == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, score)
|
||||
fmt.Fprint(w, score)
|
||||
}
|
||||
|
||||
func (p *PlayerServer) processWin(w http.ResponseWriter, player string) {
|
||||
p.store.RecordWin(player)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
p.store.RecordWin(player)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
```
|
||||
|
||||
@ -58,19 +58,19 @@ func (p *PlayerServer) processWin(w http.ResponseWriter, player string) {
|
||||
package main
|
||||
|
||||
func NewInMemoryPlayerStore() *InMemoryPlayerStore {
|
||||
return &InMemoryPlayerStore{map[string]int{}}
|
||||
return &InMemoryPlayerStore{map[string]int{}}
|
||||
}
|
||||
|
||||
type InMemoryPlayerStore struct {
|
||||
store map[string]int
|
||||
store map[string]int
|
||||
}
|
||||
|
||||
func (i *InMemoryPlayerStore) RecordWin(name string) {
|
||||
i.store[name]++
|
||||
i.store[name]++
|
||||
}
|
||||
|
||||
func (i *InMemoryPlayerStore) GetPlayerScore(name string) int {
|
||||
return i.store[name]
|
||||
return i.store[name]
|
||||
}
|
||||
|
||||
```
|
||||
@ -80,16 +80,16 @@ func (i *InMemoryPlayerStore) GetPlayerScore(name string) int {
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := &PlayerServer{NewInMemoryPlayerStore()}
|
||||
server := &PlayerServer{NewInMemoryPlayerStore()}
|
||||
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
if err := http.ListenAndServe(":5000", server); err != nil {
|
||||
log.Fatalf("could not listen on port 5000 %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -103,17 +103,17 @@ We'll extend the existing suite as we have some useful test functions and a fake
|
||||
|
||||
```go
|
||||
func TestLeague(t *testing.T) {
|
||||
store := StubPlayerStore{}
|
||||
server := &PlayerServer{&store}
|
||||
store := StubPlayerStore{}
|
||||
server := &PlayerServer{&store}
|
||||
|
||||
t.Run("it returns 200 on /league", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("it returns 200 on /league", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
})
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -152,24 +152,24 @@ Let's commit some sins and get the tests passing in the quickest way we can, kno
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
router := http.NewServeMux()
|
||||
router := http.NewServeMux()
|
||||
|
||||
router.Handle("/league", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
router.Handle("/league", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
router.Handle("/players/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
player := r.URL.Path[len("/players/"):]
|
||||
router.Handle("/players/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
player := r.URL.Path[len("/players/"):]
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
}))
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
}))
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
```
|
||||
|
||||
@ -187,26 +187,26 @@ The tests should now pass.
|
||||
```go
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/league", http.HandlerFunc(p.leagueHandler))
|
||||
router.Handle("/players/", http.HandlerFunc(p.playersHandler))
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/league", http.HandlerFunc(p.leagueHandler))
|
||||
router.Handle("/players/", http.HandlerFunc(p.playersHandler))
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (p *PlayerServer) playersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
player := r.URL.Path[len("/players/"):]
|
||||
player := r.URL.Path[len("/players/"):]
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
p.processWin(w, player)
|
||||
case http.MethodGet:
|
||||
p.showScore(w, player)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -214,24 +214,24 @@ It's quite odd (and inefficient) to be setting up a router as a request comes in
|
||||
|
||||
```go
|
||||
type PlayerServer struct {
|
||||
store PlayerStore
|
||||
router *http.ServeMux
|
||||
store PlayerStore
|
||||
router *http.ServeMux
|
||||
}
|
||||
|
||||
func NewPlayerServer(store PlayerStore) *PlayerServer {
|
||||
p := &PlayerServer{
|
||||
store,
|
||||
http.NewServeMux(),
|
||||
}
|
||||
p := &PlayerServer{
|
||||
store,
|
||||
http.NewServeMux(),
|
||||
}
|
||||
|
||||
p.router.Handle("/league", http.HandlerFunc(p.leagueHandler))
|
||||
p.router.Handle("/players/", http.HandlerFunc(p.playersHandler))
|
||||
p.router.Handle("/league", http.HandlerFunc(p.leagueHandler))
|
||||
p.router.Handle("/players/", http.HandlerFunc(p.playersHandler))
|
||||
|
||||
return p
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.router.ServeHTTP(w, r)
|
||||
p.router.ServeHTTP(w, r)
|
||||
}
|
||||
```
|
||||
|
||||
@ -245,22 +245,22 @@ Try changing the code to the following.
|
||||
|
||||
```go
|
||||
type PlayerServer struct {
|
||||
store PlayerStore
|
||||
http.Handler
|
||||
store PlayerStore
|
||||
http.Handler
|
||||
}
|
||||
|
||||
func NewPlayerServer(store PlayerStore) *PlayerServer {
|
||||
p := new(PlayerServer)
|
||||
p := new(PlayerServer)
|
||||
|
||||
p.store = store
|
||||
p.store = store
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/league", http.HandlerFunc(p.leagueHandler))
|
||||
router.Handle("/players/", http.HandlerFunc(p.playersHandler))
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/league", http.HandlerFunc(p.leagueHandler))
|
||||
router.Handle("/players/", http.HandlerFunc(p.playersHandler))
|
||||
|
||||
p.Handler = router
|
||||
p.Handler = router
|
||||
|
||||
return p
|
||||
return p
|
||||
}
|
||||
```
|
||||
|
||||
@ -284,8 +284,8 @@ Embedding is a very interesting language feature. You can use it with interfaces
|
||||
|
||||
```go
|
||||
type Animal interface {
|
||||
Eater
|
||||
Sleeper
|
||||
Eater
|
||||
Sleeper
|
||||
}
|
||||
```
|
||||
|
||||
@ -324,25 +324,25 @@ We'll start by trying to parse the response into something meaningful.
|
||||
|
||||
```go
|
||||
func TestLeague(t *testing.T) {
|
||||
store := StubPlayerStore{}
|
||||
server := NewPlayerServer(&store)
|
||||
store := StubPlayerStore{}
|
||||
server := NewPlayerServer(&store)
|
||||
|
||||
t.Run("it returns 200 on /league", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
response := httptest.NewRecorder()
|
||||
t.Run("it returns 200 on /league", func(t *testing.T) {
|
||||
request, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
var got []Player
|
||||
var got []Player
|
||||
|
||||
err := json.NewDecoder(response.Body).Decode(&got)
|
||||
err := json.NewDecoder(response.Body).Decode(&got)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf ("Unable to parse response from server %q into slice of Player, '%v'", response.Body, err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", response.Body, err)
|
||||
}
|
||||
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
})
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -365,8 +365,8 @@ Given the JSON data model, it looks like we need an array of `Player` with some
|
||||
|
||||
```go
|
||||
type Player struct {
|
||||
Name string
|
||||
Wins int
|
||||
Name string
|
||||
Wins int
|
||||
}
|
||||
```
|
||||
|
||||
@ -397,13 +397,13 @@ Our endpoint currently does not return a body so it cannot be parsed into JSON.
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) {
|
||||
leagueTable := []Player{
|
||||
{"Chris", 20},
|
||||
}
|
||||
leagueTable := []Player{
|
||||
{"Chris", 20},
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(leagueTable)
|
||||
json.NewEncoder(w).Encode(leagueTable)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
```
|
||||
|
||||
@ -424,14 +424,14 @@ It would be nice to introduce a separation of concern between our handler and ge
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(p.getLeagueTable())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(p.getLeagueTable())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (p *PlayerServer) getLeagueTable() []Player{
|
||||
return []Player{
|
||||
{"Chris", 20},
|
||||
}
|
||||
func (p *PlayerServer) getLeagueTable() []Player {
|
||||
return []Player{
|
||||
{"Chris", 20},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -445,9 +445,9 @@ Update `StubPlayerStore` to let it store a league, which is just a slice of `Pla
|
||||
|
||||
```go
|
||||
type StubPlayerStore struct {
|
||||
scores map[string]int
|
||||
winCalls []string
|
||||
league []Player
|
||||
scores map[string]int
|
||||
winCalls []string
|
||||
league []Player
|
||||
}
|
||||
```
|
||||
|
||||
@ -456,35 +456,35 @@ Next, update our current test by putting some players in the league property of
|
||||
```go
|
||||
func TestLeague(t *testing.T) {
|
||||
|
||||
t.Run("it returns the league table as JSON", func(t *testing.T) {
|
||||
wantedLeague := []Player{
|
||||
{"Cleo", 32},
|
||||
{"Chris", 20},
|
||||
{"Tiest", 14},
|
||||
}
|
||||
t.Run("it returns the league table as JSON", func(t *testing.T) {
|
||||
wantedLeague := []Player{
|
||||
{"Cleo", 32},
|
||||
{"Chris", 20},
|
||||
{"Tiest", 14},
|
||||
}
|
||||
|
||||
store := StubPlayerStore{nil, nil, wantedLeague}
|
||||
server := NewPlayerServer(&store)
|
||||
store := StubPlayerStore{nil, nil, wantedLeague}
|
||||
server := NewPlayerServer(&store)
|
||||
|
||||
request, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
response := httptest.NewRecorder()
|
||||
request, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
var got []Player
|
||||
var got []Player
|
||||
|
||||
err := json.NewDecoder(response.Body).Decode(&got)
|
||||
err := json.NewDecoder(response.Body).Decode(&got)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", response.Body, err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", response.Body, err)
|
||||
}
|
||||
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
|
||||
if !reflect.DeepEqual(got, wantedLeague) {
|
||||
t.Errorf("got %v want %v", got, wantedLeague)
|
||||
}
|
||||
})
|
||||
if !reflect.DeepEqual(got, wantedLeague) {
|
||||
t.Errorf("got %v want %v", got, wantedLeague)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -513,9 +513,9 @@ We know the data is in our `StubPlayerStore` and we've abstracted that away into
|
||||
|
||||
```go
|
||||
type PlayerStore interface {
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
GetLeague() []Player
|
||||
GetPlayerScore(name string) int
|
||||
RecordWin(name string)
|
||||
GetLeague() []Player
|
||||
}
|
||||
```
|
||||
|
||||
@ -523,8 +523,8 @@ Now we can update our handler code to call that rather than returning a hard-cod
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(p.store.GetLeague())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(p.store.GetLeague())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
```
|
||||
|
||||
@ -550,7 +550,7 @@ For `StubPlayerStore` it's pretty easy, just return the `league` field we added
|
||||
|
||||
```go
|
||||
func (s *StubPlayerStore) GetLeague() []Player {
|
||||
return s.league
|
||||
return s.league
|
||||
}
|
||||
```
|
||||
|
||||
@ -558,7 +558,7 @@ Here's a reminder of how `InMemoryStore` is implemented.
|
||||
|
||||
```go
|
||||
type InMemoryPlayerStore struct {
|
||||
store map[string]int
|
||||
store map[string]int
|
||||
}
|
||||
```
|
||||
|
||||
@ -568,7 +568,7 @@ So let's just get the compiler happy for now and live with the uncomfortable fee
|
||||
|
||||
```go
|
||||
func (i *InMemoryPlayerStore) GetLeague() []Player {
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
@ -582,23 +582,23 @@ The test code does not convey out intent very well and has a lot of boilerplate
|
||||
|
||||
```go
|
||||
t.Run("it returns the league table as JSON", func(t *testing.T) {
|
||||
wantedLeague := []Player{
|
||||
{"Cleo", 32},
|
||||
{"Chris", 20},
|
||||
{"Tiest", 14},
|
||||
}
|
||||
wantedLeague := []Player{
|
||||
{"Cleo", 32},
|
||||
{"Chris", 20},
|
||||
{"Tiest", 14},
|
||||
}
|
||||
|
||||
store := StubPlayerStore{nil, nil, wantedLeague}
|
||||
server := NewPlayerServer(&store)
|
||||
store := StubPlayerStore{nil, nil, wantedLeague}
|
||||
server := NewPlayerServer(&store)
|
||||
|
||||
request := newLeagueRequest()
|
||||
response := httptest.NewRecorder()
|
||||
request := newLeagueRequest()
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(response, request)
|
||||
server.ServeHTTP(response, request)
|
||||
|
||||
got := getLeagueFromResponse(t, response.Body)
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
assertLeague(t, got, wantedLeague)
|
||||
got := getLeagueFromResponse(t, response.Body)
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
assertLeague(t, got, wantedLeague)
|
||||
})
|
||||
```
|
||||
|
||||
@ -606,26 +606,26 @@ Here are the new helpers
|
||||
|
||||
```go
|
||||
func getLeagueFromResponse(t *testing.T, body io.Reader) (league []Player) {
|
||||
t.Helper()
|
||||
err := json.NewDecoder(body).Decode(&league)
|
||||
t.Helper()
|
||||
err := json.NewDecoder(body).Decode(&league)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", body, err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", body, err)
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func assertLeague(t *testing.T, got, want []Player) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func newLeagueRequest() *http.Request {
|
||||
req, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
return req
|
||||
req, _ := http.NewRequest(http.MethodGet, "/league", nil)
|
||||
return req
|
||||
}
|
||||
```
|
||||
|
||||
@ -637,7 +637,7 @@ Add this assertion to the existing test
|
||||
|
||||
```go
|
||||
if response.Result().Header.Get("content-type") != "application/json" {
|
||||
t.Errorf("response did not have content-type of application/json, got %v", response.Result().Header)
|
||||
t.Errorf("response did not have content-type of application/json, got %v", response.Result().Header)
|
||||
}
|
||||
```
|
||||
|
||||
@ -655,8 +655,8 @@ Update `leagueHandler`
|
||||
|
||||
```go
|
||||
func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(p.store.GetLeague())
|
||||
w.Header().Set("content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(p.store.GetLeague())
|
||||
}
|
||||
```
|
||||
|
||||
@ -670,10 +670,10 @@ Add a helper for `assertContentType`.
|
||||
const jsonContentType = "application/json"
|
||||
|
||||
func assertContentType(t *testing.T, response *httptest.ResponseRecorder, want string) {
|
||||
t.Helper()
|
||||
if response.Result().Header.Get("content-type") != want {
|
||||
t.Errorf("response did not have content-type of %s, got %v", want, response.Result().Header)
|
||||
}
|
||||
t.Helper()
|
||||
if response.Result().Header.Get("content-type") != want {
|
||||
t.Errorf("response did not have content-type of %s, got %v", want, response.Result().Header)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -693,33 +693,33 @@ We can use `t.Run` to break up this test a bit and we can reuse the helpers from
|
||||
|
||||
```go
|
||||
func TestRecordingWinsAndRetrievingThem(t *testing.T) {
|
||||
store := NewInMemoryPlayerStore()
|
||||
server := NewPlayerServer(store)
|
||||
player := "Pepper"
|
||||
store := NewInMemoryPlayerStore()
|
||||
server := NewPlayerServer(store)
|
||||
player := "Pepper"
|
||||
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
|
||||
|
||||
t.Run("get score", func(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
server.ServeHTTP(response, newGetScoreRequest(player))
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
t.Run("get score", func(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
server.ServeHTTP(response, newGetScoreRequest(player))
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
|
||||
assertResponseBody(t, response.Body.String(), "3")
|
||||
})
|
||||
assertResponseBody(t, response.Body.String(), "3")
|
||||
})
|
||||
|
||||
t.Run("get league", func(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
server.ServeHTTP(response, newLeagueRequest())
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
t.Run("get league", func(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
server.ServeHTTP(response, newLeagueRequest())
|
||||
assertStatus(t, response.Code, http.StatusOK)
|
||||
|
||||
got := getLeagueFromResponse(t, response.Body)
|
||||
want := []Player{
|
||||
{"Pepper", 3},
|
||||
}
|
||||
assertLeague(t, got, want)
|
||||
})
|
||||
got := getLeagueFromResponse(t, response.Body)
|
||||
want := []Player{
|
||||
{"Pepper", 3},
|
||||
}
|
||||
assertLeague(t, got, want)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@ -737,11 +737,11 @@ func TestRecordingWinsAndRetrievingThem(t *testing.T) {
|
||||
|
||||
```go
|
||||
func (i *InMemoryPlayerStore) GetLeague() []Player {
|
||||
var league []Player
|
||||
for name, wins := range i.store {
|
||||
league = append(league, Player{name, wins})
|
||||
}
|
||||
return league
|
||||
var league []Player
|
||||
for name, wins := range i.store {
|
||||
league = append(league, Player{name, wins})
|
||||
}
|
||||
return league
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
266
select.md
266
select.md
@ -17,15 +17,15 @@ Let's start with something naive to get us going.
|
||||
|
||||
```go
|
||||
func TestRacer(t *testing.T) {
|
||||
slowURL := "http://www.facebook.com"
|
||||
fastURL := "http://www.quii.co.uk"
|
||||
slowURL := "http://www.facebook.com"
|
||||
fastURL := "http://www.quii.co.uk"
|
||||
|
||||
want := fastURL
|
||||
got := Racer(slowURL, fastURL)
|
||||
want := fastURL
|
||||
got := Racer(slowURL, fastURL)
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -39,7 +39,7 @@ We know this isn't perfect and has problems but it will get us going. It's impor
|
||||
|
||||
```go
|
||||
func Racer(a, b string) (winner string) {
|
||||
return
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
@ -49,19 +49,19 @@ func Racer(a, b string) (winner string) {
|
||||
|
||||
```go
|
||||
func Racer(a, b string) (winner string) {
|
||||
startA := time.Now()
|
||||
http.Get(a)
|
||||
aDuration := time.Since(startA)
|
||||
startA := time.Now()
|
||||
http.Get(a)
|
||||
aDuration := time.Since(startA)
|
||||
|
||||
startB := time.Now()
|
||||
http.Get(b)
|
||||
bDuration := time.Since(startB)
|
||||
startB := time.Now()
|
||||
http.Get(b)
|
||||
bDuration := time.Since(startB)
|
||||
|
||||
if aDuration < bDuration {
|
||||
return a
|
||||
}
|
||||
if aDuration < bDuration {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
@ -92,27 +92,27 @@ Let's change our tests to use mocks so we have reliable servers to test against
|
||||
```go
|
||||
func TestRacer(t *testing.T) {
|
||||
|
||||
slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
slowURL := slowServer.URL
|
||||
fastURL := fastServer.URL
|
||||
slowURL := slowServer.URL
|
||||
fastURL := fastServer.URL
|
||||
|
||||
want := fastURL
|
||||
got := Racer(slowURL, fastURL)
|
||||
want := fastURL
|
||||
got := Racer(slowURL, fastURL)
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
slowServer.Close()
|
||||
fastServer.Close()
|
||||
slowServer.Close()
|
||||
fastServer.Close()
|
||||
}
|
||||
```
|
||||
|
||||
@ -136,20 +136,20 @@ We have some duplication in both our production code and test code.
|
||||
|
||||
```go
|
||||
func Racer(a, b string) (winner string) {
|
||||
aDuration := measureResponseTime(a)
|
||||
bDuration := measureResponseTime(b)
|
||||
aDuration := measureResponseTime(a)
|
||||
bDuration := measureResponseTime(b)
|
||||
|
||||
if aDuration < bDuration {
|
||||
return a
|
||||
}
|
||||
if aDuration < bDuration {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
return b
|
||||
}
|
||||
|
||||
func measureResponseTime(url string) time.Duration {
|
||||
start := time.Now()
|
||||
http.Get(url)
|
||||
return time.Since(start)
|
||||
start := time.Now()
|
||||
http.Get(url)
|
||||
return time.Since(start)
|
||||
}
|
||||
```
|
||||
|
||||
@ -158,28 +158,28 @@ This DRY-ing up makes our `Racer` code a lot easier to read.
|
||||
```go
|
||||
func TestRacer(t *testing.T) {
|
||||
|
||||
slowServer := makeDelayedServer(20 * time.Millisecond)
|
||||
fastServer := makeDelayedServer(0 * time.Millisecond)
|
||||
slowServer := makeDelayedServer(20 * time.Millisecond)
|
||||
fastServer := makeDelayedServer(0 * time.Millisecond)
|
||||
|
||||
defer slowServer.Close()
|
||||
defer fastServer.Close()
|
||||
defer slowServer.Close()
|
||||
defer fastServer.Close()
|
||||
|
||||
slowURL := slowServer.URL
|
||||
fastURL := fastServer.URL
|
||||
slowURL := slowServer.URL
|
||||
fastURL := fastServer.URL
|
||||
|
||||
want := fastURL
|
||||
got := Racer(slowURL, fastURL)
|
||||
want := fastURL
|
||||
got := Racer(slowURL, fastURL)
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func makeDelayedServer(delay time.Duration) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(delay)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(delay)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
@ -204,21 +204,21 @@ To do this, we're going to introduce a new construct called `select` which helps
|
||||
|
||||
```go
|
||||
func Racer(a, b string) (winner string) {
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a
|
||||
case <-ping(b):
|
||||
return b
|
||||
}
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a
|
||||
case <-ping(b):
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func ping(url string) chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
http.Get(url)
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
http.Get(url)
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
```
|
||||
|
||||
@ -258,17 +258,17 @@ Our final requirement was to return an error if `Racer` takes longer than 10 sec
|
||||
|
||||
```go
|
||||
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
|
||||
serverA := makeDelayedServer(11 * time.Second)
|
||||
serverB := makeDelayedServer(12 * time.Second)
|
||||
serverA := makeDelayedServer(11 * time.Second)
|
||||
serverB := makeDelayedServer(12 * time.Second)
|
||||
|
||||
defer serverA.Close()
|
||||
defer serverB.Close()
|
||||
defer serverA.Close()
|
||||
defer serverB.Close()
|
||||
|
||||
_, err := Racer(serverA.URL, serverB.URL)
|
||||
_, err := Racer(serverA.URL, serverB.URL)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error but didn't get one")
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("expected an error but didn't get one")
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@ -282,12 +282,12 @@ We've made our test servers take longer than 10s to return to exercise this scen
|
||||
|
||||
```go
|
||||
func Racer(a, b string) (winner string, error error) {
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
}
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -307,14 +307,14 @@ If you run it now after 11 seconds it will fail.
|
||||
|
||||
```go
|
||||
func Racer(a, b string) (winner string, error error) {
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
case <-time.After(10 * time.Second):
|
||||
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
|
||||
}
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
case <-time.After(10 * time.Second):
|
||||
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -330,14 +330,14 @@ What we can do is make the timeout configurable. So in our test, we can have a v
|
||||
|
||||
```go
|
||||
func Racer(a, b string, timeout time.Duration) (winner string, error error) {
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
case <-time.After(timeout):
|
||||
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
|
||||
}
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
case <-time.After(timeout):
|
||||
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -354,18 +354,18 @@ Given this knowledge, let's do a little refactoring to be sympathetic to both ou
|
||||
var tenSecondTimeout = 10 * time.Second
|
||||
|
||||
func Racer(a, b string) (winner string, error error) {
|
||||
return ConfigurableRacer(a, b, tenSecondTimeout)
|
||||
return ConfigurableRacer(a, b, tenSecondTimeout)
|
||||
}
|
||||
|
||||
func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) {
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
case <-time.After(timeout):
|
||||
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
|
||||
}
|
||||
select {
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
case <-time.After(timeout):
|
||||
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -374,39 +374,39 @@ Our users and our first test can use `Racer` (which uses `ConfigurableRacer` und
|
||||
```go
|
||||
func TestRacer(t *testing.T) {
|
||||
|
||||
t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) {
|
||||
slowServer := makeDelayedServer(20 * time.Millisecond)
|
||||
fastServer := makeDelayedServer(0 * time.Millisecond)
|
||||
t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) {
|
||||
slowServer := makeDelayedServer(20 * time.Millisecond)
|
||||
fastServer := makeDelayedServer(0 * time.Millisecond)
|
||||
|
||||
defer slowServer.Close()
|
||||
defer fastServer.Close()
|
||||
defer slowServer.Close()
|
||||
defer fastServer.Close()
|
||||
|
||||
slowURL := slowServer.URL
|
||||
fastURL := fastServer.URL
|
||||
slowURL := slowServer.URL
|
||||
fastURL := fastServer.URL
|
||||
|
||||
want := fastURL
|
||||
got, err := Racer(slowURL, fastURL)
|
||||
want := fastURL
|
||||
got, err := Racer(slowURL, fastURL)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect an error but got one %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect an error but got one %v", err)
|
||||
}
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
|
||||
server := makeDelayedServer(25 * time.Millisecond)
|
||||
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
|
||||
server := makeDelayedServer(25 * time.Millisecond)
|
||||
|
||||
defer server.Close()
|
||||
defer server.Close()
|
||||
|
||||
_, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond)
|
||||
_, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error but didn't get one")
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("expected an error but didn't get one")
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user