JSON, roteamento e aninhamento

Você pode encontrar todo o código para este capítulo aqui

No capítulo anterior nós criamos um servidor web para armazenar quantos jogos nossos jogadores venceram.

Nossa gerente de produtos veio com um novo requisito; criar um novo endpoint chamado /liga que retorne uma lista contendo todos os jogadores armazenados. Ela gostaria que isto fosse retornado como um JSON.

Este é o código que temos até agora

// servidor.go
package main
import (
"fmt"
"net/http"
)
type ArmazenamentoJogador interface {
ObtemPontuacaoDoJogador(nome string) int
GravarVitoria(nome string)
}
type ServidorJogador struct {
armazenamento ArmazenamentoJogador
}
func (s *ServidorJogador) ServeHTTP(w http.ResponseWriter, r *http.Request) {
jogador := r.URL.Path[len("/jogadores/"):]
switch r.Method {
case http.MethodPost:
s.processarVitoria(w, jogador)
case http.MethodGet:
s.mostrarPontuacao(w, jogador)
}
}
func (s *ServidorJogador) mostrarPontuacao(w http.ResponseWriter, jogador string) {
pontuação := s.armazenamento.ObtemPontuacaoDoJogador(jogador)
if pontuação == 0 {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, pontuação)
}
func (s *ServidorJogador) processarVitoria(w http.ResponseWriter, jogador string) {
s.armazenamento.GravarVitoria(jogador)
w.WriteHeader(http.StatusAccepted)
}
// ArmazenamentoDeJogadorNaMemoria.go
package main
func NovoArmazenamentoDeJogadorNaMemoria() *ArmazenamentoDeJogadorNaMemoria {
return &ArmazenamentoDeJogadorNaMemoria{map[string]int{}}
}
type ArmazenamentoDeJogadorNaMemoria struct {
armazenamento map[string]int
}
func (a *ArmazenamentoDeJogadorNaMemoria) GravarVitoria(nome string) {
a.armazenamento[nome]++
}
func (a *ArmazenamentoDeJogadorNaMemoria) ObtemPontuacaoDoJogador(nome string) int {
return a.armazenamento[nome]
}
// main.go
package main
import (
"log"
"net/http"
)
func main() {
servidor := &ServidorJogador{NovoArmazenamentoDeJogadorNaMemoria()}
if err := http.ListenAndServe(":5000", servidor); err != nil {
log.Fatalf("não foi possível ouvir na porta 5000 %v", err)
}
}

Você pode encontrar os testes correspondentes no endereço no topo do capítulo.

Nós vamos começar criando o endpoint para a tabela de liga.

Escreva os testes primeiro

Ampliaremos a suite de testes existente, pois temos algumas funções de teste úteis e um ArmazenamentoJogador falso para usar.

// server_test.go
func TestLiga(t *testing.T) {
armazenamento := EsbocoArmazenamentoJogador{}
servidor := &ServidorJogador{&armazenamento}
t.Run("retorna 200 em /liga", func(t *testing.T) {
requisicao, _ := http.NewRequest(http.MethodGet, "/liga", nil)
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, requisicao)
verificaStatus(t, resposta.Code, http.StatusOK)
})
}

Antes de nos preocuparmos sobre as pontuações atuais e o JSON, nós vamos tentar manter as mudanças pequenas com o plano de ir passo a passo rumo ao nosso objetivo. O início mais simples é checar se nós conseguimos consultar /liga e obter um OK de retorno.

Tente rodar os testes

=== RUN TestLiga/retorna_200_em_/liga
panic: runtime error: slice bounds out of range [recovered]
panic: runtime error: slice bounds out of range
goroutine 6 [running]:
testing.tRunner.func1(0xc42010c3c0)
/usr/local/Cellar/go/1.10/libexec/src/testing/testing.go:742 +0x29d
panic(0x1274d60, 0x1438240)
/usr/local/Cellar/go/1.10/libexec/src/runtime/panic.go:505 +0x229
github.com/larien/aprenda-go-com-testes/json-and-io/v2.(*ServidorJogador).ServeHTTP(0xc420048d30, 0x12fc1c0, 0xc420010940, 0xc420116000)
/Users/larien/go/src/github.com/larien/aprenda-go-com-testes/json-and-io/v2/servidor.go:20 +0xec

Seu ServidorJogador deve estar sendo abortado por um panic como acima. Vá para a linha de código que está apontando para servidor.go no stack trace.

jogador := r.URL.Path[len("/jogadores/"):]

No capítulo anterior, nós mencionamos que esta era uma maneira bastante ingênua de fazer o nosso roteamento. O que está acontecendo é que ele está tentando cortar a string do caminho da URL começando do índice após /liga e então, isto nos dá um slice bounds out of range.

Escreva somente o código suficiente para fazê-lo passar

Go tem um mecanismo de rotas nativo (built-in) chamado ServeMux (requisição multiplexadora) que nos permite atracar um http.Handler para caminhos de uma requisição em específico.

Vamos cometer alguns pecados e obter os testes passando da maneira mais rápida que pudermos, sabendo que nós podemos refatorar isto com segurança uma vez que nós soubermos que os testes estão passando.

func (s *ServidorJogador) ServeHTTP(w http.ResponseWriter, r *http.Request) {
roteador := http.NewServeMux()
roteador.Handle("/liga", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
roteador.Handle("/jogadores/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jogador := r.URL.Path[len("/jogadores/"):]
switch r.Method {
case http.MethodPost:
s.processarVitoria(w, jogador)
case http.MethodGet:
s.mostrarPontuacao(w, jogador)
}
}))
roteador.ServeHTTP(w, r)
}
  • Quando a requisição começa nós criamos um roteador e então dizemos para o caminho x usar o handler y.

  • Então para nosso novo endpoint, nós usamos http.HandlerFunc e uma função anônima para w.WriteHeader(http.StatusOK) quando /liga é requisitada para fazer nosso novo teste passar.

  • Para a rota /jogadores/ nós somente recortamos e colamos nosso código dentro de outro http.HandlerFunc.

  • Finalmente, nós lidamos com a requisição que está vindo chamando nosso novo roteador ServeHTTP (notou como ServeMux é também um http.Handler?)

Refatorando

ServeHTTP parece um pouco grande, nós podemos separar as coisas um pouco refatorando nossos handlers em métodos separados.

func (s *ServidorJogador) ServeHTTP(w http.ResponseWriter, r *http.Request) {
roteador := http.NewServeMux()
roteador.Handle("/liga", http.HandlerFunc(s.manipulaLiga))
roteador.Handle("/jogadores/", http.HandlerFunc(s.manipulaJogadores))
roteador.ServeHTTP(w, r)
}
func (s *ServidorJogador) manipulaLiga(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func (s *ServidorJogador) manipulaJogadores(w http.ResponseWriter, r *http.Request) {
jogador := r.URL.Path[len("/jogadores/"):]
switch r.Method {
case http.MethodPost:
s.processarVitoria(w, jogador)
case http.MethodGet:
s.mostrarPontuacao(w, jogador)
}
}

É um pouco estranho (e ineficiente) estar configurando um roteador quando uma requisição chegar e então chamá-lo. O que idealmente queremos fazer é uma função do tipo NovoServidorJogador que pegará nossas dependências e ao ser chamada, irá fazer a configuração única da criação do roteador. Desta forma, cada requisição pode usar somente uma instância do nosso roteador.

type ServidorJogador struct {
armazenamento ArmazenamentoJogador
roteador *http.ServeMux
}
func NovoServidorJogador(armazenamento ArmazenamentoJogador) *ServidorJogador {
p := &ServidorJogador{
armazenamento,
http.NewServeMux(),
}
s.roteador.Handle("/liga", http.HandlerFunc(s.manipulaLiga))
s.roteador.Handle("/jogadores/", http.HandlerFunc(s.manipulaJogadores))
return s
}
func (s *ServidorJogador) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.roteador.ServeHTTP(w, r)
}
  • ServidorJogador agora precisa armazenar um roteador.

  • Nós movemos a criação do roteador para fora de ServeHTTP e colocamos dentro do nosso NovoServidorJogador, então isto só será feito uma vez, não por requisição.

  • Você vai precisar atualizar todos os testes e código de produção onde nós costumávamos fazer ServidorJogador{&armazenamento} por NovoServidorJogador(&armazenamento).

Uma refatoração final

Tente mudar o código para o seguinte:

type ServidorJogador struct {
armazenamento ArmazenamentoJogador
http.Handler
}
func NovoServidorJogador(armazenamento ArmazenamentoJogador) *ServidorJogador {
s := new(ServidorJogador)
s.armazenamento = armazenamento
roteador := http.NewServeMux()
roteador.Handle("/liga", http.HandlerFunc(s.manipulaLiga))
roteador.Handle("/jogadores/", http.HandlerFunc(s.manipulaJogadores))
s.Handler = roteador
return s
}

Finalmente, se certifique de que você deletou func (s *ServidorJogador) ServeHTTP(w http.ResponseWriter, r *http.Request) por não ser mais necessária!

Incorporando

Nós mudamos a segunda propriedade de ServidorJogador removendo a propriedade nomeada roteador http.ServeMux e substituindo por http.Handler; isto é chamado de incorporar.

O Go não provê a noção típica de subclasses orientada por tipo, mas tem a habilidade de "emprestar" partes de uma implementação por incorporar tipos dentro de uma struct ou interface.

Effective Go - Embedding

O que isto quer dizer é que nosso ServidorJogador agora tem todos os métodos que http.Handler têm, que é somente o ServeHTTP.

Para "preencher" o http.Handler nós atribuímos ele para o roteador que nós criamos em NovoServidorJogador. Nós podemos fazer isso porque http.ServeMux tem o método ServeHTTP.

Isto nos permite remover nosso próprio método ServeHTTP, pois nós já estamos expondo um via o tipo incorporado.

Incorporamento é um recurso muito interessante da linguagem. Você pode usar isto com interfaces para compor novas interfaces.

type Animal interface {
Comedor
Dormente
}

E você pode usar isto com tipos concretos também, não somente interfaces. Como você pode esperar, se você incorporar um tipo concreto você vai ter acesso a todos os seus métodos e campos públicos.

Alguma desvantagem?

Você deve ter cuidado ao incorporar tipos porque você vai expor todos os métodos e campos públicos do tipo que você incorporou. Em nosso caso, está tudo bem porque nós haviamos incorporado apenas a interface que nós queremos expôr (http.Handler).

Se nós tivéssemos sido "preguiçosos" e incorporado http.ServeMux (o tipo concreto) por exemplo, também funcionaria porém os usuários de ServidorJogador seriam capazes de adicionar novas rotas ao nosso servidor porque o método Handle(path, handler) seria público.

Quando incorporamos tipos, realmente devemos pensar sobre qual o impacto que isto terá em nossa API pública

Isto é um erro muito comum de mau uso de incorporamento, que termina poluindo nossas APIs e expondo os métodos internos dos seus tipos incorporados.

Agora que nós reestruturamos nossa aplicação, nós podemos facilmente adicionar novas rotas e botar para funcionar nosso endpoint /liga. Agora precisamos fazê-lo retornar algumas informações úteis.

Nós devemos retornar um JSON semelhante a este:

[
{
"Nome":"Bill",
"Vitórias":10
},
{
"Nome":"Alice",
"Vitórias":15
}
]

Escreva o teste primeiro

Nós vamos começar tentando analizar a resposta dentro de algo mais significativo.

func TestLiga(t *testing.T) {
armazenamento := EsbocoArmazenamentoJogador{}
servidor := NovoServidorJogador(&armazenamento)
t.Run("retorna 200 em /liga", func(t *testing.T) {
requisicao, _ := http.NewRequest(http.MethodGet, "/liga", nil)
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, requisicao)
var obtido []Jogador
err := json.NewDecoder(resposta.Body).Decode(&obtido)
if err != nil {
t.Fatalf ("Não foi possível fazer parse da resposta do servidor '%s' no slice de Jogador, '%v'", resposta.Body, err)
}
verificaStatus(t, resposta.Code, http.StatusOK)
})
}

Por que não testar o JSON como texto puro?

Você pode argumentar que um simples teste inicial poderia só comparar que o não foi possível ouvir na porta 5000 tem um particular texto em JSON.

Na minha experiência, testes que comparam JSONs de forma literal possuem os seguintes problemas:

  • Fragilidade. Se você mudar o modelo dos dados seu teste irá falhar.

  • Difícil de debugar. Pode ser complicado de entender qual é o problema real ao se comparar dois textos JSON.

  • Má intenção. Embora a saída deva ser JSON, o que é realmente importante é exatamente o que o dado é, ao invés de como ele está codificado.

  • Re-testando a biblioteca padrão. Não há a necessidade de testar como a biblioteca padrão gera JSON, ela já está testada. Não teste o código de outras pessoas.

Ao invés disso, nós poderíamos analisar o JSON dentro de estruturas de dados que são relevantes para nós e nossos testes.

Modelagem de dados

Dado o modelo de dados do JSON, parece que nós precisamos de uma lista de Jogador com alguns campos, sendo assim nós criaremos um novo tipo para capturarmos isso.

type Jogador struct {
Nome string
Vitorias int
}

Decodificação de JSON

var obtido []Jogador
err := json.NewDecoder(resposta.Body).Decode(&obtido)

Para analizar o JSON dentro de nosso modelo de dados nós criamos um Decoder do pacote encoding/json e então chamamos seu método Decode. Para criar um Decoder é necessário ler de um io.Reader, que em nosso caso é nossa própria resposta Body.

Decode pega o endereço da coisa que nós estamos tentando decodificar, e é por isso que nós declaramos um slice vazio de Jogador na linha anterior.

Esse processo de analisar um JSON pode falhar, então Decode pode retornar um error. Não há ponto de continuidade para o teste se isto acontecer, então nós checamos o erro e paramos o teste com t.Fatalf. Note que nós exibimos o não foi possível ouvir na porta 5000 junto do erro, pois é importante para qualquer outra pessoa que esteja rodando os testes ver que o texto não pôde ser analisado.

Tente rodar o teste

=== RUN TestLiga/retorna_200_em_/liga
--- FAIL: TestLiga/retorna_200_em_/liga (0.00s)
server_test.go:107: Não foi possível fazer parse da resposta do servidor '' no slice de Jogador, 'unexpected end of JSON input'

Nosso endpoint atualmente não retorna um corpo, então isso não pode ser analisado como JSON.

Escreva código suficiente para fazê-lo passar

func (s *ServidorJogador) manipulaLiga(w http.ResponseWriter, r *http.Request) {
tabelaDaLiga := []Jogador{
{"Chris", 20},
}
json.NewEncoder(w).Encode(tabelaDaLiga)
w.WriteHeader(http.StatusOK)
}

Os testes agora passam.

Codificando e decodificando

Note a amável simetria na biblioteca padrão.

  • Para criar um Encoder você precisa de um io.Writer que é o que http.ResponseWriter implementa.

  • Para criar um Decoder você precisa de um io.Reader que o campo Body da nossa resposta implementa.

Ao longo deste livro, nós temos usado io.Writer. Isso é uma outra demonstração desta prevalência nas bibliotecas padrões e de como várias bibliotecas facilmente trabalham em conjunto com elas.

Refatoração

Seria legal introduzir uma separação de conceitos entre nosso handler e o trecho de obter o tabelaDaLiga. Como sabemos, nós não vamos codificar isso por agora.

func (s *ServidorJogador) manipulaLiga(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(s.obterTabelaDaLiga())
w.WriteHeader(http.StatusOK)
}
func (s *ServidorJogador) obterTabelaDaLiga() []Jogador{
return []Jogador{
{"Chris", 20},
}
}

Mais adiante, nós vamos querer estender nossos testes para então podermos controlar exatamente qual dado nós queremos receber de volta.

Escreva o teste primeiro

Nós podemos atualizar o teste para afirmar que a tabela das ligas contem alguns jogadores que nós vamos pôr em nossa loja.

Atualize EsbocoArmazenamentoJogador para permitir que ele armazene uma liga, que é apenas um slice de Jogador. Nós vamos armazenar nossos dados esperados lá.

type EsbocoArmazenamentoJogador struct {
pontuações map[string]int
chamadasDeVitoria []string
liga []Jogador
}

Adiante, atualize nossos testes colocando alguns jogadores na propriedade da liga, para então afirmar que eles foram retornados do nosso servidor.

func TestLiga(t *testing.T) {
t.Run("retorna a tabela da Liga como JSON", func(t *testing.T) {
ligaEsperada := []Jogador{
{"Cleo", 32},
{"Chris", 20},
{"Tiest", 14},
}
armazenamento := EsbocoArmazenamentoJogador{nil, nil, ligaEsperada}
servidor := NovoServidorJogador(&armazenamento)
requisicao, _ := http.NewRequest(http.MethodGet, "/liga", nil)
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, requisicao)
var obtido []Jogador
err := json.NewDecoder(resposta.Body).Decode(&obtido)
if err != nil {
t.Fatalf("Não foi possível fazer parse da resposta do servidor '%s' no slice de Jogador, '%v'", resposta.Body, err)
}
verificaStatus(t, resposta.Code, http.StatusOK)
if !reflect.DeepEqual(obtido, ligaEsperada) {
t.Errorf("obtido %v esperado %v", obtido, ligaEsperada)
}
})
}

Tente rodar o teste

./server_test.go:33:3: too few values in struct initializer
./server_test.go:70:3: too few values in struct initializer

Escreva o minimo de código para que o teste rode e cheque as falhas na saída dele.

Você vai precisar atualizar os outros testes, assim como nós temos um novo campo em EsbocoArmazenamentoJogador; ponha-o como nulo para os outros testes.

Tente executar os testes novamente e você deverá ter:

=== RUN TestLiga/retorna_a_tabela_da_liga_como_JSON
--- FAIL: TestLiga/retorna_a_tabela_da_liga_como_JSON (0.00s)
server_test.go:124: obtido [{Chris 20}] esperado [{Cleo 32} {Chris 20} {Tiest 14}]

Escreva código suficiente para fazê-lo passar

Nós sabemos que o dado está em nosso EsbocoArmazenamentoJogador e nós abstraímos esses dados para uma interface ArmazenamentoJogador. Nós precisamos atualizar isto então qualquer um passando-nos um ArmazenamentoJogador pode prover-nos com dados para as ligas.

type ArmazenamentoJogador interface {
ObtemPontuacaoDoJogador(nome string) int
GravarVitoria(nome string)
ObterLiga() []Jogador
}

Agora nós podemos atualizar o código do nosso handler para chamar isto ao invés de retornar uma lista manualmente escrita. Delete nosso método obterTabelaDaLiga() e então atualize manipulaLiga para chamar ObterLiga().

func (s *ServidorJogador) manipulaLiga(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(s.armazenamento.ObterLiga())
w.WriteHeader(http.StatusOK)
}

Tente executar os testes:

# github.com/larien/aprenda-go-com-testes/json-and-io/v4
./main.go:9:50: cannot use NovoArmazenamentoDeJogadorNaMemoria() (type *ArmazenamentoDeJogadorNaMemoria) as type ArmazenamentoJogador in argument to NovoServidorJogador:
*ArmazenamentoDeJogadorNaMemoria does not implement ArmazenamentoJogador (missing ObterLiga method)
./servidor_integration_test.go:11:27: cannot use armazenamento (type *ArmazenamentoDeJogadorNaMemoria) as type ArmazenamentoJogador in argument to NovoServidorJogador:
*ArmazenamentoDeJogadorNaMemoria does not implement ArmazenamentoJogador (missing ObterLiga method)
./server_test.go:36:28: cannot use &armazenamento (type *EsbocoArmazenamentoJogador) as type ArmazenamentoJogador in argument to NovoServidorJogador:
*EsbocoArmazenamentoJogador does not implement ArmazenamentoJogador (missing ObterLiga method)
./server_test.go:74:28: cannot use &armazenamento (type *EsbocoArmazenamentoJogador) as type ArmazenamentoJogador in argument to NovoServidorJogador:
*EsbocoArmazenamentoJogador does not implement ArmazenamentoJogador (missing ObterLiga method)
./server_test.go:106:29: cannot use &armazenamento (type *EsbocoArmazenamentoJogador) as type ArmazenamentoJogador in argument to NovoServidorJogador:
*EsbocoArmazenamentoJogador does not implement ArmazenamentoJogador (missing ObterLiga method)

O compilador está reclamando porque ArmazenamentoDeJogadorNaMemoria e EsbocoArmazenamentoJogador não tem os novos métodos que nós adicionamos em nossa interface.

Para EsbocoArmazenamentoJogador isto é bem fácil, apenas retorne o campo liga que nós adicionamos anteriormente.

func (s *EsbocoArmazenamentoJogador) ObterLiga() []Jogador {
return s.liga
}

Aqui está uma lembrança de como InMemoryStore é implementado:

type ArmazenamentoDeJogadorNaMemoria struct {
armazenamento map[string]int
}

Embora seja bastante simples para implementar ObterLiga "propriamente", iterando sobre o map, lembre que nós estamos apenas tentando escrever o mínimo de código para fazer os testes passarem.

Então vamos apenas deixar o compilador feliz por enquanto e viver com o desconfortável sentimento de uma implementação incompleta em nosso InMemoryStore.

func (a *ArmazenamentoDeJogadorNaMemoria) ObterLiga() []Jogador {
return nil
}

O que isto está realmente nos dizendo é que depois nós vamos querer testar isto, porém vamos estacionar isto por hora.

Tente executar os testes, o compilador deve passar e os testes deverão estar passando!

Refatoração

O código de teste não transmite suas intenções muito bem e possui vários trechos que podem ser refatorados.

t.Run("retorna a tabela da Liga como JSON", func(t *testing.T) {
ligaEsperada := []Jogador{
{"Cleo", 32},
{"Chris", 20},
{"Tiest", 14},
}
armazenamento := EsbocoArmazenamentoJogador{nil, nil, ligaEsperada}
servidor := NovoServidorJogador(&armazenamento)
requisicao := novaRequisicaoDeLiga()
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, requisicao)
obtido := obterLigaDaResposta(t, resposta.Body)
verificaStatus(t, resposta.Code, http.StatusOK)
verificaLiga(t, obtido, ligaEsperada)
})

Aqui estão os novos helpers:

func obterLigaDaResposta(t *testing.T, body io.Reader) (liga []Jogador) {
t.Helper()
err := json.NewDecoder(body).Decode(&liga)
if err != nil {
t.Fatalf("Não foi possível fazer parse da resposta do servidor '%s' no slice de Jogador, '%v'", body, err)
}
return
}
func verificaLiga(t *testing.T, obtido, esperado []Jogador) {
t.Helper()
if !reflect.DeepEqual(obtido, esperado) {
t.Errorf("obtido %v esperado %v", obtido, esperado)
}
}
func novaRequisicaoDeLiga() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "/liga", nil)
return req
}

Uma última coisa que nós precisamos fazer para nosso servidor funcionar é ter certeza de que nós retornamos um content-type correto na resposta, então as máquinas podem reconhecer que nós estamos retornando um JSON.

Escreva os testes primeiro

Adicione essa afirmação no teste existente

if resposta.Result().Header.Get("content-type") != "application/json" {
t.Errorf("resposta não tinha o tipo de conteúdo de application/json, obtido %v", resposta.Result().Header)
}

Tente rodar o teste

=== RUN TestLiga/retorna_a_tabela_da_liga_como_JSON
--- FAIL: TestLiga/retorna_a_tabela_da_liga_como_JSON (0.00s)
server_test.go:124: resposta não tinha o tipo de conteúdo de application/json, obtido map[Content-Type:[text/plain; charset=utf-8]]

Escreva código suficiente para fazê-lo passar

Atualize manipulaLiga

func (s *ServidorJogador) manipulaLiga(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
json.NewEncoder(w).Encode(s.armazenamento.ObterLiga())
}

O teste deve passar.

Refatoração

Adicione um helper para verificaTipoDoConteudo.

const tipoDoConteudoJSON = "application/json"
func verificaTipoDoConteudo(t *testing.T, resposta *httptest.ResponseRecorder, esperado string) {
t.Helper()
if resposta.Result().Header.Get("content-type") != esperado {
t.Errorf("resposta não obteve content-type de %s, obtido %v", esperado, resposta.Result().Header)
}
}

Use isso no teste.

verificaTipoDoConteudo(t, resposta, tipoDoConteudoJSON)

Agora que nós resolvemos ServidorJogador, por agora podemos mudar nossa atenção para ArmazenamentoDeJogadorNaMemoria porque no momento se nós tentarmos demonstrá-lo para o gerente de produto, /liga não vai funcionar.

A forma mais rápida de nós termos alguma confiança é adicionar a nosso teste de integração, nós podemos bater no novo endpoint e checar se nós recebemos a resposta correta de /liga.

Escreva o teste primeiro

Nós podemos usar t.Run para parar este teste um pouco e então reusar os helpers dos testes do nosso servidor - novamente mostrando a importância de refatoração dos testes.

func TestGravaVitoriasEAsRetorna(t *testing.T) {
armazenamento := NovoArmazenamentoDeJogadorNaMemoria()
servidor := NovoServidorJogador(armazenamento)
jogador := "Pepper"
servidor.ServeHTTP(httptest.NewRecorder(), novaRequisiçãoPostDeVitoria(jogador))
servidor.ServeHTTP(httptest.NewRecorder(), novaRequisiçãoPostDeVitoria(jogador))
servidor.ServeHTTP(httptest.NewRecorder(), novaRequisiçãoPostDeVitoria(jogador))
t.Run("obter pontuação", func(t *testing.T) {
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, novaRequisicaoObterPontuacao(jogador))
verificaStatus(t, resposta.Code, http.StatusOK)
verificaCorpoDaResposta(t, resposta.Body.String(), "3")
})
t.Run("obter liga", func(t *testing.T) {
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, novaRequisicaoDeLiga())
verificaStatus(t, resposta.Code, http.StatusOK)
obtido := obterLigaDaResposta(t, resposta.Body)
esperado := []Jogador{
{"Pepper", 3},
}
verificaLiga(t, obtido, esperado)
})
}

Tente rodar o teste

=== RUN TestGravaVitoriasEAsRetorna/obter_liga
--- FAIL: TestGravaVitoriasEAsRetorna/obter_liga (0.00s)
servidor_integration_test.go:35: obtido [] esperado [{Pepper 3}]

Escreva código suficiente para fazê-lo passar

ArmazenamentoDeJogadorNaMemoria is returning nil when you call ObterLiga() so we'll need to fix that.

func (a *ArmazenamentoDeJogadorNaMemoria) ObterLiga() []Jogador {
var liga []Jogador
for nome, vitórias := range a.armazenamento {
liga = append(liga, Jogador{nome, vitórias})
}
return liga
}

Tudo que nós precisamos fazer é iterar através do map e converter cada chave/valor para um Jogador

O teste deve passar agora.

Concluindo

Nós temos continuado a seguramente iterar no nosso programa usando TDD, fazendo ele suportar novos endpoints de uma forma manutenível com um roteador e isso pode agora retornar JSON para nossos consumidores. No próximo capítulo, nós vamos cobrir persistência de dados e ordenação de nossas ligas.

O que nós cobrimos:

  • Roteamento. A biblioteca padrão oferece uma fácil forma de usar tipos para fazer roteamento. Ela abraça completamente a interface http.Handler nela, tanto que você pode atribuir rotas para Handlers e a rota em si também é um Handler. Ela não tem alguns recursos que você pode esperar, como caminhos para variáveis (ex. /users/{id}). Você pode facilmente analisar esta informação por si mesmo porém você pode querer considerar olhar para outras bibliotecas de roteamento se isso se tornar um fardo. Muitas das mais populares seguem a filosofia das bibliotecas padrões e também implementam http.Handler.

  • Composição. Nós tocamos um pouco nesta técnica porém você pode ler mais sobre isso de Effective Go. Se há uma coisa que você deve tirar disso é que composições podem ser extremamente úteis, porém sempre pensando na sua API pública, só exponha o que é apropriado.

  • Serialização e Desserialização de JSON. A biblioteca padrão faz isto de forma bastante trivial ao serializar e desserializar nosso dado. Isto também abre para configurações e você pode customizar como esta transformação de dados funciona se necessário.