Olá Habr! Meu nome é Egor Danilenko. Estou desenvolvendo uma plataforma digital para o Internet banking corporativo do Sberbank Business Online e hoje quero falar sobre o procedimento de desenvolvimento de IC adotado por nós.
Como as alterações do desenvolvedor chegam a uma infusão no ramo de lançamento? O desenvolvedor faz alterações localmente e entra no nosso sistema de controle de versão. Usamos o Bitbucket com o plug-in de um autor (escrevemos sobre esse plug-in anteriormente
aqui ). Nessas mudanças, a montagem é iniciada e os testes são perseguidos (unidade, integração, funcional). Se a montagem não falhar e todos os testes forem bem-sucedidos, bem como após uma revisão bem-sucedida, a solicitação de extração será derramada na ramificação principal.
Mas com o tempo, o número de equipes aumentou. O número de testes aumentou proporcionalmente. Entendemos que um número tão grande de equipes aceleraria o aparecimento do problema de "solicitação lenta de solicitação de solicitação" e seria impossível desenvolver um produto. Atualmente, temos cerca de 40 equipes. Juntamente com os novos recursos, eles trazem novos testes, que também precisam ser executados em solicitações pull.
Nós pensamos que seria legal se soubéssemos quais testes executar para alterar uma parte específica do código.
E foi assim que resolvemos esse problema.
Declaração do problema
Existe um projeto com testes, e queremos determinar quais testes precisam ser executados quando um determinado arquivo é "tocado".
Todos sabemos sobre a biblioteca de cobertura de código EclEmma JaCoCo. Tomamos como base.
Um pouco sobre a JaCoCo
JaCoCo é uma biblioteca para medir a cobertura de código com testes. O trabalho é baseado na análise de bytes de código. O agente coleta informações de execução e as carrega na solicitação ou desligamento da JVM.
Existem três modos de coleta de dados:
- Sistema de arquivos: depois de parar a JVM, os dados serão gravados em um arquivo.
- Servidor de soquete TCP: Você pode conectar ferramentas externas à JVM e receber dados através do soquete.
- Cliente de soquete TCP: quando iniciado, o agente JaCoCo se conecta a um terminal TCP específico.
Nós escolhemos a segunda opção.
Solução
É necessário perceber a capacidade de executar aplicativos e os próprios testes com o agente JaCoCo.
Antes de mais nada, adicionamos ao gradle a capacidade de executar testes com o agente JaCoCo.
O agente Java pode ser iniciado:
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
Adicione uma dependência ao nosso projeto:
dependencies { compile 'org.jacoco:org.jacoco.agent:0.8.0' }
Precisamos apenas começar com o agente para coletar estatísticas, portanto, adicionamos o sinalizador withJacoco com valor padrão false a gradle.properties. Também especificamos o diretório em que estatísticas, endereço e porta serão coletados.
Inclua o argumento jvm com o agente na tarefa de inicialização do teste:
if (withJacoco.toBoolean()) { … jvmArgs "-javaagent:${tempPath}=${jacocoArgs.join(',')}".toString() }
Agora, após cada conclusão bem-sucedida do teste, precisamos coletar estatísticas com a JaCoCo. Para fazer isso, escreva o ouvinte TestNG.
public class JacocoCoverageTestNGListener implements ITestListener { private static final IntegrationTestsCoverageReporter reporter = new IntegrationTestsCoverageReporter(); private static final String TEST_NAME_PATTERN = "%s.%s"; @Override public void onTestStart(ITestResult result) { reporter.resetCoverageDumpers(String.format(TEST_NAME_PATTERN, result.getInstanceName(), result.getMethod().getMethodName())); } @Override public void onTestSuccess(ITestResult result) { reporter.report(String.format(TEST_NAME_PATTERN, result.getInstanceName(), result.getMethod().getMethodName())); } }
Adicione um ouvinte ao testng.xml e comente-o, pois não precisamos dele em uma execução de teste normal.
Agora temos a oportunidade de executar testes com o agente JaCoCo, com cada estatística de teste bem-sucedida sendo coletada.
Um pouco mais sobre como o repórter é implementado para coletar estatísticas.
Durante a inicialização do repórter, é feita uma conexão com os agentes, um diretório é criado onde as estatísticas serão armazenadas e as estatísticas serão coletadas.
Adicione o método de relatório:
public void report(String test) { reportClassFiles(test); reportResources(test); }
O método reportClassFile cria a pasta jvm no diretório de estatísticas, na qual as estatísticas coletadas pelos arquivos de classe são armazenadas.
O método reportResources cria a pasta resources, que armazena as estatísticas coletadas nos recursos (para todos os arquivos que não são de classe).
O relatório contém toda a lógica para conectar-se a um agente, ler dados de um soquete e gravar em um arquivo. Implementado por ferramentas fornecidas pela JaCoCo, como org.jacoco.core.runtime.RemoteControlReader / RemoteControlWriter.
As funções reportClassFiles e reportResources usam a função dumpToFile genérica.
public void dumpToFile(File file) { try (Writer fileWriter = new BufferedWriter(new FileWriter(file))) { for (RemoteControlReader remoteControlReader : remoteControlReaders) { remoteControleReader.setExecutionDataVisitor(new IExecutionDataVisitor() { @Override public void visitClassExecution(ExecutionData data) { if (data.hasHits()) { String name = data.getName(); try { fileWriter.write(name); fileWriter.write('\n'); } catch (IOException e) { throw new RuntimeException(e); } } } }); } } }
O resultado da função será um arquivo com um conjunto de classes / recursos que esse teste afeta.
E assim, depois de executar todos os testes, temos um diretório com estatísticas sobre arquivos e recursos de classe.
Resta escrever um pipeline para a coleta diária de estatísticas e adicionar ao lançamento do pipeline as verificações de solicitação de recebimento.
Não estamos interessados nos estágios de montagem do projeto, mas consideraremos o estágio para a publicação de estatísticas com mais detalhes.
stage('Agregate and parse result') { def inverterInJenkins = downloadMavenDependency( url: NEXUS_RELEASE_REPOSITORY, group: '', name: 'coverage-inverter', version: '0', type: 'jar', mavenHome: wsp ) dir('coverage-mapping') { gitFullCheckoutRef '', '', 'coverage-mapping', "refs/heads/${params.targetBranch}-integration-tests" sh 'rm -rf *' } sh "ls -lRa ..//out/coverage/" def inverter = wsp + inverterInJenkins.substring(wsp.length()) sh "java -jar ${inverter} " + "-d ..//out/coverage/jvm " + "-o coverage-mapping//jvm " + "-i coverage-config/jvm-include " + "-e coverage-config/jvm-exclude" sh "java -jar ${inverter} " + "-d ..//out/coverage/resources " + "-o coverage-mapping//resources " + "-i coverage-config/resources-include " + "-e coverage-config/resources-exclude" gitPush '', '', 'coverage-mapping', "${params.targetBranch}-integration-tests" }
No mapeamento de cobertura, precisamos armazenar o nome do arquivo e dentro dele uma lista de testes que precisam ser executados. Como o resultado da coleta de estatísticas é o nome do teste que armazena o conjunto de classes e recursos, precisamos inverter tudo e excluir dados desnecessários (classes de bibliotecas de terceiros).
Invertemos nossas estatísticas e enviamos para o nosso repositório.
As estatísticas são coletadas todas as noites. Ele é armazenado em um repositório separado para cada ramificação de liberação.
Bingo!
Agora, ao executar os testes, precisamos encontrar o arquivo modificado e determinar os testes que precisam ser executados.
Os problemas que encontramos:
- Como o JaCoCo funciona apenas com bytecode, é impossível coletar estatísticas em arquivos como .xml, .gradle, .sql da caixa. Portanto, tivemos que "fixar" nossas decisões.
- Monitoramento constante da relevância das estatísticas e da frequência da montagem, se a montagem noturna falhar por algum motivo, as estatísticas de ontem serão usadas para verificação nas solicitações de recebimento.