Select
Você pode encontrar todos os códigos desse capítulo aqui
Te pediram para fazer uma função chamada Corredor
que recebe duas URLs que "competirão" entre si através de uma chamada HTTP GET onde a primeira URL a responder será retornada. Se nenhuma delas responder dentro de 10 segundos a função deve retornar um erro
.
Para isso, vamos utilizar:
net/http
para chamadas HTTP.net/http/httptest
para nos ajudar a testar.goroutines.
select
para sincronizar processos.
Escreva o teste primeiro
Vamos começar com algo simples.
Sabemos que não está perfeito e que existem problemas, mas é um bom início. É importante não perder tanto tempo deixando as coisas perfeitas de primeira.
Execute o teste
./corredor_test.go:14:9: undefined: Corredor
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
corredor_test.go:25: resultado '', esperado 'http://www.quii.co.uk'
Escreva código suficiente para que o teste passe
Para cada URL:
Usamos
time.Now()
para marcar o tempo antes de tentarmos pegar aURL
.Então usamos
http.Get
para tentar capturar os conteúdos daURL
. Essa função retornahttp.Response
e umerro
, mas não temos interesse nesses valores.time.Since
pega o tempo inicial e retorna a diferença na forma detime.Duration
.
Feito isso, podemos simplesmente comparar as durações e ver qual é mais rápida.
Problemas
Isso pode ou não fazer com que o teste passe para você. O problema é que estamos acessando sites reais para testar nossa lógica.
Testar códigos que usam HTTP é tão comum que Go tem ferramentas na biblioteca padrão para te ajudar a testá-los.
Nos capítulos de mock e injeção de dependências, falamos sobre como idealmente não queremos depender de serviços externos para testar nosso código, pois:
Podem ser lentos
Podem ser inconsistentes
Não conseguimos testar casos extremos
Na biblioteca padrão, existe um pacote chamado net/http/httptest
onde é possível simular um servidor HTTP facilmente.
Vamos alterar nosso teste para usar essas simulações para termos servidores confiáveis para testar sob nosso controle.
A sintaxe pode parecer um pouco complicada, mas não tenha pressa.
httptest.NewServer
recebe um http.HandlerFunc
que vamos enviar para uma função anônima.
http.HandlerFunc
é um tipo que se parece com isso: type HandlerFunc func(ResponseWriter, *Requisicao)
.
Tudo o que assinatura diz é que ela precisa de uma função que recebe um ResponseWriter
e uma Requisição
, o que não é novidade para um servidor HTTP.
Acontece que não existe nenhuma mágica aqui, também é assim que você escreveria um servidor HTTP real em Go. A única diferença é que estamos utilizando ele dentro de um httptest.NewServer
,o que facilita seu uso em testes por ele encontrar uma porta aberta para escutar e você poder fechá-la quando estiverem concluídos dentro dos próprios testes.
Dentro de nossos dois servidores, fazemos com que um deles tenha um time.Sleep
quando receber a requisição para torná-lo propositalmente mais lento que o outro. Ambos os servidores, então, devolvem uma resposta OK
com w.WriteHeader(http.StatusOK)
a quem realizou a chamada.
Se você rodar o teste novamente, ele definitivamente irá passar e deve ser mais rápido. Brinque com os sleeps para quebrar o teste propositalmente.
Refatoração
Temos algumas duplicações tanto em nosso código de produção quanto em nosso código de teste.
Essa "enxugada" torna nosso código Corredor
bem mais legível.
Fizemos a refatoração criando nossos servidores falsos numa função chamada criarServidorComAtraso
para remover alguns códigos desnecessários do nosso teste e reduzir repetições.
defer
defer
Ao chamar uma função com o prefixo defer
, ela será chamada após o término da função que a contém.
Às vezes você vai precisar liberar recursos, como fechar um arquivo ou, como no nosso caso, fechar um servidor para que esse não continue escutando a uma porta.
Utilizamos o defer
quando queremos que a função seja executada no final de uma função, mas mantendo essa instrução próxima de onde o servidor foi criado para facilitar a vida das pessoas que forem ler o código futuramente.
Nossa refatoração é uma melhoria e uma solução razoável dados os recursos de Go que vimos até aqui, mas podemos deixar essa solução ainda mais simples.
Sincronizando processos
Por que estamos testando a velocidade dos sites sequencialmente quando Go é ótimo com concorrência? Devemos conseguir verificar ambos ao mesmo tempo.
Não nos preocupamos com o tempo exato de resposta das requisições, apenas queremos saber qual retorna primeiro.
Para fazer isso, vamos apresentar uma nova construção chamada select
que nos ajudará a sincronizar os processos de forma mais fácil e clara.
ping
ping
Definimos a função ping
que cria um chan bool
e a retorna.
No nosso caso, não nos importamos com o tipo enviado no canal, só queremos enviar um sinal para dizer que terminamos, então booleanos já servem.
Dentro da mesma função, iniciamos a goroutine que enviará um sinal a esse canal uma vez que a função http.Get(URL)
tenha sido finalizada.
select
select
Se você se lembrar do capítulo de concorrência, é possível esperar os valores serem enviados a um canal com variavel := <-ch
. Isso é uma chamada bloqueante, pois está aguardando por um valor.
O que o select
te permite fazer é aguardar múltiplos canais. O primeiro a enviar um valor "vence" e o código abaixo do case
é executado.
Nós usamos ping
em nosso select
para configurar um canal para cada uma de nossas URL
s. Qualquer um que enviar para esse canal primeiro vai ter seu código executado no select
, que resultará nessa URL
sendo retornada (que consequentemente será a vencedora).
Após essas mudanças, a intenção por trás de nosso código fica bem clara e sua implementação efetivamente mais simples.
Limites de tempo
Nosso último requisito era retornar um erro se o Corredor
demorar mais que 10 segundos.
Escreva o teste primeiro
Fizemos nossos servidores de teste demorarem mais que 10s para retornar para exercitar esse cenário e agora estamos esperando que Corredor
retorne dois valores: a URL vencedora (que ignoramos nesse teste com _
) e um erro
.
Execute o teste
./corredor_test.go:37:10: assignment mismatch: 2 variables but 1 values
Escreva a menor quantidade de código para rodar o teste e verifique a saída do teste que falhou
Alteramos a assinatura de Corredor
para retornar o vencedor e um erro
. Retornamos nil
para nossos casos de sucesso.
O compilador vai reclamar sobre seu primeiro teste esperar apenas um valor, então altere essa linha para obteve, _ := Corredor(urlLenta, urlRapida)
. Sabendo disso devemos verificar se não obteremos um erro em nosso caso de sucesso.
Se executar isso agora, o teste irá falhar após 11 segundos.
Escreva código o suficiente para fazer o teste passar
time.After
é uma função muito útil quando usamos select
. Embora não ocorra em nosso caso, você pode escrever um código que bloqueia para sempre se os canais que o select
estiver ouvindo nunca retornarem um valor. time.After
retorna um chan
(como ping
) e te enviará um sinal após a quantidade de tempo definida.
Para nós isso é perfeito; se a
ou b
conseguir retornar teremos um vencedor, mas se chegar a 10 segundos nosso time.After
nos enviará um sinal e retornaremos um erro
.
Testes lentos
O problema que temos é que esse teste demora 10 segundos para rodar. Para uma lógica tão simples, isso não parece ótimo.
O que podemos fazer é deixar esse esgotamento de tempo configurável. Então, em nosso teste, podemos ter um tempo bem curto e, quando utilizado no mundo real, esse tempo ser definido para 10 segundos.
Nosso teste não irá compilar pois não fornecemos um tempo de expiração.
Antes de nos apressar para adicionar esse valor padrão a ambos os testes, vamos ouvi-los.
Nos importamos com o tempo excedido em nosso caso de teste de sucesso?
Os requisitos foram explícitos sobre o tempo limite?
Dado esse conhecimento, vamos fazer uma pequena refatoração para ser simpático aos nossos testes e aos usuários de nosso código.
Nossos usuários e nosso primeiro teste podem utilizar Corredor
(que usa Configuravel
por baixo dos panos) e nosso caminho triste pode usar Configuravel
.
Adicionei uma verificação final ao primeiro teste para saber se não pegamos um erro
.
Resumo
select
select
Ajuda você a escutar vários canais.
Às vezes você pode precisar incluir
time.After
em um de seuscases
para prevenir que seu sistema fique bloqueado para sempre.
httptest
httptest
Uma forma conveniente de criar servidores de teste para que se tenha testes confiáveis e controláveis.
Usa as mesmas interfaces que servidores
net/http
reais, o que torna seu sistema consistente e gera menos coisas para você aprender.
Last updated