Websockets

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:
1
type ServidorJogador struct {
2
armazenamento ArmazenamentoJogador
3
http.Handler
4
}
5
6
const tipoConteudoJSON = "application/json"
7
8
func NovoServidorJogador(armazenamento ArmazenamentoJogador) *ServidorJogador {
9
p := new(ServidorJogador)
10
11
p.armazenamento = armazenamento
12
13
roteador := http.NewServeMux()
14
roteador.Handle("/liga", http.HandlerFunc(p.manipulaLiga))
15
roteador.Handle("/jogadores/", http.HandlerFunc(p.manipulaJogadores))
16
17
p.Handler = roteador
18
19
return p
20
}
Copied!
A maneira mais fácil que podemos fazer por agora é checar que recebemos um código 200 quando acessamos o GET /jogo.
1
func TestJogo(t *testing.T) {
2
t.Run("GET /jogo retorna 200", func(t *testing.T) {
3
servidor := NovoServidorJogador(&EsbocoDeArmazenamentoJogador{})
4
5
requisicao, _ := http.NewRequest(http.MethodGet, "/jogo", nil)
6
resposta := httptest.NewRecorder()
7
8
servidor.ServeHTTP(resposta, requisicao)
9
10
verificaStatus(t, resposta.Code, http.StatusOK)
11
})
12
}
Copied!

Tente rodar o teste

1
--- FAIL: TestJogo (0.00s)
2
=== RUN TestJogo/GET_/game_returns_200
3
--- FAIL: TestJogo/GET_/game_returns_200 (0.00s)
4
server_test.go:109: não obteve o status correto, obtido 404, esperado 200
Copied!

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:
1
roteador.Handle("/jogo", http.HandlerFunc(p.jogo))
Copied!
E então escreva o método jogo:
1
func (p *ServidorJogador) jogo(w http.ResponseWriter, r *http.Request) {
2
w.WriteHeader(http.StatusOK)
3
}
Copied!

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.
1
func TestJogo(t *testing.T) {
2
t.Run("GET /jogo retorna 200", func(t *testing.T) {
3
servidor := NovoServidorJogador(&EsbocoDeArmazenamentoJogador{})
4
5
requisicao := novaRequisicaoJogo()
6
resposta := httptest.NewRecorder()
7
8
servidor.ServeHTTP(resposta, requisicao)
9
10
verificaStatus(t, resposta, http.StatusOK)
11
})
12
}
Copied!
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:
1
<!DOCTYPE html>
2
<html lang="pt-br">
3
<head>
4
<meta charset="UTF-8">
5
<title>Vamos jogar pôquer</title>
6
</head>
7
<corpo>
8
<section id="jogo">
9
<div id="declare-vencedor">
10
<label for="vencedor">Vencedor</label>
11
<input type="text" id="vencedor"/>
12
<button id="vencedor-button">Declare vencedor</button>
13
</div>
14
</section>
15
</corpo>
16
<script type="application/javascript">
17
18
const submitWinnerButton = document.getElementById('vencedor-button')
19
const entradaVencedor = document.getElementById('vencedor')
20
21
if (window['WebSocket']) {
22
const conexão = new WebSocket('ws://' + document.location.host + '/ws')
23
24
submitWinnerButton.onclick = event => {
25
conexão.send(entradaVencedor.value)
26
}
27
}
28
</script>
29
</html>
Copied!
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.
    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:
1
func (p *ServidorJogador) jogo(w http.ResponseWriter, r *http.Request) {
2
tmpl, err := template.ParseFiles("jogo.html")
3
4
if err != nil {
5
http.Error(w, fmt.Sprintf("problem loading template %s", err.Error()), http.StatusInternalServerError)
6
return
7
}
8
9
tmpl.Execute(w, nil)
10
}
Copied!
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.

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.
1
t.Run("quando recebemos uma mensagem de um websocket que é vencedor da jogo", func(t *testing.T) {
2
armazenamento := &EsbocoDeArmazenamentoJogador{}
3
vencedor := "Ruth"
4
servidor := httptest.NewServer(NovoServidorJogador(armazenamento))
5
defer servidor.Close()
6
7
wsURL := "ws" + strings.TrimPrefix(servidor.URL, "http") + "/ws"
8
9
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
10
if err != nil {
11
t.Fatalf("não foi possível abrir uma conexão de websocket em %s %v", wsURL, err)
12
}
13
defer ws.Close()
14
15
if err := ws.WriteMessage(websocket.TextMessage, []byte(vencedor)); err != nil {
16
t.Fatalf("não foi possível enviar mensagem na conexão websocket %v", err)
17
}
18
19
VerificaVitoriaDoVencedor(t, armazenamento, vencedor)
20
})
Copied!
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

1
=== RUN TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo
2
--- FAIL: TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo (0.00s)
3
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
Copied!
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:
1
roteador.Handle("/ws", http.HandlerFunc(p.webSocket))
Copied!
E adicione nosso novo manipulador webSocket:
1
func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
2
upgrader := websocket.Upgrader{
3
ReadBufferSize: 1024,
4
WriteBufferSize: 1024,
5
}
6
upgrader.Upgrade(w, r, nil)
7
}
Copied!
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.
1
=== RUN TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo
2
--- FAIL: TestJogo/quando_recebemos_uma_mensagem_via_websocket_que_ha_um_vencedor_de_uma_jogo (0.00s)
3
server_test.go:132: obtido 0 chamadas paraGravarVitoria esperado 1
Copied!
Agora que temos uma conexão aberta, vamos esperar por uma pensagem e então gravá-la como vencedor.
1
func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
2
upgrader := websocket.Upgrader{
3
ReadBufferSize: 1024,
4
WriteBufferSize: 1024,
5
}
6
conexão, _ := upgrader.Upgrade(w, r, nil)
7
_, winnerMsg, _ := conexão.ReadMessage()
8
p.armazenamento.GravarVitoria(string(winnerMsg))
9
}
Copied!
(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.
1
time.Sleep(10 * time.Millisecond)
2
VerificaVitoriaDoVencedor(t, armazenamento, vencedor)
Copied!

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.
1
var atualizadorDeWebsocket = websocket.Upgrader{
2
ReadBufferSize: 1024,
3
WriteBufferSize: 1024,
4
}
5
6
func (p *ServidorJogador) webSocket(w http.ResponseWriter, r *http.Request) {
7
conexão, _ := atualizadorDeWebsocket.Upgrade(w, r, nil)
8
_, winnerMsg, _ := conexão.ReadMessage()
9
p.armazenamento.GravarVitoria(string(winnerMsg))
10
}
Copied!
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:
1
type ServidorJogador struct {
2
armazenamento ArmazenamentoJogador
3
http.Handler
4
template *template.Template
5
}
6
7
const caminhoTemplateHTML = "jogo.html"
8
9
func NovoServidorJogador(armazenamento ArmazenamentoJogador) (*ServidorJogador, error) {
10
p := new(ServidorJogador)
11
12
tmpl, err := template.ParseFiles("jogo.html")
13
14
if err != nil {
15
return nil, fmt.Errorf("problema ao abrir %s %v", caminhoTemplateHTML, err)
16
}
17
18
p.template = tmpl
19
p.armazenamento = armazenamento
20
21
roteador := http.NewServeMux()
22
roteador.Handle("/liga", http.HandlerFunc(p.manipulaLiga))
23
roteador.Handle("/jogadores/", http.HandlerFunc(p.manipulaJogadores))
24
roteador.Handle("/jogo", http.HandlerFunc(p.jogo))
25
roteador.Handle("/ws", http.HandlerFunc(p.webSocket))
26
27
p.Handler = roteador
28
29
return p, nil
30
}
31
32
func (p *ServidorJogador) jogo(w http.ResponseWriter, r *http.Request) {
33
p.template.Execute(w, nil)
34
}
Copied!
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.
1
func deveFazerServidorJogador(t *testing.T, armazenamento ArmazenamentoJogador) *ServidorJogador {
2
servidor, err := NovoServidorJogador(armazenamento)
3
if err != nil {
4
t.Fatal("problema ao criar o servidor do jogador", err)
5
}
6
return servidor
7
}
Copied!
Da mesma forma, criei outra função auxiliar deveConectarAoWebSocket para que eu possa esconder um erro ao criar uma conexão de WebSocket.
1
func deveConectarAoWebSocket(t *testing.T, url string) *websocket.Conn {
2
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
3
4
if err != nil {
5
t.Fatalf("não foi possível abrir uma conexão de websocket em %s %v", url, err)
6
}
7
8
return ws
9
}
Copied!
Finalmente, podemos criar uma função auxiliar no nosso código de teste para enviar mensagens:
1
func escreverMensagemNoWebsocket(t *testing.T, conexão *websocket.Conn, mensagem string) {
2
t.Helper()
3
if err := conexão.WriteMessage(websocket.TextMessage, []byte(mensagem)); err != nil {
4
t.Fatalf("não foi possível enviar mensagem na conexão websocket %v", err)
5
}
6
}
Copied!
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:
1
<!DOCTYPE html>
2
<html lang="pt-br">
3
<head>
4
<meta charset="UTF-8">
5
<title>Vamos jogar pôquer</title>
6
</head>
7
<corpo>
8
<section id="jogo">
9
<div id="jogo-start">
10
<label for="jogador-count">Número de jogadores</label>
11
<input type="number" id="jogador-count"/>
12
<button id="start-jogo">Começar</button>
13
</div>
14
15
<div id="declare-vencedor">
16
<label for="vencedor">Vencedor</label>
17
<input type="text" id="vencedor"/>
18
<button id="vencedor-button">Declare vencedor</button>
19
</div>
20
21
<div id="blind-value"/>
22
</section>
23
24
<section id="jogo-end">
25
<h1>Outra ótima jogo de pôquer, pessoal!!</h1>
26
<p><a href="/liga">Verifique a tabela da liga</a></p>
27
</section>
28
29
</corpo>
30
<script type="application/javascript">
31
const startGame = document.getElementById('jogo-start')
32
33
const declareWinner = document.getElementById('declare-vencedor')
34
const submitWinnerButton = document.getElementById('vencedor-button')
35
const entradaVencedor = document.getElementById('vencedor')
36
37
const blindContainer = document.getElementById('blind-value')
38
39
const gameContainer = document.getElementById('jogo')
40
const gameEndContainer = document.getElementById('jogo-end')
41
42
declareWinner.hidden = true
43
gameEndContainer.hidden = true
44
45
document.getElementById('start-jogo').addEventListener('click', event => {
46
startGame.hidden = true
47
declareWinner.hidden = false
48
49
const numeroDeJogadores = document.getElementById('jogador-count').value
50
51
if (window['WebSocket']) {
52
const conexão = new WebSocket('ws://' + document.location.host + '/ws')
53
54
submitWinnerButton.onclick = event => {
55
conexão.send(entradaVencedor.value)
56
gameEndContainer.hidden = false
57
gameContainer.hidden = true
58
}
59
60
conexão.onclose = evt => {
61
blindContainer.innerText = 'Connection closed'
62
}
63
64
conexão.onmessage = evt => {
65
blindContainer.innerText = evt.data
66
}
67
68
conexão.onopen = function () {
69
conexão.send(numeroDeJogadores)
70
}
71
}
72
})
73
</script>
74
</html>
Copied!
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.
1
type Jogo interface {
2
Começar(numeroDeJogadores int)
3
Terminar(vencedor string)
4
}
Copied!
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:
1
type TexasHoldem struct {
2
alertador AlertadorDeBlind
3
armazenamento ArmazenamentoJogador
4
}
Copied!
Ao enviar um AlertadorDeBlind, o TexasHoldem pode agendar alertas de blind para enviar para qualquer lugar.
1
type AlertadorDeBlind interface {
2
AgendarAlertaPara(duracao time.Duration, quantia int)
3
}
Copied!
E só para lembrar, aqui está nossa implementação do AlertadorDeBlind que usamos na CLI.
1
func SaidaAlertador(duracao time.Duration, quantia int) {
2
time.AfterFunc(duracao, func() {
3
fmt.Fprintf(os.Stdout, "Blind agora é %d\n", quantia)
4
})
5
}
Copied!
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`:
1
type AlertadorDeBlind interface {
2
AgendarAlertaPara(duracao time.Duration, quantia int, para io.Writer)
3
}
4
5
type AlertadorDeBlindFunc func(duracao time.Duration, quantia int, para io.Writer)
6
7
func (a AlertadorDeBlindFunc) AgendarAlertaPara(duracao time.Duration, quantia int, para io.Writer) {
8
a(duracao, quantia, para)
9
}
Copied!
A ideia de um SaidaAlertador não encaixa bem no nosso modelo, então vamos apenas renomeá-lo para Alertador:
1
func Alertador(duracao time.Duration, quantia int, para io.Writer) {
2
time.AfterFunc(duracao, func() {
3
fmt.Fprintf(para, "Blind agora é %d\n", quantia)
4
})
5
}
Copied!
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.
1
type Jogo interface {
2
Começar(numeroDeJogadores int, destinoDosAlertas io.Writer)
3
Terminar(vencedor string)
4
}
Copied!
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 implemente Jogo corretamente
    No CLI, quando começamos a jogo, preciamos passar nosssa propriedade saida (cli.jogo.Começar(numeroDeJogadores, cli.saida)
    No teste do TexasHoldem, precisamos usar jogo.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.
1
t.Run("começa jogo com 3 jogadores e termina jogo com 'Chris' como vencedor", func(t *testing.T) {
2
jogo := &JogoEspiao{}
3
4
saida := &bytes.Buffer{}
5
in := usuarioEnvia("3", "Chris venceu")
6
7
poquer.NovaCLI(in, saida, jogo).JogarPoquer()
8
9
verificaMensagensEnviadasParaUsuario(t, saida, poquer.PromptJogador)
10
verificaJogoComeçadoCom(t, jogo, 3)
11
verificaTerminosChamadosCom(t, jogo, "Chris")
12
})
Copied!
Parece que devemos ser capazes de testar um resultado semelhante usando JogoEspiao.
Substitua o antigo teste de websocket com o seguinte:
1
t.Run("começa uma jogo com 3 jogadores e declara Ruth vencedora", func(t *testing.T) {
2
jogo := &poquer.JogoEspiao{}
3
vencedor := "Ruth"
4
servidor := httptest.NewServer(deveFazerServidorJogador(t, ArmazenamentoJogadorTosco, jogo))
5
ws := deveConectarAoWebSocket(t, "ws"+strings.TrimPrefix(servidor.URL, "http")+"/ws")
6
7
defer servidor.Close()
8
defer ws.Close()
9
10
escreverMensagemNoWebsocket(t, ws, "3")
11
escreverMensagemNoWebsocket(t, ws, vencedor)
12
13
time.Sleep(10 * time.Millisecond)
14
verificaJogoComeçadoCom(t, jogo, 3)
15
verificaTerminosChamadosCom(t, jogo, vencedor)
16
})
Copied!
    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:
1
var (
2
jogoTosco = &JogoEspiao{}
3
)
Copied!
O erro final se encontra onde estamos tentando passar em Jogo, pois NovoServidorJogador ainda não o suporta:
1
./server_test.go:21:38: too many arguments in call para "github.com/larien/aprenda-go-com-testes/WebSockets/v2".NovoServidorJogador
2
have ("github.com/larien/aprenda-go-com-testes/WebSockets/v2".ArmazenamentoJogador, "github.com/larien/aprenda-go-com-testes/WebSockets/v2".Jogo)
3
esperado ("github.com/larien/aprenda-go-com-testes/WebSockets/v2".ArmazenamentoJogador)
Copied!

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:
1
func NovoServidorJogador(armazenamento ArmazenamentoJogador, jogo Jogo) (*ServidorJogador, error) {
Copied!
Finalmente!
1
=== RUN TestJogo/começa_um_jogo_com_3_jogadores_e_declara_Ruth_a_vencedora
2
--- FAIL: TestJogo (0.01s)
3
--- FAIL: TestJogo/começa_um_jogo_com_3_jogadores_e_declara_Ruth_a_vencedora (0.01s)
4
server_test.go:146: esperava Começar chamado com 3 mas obteve 0
5
server_test.go:147: esperava Terminar chamado com 'Ruth' mas obteve ''
6
FAIL
Copied!

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.
1
type ServidorJogador struct {
2
armazenamento ArmazenamentoJogador
3
http.Handler
4
template *template.Template
5
jogo Jogo
6
}
Copied!
(Já temos um método chamado jogo, então é só renomeá-lo para jogarJogo)
A seguir, vamos atribui-lo no nosso construtor:
1
func NovoServidorJogador(armazenamento ArmazenamentoJogador, jogo Jogo) (*ServidorJogador, error) {
2
p := new(ServidorJogador)
3
4
tmpl, err := template.ParseFiles("jogo.html")
5
6
if err != nil {
7
return nil, fmt.Errorf("problema ao abrir %s %v", caminhoTemplateHTML, err)
8
}
9
10
p.jogo = jogo
11
12
// etc
Copied!
Agora podemos usar nosso Jogo dentro de webSocket.
1
func (p *ServidorJogador) webSocket(w http.