Websockets
Você pode encontrar todo o código para esse capítulo aqui
Recapitulando o projeto
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.
Próximos passos
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.
E os testes para o JavaScript?
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".
Escreva o teste primeiro
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:
A maneira mais fácil que podemos fazer por agora é checar que recebemos um código 200
quando acessamos o GET /jogo
.
Tente rodar o teste
Escreva código suficiente para fazer o teste passar
Nosso servidor tem um roteador definido, então deve ser relativamente fácil corrigir isso.
Adicione o seguinte no nosso roteador:
E então escreva o método jogo
:
Refatore
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.
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:
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 e assim 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.
Como testamos que retornamos a marcação correta?
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.
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.
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.
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:
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.htmlno 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.
Escreva o teste primeiro
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.
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.
Execute o teste
Não mudamos nosso servidor para aceitar conexões WebSocket em /ws
, então ainda não estamos apertando as mãos.
Escreva código suficiente para fazer o teste passar
Adicione outra linha no nosso roteador:
E adicione nosso novo manipulador webSocket
:
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.
Agora que temos uma conexão aberta, vamos esperar por uma pensagem e então gravá-la como vencedor.
(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.
Refatore
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.
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
:
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.
Da mesma forma, criei outra função auxiliar deveConectarAoWebSocket
para que eu possa esconder um erro ao criar uma conexão de WebSocket.
Finalmente, podemos criar uma função auxiliar no nosso código de teste para enviar mensagens:
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:
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.
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
:
Ao enviar um AlertadorDeBlind
, o TexasHoldem
pode agendar alertas de blind para enviar para qualquer lugar.
E só para lembrar, aqui está nossa implementação do AlertadorDeBlind
que usamos na CLI.
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`:
A ideia de um SaidaAlertador
não encaixa bem no nosso modelo, então vamos apenas renomeá-lo para Alertador
:
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.
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
corretamenteNo
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
.
Escreva os testes primeiro
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.
Parece que devemos ser capazes de testar um resultado semelhante usando JogoEspiao
.
Substitua o antigo teste de websocket com o seguinte:
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.
Execute o teste
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:
O erro final se encontra onde estamos tentando passar em Jogo
, pois NovoServidorJogador
ainda não o suporta:
Escreva o mínimo de código possível para o teste funcionar e verifique a saída do teste falhado
Basta adicionar um argumento por enquanto para fazer o teste funcionar:
Finalmente!
Escreva código suficiente para fazer o teste passar
Precisamos adicionar Jogo
como campo para ServidorJogador
para que possamos usá-lo quando ele obtiver requisições.
(Já temos um método chamado jogo
, então é só renomeá-lo para jogarJogo
)
A seguir, vamos atribui-lo no nosso construtor:
Agora podemos usar nosso Jogo
dentro de webSocket
.
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.gopara passar um
Jogopara o
ServidorJogador`:
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.
Refatore
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.
Agora o código do servidor fica um pouco mais simples:
Quando descobrirmos como não descartar as mensagens de blind teremos terminado essa etapa.
Não vamos escrever um teste!
À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 é:
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:
O compilador reclama:
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.
Isso parece fácil demais! Execute a aplicação para ver se funciona.
Mas antes edite o TexasHoldem
para que o tempo de incremento do blind seja mais curto para que você possa ver as coisas em ação:
As coisas devem estar funcionando! A quantidade do blind é incrementada no computador como se fosse mágica.
Agora vamos reverter o código e pensar como testá-lo. Para implementar isso tudo o que precisamos fazer foi passar o websocketServidorJogador
para ComeçarJogo
no lugar do ioutil.Discard
, então isso faz parecer que tenhamos que espionar a chamada para verificar se ela funciona.
Espionar é ótimo e nos ajuda a verificar os detalhes de implementação, mas sempre devemos favorecer o teste do comportamento real se possível, porque caso seja necessário refatorar isso os testes espiões são os primeiros a começar a falhar por geralmente verificarem os detalhes de implementação que estamos tentando alterar.
Nosso teste atualmente abre uma conexão websocket para nosso servidor em execução e envia mensagens para fazê-lo efetuar ações. De forma semelhante, devemos ser capazes de testar as mensagens que o nosso servidor envia de volta para a conexão de websocket.
Escreva o teste primeiro
Vamos editar nosso teste existente.
Atualmente, nosso JogoEspiao
não envia nenhum dado para a saida
quando você chama Começar
. Devemos alterar isso para que possamos configurá-lo para enviar uma mensagem e então verificar se a mensagem é enviada para o websocket. Isso deve nos dar confiança que configuramos as coisas corretamente enquanto ainda exercitamos o comportamento real do que esperamos.
Adicione o campo de AlertaDeBlind
.
Atualize o Começar
do JogoEspiao
para enviar a mensagem para a saída
.
Agora isso significa que quando usarmos o ServidorJogador
, quando ele tentar Começar
o jogo, deve acabar enviando mensagens pelo websocket se as coisas estiverem funcionando direito.
Finalmente podemos atualizar o teste:
Adicionamos um
alertaDeBlindEsperado
e configuramos nossoJogoEspiao
para enviá-lo para asaida
seComeçar
for chamado.Esperamos que ela seja enviada na conexão do websocket, então adicionamos uma chamada para
ws.ReadMessage()
para esperar por uma mensagem ser enviada e então verificamos se é aquela que esperamos.
Execute o teste
Talvez você pense que o teste demora demais. Isso acontece porque o ``ws.ReadMessage()` vai bloqueá-lo até obter a mensagem, que nunca vai chegar.
Escreva o mínimo de código necessário para o teste ser executado e verifique a saída do teste falhando
Nunca devemos ter testes que demoram, então vamos apresentar uma nova forma de lidar com coigo que esperamos com um timeout.
O que o within
faz é pegar uma função assert
como argumento e então o executa dentro de uma goroutine. Se/Quando a função termina, ela avisa que terminou através do canal done
.
Enquanto isso acontece, usamos uma declaração select
que nos permite esperar por um canal para enviar uma mensagem. A partir daí é uma corrida entre a função de assert
e o time.After
que vai enviar um sinal qunado a duração chega ao fim.
Por mim, fiz uma função auxiliar para a nossa verificação so para melhorar um pouco as coisas:
É assim que o teste fica agora:
Agora se você rodar o teste...
Escreva código suficiente para fazer o teste passar
Finalmente podemos alterar o código do nosso servidor para que ele envie a mensagem para nossa conexão com o WebSocket para a jogo quando ela começa:
Refatorar
O código do servidor sofreu uma mudança bem pequena, então não tem muito o que mudar aqui, mas o código de teste ainda tem uma chamada time.Sleep
porque temos que esperar até que o nosso servidor termina sua tarefa assíncronamente.
Podemos refatorar nossas funções auxiliares verificaJogoComeçadoCom
e verificaTerminosChamadosCom
para que possam tentar as verificações novamente logo após falharem.
Abaixo esta como fazer isso com o verificaTerminosChamadosCom
e você pode usar a mesma abordagem para a outra função auxiliar.
Aqui está como tentarNovamenteAte
está definida:
Resumindo
Nossa aplicação agora está completa. Um jogo de pôquer agora pode ser iniciado pelo navegador web e os usuários são informados sobre o valor da aposta cega enquanto o tempo passa por meio de WebSockets. Quando o jogo for encerrado, eles podem salvar o vencedor, o que é persistente uma vez que estamos usando o código que escrevemos há alguns capítulos atrás. Os jogadores podem descobrir quem é o melhor (ou o mais sortudo) jogador de pôquer utilizando o endpoint /liga
do nosso website.
No decorrer da nossa jornada cometemos diversos erros, mas com o fluxo de desenvolvimento orientado a testes (TDD) nunca estivemos com um programa que não rodava de jeito nenhum. Somos livres para continuar iterando e experimentando outras coisas.
O capítulo final vai recapitular o nosso método, o design que alcançamos e por fim apertar alguns nós que possam parecer soltos.
Nós cobrimos algumas coisas nesse capítulo.
WebSockets
Maneira conveniente de enviar mensagens entre clientes e servidores sem precisar que o cliente fique sondando (?) o servidor. O código que fizemos tanto do cliente quanto do servidor são muito simples.
É trivial para testar, mas você tem que se atentar com a natureza assíncrona dos testes.
Lidando com código em testes qeu podem ter sido atrasados ou nunca terem terminado
Crie funções utilitárias para tentar verificações novamente e adicione timeouts.
Podemos usar go routines para certificar que as verificações não bloqueiam nada e então usar canais para deixá-los sinalizar se tiverem terminado ou não;
O pacote
time
tem algumas funções úteis que também enviam sinais para canais sobre eventos no tempo para que possamos definir timeouts.
Last updated