Skip to content

Commit

Permalink
continue with more iterations on http server
Browse files Browse the repository at this point in the history
  • Loading branch information
quii committed Apr 26, 2018
1 parent fefb508 commit b9fb7a0
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 1 deletion.
114 changes: 113 additions & 1 deletion http-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,4 +718,116 @@ func (p *PlayerServer) processWin(w http.ResponseWriter) {
}
```

This makes the routing aspect of `ServeHTTP` a bit clearer and means our next iterations on storing can just be inside `processWin`.
This makes the routing aspect of `ServeHTTP` a bit clearer and means our next iterations on storing can just be inside `processWin`.

Next we want to check that when we do our `POST /players/{name}/win` that our `PlayerStore` is told to record the win.

## Write the test first

We can accomplish this by extending our `StubPlayerScore` with a new `Store` method and then spy on its invocations.

```go
type StubPlayerStore struct {
scores map[string]string
winCalls []string
}

func (s *StubPlayerStore) GetPlayerScore(name string) string {
score := s.scores[name]
return score
}

func (s *StubPlayerStore) RecordWin(name string) {
s.winCalls = append(s.winCalls, name)
}
```

Now extend our test to check the number of invocations for a start

```go
func TestStoreWins(t *testing.T) {
store := StubPlayerStore{
map[string]string{},
}
server := &PlayerServer{&store}

t.Run("it accepts POSTs to /win", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, "/players/Pepper/win", nil)
res := httptest.NewRecorder()

server.ServeHTTP(res, req)

assertStatus(t, res.Code, http.StatusAccepted)

if len(store.winCalls) != 1 {
t.Errorf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
}
})
}
```

## Try to run the test

```
./server_test.go:26:20: too few values in struct initializer
./server_test.go:65:20: too few values in struct initializer
```


## Write the minimal amount of code for the test to run and check the failing test output

We need to update our code where we create a `StubPlayerStore` as we've added a new field

```go
store := StubPlayerStore{
map[string]string{},
nil,
}
```

```
--- FAIL: TestStoreWins (0.00s)
--- FAIL: TestStoreWins/it_accepts_POSTs_to_/win (0.00s)
server_test.go:80: got 0 calls to RecordWin want 1
```
## Write enough code to make it pass

As we're only asserting the number of calls rather than the specific values it makes our initial iteration a little smaller.

We need to update `PlayerServer`'s idea of what a `PlayerStore` is by changing the interface if we're going to be able to call `RecordWin`

```go
// PlayerStore stores score information about players
type PlayerStore interface {
GetPlayerScore(name string) string
RecordWin(name string)
}
```

By doing this `main` no longer compiles

```
./main.go:17:46: cannot use InMemoryPlayerStore literal (type *InMemoryPlayerStore) as type PlayerStore in field value:
*InMemoryPlayerStore does not implement PlayerStore (missing RecordWin method)
```

The compiler tells us what's wrong. Let's update `InMemoryPlayerStore` to have that method.

```go
type InMemoryPlayerStore struct{}

func (i *InMemoryPlayerStore) RecordWin(name string) {}
```

Try and run the tests and we should be back to compiling code but the test still failing.

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

Run the tests and it should be passing! Obviously `bob` isn't exactly what we want to lets further refine the test.
26 changes: 26 additions & 0 deletions http-server/v4/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"log"
"net/http"
)

// InMemoryPlayerStore collects data about players in memory
type InMemoryPlayerStore struct{}

// RecordWin will record a player's win
func (i *InMemoryPlayerStore) RecordWin(name string) {
}

// GetPlayerScore retrieves scores for a given player
func (i *InMemoryPlayerStore) GetPlayerScore(name string) string {
return "123"
}

func main() {
server := &PlayerServer{&InMemoryPlayerStore{}}

if err := http.ListenAndServe(":5000", server); err != nil {
log.Fatalf("could not listen on port 5000 %v", err)
}
}
45 changes: 45 additions & 0 deletions http-server/v4/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"fmt"
"net/http"
)

// PlayerStore stores score information about players
type PlayerStore interface {
GetPlayerScore(name string) string
RecordWin(name string)
}

// PlayerServer is a HTTP interface for player information
type PlayerServer struct {
store PlayerStore
}

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

}

func (p *PlayerServer) showScore(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]

score := p.store.GetPlayerScore(player)

if score == "" {
w.WriteHeader(http.StatusNotFound)
}

fmt.Fprint(w, score)
}

func (p *PlayerServer) processWin(w http.ResponseWriter) {
p.store.RecordWin("Bob")
w.WriteHeader(http.StatusAccepted)
}
102 changes: 102 additions & 0 deletions http-server/v4/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)

type StubPlayerStore struct {
scores map[string]string
winCalls []string
}

func (s *StubPlayerStore) GetPlayerScore(name string) string {
score := s.scores[name]
return score
}

func (s *StubPlayerStore) RecordWin(name string) {
s.winCalls = append(s.winCalls, name)
}

func TestGETPlayers(t *testing.T) {
store := StubPlayerStore{
map[string]string{
"Pepper": "20",
"Floyd": "10",
},
nil,
}
server := &PlayerServer{&store}

t.Run("returns Pepper's score", func(t *testing.T) {
req := newGetScoreRequest("Pepper")
res := httptest.NewRecorder()

server.ServeHTTP(res, req)

assertStatus(t, res.Code, http.StatusOK)
assertResponseBody(t, res.Body.String(), "20")
})

t.Run("returns Floyd's score", func(t *testing.T) {
req := newGetScoreRequest("Floyd")
res := httptest.NewRecorder()

server.ServeHTTP(res, req)

assertStatus(t, res.Code, http.StatusOK)
assertResponseBody(t, res.Body.String(), "10")
})

t.Run("returns 404 on missing players", func(t *testing.T) {
req := newGetScoreRequest("Apollo")
res := httptest.NewRecorder()

server.ServeHTTP(res, req)

assertStatus(t, res.Code, http.StatusNotFound)
})
}

func TestStoreWins(t *testing.T) {
store := StubPlayerStore{
map[string]string{},
nil,
}
server := &PlayerServer{&store}

t.Run("it accepts POSTs to /win", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, "/players/Pepper/win", nil)
res := httptest.NewRecorder()

server.ServeHTTP(res, req)

assertStatus(t, res.Code, http.StatusAccepted)

if len(store.winCalls) != 1 {
t.Errorf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
}
})
}

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

func newGetScoreRequest(name string) *http.Request {
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 '%s' want '%s'", got, want)
}
}

0 comments on commit b9fb7a0

Please sign in to comment.