Práticas recomendadas da Go - Teste

Nos primeiros dias de minha carreira em programação, eu realmente não via o valor e, principalmente, achava que duplicava o trabalho. Agora, no entanto, geralmente busco uma cobertura de teste de 90 a 100% em tudo o que escrevo. E geralmente acredito que testar em todas as camadas é uma boa prática (voltaremos a isso).

De fato, ao olhar para as bases de código que tenho diante de mim diariamente, as que mais temo mudar são as que têm menos cobertura de teste. E isso diminui minha produtividade e nossos produtos finais. Então, para mim, é bastante claro que a alta cobertura de teste é igual a maior qualidade e maior produtividade.

Teste em todas as camadas

Vamos mergulhar em um exemplo imediatamente. Suponha que você tenha um aplicativo com a seguinte estrutura.

Modelo de aplicação

Existem alguns componentes compartilhados, como os modelos e os manipuladores. Você tem algumas maneiras diferentes de interagir com esse aplicativo, por exemplo, CLI, API HTTP ou RPCs Thrift. Eu achei uma boa prática garantir que você testasse não apenas os modelos ou apenas os manipuladores, mas todos eles. Mesmo para o mesmo recurso. Como é verdade, mas necessariamente verdade, que se você implementou o suporte ao Recurso X no manipulador, ele está realmente disponível nas interfaces HTTP e Thrift, por exemplo.

Era assim que você ficaria mais confiante em fazer alterações em sua lógica, mesmo no fundo do núcleo do aplicativo.

Testes baseados em tabela

Em quase todos os casos, ao testar um método, você deseja testar alguns cenários na função. Geralmente com diferentes parâmetros de entrada ou diferentes respostas simuladas. Eu gosto de agrupar todos esses testes em uma função Test * e, em seguida, ter um loop executando todos os casos de teste. Aqui está um exemplo básico:

func TestDivision (t * testing.T) {
    testes: = [] struct {
        x float64
        y float64
        resultado float64
        errar
    } {
        {x: 1.0, y: 2.0, resultado: 0.5, erro: zero},
        {x: -1,0, y: 2,0, resultado: -0,5, erro: zero},
        {x: 1.0, y: 0.0, resultado: 0.0, erro: ErrZeroDivision},
    }
    para _, test: = testes de intervalo {
        resultado, err: = dividir (test.x, test.y)
        assert.IsType (t, test.err, err)
        assert.Equal (t, teste.resultado, resultado)
    }
}

Os testes acima não estão cobrindo tudo, mas servem como um exemplo de como testar os resultados e erros esperados. O código acima também usa o ótimo pacote testify para asserções.

Um aprimoramento, testes baseados em tabela com casos de teste nomeados

Se você tiver muitos testes ou, frequentemente, novos desenvolvedores que não estão familiarizados com a base de código, pode ser útil nomear seus testes. Aqui está um pequeno exemplo de como isso seria

testes: = mapear [string] struct {
    number int
    Erro smsErr
    errar
} {
    "com êxito": {0132423444, nil, nil},
    "propaga erro": {0132423444, sampleErr, sampleErr},
}

Observe que há uma diferença aqui entre ter um mapa e uma fatia. O mapa não garante a ordem, enquanto a fatia garante.

Zombando usando zombaria

As interfaces são naturalmente super bons pontos de integração para testes, pois a implementação de uma interface pode ser facilmente substituída por uma implementação simulada. No entanto, escrever zombarias pode ser bastante entediante e chato. Para facilitar a vida, estou usando zombaria para gerar minhas zombarias com base em uma determinada interface.

Vamos dar uma olhada em como trabalhar com isso. Suponha que tenhamos a seguinte interface.

digite interface SMS {
    Erro de envio (número int, sequência de texto)
}

Aqui está uma implementação fictícia usando esta interface:

// Messager é uma estrutura que manipula mensagens de vários tipos.
tipo messager struct {
    sms sms
}
// SendHelloWorld envia um SMS Hello world.
erro sendHelloWorld (número int) func (m * Messager) {
    err: = m.sms.Send (número, "Olá, mundo!")
    se errar! = nulo {
        retornar errado
    }
    retorno nulo
}

Agora podemos usar o Mockery para gerar uma simulação para a interface do SMS. Aqui está como isso seria (este exemplo está usando o sinalizador -inpkg, que coloca o mock no mesmo pacote da interface).

// MockSMS é um tipo de simulação gerado automaticamente para o tipo de SMS
tipo MockSMS struct {
    mock.Mock
}
// Send fornece uma função simulada com campos fornecidos: número, texto
func (_m * MockSMS) Erro de envio (número int, sequência de texto) {
    ret: = _m.Chamado (número, texto)
    erro var r0
    if rf, ok: = ret.Get (0). (func (int, string) erro); Está bem {
        r0 = rf (número, texto)
    } outro {
        r0 = ret.Error (0)
    }
    retornar r0
}
var _ SMS = (* MockSMS) (nulo)

A estrutura do SMS é herdada do testify mock.Mock, que fornece algumas opções interessantes ao escrever os casos de teste. Então, agora é hora de escrever nosso teste para o método SendHelloWorld usando a simulação da Mockery.

func TestSendHelloWorld (t * testing.T) {
    sampleErr: = errors.New ("some error")
    testes: = mapear [string] struct {
        number int
        Erro smsErr
        errar
    } {
        "com êxito": {0132423444, nil, nil},
        "propaga erro": {0132423444, sampleErr, sampleErr},
    }
    para _, test: = testes de intervalo {
        sms: = & MockSMS {}
        sms.On ("Send", test.number, "Olá, mundo!"). Return (test.smsErr) .Once ()
        m: = & Messager {
            sms: sms,
        }
   
        err: = m.SendHelloWorld (número de teste)
        assert.Equal (t, test.err, err)
        sms.AssertExpectations (t)
    }
}

Há alguns pontos que vale a pena mencionar no código de exemplo acima. No teste, você notará que eu instanciao o MockSMS e, em seguida, usando .On (), posso ditar o que deve acontecer (.Return ()) quando determinados parâmetros são enviados para a simulação.

Finalmente, estou usando o sms.AssertExpectations para garantir que a interface do SMS tenha sido chamada o número esperado de vezes. Nesse caso, Once ().

Todos os arquivos acima podem ser encontrados nesta lista.

Testes de arquivo dourado

Em alguns casos, achei útil apenas afirmar que um grande blob de resposta permanece o mesmo. Por exemplo, podem ser dados retornados de dados JSON de uma API. Nesse caso, aprendi com Michell Hashimoto sobre o uso de arquivos dourados combinados com um inteligente, que era expor sinalizadores de linha de comando para serem testados.

A ideia básica é que você escreva o corpo de resposta correto em um arquivo (o arquivo de ouro). Em seguida, ao executar os testes, você faz uma comparação de bytes entre o arquivo dourado e a resposta do teste.

Para facilitar, criei o pacote goldie, que lida com a configuração do sinalizador da linha de comando e a gravação e comparação de arquivos dourados de forma transparente.

Aqui está um exemplo de como usar o goldie para esse tipo de teste:

func TestExample (t * testing.T) {
    gravador: = activationptest.NewRecorder ()

    req, err: = http.NewRequest ("GET", "/ example", zero)
    assert.Nil (t, err)

    manipulador: = http.HandlerFunc (ExampleHandler)
    handler.ServeHTTP ()

    goldie.Assert (t, "exemplo", gravador.Body.Bytes ())
}

Quando você precisar atualizar seu arquivo dourado, execute o seguinte:

teste -update. / ...

E quando você quiser apenas executar os testes, faça isso como sempre:

vai testar. / ...

Tchau tchau!

Obrigado por permanecer até o fim! Espero que você tenha encontrado algo útil no artigo.