
Nesta postagem, falarei sobre como escrevi um programa de console no idioma Go para fazer upload de dados de um banco de dados para arquivos, tentando cobrir todo o código com 100% de testes. Começarei com uma descrição do motivo pelo qual eu precisava deste programa. Continuarei descrevendo as primeiras dificuldades, algumas das quais são causadas pelos recursos do idioma Go. A seguir, mencionarei uma pequena compilação no Travis CI e depois falarei sobre como escrevi testes, tentando cobrir o código 100%. Vou testar um pouco o trabalho com o banco de dados e o sistema de arquivos. Concluindo, direi a que o desejo de cobrir o código com testes leva e o que esse indicador diz. Fornecerei material com links para a documentação e exemplos de confirmações do meu projeto.
Objetivo do Programa
O programa deve ser iniciado a partir da linha de comando com uma indicação da lista de tabelas e algumas de suas colunas, um intervalo de dados para a primeira coluna especificada, uma enumeração dos relacionamentos das tabelas selecionadas entre si, com a capacidade de especificar um arquivo com as configurações de conexão com o banco de dados. O resultado do trabalho deve ser um arquivo que descreve solicitações para criar as tabelas especificadas com as colunas especificadas e inserir expressões dos dados selecionados. Supunha-se que o uso de um programa desse tipo simplificasse o cenário de extrair uma parte dos dados de um grande banco de dados e implantar essa parte localmente. Além disso, esses arquivos sql de descarga deveriam ser processados por outro programa, que substitui parte dos dados de acordo com um modelo específico.
O mesmo resultado pode ser alcançado usando qualquer um dos clientes populares do banco de dados e uma quantidade suficientemente grande de trabalho manual. O aplicativo deveria simplificar esse processo e automatizar o máximo possível.
Este programa deveria ter sido desenvolvido por meus estagiários com o objetivo de treinamento e uso subseqüente em seu treinamento adicional. Mas a situação acabou de tal forma que eles recusaram essa ideia. Mas eu decidi tentar escrever um programa desse tipo no meu tempo livre para o propósito de minha prática de desenvolvimento na linguagem Go.
A solução está incompleta e possui várias limitações, descritas em README. De qualquer forma, este não é um projeto de combate.
Exemplos de uso e código fonte .
Primeiras dificuldades
A lista de tabelas e suas colunas é passada para o programa como um argumento na forma de uma string, ou seja, não é conhecido antecipadamente. A maioria dos exemplos de trabalho com um banco de dados no Go implica que a estrutura do banco de dados é conhecida antecipadamente; simplesmente criamos uma struct
indicando os tipos de cada coluna. Mas, neste caso, não funciona dessa maneira.
A solução para isso foi usar o método MapScan
em github.com/jmoiron/sqlx
, que criou uma fatia de interface no tamanho igual ao número de colunas de amostra. A próxima pergunta era como obter um tipo de dados real dessas interfaces. A solução é um caso de opção por tipo . Essa solução não parece muito bonita, porque será necessário converter todos os tipos em uma string: números inteiros como são, strings para escapar e colocar entre aspas, mas ao mesmo tempo descrever todos os tipos que podem vir do banco de dados. Não encontrei uma maneira mais elegante de resolver esse problema.
Com os tipos, um recurso de idioma Go também foi manifestado - uma variável do tipo string não pode assumir o valor nil
, mas uma string vazia e NULL
podem vir do banco de dados. Para resolver esse problema, existe uma solução no pacote database/sql
- use uma strut
especial, que armazene o valor e o sinal, seja NULL
ou não.
Montagem e cálculo da porcentagem de cobertura de código por testes
Para montagem, uso o Travis CI, para obter a porcentagem de cobertura de código com testes - Macacões. O arquivo .travis.yml
para o assembly é bastante simples:
language: go go: - 1.9 script: - go get -t -v ./... - go get golang.org/x/tools/cmd/cover - go get github.com/mattn/goveralls - go test -v -covermode=count -coverprofile=coverage.out ./... - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
Nas configurações do IC de Travis, você só precisa especificar a variável de ambiente COVERALLS_TOKEN
, cujo valor deve ser considerado no site .
O Coveralls permite que você descubra convenientemente qual porcentagem do projeto inteiro, para cada arquivo, realça uma linha de código-fonte que acabou sendo um teste descoberto. Por exemplo, na primeira compilação , fica claro que eu não escrevi testes para alguns casos de erros ao analisar uma solicitação do usuário.
100% de cobertura do código significa que são escritos testes que, entre outras coisas, executam o código para cada ramificação no if
. Este é o trabalho mais volumoso ao escrever testes e, em geral, ao desenvolver um aplicativo.
Você pode calcular a cobertura com testes localmente, por exemplo, com o mesmo go test -v -covermode=count -coverprofile=coverage.out ./...
, mas você pode fazê-lo mais bem no CI, pode colocar uma placa no Github.
Como estamos falando de dados, acho úteis em https://goreportcard.com , que analisa os seguintes indicadores:
- gofmt - formatação de código, incluindo simplificação de construções
- go_vet - verifica construções suspeitas
- gociclo - mostra problemas na complexidade ciclomática
- golint - para mim, está verificando a disponibilidade de todos os comentários necessários
- licença - o projeto deve ter uma licença
- ineffassign - verifica se há atribuições ineficazes
- erro ortográfico - verifica erros de digitação
Dificuldades para cobrir o código com testes 100%
Se a análise de uma pequena solicitação do usuário por componentes trabalha principalmente com a conversão de strings para algumas estruturas a partir de strings e é muito facilmente coberta por testes, então, para testar o código que funciona com um banco de dados, a solução não é tão óbvia.
Como alternativa, conecte-se a um servidor de banco de dados real, preencha previamente os dados em cada teste, faça seleções e desmarque. Mas essa é uma solução difícil, longe do teste de unidade e impõe seus requisitos ao ambiente, inclusive ao servidor de IC.
Outra opção poderia ser usar um banco de dados na memória, por exemplo, sqlite ( sqlx.Open("sqlite3", ":memory:")
), mas isso implica que o código deve estar o mais fraco possível possível ao mecanismo de banco de dados, o que complica bastante o projeto mas para o teste de integração é muito bom.
Para teste de unidade, usar simulação para o banco de dados é adequado. Eu encontrei este . Usando este pacote, você pode testar o comportamento no caso de um resultado normal e no caso de erros, indicando qual solicitação deve retornar qual erro.
Os testes de gravação mostraram que a função que se conecta ao banco de dados real precisa ser movida para main.go, para que possa ser redefinida nos testes daquele que retornará a instância simulada.
Além de trabalhar com o banco de dados, é necessário tornar o trabalho com o sistema de arquivos uma dependência separada. Isso permitirá substituir a gravação de arquivos reais pela gravação na memória para facilitar o teste e reduzir o acoplamento. É assim que a interface FileWriter
apareceu e, com ela, a interface do arquivo que ele retorna. Para testar os cenários de erro, as implementações auxiliares dessas interfaces foram criadas e colocadas no arquivo filewriter_test.go
, para que não caiam na construção geral, mas podem ser usadas em testes.
Depois de algum tempo, eu tive uma pergunta sobre como cobrir main()
testes. Naquela época, eu tinha código suficiente lá. Como os resultados da pesquisa mostraram, isso não é feito no Go . Em vez disso, todo o código que pode ser extraído de main()
precisa ser extraído. No meu código, deixei apenas as opções de análise e os argumentos da linha de comando (pacote de flag
), conectando-me ao banco de dados, instanciando um objeto que gravará arquivos e chamando um método que fará o restante do trabalho. Mas essas linhas não permitem obter exatamente 100% de cobertura.
No teste Go, existe algo como " Exemplo de funções ". Essas são funções de teste que comparam a saída com o que é descrito no comentário dentro dessa função. Exemplos de tais testes podem ser encontrados no código-fonte para pacotes go . Se esses arquivos não contiverem testes e referências, serão nomeados com o prefixo example_
e terminarão com _test.go
. O nome de cada função de teste deve começar com Example
. Sobre isso, escrevi um teste para um objeto que grava sql em um arquivo, substituindo o registro real no arquivo por um mock, do qual você pode obter o conteúdo e exibi-lo. Esta conclusão é comparada com o padrão. Convenientemente, você não precisa escrever uma comparação com as mãos e é conveniente escrever várias linhas nos comentários. Mas quando se tratava de um objeto que grava dados em um arquivo csv, surgiram dificuldades. De acordo com o RFC4180, as linhas no CSV devem ser separadas pelo CRLF e o go fmt
substitui todas as linhas pelo LF, o que leva ao fato de que o padrão do comentário não coincide com a saída real devido aos diferentes separadores de linhas. Eu tive que escrever um teste regular para esse objeto, além de renomear o arquivo removendo example_
dele.
A questão permanece: se o arquivo, por exemplo, query.go
testado usando os testes Example e convencional, deve haver dois arquivos example_query_test.go
e query_test.go
? Aqui, por exemplo, há apenas um example_test.go
. Use a busca por "exemplo de teste" ainda é divertido.
Aprendi a escrever testes no Go, de acordo com os guias que o Google fornece para "vá escrever testes". A maioria dos que me deparei ( 1 , 2 , 3 , 4 ) sugere comparar o resultado com o design esperado do formulário
if v != 1.5 { t.Error("Expected 1.5, got ", v) }
Mas quando se trata de comparar tipos, uma construção familiar evolui evolutivamente para um monte de uso de "refletir" ou asserção de tipo. Ou outro exemplo, quando você precisa verificar se a fatia ou o mapa tem o valor necessário. O código se torna complicado. Então, eu quero escrever minhas funções auxiliares para o teste. Embora uma boa solução aqui seja usar uma biblioteca para teste. Encontrei https://github.com/stretchr/testify . Ele permite que você faça comparações em uma única linha . Esta solução reduz a quantidade de código e simplifica a leitura e o suporte de testes.
Fragmentação e teste de código
Escrever um teste para uma função de alto nível que funcione com vários objetos permite aumentar significativamente o valor da cobertura de código por testes de uma só vez, porque durante esse teste são executadas muitas linhas de código de objetos individuais. Se você definir a meta de apenas 100% de cobertura, a motivação para escrever testes de unidade em pequenos componentes do sistema desaparecerá, pois isso não afeta o valor da cobertura do código.
Além disso, se você não verificar o resultado na função de teste, isso também não afetará o valor da cobertura do código. Você pode obter um alto valor de cobertura, mas não pode detectar erros graves no aplicativo.
Por outro lado, se você tiver código com muitos ramos , após o qual uma função volumosa é chamada, será difícil cobri-lo com testes. E aqui você tem um incentivo para melhorar esse código, por exemplo, para colocar todos os ramos em uma função separada e escrever um teste separado . Isso afetará positivamente a legibilidade do código.
Se o código tiver um forte acoplamento, provavelmente você não poderá escrever um teste, o que significa que será necessário fazer alterações, o que afetará positivamente a qualidade do código.
Conclusão
Antes deste projeto, não era necessário definir uma meta de 100% de cobertura do código com testes. Eu poderia obter um aplicativo funcional em 10 horas de desenvolvimento, mas levei 20 a 30 horas para atingir 95% de cobertura. Usando um pequeno exemplo, tive uma idéia de como o valor da cobertura do código afeta sua qualidade e quanto esforço é necessário para mantê-lo.
Minha conclusão é que, se você vê um painel com um alto valor de cobertura de código para alguém, ele não diz quase nada sobre o quão bem esse aplicativo foi testado. De qualquer forma, você precisa assistir aos testes. Mas se você definiu um curso para 100% honestos, isso ajudará você a escrever um aplicativo melhor.
Você pode ler mais sobre isso nos seguintes materiais e comentários:
SpoilerA palavra "revestimento" é usada cerca de 20 vezes. Desculpe.