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.

func TestCorredor(t *testing.T) {
    URLLenta := "http://www.facebook.com"
    URLRapida := "http://www.quii.co.uk"

    esperado := URLRapida
    resultado := Corredor(URLLenta, urlRapida)

    if resultado != esperado {
        t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
    }
}

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

func Corredor(a, b string) (vencedor string) {
    return
}

corredor_test.go:25: resultado '', esperado 'http://www.quii.co.uk'

Escreva código suficiente para que o teste passe

func Corredor(a, b string) (vencedor string) {
    inicioA := time.Now()
    http.Get(a)
    duracaoA := time.Since(inicioA)

    inicioB := time.Now()
    http.Get(b)
    duracaoB := time.Since(inicioB)

    if duracaoA < duracaoB {
        return a
    }

    return b
}

Para cada URL:

  1. Usamos time.Now() para marcar o tempo antes de tentarmos pegar a URL.

  2. Então usamos http.Get para tentar capturar os conteúdos da URL. Essa função retorna http.Response e um erro, mas não temos interesse nesses valores.

  3. time.Since pega o tempo inicial e retorna a diferença na forma de time.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.

func TestCorredor(t *testing.T) {

    servidorLento := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(20 * time.Millisecond)
        w.WriteHeader(http.StatusOK)
    }))

    servidorRapido := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    }))

    URLLenta := servidorLento.URL
    URLRapida := servidorRapido.URL

    esperado := URLRapida
    resultado := Corredor(URLLenta, URLRapida)

    if resultado != esperado {
        t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
    }

    servidorLento.Close()
    servidorRapido.Close()
}

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.

func Corredor(a, b string) (vencedor string) {
    duracaoA := medirTempoDeResposta(a)
    duracaoB := medirTempoDeResposta(b)

    if duracaoA < duracaoB {
        return a
    }

    return b
}

func medirTempoDeResposta(URL string) time.Duration {
    inicio := time.Now()
    http.Get(URL)
    return time.Since(inicio)
}

Essa "enxugada" torna nosso código Corredor bem mais legível.

func TestCorredor(t *testing.T) {

    servidorLento := criarServidorComAtraso(20 * time.Millisecond)
    servidorRapido := criarServidorComAtraso(0 * time.Millisecond)

    defer servidorLento.Close()
    defer servidorRapido.Close()

    URLLenta := servidorLento.URL
    URLRapida := servidorRapido.URL

    esperado := URLRapida
    resultado := Corredor(URLLenta, URLRapida)

    if resultado != esperado {
        t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
    }
}

func criarServidorComAtraso(atraso time.Duration) *httptest.Server {
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(atraso)
        w.WriteHeader(http.StatusOK)
    }))
}

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

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.

func Corredor(a, b string) (vencedor string) {
    select {
    case <-ping(a):
        return a
    case <-ping(b):
        return b
    }
}

func ping(URL string) chan bool {
    ch := make(chan bool)
    go func() {
        http.Get(URL)
        ch <- true
    }()
    return ch
}

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

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 URLs. 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

t.Run("retorna um erro se o servidor não responder dentro de 10s", func(t *testing.T) {
    servidorA := criarServidorComAtraso(11 * time.Second)
    servidorB := criarServidorComAtraso(12 * time.Second)

    defer servidorA.Close()
    defer servidorB.Close()

    _, err := Corredor(servidorA.URL, servidorB.URL)

    if err == nil {
        t.Error("esperava um erro, mas não obtive um")
    }
})

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

func Corredor(a, b string) (vencedor string, erro error) {
    select {
    case <-ping(a):
        return a, nil
    case <-ping(b):
        return b, nil
    }
}

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.

--- FAIL: TestCorredor (12.00s)
    --- FAIL: TestCorredor/retorna_um_erro_se_o_teste_não_responder_dentro_de_10s (12.00s)
        corredor_test.go:40: esperava um erro, mas não obtive um.

Escreva código o suficiente para fazer o teste passar

func Corredor(a, b string) (vencedor string, erro error) {
    select {
    case <-ping(a):
        return a, nil
    case <-ping(b):
        return b, nil
    case <-time.After(10 * time.Second):
        return "", fmt.Errorf("tempo limite de espera excedido para %s e %s", a, b)
    }
}

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.

func Corredor(a, b string, tempoLimite time.Duration) (vencedor string, erro error) {
    select {
    case <-ping(a):
        return a, nil
    case <-ping(b):
        return b, nil
    case <-time.After(tempoLimite):
        return "", fmt.Errorf("tempo limite de espera excedido para %s e %s", a, b)
    }
}

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.

var limiteDeDezSegundos = 10 * time.Second

func Corredor(a, b string) (vencedor string, erro error) {
    return Configuravel(a, b, limiteDeDezSegundos)
}

func Configuravel(a, b string, tempoLimite time.Duration) (vencedor string, erro error) {
    select {
    case <-ping(a):
        return a, nil
    case <-ping(b):
        return b, nil
    case <-time.After(tempoLimite):
        return "", fmt.Errorf("tempo limite de espera excedido para %s e %s", a, b)
    }
}

Nossos usuários e nosso primeiro teste podem utilizar Corredor (que usa Configuravel por baixo dos panos) e nosso caminho triste pode usar Configuravel.

func TestCorredor(t *testing.T) {
    t.Run("compara a velocidade de servidores, retornando o endereço do mais rápido", func(t *testing.T) {
        servidorLento := criarServidorComAtraso(20 * time.Millisecond)
        servidorRapido := criarServidorComAtraso(0 * time.Millisecond)

        defer servidorLento.Close()
        defer servidorRapido.Close()

        URLLenta := servidorLento.URL
        URLRapida := servidorRapido.URL

        esperado := URLRapida
        resultado, err := Corredor(URLLenta, URLRapida)

        if err != nil {
            t.Fatalf("não esperava um erro, mas obteve um %v", err)
        }

        if resultado != esperado {
            t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
        }
    })

    t.Run("retorna um erro se o servidor não responder dentro de 10s", func(t *testing.T) {
        servidor := criarServidorComAtraso(25 * time.Millisecond)

        defer servidor.Close()

        _, err := Configuravel(servidor.URL, servidor.URL, 20*time.Millisecond)

        if err == nil {
            t.Error("esperava um erro, mas não obtive um")
        }
    })
}

Adicionei uma verificação final ao primeiro teste para saber se não pegamos um erro.

Resumo

select

  • Ajuda você a escutar vários canais.

  • Às vezes você pode precisar incluir time.After em um de seus cases para prevenir que seu sistema fique bloqueado para sempre.

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