/league
which returns a list of all players stored. She would like this to be returned as JSON.PlayerStore
to use./league
and get an OK
back.PlayerServer
returns a 404 Not Found
, as if we were trying to get the wins for an unknown player. Looking at how server.go
implements ServeHTTP
, we realize that it always assumes to be called with a URL pointing to a specific player:ServeMux
(request multiplexer) which lets you attach http.Handler
s to particular request paths.x
path use y
handler.http.HandlerFunc
and an anonymous function to w.WriteHeader(http.StatusOK)
when /league
is requested to make our new test pass./players/
route we just cut and paste our code into another http.HandlerFunc
.ServeHTTP
(notice how ServeMux
is also an http.Handler
?)ServeHTTP
is looking quite big, we can separate things out a bit by refactoring our handlers into separate methods.NewPlayerServer
function which will take our dependencies and do the one-time setup of creating the router. Each request can then just use that one instance of the router.PlayerServer
now needs to store a router.ServeHTTP
and into our NewPlayerServer
so this only has to be done once, not per request.PlayerServer{&store}
with NewPlayerServer(&store)
.server := &PlayerServer{&store}
with server := NewPlayerServer(&store)
in server_test.go
, server_integration_test.go
, and main.go
.func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request)
as it is no longer needed!PlayerServer
, removing the named property router http.ServeMux
and replaced it with http.Handler
; this is called embedding.Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface.
PlayerServer
now has all the methods that http.Handler
has, which is just ServeHTTP
.http.Handler
we assign it to the router
we create in NewPlayerServer
. We can do this because http.ServeMux
has the method ServeHTTP
.ServeHTTP
method, as we are already exposing one via the embedded type.http.Handler
).http.ServeMux
instead (the concrete type) it would still work but users of PlayerServer
would be able to add new routes to our server because Handle(path, handler)
would be public./league
endpoint. We now need to make it return some useful information.Player
with some fields so we have created a new type to capture this.Decoder
from encoding/json
package and then call its Decode
method. To create a Decoder
it needs an io.Reader
to read from which in our case is our response spy's Body
.Decode
takes the address of the thing we are trying to decode into which is why we declare an empty slice of Player
the line before.Decode
can return an error
. There's no point continuing the test if that fails so we check for the error and stop the test with t.Fatalf
if it happens. Notice that we print the response body along with the error as it's important for someone running the test to see what string cannot be parsed.Encoder
you need an io.Writer
which is what http.ResponseWriter
implements.Decoder
you need an io.Reader
which the Body
field of our response spy implements.io.Writer
and this is another demonstration of its prevalence in the standard library and how a lot of libraries easily work with it.leagueTable
as we know we're going to not hard-code that very soon.StubPlayerStore
to let it store a league, which is just a slice of Player
. We'll store our expected data in there.StubPlayerStore
; set it to nil for the other tests.StubPlayerStore
and we've abstracted that away into an interface PlayerStore
. We need to update this so anyone passing us in a PlayerStore
can provide us with the data for leagues.getLeagueTable()
and then update leagueHandler
to call GetLeague()
.InMemoryPlayerStore
and StubPlayerStore
do not have the new method we added to our interface.StubPlayerStore
it's pretty easy, just return the league
field we added earlier.InMemoryStore
is implemented.GetLeague
"properly" by iterating over the map remember we are just trying to write the minimal amount of code to make the tests pass.InMemoryStore
.content-type
header in the response so machines can recognise we are returning JSON
.leagueHandler
leagueHandler
assertContentType
.PlayerServer
for now we can turn our attention to InMemoryPlayerStore
because right now if we tried to demo this to the product owner /league
will not work./league
.t.Run
to break up this test a bit and we can reuse the helpers from our server tests - again showing the importance of refactoring tests.InMemoryPlayerStore
is returning nil
when you call GetLeague()
so we'll need to fix that.Player
.http.Handler
interface in that you assign routes to Handler
s and the router itself is also a Handler
. It does not have some features you might expect though such as path variables (e.g /users/{id}
). You can easily parse this information yourself but you might want to consider looking at other routing libraries if it becomes a burden. Most of the popular ones stick to the standard library's philosophy of also implementing http.Handler
.