JSON, roteamento e aninhamento
​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.// 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
.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.=== 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
.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 handlery
. - Então para nosso novo endpoint, nós usamos
http.HandlerFunc
e uma função anônima paraw.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 outrohttp.HandlerFunc
. - Finalmente, nós lidamos com a requisição que está vindo chamando nosso novo roteador
ServeHTTP
(notou comoServeMux
é também umhttp.Handler
?)
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 nossoNovoServidorJogador
, 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}
porNovoServidorJogador(&armazenamento)
.
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!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.
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.
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
}
]
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)
})
}
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.
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
}
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.=== 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.
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.
Note a amável simetria na biblioteca padrão.
- Para criar um
Encoder
você precisa de umio.Writer
que é o quehttp.ResponseWriter
implementa. - Para criar um
Decoder
você precisa de umio.Reader
que o campoBody
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.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.
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)
}
})
}
./server_test.go:33:3: too few values in struct initializer
./server_test.go:70:3: too few values in struct initializer
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}]
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!
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
.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)
}
=== 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]]
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.
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
.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)
})
}
=== RUN TestGravaVitoriasEAsRetorna/obter_liga
--- FAIL: TestGravaVitoriasEAsRetorna/obter_liga (0.00s)
servidor_integration_test.go:35: obtido [] esperado [{Pepper 3}]
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.
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 paraHandler
s e a rota em si também é umHandler
. 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 implementamhttp.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.