Testes bem escritos reduzem significativamente o risco de "quebrar" o aplicativo ao adicionar um novo recurso ou corrigir um bug. Em sistemas complexos que consistem em vários componentes interconectados, o mais difícil é testar seu terreno comum.
Neste artigo, falarei sobre como encontramos a dificuldade de escrever bons testes ao desenvolver um componente no Go e como resolvemos esse problema usando a biblioteca RSpec no Ruby on Rails.
Adicionando Go à pilha de tecnologia do projeto
Um dos projetos que a eTeam está desenvolvendo, onde trabalho, pode ser dividido em: painel de administração, conta de usuário, gerador de relatórios e solicitações de processamento de vários serviços aos quais estamos integrados.
A parte responsável pelo processamento de solicitações é a mais importante, então eu queria torná-la o mais confiável e acessível possível. Como parte de um aplicativo monolítico, ela se arriscava a receber um bug ao alterar seções de código não relacionadas a ele. Também havia o risco de interromper o processamento ao carregar outros componentes do aplicativo. O número de trabalhadores do Ngnix por aplicativo é limitado e, à medida que a carga aumentou, por exemplo, abrindo muitas páginas pesadas no painel de administração, os trabalhadores livres pararam e o processamento de solicitações ficou mais lento ou até caiu.
Esses riscos, bem como a maturidade desse sistema (durante meses não foram necessários alterações), tornaram-no um candidato ideal para a separação em um serviço separado.
Foi decidido escrever este serviço separado no Go. Ele teve que compartilhar o acesso ao banco de dados com o aplicativo Rails. A responsabilidade por possíveis alterações na estrutura da tabela permaneceu no Rails. Em princípio, esse esquema com um banco de dados comum funciona bem, enquanto existem apenas dois aplicativos. Parecia assim:

O serviço foi gravado e implantado em instâncias separadas do Rails. Agora, ao implantar aplicativos Rails, você não precisa se preocupar que isso afetaria o processamento de consultas. O serviço aceitou solicitações HTTP diretamente, sem o Ngnix, usou um pouco de memória e foi de alguma forma minimalista.
O problema com nossos testes de unidade no Go
Os testes de unidade foram implementados no aplicativo Go e todas as consultas ao banco de dados foram bloqueadas. Entre outros argumentos a favor dessa solução, havia os seguintes: o aplicativo principal do Rails é responsável pela estrutura do banco de dados, portanto o aplicativo go não “possui” as informações para a criação de um banco de dados de teste. O processamento de solicitações para metade consistiu em lógica comercial e metade do trabalho com o banco de dados, e essa metade foi completamente bloqueada. O Moki in Go parece menos "legível" do que no Ruby. Ao adicionar uma nova função para ler dados do banco de dados, foi necessário adicionar o moki para ele no conjunto de testes que já funcionaram anteriormente. Como resultado, esses testes de unidade foram ineficazes e extremamente frágeis.
Método de solução
Para eliminar essas deficiências, foi decidido cobrir o serviço com testes funcionais localizados no aplicativo Rails e testar o serviço no Go como uma caixa preta. Como uma caixa branca, ainda não funcionaria, porque a partir do ruby, mesmo com todo o desejo, seria impossível intervir no serviço, por exemplo, obter algum tipo de método para verificar se é chamado. Isso também significava que as solicitações enviadas pelo serviço testado também eram impossíveis de bloquear, portanto, era necessário outro aplicativo para capturá-las e registrá-las. Algo como RequestBin, mas local. Já escrevemos um utilitário semelhante, então o usamos.
O seguinte esquema acabou:
- O rspec compila e inicia o serviço em movimento, transmitindo-lhe uma configuração, que contém acesso à base de teste e uma certa porta para receber solicitações HTTP, por exemplo 8082
- um utilitário também é iniciado para registrar solicitações HTTP recebidas nele, na porta 8083
- escrevemos testes comuns no RSpec, ou seja, crie os dados necessários no banco de dados e envie uma solicitação para localhost: 8082, como se fosse um serviço externo, por exemplo, usando HTTParty
- resposta parsim; verificar alterações no banco de dados; nós obtemos a lista de solicitações registradas no "RequestBin" e as verificamos.
Detalhes da implementação:
Agora sobre como foi implementado. Para fins de demonstração, vamos nomear o serviço testado: "TheService" e criar um wrapper para ele:
Por via das dúvidas, farei uma reserva de que no Rspec ele deve ser configurado para carregar automaticamente arquivos da pasta "support":
Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f}
O método de início:
- lê de uma configuração separada o caminho para as origens do TheService e as informações necessárias para executar. Porque essas informações podem diferir de diferentes desenvolvedores; essa configuração é excluída do Git. A mesma configuração contém as configurações necessárias para o lançamento do programa. Essas configurações heterogêneas estão localizadas em um único local para não produzir arquivos extras.
- compila e executa o programa através de "go run {path to main.go} {path to config}"
- a cada segundo, ele espera até que o programa em execução esteja pronto para aceitar solicitações
- lembra o identificador do processo para não reiniciar e poder pará-lo.
configuração em si:
#/spec/support/the_service_config.yml server: addr: 127.0.0.1:8082 db: dsn: dbname=project_test sslmode=disable user=postgres password=secret redis: url: redis://127.0.0.1:6379/1 rails: main_go: /home/me/go/src/github.com/company/theservice/main.go recorder_addr: 127.0.0.1:8083 env: PATH: '/home/me/.gvm/gos/go1.10.3/bin' GOROOT: '/home/me/.gvm/gos/go1.10.3' GOPATH: '/home/me/go'
O método stop simplesmente interrompe o processo. A novidade é que o ruby executa o comando "go run", que executa o binário compilado em um processo filho cujo ID é desconhecido. Se você simplesmente interromper o processo iniciado pelo ruby, o processo filho não será interrompido automaticamente e a porta permanecerá ocupada. Portanto, a parada ocorre pela identificação do grupo de processos:
Agora, prepararemos um contexto_compartilhado onde definiremos as variáveis padrão, iniciaremos o TheService se não tiver sido iniciado e desativaremos temporariamente o videocassete (do ponto de vista dele, falamos com um serviço externo, mas para nós agora não é assim):
e agora você pode começar a escrever as especificações:
TheService pode fazer suas solicitações HTTP para serviços externos. Usando a configuração, redirecionamos para um utilitário local que os escreve. Também existe um invólucro para iniciar e parar; é semelhante à classe "TheServiceControl", exceto que o utilitário pode simplesmente ser iniciado sem compilação.
Pãezinhos extras
O aplicativo Go foi gravado para que todos os logs e informações de depuração sejam exibidos em STDOUT. Quando iniciada na produção, essa saída é enviada para um arquivo. E quando lançado a partir do Rspec, é exibido no console, o que ajuda bastante na depuração.
Se as especificações forem executadas seletivamente, para as quais o TheService não é necessário, ele não será iniciado.
Para evitar perder tempo desenvolvendo o serviço sempre que você reiniciar as especificações durante o desenvolvimento, você pode iniciar o serviço manualmente no terminal e não desligá-lo. Se necessário, você pode até executá-lo no IDE no modo de depuração, e as especificações preparam tudo o que você precisa, emitem uma solicitação de serviço, ele pára e você pode se degradar sem problemas. Isso torna a abordagem TDD muito conveniente.
Conclusões
Esse esquema trabalha há cerca de um ano e nunca falha. As especificações são muito mais legíveis que os testes de unidade no Go e não dependem do conhecimento da estrutura interna do serviço. Se, por algum motivo, precisarmos reescrever o serviço em outro idioma, não precisaremos alterar as especificações, exceto o wrapper, que só precisa iniciar o serviço de teste com outro comando.