5 técnicas avançadas de teste

Saudação a todos! Falta menos de uma semana para o início do curso "Golang Developer" e continuamos a compartilhar material útil sobre o assunto. Vamos lá!



O Go possui uma biblioteca interna boa e confiável para teste. Se você escreve no Go, já sabe disso. Neste artigo, falaremos sobre várias estratégias que podem melhorar suas habilidades de teste com o Go. Com a experiência de escrever nossa impressionante base de códigos no Go, aprendemos que essas estratégias realmente funcionam e, portanto, ajudam a economizar tempo e esforço ao trabalhar com o código.

Use suítes de teste

Se você aprender apenas uma coisa útil deste artigo, deve ser o uso de suítes de teste. Para aqueles que não estão familiarizados com esse conceito, o teste por kits é o processo de desenvolvimento de um teste para testar uma interface comum que pode ser usada em muitas implementações dessa interface. Abaixo, você pode ver como passamos por várias implementações diferentes do Thinger e as executamos com os mesmos testes.

 type Thinger interface { DoThing(input string) (Result, error) } // Suite tests all the functionality that Thingers should implement func Suite(t *testing.T, impl Thinger) { res, _ := impl.DoThing("thing") if res != expected { t.Fail("unexpected result") } } // TestOne tests the first implementation of Thinger func TestOne(t *testing.T) { one := one.NewOne() Suite(t, one) } // TestOne tests another implementation of Thinger func TestTwo(t *testing.T) { two := two.NewTwo() Suite(t, two) } 

Os leitores sortudos trabalharam com bases de código que usam esse método. Geralmente usado em testes de sistemas baseados em plug-ins, escritos para testar uma interface, pode ser usado por todas as implementações dessa interface para entender como eles atendem aos requisitos de comportamento.

O uso dessa abordagem ajudará a economizar horas, dias e até tempo suficiente para resolver o problema da igualdade das classes P e NP . Além disso, ao substituir um sistema básico por outro, a necessidade de gravar (um grande número) de testes adicionais desaparece e também há confiança de que essa abordagem não interromperá a operação do seu aplicativo. Implicitamente, você precisa criar uma interface que defina a área da área testada. Usando a injeção de dependência, você pode personalizar um conjunto de um pacote que é passado para a implementação de todo o pacote.

Você pode encontrar um exemplo completo aqui . Apesar do fato de este exemplo ser exagerado, pode-se imaginar que um banco de dados é remoto e o outro está na memória.

Outro exemplo interessante dessa técnica está localizado na biblioteca padrão no pacote golang.org/x/net/nettest . Ele fornece um meio de verificar se o net.Conn está satisfazendo a interface.

Evite a contaminação da interface

Você não pode falar sobre testes no Go, mas não fala sobre interfaces.

As interfaces são importantes no contexto dos testes, pois são a ferramenta mais poderosa em nosso arsenal de testes; portanto, você precisa usá-las corretamente.

Os pacotes geralmente exportam interfaces para desenvolvedores, e isso leva ao fato de que:

A) Os desenvolvedores criam seu próprio mock para implementar o pacote;
B) O pacote exporta seu próprio mock.

"Quanto maior a interface, mais fraca a abstração"
- Rob Pike, Provérbios de Go

As interfaces devem ser cuidadosamente verificadas antes da exportação. Muitas vezes, é tentador exportar interfaces para dar aos usuários a capacidade de simular o comportamento de que precisam. Em vez disso, documente quais interfaces se adaptam às suas estruturas para não criar um relacionamento estreito entre o pacote do consumidor e o seu. Um ótimo exemplo disso é o pacote de erros .

Quando temos uma interface que não queremos exportar, podemos usar a subárvore interna / pacote para salvá-la dentro do pacote. Portanto, não podemos ter medo de que o usuário final dependa dele e, portanto, seja flexível na alteração da interface de acordo com os novos requisitos. Normalmente, criamos interfaces com dependências externas para poder executar testes localmente.

Essa abordagem permite que o usuário implemente suas próprias interfaces pequenas, simplesmente agrupando parte da biblioteca para teste. Para mais informações sobre esse conceito, leia o artigo rakyl sobre poluição de interface .

Não exportar primitivas de simultaneidade

O Go oferece primitivas de simultaneidade fáceis de usar que às vezes também podem levar ao uso excessivo devido à mesma simplicidade. Antes de tudo, estamos preocupados com os canais e o pacote de sincronização. Às vezes, é tentador exportar um canal do seu pacote para que outros possam usá-lo. Além disso, um erro comum é incorporar o sync.Mutex sem defini-lo como privado. Isso, como sempre, nem sempre é ruim, mas cria certos problemas ao testar seu programa.

Se você exportar canais, também complica a vida do usuário do pacote, o que não vale a pena. Assim que o canal é exportado do pacote, você cria dificuldades ao testar para quem usa esse canal. Para um teste bem-sucedido, o usuário deve saber:

  • Quando os dados acabam sendo enviados pelo canal.
  • Houve algum erro ao receber dados.
  • Como um pacote libera o canal após a conclusão, se é que libera?
  • Como agrupar uma API de pacote para que você não a chame diretamente?

Dê uma olhada no exemplo de leitura da fila. Aqui está um exemplo de biblioteca que lê da fila e fornece ao usuário um feed para leitura.

 type Reader struct {...} func (r *Reader) ReadChan() <-chan Msg {...} 

Agora, o usuário da sua biblioteca deseja implementar um teste para o consumidor:

 func TestConsumer(t testing.T) { cons := &Consumer{ r: libqueue.NewReader(), } for msg := range cons.r.ReadChan() { // Test thing. } } 


O usuário pode então decidir que a injeção de dependência é uma boa ideia e escrever suas próprias mensagens no canal:

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for msg := range cons.r.ReadChan() { // Test thing. } } 


Espere, e os erros?

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for { select { case msg := <-cons.r.ReadChan(): // Test thing. case err := <-cons.r.ErrChan(): // What caused this again? } } } 


Agora, de alguma forma, precisamos gerar eventos para realmente gravar neste esboço, que replica o comportamento da biblioteca que estamos usando. Se a biblioteca acabou de escrever a API síncrona, poderíamos adicionar todo o paralelismo ao código do cliente, para que o teste se torne mais fácil.

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } msg, err := cons.r.ReadMsg() // handle err, test thing } 


Em caso de dúvida, lembre-se de que é sempre fácil adicionar paralelismo ao pacote do consumidor (pacote consumidor) e é difícil ou impossível removê-lo após a exportação da biblioteca. E o mais importante, não se esqueça de escrever na documentação do pacote se a estrutura / pacote é seguro para acesso simultâneo a várias goroutines.
Às vezes, ainda é desejável ou necessário exportar o canal. Para atenuar alguns dos problemas mencionados acima, você pode fornecer canais por meio de acessadores em vez de acesso direto e deixá-los abertos apenas para leitura ( ←chan ) ou somente para escrita ( chan← ) ao declarar.

Use net/http/httptest

Httptest permite executar o código http.Handler sem iniciar um servidor ou vincular-se a uma porta. Isso acelera os testes e permite executar testes em paralelo a um custo menor.

Aqui está um exemplo do mesmo teste implementado de duas maneiras. Não há nada de grandioso aqui, mas essa abordagem reduz a quantidade de código e economiza recursos.

 func TestServe(t *testing.T) { // The method to use if you want to practice typing s := &http.Server{ Handler: http.HandlerFunc(ServeHTTP), } // Pick port automatically for parallel tests and to avoid conflicts l, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) } defer l.Close() go s.Serve(l) res, err := http.Get("http://" + l.Addr().String() + "/?sloths=arecool") if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } func TestServeMemory(t *testing.T) { // Less verbose and more flexible way req := httptest.NewRequest("GET", "http://example.com/?sloths=arecool", nil) w := httptest.NewRecorder() ServeHTTP(w, req) greeting, err := ioutil.ReadAll(w.Body) if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } 

Talvez a característica mais importante seja que, com o httptest você só pode dividir o teste na função que deseja testar. Nenhum roteador, middleware ou qualquer outro efeito colateral que surja ao configurar servidores, serviços, fábricas de processadores, fábricas de processadores ou quaisquer outras coisas que você acha que seria uma boa idéia.

Para ver esse princípio em ação, confira o artigo de Marc Berger .

Use o pacote separado _test

A maioria dos testes no ecossistema é criada nos arquivos pkg_test.go , mas ainda permanece no mesmo pacote: package pkg . Um pacote de teste separado é o pacote que você cria no novo arquivo, foo_test.go , no diretório do módulo que deseja testar, foo/ , com o package foo_test declaração package foo_test . A partir daqui, você pode importar o github.com/example/foo e outras dependências. Esse recurso permite que você faça muitas coisas. Essa é a solução recomendada para dependências cíclicas nos testes, evita o aparecimento de "testes quebradiços" e permite que o desenvolvedor sinta como é usar seu próprio pacote. Se o seu pacote for difícil de usar, também será difícil testar com esse método.

Essa estratégia evita testes frágeis, restringindo o acesso a variáveis ​​privadas. Em particular, se seus testes forem interrompidos e você usar pacotes de testes separados, é quase garantido que um cliente usando uma função que interrompa os testes também será interrompido quando chamado.

Por fim, ajuda a evitar ciclos de importação nos testes. Como a maioria dos pacotes depende mais de outros pacotes que você escreveu, além dos de teste, você terá uma situação em que o ciclo de importação ocorre naturalmente. Um pacote externo está localizado acima dos dois pacotes na hierarquia de pacotes. Tomemos um exemplo de The Go Programming Language (Capítulo 11, Seção 2.4), em que o net/url implementa um analisador de URL que o net/http importa para uso. No entanto, net / url precisa ser testado com um caso de uso real importando net / http . Assim, net/url_test .

Agora, quando você usa um pacote de teste separado, pode precisar de acesso a entidades não exportadas no pacote em que elas estavam disponíveis anteriormente. Alguns desenvolvedores enfrentam isso pela primeira vez ao testar algo com base no tempo (por exemplo, time.Now se torna um esboço usando uma função). Nesse caso, podemos usar um arquivo adicional para fornecer entidades exclusivamente durante o teste, pois os arquivos _test.go excluídos das compilações regulares.

Do que você precisa se lembrar?

É importante lembrar que nenhum dos métodos descritos acima é uma panacéia. A melhor abordagem em qualquer empresa é analisar criticamente a situação e escolher independentemente a melhor solução para o problema.

Deseja saber mais sobre os testes com o Go?
Leia estes artigos:

Testes conduzidos por Dave Cheney na mesa de escrita em Go
O capítulo sobre linguagem de programação Go, sobre testes.
Ou assista a estes vídeos:
Hashimoto's Advanced Testing With Go fala de Gophercon 2017
Técnicas de Teste de Andrew Gerrand falam de 2014

Esperamos que esta tradução tenha sido útil para você. Estamos aguardando comentários e, todos que quiserem saber mais sobre o curso, convidamos você a abrir o dia , que será realizado em 23 de maio.

Source: https://habr.com/ru/post/pt452772/


All Articles