Estou investigando técnicas e estratégias para dimensionar nosso crescente número de testes de integração em nosso produto atual, para que eles possam (humanamente) permanecer parte de nosso desenvolvimento e processo de IC.
Em mais de 200 testes de integração, já estamos atingindo a marca de 1 hora para concluir uma execução de teste completa (em uma máquina de desenvolvimento de desktop), e isso está afetando negativamente a capacidade de um desenvolvedor de tolerar a execução de todo o conjunto como parte dos processos rotineiros de envio. O que está afetando a motivação para ser disciplinado sobre como criá-los bem. Testamos a integração apenas dos principais cenários frente a frente e usamos um ambiente que reflete a produção, construído do zero a cada execução de teste.
Por causa do tempo necessário para executar, está gerando um loop de feedback terrível e muitos ciclos desperdiçados aguardando que as máquinas concluam as execuções de teste, não importa o quão focadas sejam as execuções de teste. Não importa o impacto negativo mais caro no fluxo e progresso, sanidade e sustentabilidade.
Esperamos ter 10 vezes mais testes de integração antes que esse produto comece a desacelerar (não faço ideia, mas não parece que ainda estamos começando em termos de recursos). Temos que esperar razoavelmente estar em algumas centenas ou milhares de testes de integração, eu acho que em algum momento.
Para ser claro, tente impedir que isso se torne uma discussão sobre teste de unidade versus teste de integração (que nunca deve ser negociado). Estamos fazendo os dois testes de unidade com TDD E testes de integração neste produto. De fato, fazemos testes de integração nas várias camadas da arquitetura de serviços que possuímos, onde faz sentido para nós, pois precisamos verificar onde introduzimos alterações de última hora ao alterar os padrões em nossa arquitetura para as outras áreas do sistema.
Um pouco sobre a nossa pilha de tecnologia. Atualmente, estamos testando em um ambiente de emulação (CPU e memória intensiva) para executar nossos testes de ponta a ponta. Que é composto pelos serviços Web REST do Azure que enfrentam um back-end noSql (ATS). Estamos simulando nosso ambiente de produção executando no Emulador da área de trabalho do Azure + IISExpress. Estamos limitados a um emulador e um repositório de back-end local por máquina dev.
Também temos um IC baseado na nuvem, que executa o mesmo teste no mesmo ambiente emulado, e as execuções de teste demoram o dobro do tempo (2 horas +) na nuvem com nosso provedor de IC atual. Atingimos os limites do SLA dos provedores de IC na nuvem em termos de desempenho de hardware e excedemos sua permissão no tempo de execução do teste. Para ser justo com eles, suas especificações não são ruins, mas são tão boas quanto uma máquina de desktop suja interna.
Estamos usando uma estratégia de teste para reconstruir nosso armazenamento de dados para cada grupo lógico de testes e pré-carregar com dados de teste. Embora assegure de maneira abrangente a integridade dos dados, isso aumenta de 5 a 15% o impacto em cada teste. Portanto, achamos que há pouco a ganhar com a otimização dessa estratégia de teste neste momento no desenvolvimento do produto.
O longo e o curto é que: embora possamos otimizar o rendimento de cada teste (mesmo que entre 30% e 50% cada), ainda assim não escalaremos efetivamente no futuro próximo com várias centenas de testes. Hoje, mesmo que ainda exceda em muito o que é humanamente tolerável, precisamos de uma ordem de magnitude e magnitude no processo geral para torná-lo sustentável.
Portanto, estou investigando quais técnicas e estratégias podemos empregar para reduzir drasticamente o tempo de teste.
- Escrever menos testes não é uma opção. Vamos, por favor, não debater esse tópico neste tópico.
- Usar hardware mais rápido é definitivamente uma opção, embora seja muito caro.
- A execução de grupos de testes / cenários em hardware separado em paralelo também é definitivamente uma opção preferida.
- Criar um agrupamento de testes em torno de recursos e cenários em desenvolvimento é plausível, mas, em última análise, não é confiável para provar a cobertura total ou a confiança de que o sistema não é afetado por uma alteração.
- A execução em um ambiente de armazenamento temporário em escala na nuvem, em vez de no emulador de desktop, é tecnicamente possível, apesar de começarmos a adicionar tempos de implantação para as execuções de teste (~ 20 minutos cada no início da execução de teste para implantar o material).
- Dividir os componentes do sistema em partes lógicas independentes é plausível até certo ponto, mas esperamos uma quilometragem limitada, pois é esperado que as interações entre os componentes aumentem com o tempo. (ou seja, é provável que uma mudança afete outras pessoas de maneiras inesperadas - como acontece frequentemente quando um sistema é desenvolvido de forma incremental)
Eu queria ver quais estratégias (e ferramentas) outras pessoas estão usando neste espaço.
(Preciso acreditar que outras pessoas possam estar vendo esse tipo de dificuldade ao usar certos conjuntos de tecnologias.))
[Atualização: 16/12/2016: Acabamos investindo mais em testes paralelos de IC, para uma discussão sobre o resultado: http://www.mindkin.co.nz/blog/2015/12/16/16-jobs]
fonte
Respostas:
Trabalhei em um local que levou 5 horas (em 30 máquinas) para executar testes de integração. Refatorei a base de código e fiz testes de unidade para o novo material. Os testes de unidade levaram 30 segundos (em 1 máquina). Ah, e os erros também caíram. E tempo de desenvolvimento, já que sabíamos exatamente o que houve com os testes granulares.
Para encurtar a história, você não. Os testes completos de integração crescem exponencialmente à medida que a sua base de código cresce (mais código significa mais testes e mais código significa que todos os testes demoram mais para serem executados, pois há mais "integração" para trabalhar). Eu argumentaria que qualquer coisa na faixa de "horas" perde a maioria dos benefícios da integração contínua, já que o ciclo de feedback não existe. Mesmo uma melhoria de ordem de magnitude não é suficiente para melhorar seu desempenho - e não está nem perto de torná-lo escalável.
Portanto, eu recomendaria reduzir os testes de integração aos testes de fumaça mais amplos e vitais. Eles podem ser executados todas as noites ou com intervalo menos que contínuo, reduzindo grande parte de sua necessidade de desempenho. Testes de unidade, que crescem linearmente à medida que você adiciona mais código (os testes aumentam, o tempo de execução por teste não) é o caminho a seguir para a escala.
fonte
Os testes de integração sempre serão demorados, pois devem imitar um usuário real. Por esse motivo, você não deve executá-los todos de forma síncrona!
Como você já está executando coisas na nuvem, parece-me que você está em uma posição privilegiada para dimensionar seus testes em várias máquinas.
No caso extremo, crie um novo ambiente por teste e execute-os todos ao mesmo tempo. Seus testes de integração levarão apenas o maior tempo de execução.
fonte
Reduzir / otimizar os testes parece a melhor idéia para mim, mas, caso não seja uma opção, tenho uma alternativa a propor (mas requer a construção de algumas ferramentas proprietárias simples).
Eu enfrentei um problema semelhante, mas não em nossos testes de integração (aqueles executados em minutos). Em vez disso, estava simplesmente em nossas compilações: a base de código C em larga escala levaria horas para ser compilada.
O que eu vi como extremamente desperdício foi o fato de que estávamos reconstruindo a toda coisa a partir do zero (cerca de 20.000 arquivos de origem / unidades de compilação), mesmo que apenas alguns arquivos de origem mudou, e passar horas assim para uma mudança que só deve demorar alguns segundos ou minutos na pior das hipóteses.
Por isso, tentamos links incrementais em nossos servidores de construção, mas isso não era confiável. Às vezes, gerava falsos negativos e falhava em construir alguns commits, apenas para ter êxito em uma reconstrução completa. Pior ainda, às vezes daria falsos positivos e relataria um sucesso de compilação, apenas para o desenvolvedor mesclar uma compilação quebrada no ramo principal. Então, voltamos a reconstruir tudo toda vez que um desenvolvedor fazia alterações em sua filial particular.
Eu odiava tanto isso. Eu entrava nas salas de conferência com metade dos desenvolvedores jogando videogame e simplesmente porque havia pouco mais a fazer enquanto aguardava as compilações. Tentei obter uma vantagem na produtividade multitarefa e iniciei uma nova ramificação depois de confirmar, para poder trabalhar no código enquanto aguarda as compilações, mas quando um teste ou compilação falhava, ficava muito doloroso enfileirar as mudanças além desse ponto e tente consertar tudo e costurar tudo de volta.
Projeto paralelo enquanto aguarda, integre mais tarde
Então, o que fiz foi criar uma estrutura esquelética do aplicativo - o mesmo tipo de interface do usuário básica e partes relevantes do SDK para que eu desenvolvesse um projeto separado como um todo. Então, eu escreveria um código independente contra isso enquanto esperava por compilações, fora do projeto principal. Isso pelo menos me deu algumas codificações para que eu pudesse permanecer um pouco produtivo e, então, começaria a integrar esse trabalho feito completamente fora do produto no projeto posteriormente - trechos de código do lado posterior. Essa é uma estratégia para seus desenvolvedores se eles se encontrarem esperando muito.
Analisando arquivos de origem manualmente para descobrir o que reconstruir / executar novamente
No entanto, eu odiava como estávamos perdendo tanto tempo para reconstruir tudo o tempo todo. Por isso, durante alguns fins de semana, decidi escrever um código que realmente varreria os arquivos em busca de alterações e reconstruir apenas os projetos relevantes - ainda uma reconstrução completa, sem vínculos incrementais, mas apenas os projetos que precisam ser reconstruídos ( cujos arquivos dependentes, analisados recursivamente, foram alterados). Isso foi totalmente confiável e, após demonstrar e testá-lo exaustivamente, pudemos usar essa solução. Isso reduziu o tempo médio de construção de horas para alguns minutos, já que estávamos reconstruindo apenas os projetos necessários (embora as alterações no SDK central ainda levem uma hora, mas fizemos isso com muito menos frequência do que as alterações localizadas).
A mesma estratégia deve ser aplicável aos testes de integração. Apenas analise recursivamente os arquivos de origem para descobrir em quais arquivos os testes de integração dependem (ex:
import
em Java,#include
em C ou C ++) no lado do servidor e os arquivos incluídos / importados desses arquivos e assim por diante, criando um gráfico completo de arquivos de dependência de inclusão / importação para o sistema. Ao contrário da análise de compilação que forma um DAG, o gráfico deve ser não direcionado, pois está interessado em qualquer arquivo alterado que contenha código que possa ser executado indiretamente *. Execute novamente o teste de integração apenas se algum desses arquivos no gráfico para o teste de integração de interesse tiver sido alterado. Mesmo para milhões de linhas de código, era fácil fazer isso analisando em menos de um minuto. Se você tiver outros arquivos além do código-fonte que possam afetar um teste de integração, como arquivos de conteúdo, talvez seja possível gravar metadados em um comentário no código-fonte indicando essas dependências nos testes de integração, para que esses arquivos externos sejam alterados, os testes também ser executado novamente.* Por exemplo, se test.c incluir foo.h, que também é incluído por foo.c, uma alteração em test.c, foo.h ou foo.c deve marcar o teste integrado como necessitando de uma nova execução.
Isso pode levar um dia ou dois para programar e testar, especialmente no ambiente formal, mas acho que deve funcionar até para testes de integração e vale a pena se você não tiver outra escolha a não ser esperar no intervalo de horas para compilações para terminar (devido ao processo de construção ou teste ou embalagem ou o que for). Isso pode se traduzir em tantas horas de trabalho perdidas em apenas alguns meses que diminuiriam o tempo necessário para criar esse tipo de solução proprietária, além de matar a energia da equipe e aumentar o estresse causado por conflitos em fusões maiores, feitas menos frequentemente como resultado de todo o tempo perdido em espera. É ruim para a equipe como um todo quando eles passam grande parte do tempo esperando as coisas.tudo a ser reconstruído / reexecutado / reembalado a cada pequena alteração.
fonte
Parece que você tem muitos testes de integração. Lembre-se da pirâmide de teste . Os testes de integração pertencem ao meio.
Como um exemplo, ter um repositório com método
set(key,object)
,get(key)
. Este repositório é usado extensivamente em toda a sua base de código. Todos os métodos que dependem deste repositório serão testados com um repositório falso. Agora você só precisa de dois testes de integração, um para o conjunto e outro para o get.Alguns desses testes de integração provavelmente poderiam ser convertidos em testes de unidade. Por exemplo, testes de ponta a ponta, na minha opinião, devem apenas testar se o site está configurado corretamente com a seqüência de conexão correta e os domínios corretos.
Os testes de integração devem testar se o ORM, os repositórios e as abstrações da fila estão corretos. Como regra geral, nenhum código de domínio é necessário para testes de integração - apenas abstrações.
Quase todo o resto pode ser testado em unidade com implementações stubbed / mocked / faked / in-mem para dependências.
fonte
Na minha experiência em um ambiente Agile ou DevOps, onde os pipelines de entrega contínua são comuns, os testes de integração devem ser realizados à medida que cada módulo é concluído ou ajustado. Por exemplo, em muitos ambientes de pipeline de entrega contínua, não é incomum ter várias implantações de código por desenvolvedor por dia. A execução de um conjunto rápido de testes de integração no final de cada fase de desenvolvimento antes da implantação deve ser uma prática padrão nesse tipo de ambiente. Para informações adicionais, um ótimo e-book para incluir em sua leitura sobre esse assunto é um Guia Prático de Testes no DevOps , escrito por Katrina Clokie.
Para testar eficientemente dessa maneira, o novo componente deve ser testado em relação aos módulos concluídos existentes em um ambiente de teste dedicado ou em stubs e drivers. Dependendo das suas necessidades, geralmente é uma boa ideia manter uma biblioteca de Stubs e Drivers para cada módulo de aplicativo em uma pasta ou biblioteca para permitir o uso rápido e repetitivo dos testes de integração. Manter stubs e drivers organizados dessa maneira facilita a execução de alterações iterativas, mantendo-os atualizados e com o desempenho ideal para atender às suas necessidades de teste em andamento.
Outra opção a considerar é uma solução originalmente desenvolvida por volta de 2002, chamada Service Virtualization. Isso cria um ambiente virtual, simulando a interação do módulo com os recursos existentes para fins de teste em um DevOps corporativo complexo ou no ambiente Agile.
Este artigo pode ser útil para entender mais sobre como fazer testes de integração na empresa
fonte
Você mediu cada teste para ver para onde está sendo levado o tempo? E então, medimos o desempenho da base de código se houver um pouco mais lento. O problema geral é um dos testes ou a implantação, ou ambos?
Normalmente, você deseja reduzir o impacto do teste de integração para que sua execução em alterações relativamente pequenas seja minimizada. Em seguida, você pode deixar o teste completo para uma execução de 'controle de qualidade' que você executa quando a ramificação é promovida para o próximo nível. Portanto, você tem testes de unidade para ramificações de desenvolvimento, executa testes de integração reduzidos quando mesclados e executa um teste de integração completo quando mesclado a uma ramificação candidata à liberação.
Portanto, isso significa que você não precisa reconstruir, reorganizar e reimplementar tudo a cada confirmação. Você pode organizar sua configuração, no ambiente de desenvolvimento, para executar uma implantação o mais barata possível, confiando que tudo ficará bem. Em vez de girar uma VM inteira e implantar o produto inteiro, deixe a VM com a versão antiga no local e copie os novos binários, por exemplo (YMMV, dependendo do que você deve fazer).
Essa abordagem otimista geral ainda requer o teste completo, mas isso pode ser realizado posteriormente, quando o tempo gasto for menos urgente. (por exemplo, você pode executar o teste completo uma vez durante a noite, se houver algum problema, o desenvolvedor poderá resolvê-lo pela manhã). Isso também tem a vantagem de atualizar o produto na plataforma de integração para os testes do dia seguinte - pode ficar desatualizado conforme os desenvolvedores mudam as coisas, mas apenas em 1 dia.
Tivemos um problema semelhante ao executar uma ferramenta de análise estática baseada em segurança. As execuções completas levariam séculos, então passamos da execução do desenvolvedor para uma confirmação de integração (ou seja, tínhamos um sistema em que o desenvolvedor disse que estava concluído, ele foi mesclado a uma ramificação 'nível 2' onde mais testes foram realizados, incluindo perf Quando isso foi concluído, ele foi mesclado a uma ramificação de controle de qualidade para implantação. A idéia é remover as execuções regulares que ocorreriam continuamente nas execuções noturnas - os desenvolvedores obteriam os resultados pela manhã e não afetariam seu desenvolvimento foco até mais tarde em seu ciclo de desenvolvimento).
fonte
Em algum momento, um conjunto completo de testes de integração pode levar muitas horas para ser concluído, mesmo em hardware caro. Uma das opções é não executar a maioria desses testes em todas as confirmações e, em vez disso, executá-las todas as noites ou em um modo de lote contínuo (uma vez por várias confirmações).
Isso, no entanto, cria um novo problema - os desenvolvedores não recebem feedback imediato e as construções quebradas podem passar despercebidas. Para corrigir isso, é importante que eles saibam que algo está quebrado o tempo todo. Construir ferramentas de notificação como o Catlight ou o notificador de bandeja do TeamCity pode ser bastante útil.
Mas haverá ainda outro problema. Mesmo quando o desenvolvedor vê que a compilação está com problemas, ele pode não se apressar para verificá-la. Afinal, alguém já pode estar verificando, certo?
Por esse motivo, essas duas ferramentas têm um recurso "investigação de construção". Ele informará se alguém da equipe de desenvolvimento está realmente verificando e corrigindo a compilação quebrada. Os desenvolvedores podem se voluntariar para verificar a compilação e, até que isso aconteça, todos na equipe ficarão irritados com um ícone vermelho perto do relógio.
fonte
Parece que a sua base de código está crescendo e alguns gerenciamentos de código ajudarão. Nós usamos Java, então peço desculpas antecipadamente se eu assumir isso.
A loja Java em que trabalho usa essa abordagem, e raramente nos atrasamos aguardando a execução dos testes de integração.
fonte
Outra abordagem possível a ser mantida nos testes de integração de pipeline de IC (ou qualquer tipo de verificação, incluindo compilações) com longos tempos de execução ou exigindo recursos limitados e / ou caros é mudar dos sistemas tradicionais de CI com base em verificações pós-confirmação (que são suscetível a congestionamento ) a um com base em verificações pré-confirmação .
Em vez de confirmar diretamente suas alterações nas filiais, os desenvolvedores as submetem a um sistema de verificação automatizado centralizado que executa as verificações e:
Essa abordagem permite combinar e testar várias alterações enviadas, aumentando potencialmente a velocidade efetiva de verificação de IC muitas vezes.
Um exemplo é o sistema de bloqueio baseado em Gerrit / Zuul usado pelo OpenStack .
Outro é o ApartCI ( exoneração de responsabilidade - sou seu criador e o fundador da empresa que o oferece).
fonte