Websockets
Nós temos duas aplicações no nosso código-base de poquer.
- Aplicação de linha de comando. Pede ao usuário para que insira o número de jogadores. A partir daí informa os jogadores o valor da "aposta cega", que aumenta em função do tempo. A qualquer momento, um usuário pode entrar com
"{Jogador} ganhou"
para encerrar o jogo e salvar a vitória em um armazenamento. - Aplicação Web. Permite que os usuários salvem os ganhadores e mostrem uma tabela da liga. Divide o armazenamento com a aplicação de linha de comando.
A dona do produto está muito contente com a aplicação por linha de comando, mas acharia melhor se conseguíssimos levar todas essas funcionalidades para o navegador. Ela imagina uma página web com uma caixa de texto que permite que o usuário coloque o número de jogadores e, após submeter esse dado, informe o valor da "aposta cega", atualizando automaticamente quando for apropriado. Assim como a aplicação por linha de comando, ela espera que o usuário possa declarar o vencedor e que isso faça com que as devidas informações sejam salvas no banco de dados.
Descrevendo o projeto dessa forma parece bastante simples, mas sempre precisamos enfatizar que devemos ter uma abordagem iterativa pra desenvolver os nossos programas.
Em primeiro lugar, vamos precisar apresentar um HTML. Até agora, todos os nossos endpoints HTTP retornaram texto puro ou JSON. Nós poderíamos usar as mesmas técnicas que conhecemos (porque, no fim, tanto o texto puro quanto o JSON são strings), mas nós também podemos usar o pacote html/template para uma solução mais limpa.
Nós também temos que ser capazes de enviar mensagens assíncronas para o usuário dizendo
A aposta blind é *y*
sem ter que recarregar o navegador. Para facilitar isso, podemos usar WebSockets.WebSocket é uma tecnologia que permite a comunicação bidirecional por canais full-duplex sobre um único socket TCP (Transmission Control Protocol)
Como estamos adotando várias técnicas, é ainda mais importante que façamos o menor trabalho possível primeiro e só então iteramos.
Por causa disso, a primeira coisa que faremos é criar uma página web com um formulário para o usuário salvar um vencedor. Em vez de usar um formulário simples, vamos usar os WebSockets para enviar os dados para o nosso servidor o salvar.
Depois disso, iremos trabalhaor nos alertas cegos, uma vez que já teremos algum código de infraestrutura pronto.
Haverá algum JavaScript escrito para cumprir nossa tarefa, mas não vamos escrever testes para ele.
É claro que é possível, mas, em nome da breviedade, não incluíremos quaisquer explicações para isso.
Desculpem, amigos. Peçam para a O'Reilly me pagar para fazer um "Aprenda JavaScript com testes".
A primeira coisa que precisamos fazer é montar algum HTML para os usuários quando eles acessarem
/jogo
.Aqui está um lembrete do código no nosso servidor web:
type ServidorJogador struct {
armazenamento ArmazenamentoJogador
http.Handler
}
const tipoConteudoJSON = "application/json"
func NovoServidorJogador(armazenamento ArmazenamentoJogador) *ServidorJogador {
p := new(ServidorJogador)
p.armazenamento = armazenamento
roteador := http.NewServeMux()
roteador.Handle("/liga", http.HandlerFunc(p.manipulaLiga))
roteador.Handle("/jogadores/", http.HandlerFunc(p.manipulaJogadores))
p.Handler = roteador
return p
}
A maneira mais fácil que podemos fazer por agora é checar que recebemos um código
200
quando acessamos o GET /jogo
.func TestJogo(t *testing.T) {
t.Run("GET /jogo retorna 200", func(t *testing.T) {
servidor := NovoServidorJogador(&EsbocoDeArmazenamentoJogador{})
requisicao, _ := http.NewRequest(http.MethodGet, "/jogo", nil)
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, requisicao)
verificaStatus(t, resposta.Code, http.StatusOK)
})
}
--- FAIL: TestJogo (0.00s)
=== RUN TestJogo/GET_/game_returns_200
--- FAIL: TestJogo/GET_/game_returns_200 (0.00s)
server_test.go:109: não obteve o status correto, obtido 404, esperado 200
Nosso servidor tem um roteador definido, então deve ser relativamente fácil corrigir isso.
Adicione o seguinte no nosso roteador:
roteador.Handle("/jogo", http.HandlerFunc(p.jogo))
E então escreva o método
jogo
:func (p *ServidorJogador) jogo(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
O servidor já está bem graças às inserções que fizemos no código já bem refatorado.
Podemos ajeitar ainda mais o teste um pouco ao adicionarmos uma função auxiliar
novaRequisicaoDeJogo
para fazer a requisição para /jogo
. Tente escrever essa função você mesmo.func TestJogo(t *testing.T) {
t.Run("GET /jogo retorna 200", func(t *testing.T) {
servidor := NovoServidorJogador(&EsbocoDeArmazenamentoJogador{})
requisicao := novaRequisicaoJogo()
resposta := httptest.NewRecorder()
servidor.ServeHTTP(resposta, requisicao)
verificaStatus(t, resposta, http.StatusOK)
})
}
Você também vai notar que mudei o
verificaStatus
para aceitar resposta
ao invés de resposta.Code
já que parece combinar melhor.Agora precisamos que o endpoint retorne um pouco de HTML, e aqui está ele:
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>Vamos jogar pôquer</title>
</head>
<corpo>
<section id="jogo">
<div id="declare-vencedor">
<label for="vencedor">Vencedor</label>
<input type="text" id="vencedor"/>
<button id="vencedor-button">Declare vencedor</button>
</div>
</section>
</corpo>
<script type="application/javascript">
const submitWinnerButton = document.getElementById('vencedor-button')
const entradaVencedor = document.getElementById('vencedor')
if (window['WebSocket']) {
const conexão = new WebSocket('ws://' + document.location.host + '/ws')
submitWinnerButton.onclick = event => {
conexão.send(entradaVencedor.value)
}
}
</script>
</html>
Temos uma página web bem simples:
- Uma entrada de texto para a pessoa inserir a vitória
- Um botão onde pode-se clicar para declarar quem venceu
- Um pouco de JavaScript para abrir uma conexão WebSocket para nosso servidor eassim gerenciar o envio dos dados ao pressionar o botão
WebSocket
é integrado na maioria dos navegadores modernos, logo não precisamos nos preocupar em instalar bibliotecas. A página web não vai funcionar em navegadores antigos, mas para nosso caso tá tudo bem.Existem algumas formas. Como foi enfatizado no decorrer do livro, é importante que os testes que você escreve têm valor o suficiente para justificar o custo.
- 1.Escreva um teste baseado no navegador, usando algo como Selenium. Esses testes são os mais "realistas" de todas as abordagens porque começam um navegador web de verdade e simula um usuário interagindo com ele. Esses testes podem te dar muita confiança de que seu sistma funciona, mas são mais difíceis e escrever que os testes unitários e muito mais lentos de serem executados. Para os propósitos do nosso produto, isso é exagero.
- 2.Fazer uma comparação exata de textos. Isso pode funcionar, mas esses tipos de testes acabam sendo muito frágeis. No momento que alguém muda a marcação, você vai ter um teste falhando quando na prática nada está de fato falhando.
- 3.Verificar que chamamos o template correto. Vamos usar uma biblioteca de template da biblioteca padrão para servir o HTML (que falamos brevemente) e podemos injetar na coisa que gera o HTML e espionar suas chamadas para verificar que estamos fazendo tudo corretamente. Isso teria um impacto no design do nosso código, mas na realidade isso não estaríamos testando algo tão crítico além de verificar se estamos chamando o arquivo de template correto. Dito isso, só vamos ter um template no nosso projeto e a chance de falha aqui parece pequena.
Então, pela primeira vez no livro "Aprenda Go com Testes", não vamos escrever nenhum teste.
Coloque a marcação em um arquivo chamado
jogo.html
.Na próxima mudança do endoint, vamos apenas escrever o seguinte:
func (p *ServidorJogador) jogo(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("jogo.html")
if err != nil {
http.Error(w, fmt.Sprintf("problem loading template %s", err.Error()), http.StatusInternalServerError)
return
}
tmpl.Execute(w, nil)
}
O
html/template
é um pacote do Go para criar HTML. No nosso caso, chamamos template.ParseFiles
enviando o caminho do nosso arquivo HTML. Presumindo que não há nenhum erro, chamamos a função Execute
para "executar" o template, que o escreve para um ìo.Writer
. No nosso caso, esperamos que o template seja escrito na internet, então enviamos o nosso http.ResponseWriter
.Já que não escrevemos um teste, seria prudente testar nosso servidor web manualmente só para ter certeza de que as coisas estão funcionamos como esperamos. Vá para
cmd/webserver
e execute o arquivo main.go
. Visite http://localhost:5000/jogo
.Você deve ter obtido um erro sobre não ser capaz de encontrar o template. Você pode ou mudar o caminho para ser relativo à sua pasta, ou pode ter uma cópia de
`jogo.html
no diretório cmd/webserver
. Eu escolho criar um symlink (ln -s ../../jogo.html jogo.html
) para o arquivo dentro da raiz do projeto para caso eu faça alterações, elas reflitam quando o servidor estiver sendo executado.Se fizer essa alteração e rodar novamente, deve conseguir ver a interface.
Agora precisamos testar que, quando obtemos uma string sob uma conexão WebSocket para o nosso servidor, declaramos a pessoa como vencedora de um jogo.
Pela primeia vez, vamos usar uma biblioteca externa para trabalhar com WebSockets.
Rode
go get github.com/gorilla/websocket
.Isso vai obter o código para a excelente biblioteca Gorilla WebSocket. Agora podemos atualizar nossos testes para nosso novo requerimento.
t.Run("quando recebemos uma mensagem de um websocket que é vencedor da jogo", func(t *testing.T) {
armazenamento := &EsbocoDeArmazenamentoJogador{}
vencedor := "Ruth"
servidor := httptest.NewServer(NovoServidorJogador(armazenamento))
defer servidor.Close()
wsURL := "ws" + strings.TrimPrefix(servidor.URL, "http") + "/ws"
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("não foi possível abrir uma conexão de websocket em %s %v", wsURL, err)
}
defer ws.Close()
if err := ws.WriteMessage(websocket.TextMessage, []byte(vencedor)); err != nil {
t.Fatalf("não foi possível enviar mensagem na conexão websocket %v", err)
}
VerificaVitoriaDoVencedor(t, armazenamento, vencedor)
})
Certifique-se que tenha importado o pacote
websocket
. Minha IDE fez isso automaticamente para mim e a sua deve fazer o mesmo.Para testar o que acontece do navegador, temos que abrir nossa própria conexão WebSocket e escrever nela.
Nossos testes anteriores do servidor apenas chamavam métodos no nosso servidor, mas agora precisamos ter uma conexão persistente nele. Para fazer isso, usamos o
httptest.NewServer
, que recebe um http.Handler
que vai esperar conexões.Ao usar
websocket.DefaultDialer.Dial
, tentamos conectar no nosso servidor para então enviar uma mensagem com nosso vencedor
.Por fim, verificamos o armazenamento do jogador para certificar que o vencedor foi gravado.
=== RUN TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo
--- FAIL: TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo (0.00s)
server_test.go:124: não foi possível abrir uma conexão de websocket em ws://127.0.0.1:55838/ws websocket: bad handshake
Não mudamos nosso servidor para aceitar conexões WebSocket em
/ws
, então ainda não estamos apertando as mãos.Adicione outra linha no nosso roteador:
roteador.Handle("/ws", http.HandlerFunc(p.webSocket))
E adicione nosso novo manipulador
webSocket
:func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
upgrader.Upgrade(w, r, nil)
}
Para aceitar uma conexão WebSocker, precisamos de um método
Upgrade
para atualizar a requisição. Agora, se você executar o teste novamente, o próximo erro deve aparecer.=== RUN TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo
--- FAIL: TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo (0.00s)
server_test.go:132: obtido 0 chamadas paraGravarVitoria esperado 1
Agora que temos uma conexão aberta, vamos esperar por uma pensagem e então gravá-la como vencedor.
func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conexão, _ := upgrader.Upgrade(w, r, nil)
_, winnerMsg, _ := conexão.ReadMessage()
p.armazenamento.GravarVitoria(string(winnerMsg))
}
(Sim, estamos ignorando vários erros nesse momento!)
conexão.ReadMessage()
bloqueia a espera por uma mensagem na conexão. Quando obtivermos uma, vamos usá-la para GravarVitoria
. Isso finalmente fecharia a conexão WebSocket.Se tentar executar o teste, ele ainda vai falhar.
O problema está no tempo. Há um atraso entre nossa conexão WebSocket ler a mensagem e gravar a vitória e nosso teste termina sua execução antes disso acontecer. Você pode testar isso colocando um
time.Sleep
curto antes da verificação final.Vamos continuar com isso por enquanto, mas saiba que colocar sleeps arbitrários em testes é uma prática muito ruim.
time.Sleep(10 * time.Millisecond)
VerificaVitoriaDoVencedor(t, armazenamento, vencedor)
Cometemos vários pecados para fazer esse teste funcionar tanto no código do servidor quanto no código do teste, mas lembre-se que essa é a forma mais fácil para fazer as coisas funcionarem.
Temos um software horrível e cheio de gambiarras funcionando apoiado por testes, então agora temos a liberdade para torná-lo elegante sabendo que não vamos quebrar nada por acidente.
Então, vamos começar com o código do servidor.
Podemos mover o
upgrader
para um valor privado dentro do nosso pacote porque não precisamos redeclará-lo em toda requisição na conexão com o WebSocket.var atualizadorDeWebsocket = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
conexão, _ := atualizadorDeWebsocket.Upgrade(w, r, nil)
_, winnerMsg, _ := conexão.ReadMessage()
p.armazenamento.GravarVitoria(string(winnerMsg))
}
Nossa chamada para
template.ParseFiles("jogo.html")
vai ser executada a cada GET /jogo
, o que significa que vamos usar o sistema de arquivo a cada requisição apesar de não ser necessário parsear o template novamente. Vamos refatorar o código para que possamos fazer o parse do template uma vez em NovoServidorJogador
ao invés disso. Vamos ter que fazer isso para que nossa função possa retornar um erro caso tenhamos problema ao obter o template do disco ou fazer parse dele.Agora vamos às mudanças relevantes do
ServidorJogador
:type ServidorJogador struct {
armazenamento ArmazenamentoJogador
http.Handler
template *template.Template
}
const caminhoTemplateHTML = "jogo.html"
func NovoServidorJogador(armazenamento ArmazenamentoJogador) (*ServidorJogador, error) {
p := new(ServidorJogador)
tmpl, err := template.ParseFiles("jogo.html")
if err != nil {
return nil, fmt.Errorf("problema ao abrir %s %v", caminhoTemplateHTML, err)
}
p.template = tmpl
p.armazenamento = armazenamento
roteador := http.NewServeMux()
roteador.Handle("/liga", http.HandlerFunc(p.manipulaLiga))
roteador.Handle("/jogadores/", http.HandlerFunc(p.manipulaJogadores))
roteador.Handle("/jogo", http.HandlerFunc(p.jogo))
roteador.Handle("/ws", http.HandlerFunc(p.webSocket))
p.Handler = roteador
return p, nil
}
func (p *ServidorJogador) jogo(w http.ResponseWriter, r *http.Request) {
p.template.Execute(w, nil)
}
Ao alterar a assinatura de
NovoServidorJogador
, agora temos problemas de compilação. Tente corrigir por si só ou olhe para o código fonte caso para ver a solução.Para o código de teste, fiz uma função auxiliar chamada
deveFazerServidorJogador(t *testing.T, armazenamento ArmazenamentoJogador) *ServidorJogador
para que eu possa esconder o erro dos testes.func deveFazerServidorJogador(t *testing.T, armazenamento ArmazenamentoJogador) *ServidorJogador {
servidor, err := NovoServidorJogador(armazenamento)
if err != nil {
t.Fatal("problema ao criar o servidor do jogador", err)
}
return servidor
}
Da mesma forma, criei outra função auxiliar
deveConectarAoWebSocket
para que eu possa esconder um erro ao criar uma conexão de WebSocket.func deveConectarAoWebSocket(t *testing.T, url string) *websocket.Conn {
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
t.Fatalf("não foi possível abrir uma conexão de websocket em %s %v", url, err)
}
return ws
}
Finalmente, podemos criar uma função auxiliar no nosso código de teste para enviar mensagens:
func escreverMensagemNoWebsocket(t *testing.T, conexão *websocket.Conn, mensagem string) {
t.Helper()
if err := conexão.WriteMessage(websocket.TextMessage, []byte(mensagem)); err != nil {
t.Fatalf("não foi possível enviar mensagem na conexão websocket %v", err)
}
}
Agora que os testes estão passando, tent executar o servidor e declarar alguns vencedores em
/jogo
. Devemos vê-los gravados em /liga
. Lembre-se que sempre que tivermos um vencedor, vamos fechar a conexão, e você vai precisar atualizar a página para abrir a conexão novamente.Fizemos um formulário simples da web que permite que usuários gravem o vencedor de uma jogo. Vamos iterar nele para fazer com que o usuário possa começar uma jogo inserindo o número de jogadores e o servidor vai mostrar mensagens para o cliente informando-o qual é o valor do blind conforme o tempo passa.
Primeiramente, atualize o
jogo.html
para atualizar o código do lado do cliente para os novos requerimentos:<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>Vamos jogar pôquer</title>
</head>
<corpo>
<section id="jogo">
<div id="jogo-start">
<label for="jogador-count">Número de jogadores</label>
<input type="number" id="jogador-count"/>
<button id="start-jogo">Começar</button>
</div>
<div id="declare-vencedor">
<label for="vencedor">Vencedor</label>
<input type="text" id="vencedor"/>
<button id="vencedor-button">Declare vencedor</button>
</div>
<div id="blind-value"/>
</section>
<section id="jogo-end">
<h1>Outra ótima jogo de pôquer, pessoal!!</h1>
<p><a href="/liga">Verifique a tabela da liga</a></p>
</section>
</corpo>
<script type="application/javascript">
const startGame = document.getElementById('jogo-start')
const declareWinner = document.getElementById('declare-vencedor')
const submitWinnerButton = document.getElementById('vencedor-button')
const entradaVencedor = document.getElementById('vencedor')
const blindContainer = document.getElementById('blind-value')
const gameContainer = document.getElementById('jogo')
const gameEndContainer = document.getElementById('jogo-end')
declareWinner.hidden = true
gameEndContainer.hidden = true
document.getElementById('start-jogo').addEventListener('click', event => {
startGame.hidden = true
declareWinner.hidden = false
const numeroDeJogadores = document.getElementById('jogador-count').value
if (window['WebSocket']) {
const conexão = new WebSocket('ws://' + document.location.host + '/ws')
submitWinnerButton.onclick = event => {
conexão.send(entradaVencedor.value)
gameEndContainer.hidden = false
gameContainer.hidden = true
}
conexão.onclose = evt => {
blindContainer.innerText = 'Connection closed'
}
conexão.onmessage = evt => {
blindContainer.innerText = evt.data
}
conexão.onopen = function () {
conexão.send(numeroDeJogadores)
}
}
})
</script>
</html>
As principais alterações envolvem inserir uma seção para definir o número de jogadores e uma seção para mostrar o valor do blind. Temos um pouco de lógica para mostrar/esconder a interface do usuário dependendo da etapa da jogo.
Para qualquer mensagem que recebermos via
conexão.onmessage
, presumimos ser alertas de blind e então definimos o blindContainer.innerText
de acordo.Como fazemos para enviar os alertas de blind?No capítulo anterior, mostramos a ideia de
Jogo
para que nosso código CLI possa chamar um Jogo
e todo o restante se responsabilizaria por agendar os alertas de blind. Isso acabou até sendo uma boa separação de responsabilidades.type Jogo interface {
Começar(numeroDeJogadores int)
Terminar(vencedor string)
}
Quando o usuário era requisitado pela CLI pelo número de jogadores, ele precisava
Começar
a jogo, o que ativaria os alertas de blind, e quando o usuario declarava o vencedor, isso iria Terminar
. Esses sã os mesmos requerimentos que temos agora, só que a obtenção das entradas era diferente; logo, só precisamos reutilizar esse conceito aonde possível.Nossa implementação "real" de
Jogo
é TexasHoldem
:type TexasHoldem struct {
alertador AlertadorDeBlind
armazenamento ArmazenamentoJogador
}
Ao enviar um
AlertadorDeBlind
, o TexasHoldem
pode agendar alertas de blind para enviar para qualquer lugar.type AlertadorDeBlind interface {
AgendarAlertaPara(duracao time.Duration, quantia int)
}
E só para lembrar, aqui está nossa implementação do
AlertadorDeBlind
que usamos na CLI.func SaidaAlertador(duracao time.Duration, quantia int) {
time.AfterFunc(duracao, func() {
fmt.Fprintf(os.Stdout, "Blind agora é %d\n", quantia)
})
}
Isso funciona no CLI porque estamos sempre esperando para enviar os alertas para
os.Stdout
, mas isso não vai funcionar no nosso servidor web. Para cada requisição, obtemos um novo http.ResponseWriter
que então melhoramos para uma *websocket.Conn
. Logo, não odemos saber quando construímos nossas dependências para onde nossos alertas precisam ir.Por esse motivo, precisamos mudar o
AlertadorDeBlind.AgendarAlertaPara
para que ele receba um destino paara os alertas para que possamos reutiliza-lo no nosso servidor web.Abra o
AlertadorDeBlind.go
e adicione o parâmetro para io.Writer`:type AlertadorDeBlind interface {
AgendarAlertaPara(duracao time.Duration, quantia int, para io.Writer)
}
type AlertadorDeBlindFunc func(duracao time.Duration, quantia int, para io.Writer)
func (a AlertadorDeBlindFunc) AgendarAlertaPara(duracao time.Duration, quantia int, para io.Writer) {
a(duracao, quantia, para)
}
A ideia de um
SaidaAlertador
não encaixa bem no nosso modelo, então vamos apenas renomeá-lo para Alertador
:func Alertador(duracao time.Duration, quantia int, para io.Writer) {
time.AfterFunc(duracao, func() {
fmt.Fprintf(para, "Blind agora é %d\n", quantia)
})
}
Se tentar compilar, haverá uma falha em
TexasHoldem
porque estamos chamando AgendarAlertaPara
sem uma descrição. Só para deixar tudo compilando novamente, vamos escrevê-lo para os.Stdout
.Execute os testes e eles vão falhar porque o
AlertadorDeBlindEspiao
não implementa mais o AlertadorDeBlind
. Corrija isso atualizando a assinatura de AgendarAlertaPara
, execute os testes e todos devem estar passando.Não faz sentido nenhum que o
TexasHoldem
saiba para onde enviar os alertas de blind. Agora, vamos atualizar o Jogo
para que quando você começa uma jogo, declare para onde os alertas devem ir.type Jogo interface {
Começar(numeroDeJogadores int, destinoDosAlertas io.Writer)
Terminar(vencedor string)
}
Deixe o compilador te dizer o que precisa ser corrigido. As alterações não são tão ruins:
- Atualize o
TexasHoldem
para que implementeJogo
corretamente - No
CLI
, quando começamos a jogo, preciamos passar nosssa propriedadesaida
(cli.jogo.Começar(numeroDeJogadores, cli.saida
) - No teste do
TexasHoldem
, precisamos usarjogo.Começar(5, ioutil.Discard)
para corrigir o problema de compilação e configurar a saída do alerta para ser descartada
Se tiver feito tudo certo, todos os testes devem passar! Agora podemos usar
Jogo
dentro do Servidor
.Os requerimentos de
CLI
e Servidor
são os mesmos! É apenas o mecanismo de entrega que é diferente.Vamos dar uma olhada no nosso teste do
CLI
para inspiração.t.Run("começa jogo com 3 jogadores e termina jogo com 'Chris' como vencedor", func(t *testing.T) {
jogo := &JogoEspiao{}
saida := &bytes.Buffer{}
in := usuarioEnvia("3", "Chris venceu")
poquer.NovaCLI(in, saida, jogo).JogarPoquer()
verificaMensagensEnviadasParaUsuario(t, saida, poquer.PromptJogador)
verificaJogoComeçadoCom(t, jogo, 3)
verificaTerminosChamadosCom(t, jogo, "Chris")
})
Parece que devemos ser capazes de testar um resultado semelhante usando
JogoEspiao
.Substitua o antigo teste de websocket com o seguinte:
t.Run("começa uma jogo com 3 jogadores e declara Ruth vencedora", func(t *testing.T) {
jogo := &poquer.JogoEspiao{}
vencedor := "Ruth"
servidor := httptest.NewServer(deveFazerServidorJogador(t, ArmazenamentoJogadorTosco, jogo))
ws := deveConectarAoWebSocket(t, "ws"+strings.TrimPrefix(servidor.URL, "http")+"/ws")
defer servidor.Close()
defer ws.Close()
escreverMensagemNoWebsocket(t, ws, "3")
escreverMensagemNoWebsocket(t, ws, vencedor)
time.Sleep(10 * time.Millisecond)
verificaJogoComeçadoCom(t, jogo, 3)
verificaTerminosChamadosCom(t, jogo, vencedor)
})
- Conforme discutidos, criamos um espião de
Jogo
e passamos para o`deveFazerServidorJogador
(certifique-se de atualizar a função auxiliar para isso). - Depois, enviamos mensagens no web socket para uma jogo.
- Por mim, verificamos que a jogo começou e finalizamos com o que esperamos.
Você terá vários erros de compilação envolvendo
deveFazerServidorJogador
em outros testes. Crie uma variável não exportada jogoTosco
e use-a em todos os testes que não estão compilando:var (
jogoTosco = &JogoEspiao{}
)
O erro final se encontra onde estamos tentando passar em
Jogo
, pois NovoServidorJogador
ainda não o suporta:./server_test.go:21:38: too many arguments in call para "github.com/larien/aprenda-go-com-testes/WebSockets/v2".NovoServidorJogador
have ("github.com/larien/aprenda-go-com-testes/WebSockets/v2".ArmazenamentoJogador, "github.com/larien/aprenda-go-com-testes/WebSockets/v2".Jogo)
esperado ("github.com/larien/aprenda-go-com-testes/WebSockets/v2".ArmazenamentoJogador)
Basta adicionar um argumento por enquanto para fazer o teste funcionar:
func NovoServidorJogador(armazenamento ArmazenamentoJogador, jogo Jogo) (*ServidorJogador, error) {
Finalmente!
=== RUN TestJogo/começa_um_jogo_com_3_jogadores_e_declara_Ruth_a_vencedora
--- FAIL: TestJogo (0.01s)
--- FAIL: TestJogo/começa_um_jogo_com_3_jogadores_e_declara_Ruth_a_vencedora (0.01s)
server_test.go:146: esperava Começar chamado com 3 mas obteve 0
server_test.go:147: esperava Terminar chamado com 'Ruth' mas obteve ''
FAIL
Precisamos adicionar
Jogo
como campo para ServidorJogador
para que possamos usá-lo quando ele obtiver requisições.type ServidorJogador struct {
armazenamento ArmazenamentoJogador
http.Handler
template *template.Template
jogo Jogo
}
(Já temos um método chamado
jogo
, então é só renomeá-lo para jogarJogo
)A seguir, vamos atribui-lo no nosso construtor:
func NovoServidorJogador(armazenamento ArmazenamentoJogador, jogo Jogo) (*ServidorJogador, error) {
p := new(ServidorJogador)
tmpl, err := template.ParseFiles("jogo.html")
if err != nil {
return nil, fmt.Errorf("problema ao abrir %s %v", caminhoTemplateHTML, err)
}
p.jogo = jogo
// etc
Agora podemos usar nosso
Jogo
dentro de webSocket
.func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
conexão, _ := atualizadorDeWebsocket.Upgrade(w, r, nil)
_, mensagemNumeroDeJogadores, _ := conexão.ReadMessage()
numeroDeJogadores, _ := strconv.Atoi(string(mensagemNumeroDeJogadores))
p.jogo.Começar(numeroDeJogadores, ioutil.Discard) //todo: Não descartar as mensagens de blind!
_, vencedor, _ := conexão.ReadMessage()
p.jogo.Terminar(string(vencedor))
}
Uhul! Os testes estão passando.
Não vamos enviar as mensagens de blind para nenhum lugar por enquanto já que precisamos de um tempo para pensar nisso. Quando chamamos
jogo.Começar
, enviamos os dados para ioutil.Discard
que vai apenar descartar qualquer mensagem escrita nele.Por enquanto, vamos iniciar o servidor. Você vai precisar atualizar a
`main.go
para passar um Jogo
para o ServidorJogador
:func main() {
db, err := os.OpenFile(nomeArquivoBaseDeDados, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("problema ao abrir %s %v", nomeArquivoBaseDeDados, err)
}
armazenamento, err := poquer.NovoSistemaArquivoArmazenamentoJogador(db)
if err != nil {
log.Fatalf("problema ao criar sistema de arquivo de armazenamento do jogador, %v ", err)
}
jogo := poquer.NovoTexasHoldem(poquer.AlertadorDeBlindFunc(poquer.Alertador), armazenamento)
servidor, err := poquer.NovoServidorJogador(armazenamento, jogo)
if err != nil {
log.Fatalf("problema ao criar o servidor do jogador %v", err)
}
if err := http.ListenAndServe(":5000", servidor); err != nil {
log.Fatalf("não foi possível ouvir na porta 5000 %v", err)
}
}
Tirando o fato de que não temos alertas de blind por enquanto, a aplicação funciona! Conseguimos reutilizar
Jogo
com ServidorJogador
e ele toma conta dos detalhes. Quando descobrirmos como enviar mensagens de blind atraves de web sockets ao invés de descartá-las, tudo deve ficar pronto.Antes disso, vamos mexer um pouco no código.
A forma que estamos usando WebSocker é bem básica e a mnnipulação de erro é bem fraca, então gostaria de encapsular isso em um tipo só para remover essa bagunça do código do servidor. Precisaremos revisitar isso depois, mas por enqaunto isso vai melhorar um pouco as coisas.
type websocketServidorJogador struct {
*websocket.Conn
}
func novoWebsocketServidorJogador(w http.ResponseWriter, r *http.Request) *websocketServidorJogador {
conexão, err := atualizadorDeWebsocket.Upgrade(w, r, nil)
if err != nil {
log.Printf("houve um problema ao atualizar a conexão para WebSockets %v\n", err)
}
return &websocketServidorJogador{conexão}
}
func (w *websocketServidorJogador) EsperarPelaMensagem() string {
_, msg, err := w.ReadMessage()
if err != nil {
log.Printf("erro ao ler do websocket %v\n", err)
}
return string(msg)
}
Agora o código do servidor fica um pouco mais simples:
func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
ws := novoWebsocketServidorJogador(w, r)
mensagemNumeroDeJogadores := ws.EsperarPelaMensagem()
numeroDeJogadores, _ := strconv.Atoi(mensagemNumeroDeJogadores)
p.jogo.Começar(numeroDeJogadores, ioutil.Discard) //todo: Não descartar as mensagens de blind!
vencedor := ws.EsperarPelaMensagem()
p.jogo.Terminar(vencedor)
}
Quando descobrirmos como não descartar as mensagens de blind teremos terminado essa etapa.
Às vezes, quando não temos certeza de como vamos fazer algo, é melhor apenas brincar e testar coisas diferentes! Tenha certeza de que seu trabalho está salvo primeiro porque quando descobrirmos o que fazer, vamos implementá-lo junto de um teste.
A linha problemática do código que temos é:
p.jogo.Começar(numeroDeJogadores, ioutil.Discard) //todo: Não descartar as mensagens de blind!
Precisamos passar um
io.Writer
para a jogo para ter aonde escrever os alertas be blind.Não seria legal se apenas precisássemos passar o nosso
websocketServidorJogador
de antes? É o nosso wrapper em torno do nosso WebSocket, então parece que devemos ser capazes de enviá-lo para que nosso Jogo
seja capaz de enviar mensagens para ele.Vamos tentar:
func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
ws := novoWebsocketServidorJogador(w, r)
mensagemNumeroDeJogadores := ws.EsperarPelaMensagem()
numeroDeJogadores, _ := strconv.Atoi(mensagemNumeroDeJogadores)
p.jogo.Começar(numeroDeJogadores, ws)
//etc...
O compilador reclama:
./servidor.go:71:14: cannot use ws (type *websocketServidorJogador) as type io.Writer in argument para p.jogo.Começar:
*websocketServidorJogador does not implement io.Writer (missing Write method)
Parece que a coisa óbvia a se fazer é fazer com que o
websocketServidorJogador
implementa o io.Writer
. Para fazer isso, precisamos usar do *websocket.Conn
para ussar a escrita de mensagem WriteMessage
para enviar a mensagem para o websocket.func (w *websocketServidorJogador)