GET /players/{name}
should return a number indicating the total number of winsPOST /players/{name}
should record a win for that name, incrementing for every subsequent POST
Make the test work quickly, committing whatever sins necessary in process.
GET
a player without having stored something and it seems hard to know if POST
has worked without the GET
endpoint already existing.GET
will need a PlayerStore
thing to get scores for a player. This should be an interface so when we test we can create a simple stub to test our code without needing to have implemented any actual storage code.POST
we can spy on its calls to PlayerStore
to make sure it stores players correctly. Our implementation of saving won't be coupled to retrieval.Handler
.ServeHTTP
method which expects two arguments, the first is where we write our response and the second is the HTTP request that was sent to the server.server_test.go
and write a test for a function PlayerServer
that takes in those two arguments. The request sent in will be to get a player's score, which we expect to be "20"
.Request
to send in and we'll want to spy on what our handler writes to the ResponseWriter
.http.NewRequest
to create a request. The first argument is the request's method and the second is the request's path. The nil
argument refers to the request's body, which we don't need to set in this case.net/http/httptest
has a spy already made for us called ResponseRecorder
so we can use that. It has many helpful methods to inspect what has been written as a response../server_test.go:13:2: undefined: PlayerServer
server.go
and define PlayerServer
Greet
function. We learned that net/http's ResponseWriter
also implements io Writer
so we can use fmt.Fprint
to send strings as HTTP responses.main.go
file for our application and put this code ingo build
which will take all the .go
files in the directory and build you a program. You can then execute it with ./myprogram
.http.HandlerFunc
Handler
interface is what we need to implement in order to make a server. Typically we do that by creating a struct
and make it implement the interface by implementing its own ServeHTTP method. However the use-case for structs is for holding data but currently we have no state, so it doesn't feel right to be creating one.The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers. If f is a function with the appropriate signature, HandlerFunc(f) is a Handler that calls f.
HandlerFunc
has already implemented the ServeHTTP
method. By type casting our PlayerServer
function with it, we have now implemented the required Handler
.http.ListenAndServe(":5000"...)
ListenAndServe
takes a port to listen on a Handler
. If there is a problem the web server will return an error, an example of that might be the port already being listened to. For that reason we wrap the call in log.Fatal
to log the error to the user.Surely we need some kind of concept of storage to control which player gets what score. It's weird that the values seem so arbitrary in our tests.
r.URL.Path
returns the path of the request which we can then use strings.TrimPrefix
to trim away /players/
to get the requested player. It's not very robust but will do the trick for now.PlayerServer
by separating out the score retrieval into a functionGetPlayerScore
. This feels like the right place to separate the concerns using interfaces.PlayerServer
to be able to use a PlayerStore
, it will need a reference to one. Now feels like the right time to change our architecture so that our PlayerServer
is now a struct
.Handler
interface by adding a method to our new struct and putting in our existing handler code.store.GetPlayerScore
to get the score, rather than the local function we defined (which we can now delete)../main.go:9:58: type PlayerServer is not an expression
PlayerServer
and then call its method ServeHTTP
.main.go
won't compile for the same reason.PlayerStore
in our tests. We'll need to make a stub one up.map
is a quick and easy way of making a stub key/value store for our tests. Now let's create one of these stores for our tests and send it into our PlayerServer
.PlayerStore
that when you use it with a PlayerServer
you should get the following responses.http://localhost:5000/players/Pepper
.PlayerStore
.go build
again and hit the same URL you should get "123"
. Not great, but until we store data that's the best we can do. It also didn't feel great that our main application was starting up but not actually working. We had to manually test to see the problem.POST /players/{name}
scenarioPOST
scenario gets us closer to the "happy path", I feel it'll be easier to tackle the missing player scenario first as we're in that context already. We'll get to the rest later.StatusNotFound
on all responses but all our tests are passing!StatusOK
when players do exist in the store.assertStatus
to facilitate that.PlayerServer
to only return not found if the score is 0.GET /players/{name}
. Once this works we can then start asserting on our handler's interaction with the store.if
statement based on the request's method will do the trick.ServeHTTP
a bit clearer and means our next iterations on storing can just be inside processWin
.POST /players/{name}
that our PlayerStore
is told to record the win.StubPlayerStore
with a new RecordWin
method and then spy on its invocations.StubPlayerStore
as we've added a new fieldPlayerServer
's idea of what a PlayerStore
is by changing the interface if we're going to be able to call RecordWin
.main
no longer compilesInMemoryPlayerStore
to have that method.PlayerStore
has RecordWin
we can call it within our PlayerServer
"Bob"
isn't exactly what we want to send to RecordWin
, so let's further refine the test.winCalls
slice we can safely reference the first one and check it is equal to player
.processWin
to take http.Request
so we can look at the URL to extract the player's name. Once we have that we can call our store
with the correct value to make the test pass.main
and use the software as intended it doesn't work because we haven't got round to implementing PlayerStore
correctly. This is fine though; by focusing on our handler we have identified the interface that we need, rather than trying to design it up-front.InMemoryPlayerStore
but it's only here temporarily until we implement a more robust way of persisting player scores (i.e. a database).PlayerServer
and InMemoryPlayerStore
to finish off the functionality. This will let us get to our goal of being confident our application is working, without having to directly test InMemoryPlayerStore
. Not only that, but when we get around to implementing PlayerStore
with a database, we can test that implementation with the same integration test.InMemoryPlayerStore
and PlayerServer
.player
. We're not too concerned about the status codes in this test as it's not relevant to whether they are integrating well.response
) because we are going to try and get the player
's score.InMemoryPlayerStore
).InMemoryPlayerStore
to help me drive out a solution.map[string]int
to the InMemoryPlayerStore
struct