integração contínua para software científico

22

Eu não sou engenheiro de software. Sou estudante de doutorado no campo da geociência.

Há quase dois anos, comecei a programar um software científico. Eu nunca usei a integração contínua (IC), principalmente porque no começo eu não sabia que ela existia e eu era a única pessoa trabalhando nesse software.

Agora que a base do software está sendo executada, outras pessoas começam a se interessar e querem contribuir com o software. O plano é que outras pessoas de outras universidades estejam implementando adições ao software principal. (Eu tenho medo que eles possam introduzir bugs). Além disso, o software ficou bastante complexo e tornou-se cada vez mais difícil de testar e também pretendo continuar trabalhando nele.

Por esses dois motivos, agora estou pensando cada vez mais sobre o uso do IC. Como nunca tive formação em engenharia de software e ninguém ao meu redor jamais ouviu falar sobre CI (somos cientistas, não programadores), acho difícil começar o meu projeto.

Tenho algumas perguntas em que gostaria de obter alguns conselhos:

Primeiro de tudo, uma breve explicação de como o software funciona:

  • O software é controlado por um arquivo .xml contendo todas as configurações necessárias. Você inicia o software simplesmente passando o caminho para o arquivo .xml como um argumento de entrada e ele é executado e cria alguns arquivos com os resultados. Uma única execução pode levar ~ 30 segundos.

  • É um software científico. Quase todas as funções têm vários parâmetros de entrada, cujos tipos são principalmente classes bastante complexas. Eu tenho vários arquivos .txt com grandes catálogos que são usados ​​para criar instâncias dessas classes.

Agora vamos às minhas perguntas:

  1. testes de unidade, testes de integração, testes de ponta a ponta? : Meu software agora possui cerca de 30.000 linhas de código com centenas de funções e ~ 80 classes. Parece meio estranho para mim começar a escrever testes de unidade para centenas de funções que já estão implementadas. Então, pensei em simplesmente criar alguns casos de teste. Prepare 10 a 20 arquivos .xml diferentes e deixe o software funcionar. Eu acho que isso é o que é chamado de testes de ponta a ponta? Costumo ler que você não deve fazer isso, mas talvez esteja tudo bem se você já tem um software funcionando? Ou é simplesmente uma idéia idiota tentar adicionar CI a um software já em funcionamento.

  2. Como você escreve testes de unidade se os parâmetros de função são difíceis de criar? suponha que eu tenho uma função double fun(vector<Class_A> a, vector<Class_B>)e, normalmente, eu precisaria primeiro ler vários arquivos de texto para criar objetos do tipo Class_Ae Class_B. Pensei em criar algumas funções fictícias como Class_A create_dummy_object()sem ler nos arquivos de texto. Também pensei em implementar algum tipo de serialização . (Não pretendo testar a criação dos objetos de classe, pois eles dependem apenas de vários arquivos de texto)

  3. Como escrever testes se os resultados são altamente variáveis? Meu software utiliza grandes simulações de monte-carlo e funciona iterativamente. Normalmente, você tem ~ 1000 iterações e a cada iteração, você está criando ~ 500 a 20.000 instâncias de objetos com base em simulações de monte-carlo. Se apenas um resultado de uma iteração for um pouco diferente, as iterações futuras serão completamente diferentes. Como você lida com essa situação? Eu acho que isso é um grande ponto contra testes de ponta a ponta, já que o resultado final é altamente variável?

Qualquer outro conselho com a CI é muito apreciado.

user7431005
fonte
1
Como você sabe que seu software está funcionando corretamente? Você consegue encontrar uma maneira de automatizar essa verificação para executá-la em todas as alterações? Esse deve ser o seu primeiro passo ao apresentar o CI a um projeto existente.
Bart van Ingen Schenau 14/10
Como você garantiu que seu software produz resultados aceitáveis ​​em primeiro lugar? O que faz você ter certeza de que realmente "funciona"? As respostas para as duas perguntas fornecerão bastante material para testar seu software agora e no futuro.
Polygnome

Respostas:

23

Testar software científico é difícil, tanto por causa do assunto complexo quanto por causa dos processos típicos de desenvolvimento científico (também conhecido como hackear até que funcione, o que geralmente não resulta em um design testável). Isso é um pouco irônico, considerando que a ciência deve ser reproduzível. O que muda em comparação com o software "normal" não é se os testes são úteis (sim!), Mas quais tipos de teste são apropriados.

Manipulação aleatória: todas as execuções do seu software DEVEM ser reproduzíveis. Se você usar técnicas de Monte Carlo, deverá possibilitar o fornecimento de uma semente específica para o gerador de números aleatórios.

  • É fácil esquecer isso, por exemplo, ao usar a rand()função C, que depende do estado global.
  • Idealmente, um gerador de números aleatórios é passado como um objeto explícito através de suas funções. O randomcabeçalho da biblioteca padrão do C ++ 11 facilita muito isso.
  • Em vez de compartilhar o estado aleatório entre os módulos do software, achei útil criar um segundo RNG que é propagado por um número aleatório do primeiro RNG. Então, se o número de solicitações para o RNG pelo outro módulo for alterado, a sequência gerada pelo primeiro RNG permanecerá a mesma.

Os testes de integração estão perfeitamente bem. Eles são bons para verificar se diferentes partes do seu software funcionam juntas corretamente e para executar cenários concretos.

  • Como um nível mínimo de qualidade "não falha", já pode ser um bom resultado de teste.
  • Para resultados mais fortes, você também precisará verificar os resultados em relação a alguma linha de base. No entanto, essas verificações deverão ser um pouco tolerantes, por exemplo, levar em consideração erros de arredondamento. Também pode ser útil comparar estatísticas de resumo em vez de linhas de dados completas.
  • Se a verificação em uma linha de base for muito frágil, verifique se as saídas são válidas e atendem a algumas propriedades gerais. Eles podem ser gerais (“os locais selecionados devem ter pelo menos 2 km de distância”) ou específicos do cenário, por exemplo, “um local selecionado deve estar dentro desta área”.

Ao executar testes de integração, é uma boa ideia escrever um executor de teste como um programa ou script separado. Esse executor de teste executa a configuração necessária, executa o executável a ser testado, verifica todos os resultados e depois limpa.

As verificações de estilo de teste de unidade podem ser bastante difíceis de inserir no software científico, porque o software não foi projetado para isso. Em particular, os testes de unidade ficam difíceis quando o sistema em teste tem muitas dependências / interações externas. Se o software não é puramente orientado a objetos, geralmente não é possível zombar / stub dessas dependências. Achei melhor evitar amplamente testes de unidade para esse software, exceto funções matemáticas puras e funções utilitárias.

Até alguns testes são melhores que nenhum teste. Combinado com a verificação "ele precisa compilar", esse já é um bom começo para a integração contínua. Você sempre pode voltar e adicionar mais testes posteriormente. Você pode priorizar áreas do código com maior probabilidade de quebrar, por exemplo, porque elas obtêm mais atividade de desenvolvimento. Para ver quais partes do seu código não são cobertas por testes de unidade, você pode usar as ferramentas de cobertura de código.

Teste manual: Especialmente para domínios com problemas complexos, você não poderá testar tudo automaticamente. Por exemplo, atualmente estou trabalhando em um problema de pesquisa estocástica. Se eu testar se meu software sempre produz o mesmo resultado, não posso melhorá-lo sem interromper os testes. Em vez disso, facilitei os testes manuais : executo o software com uma semente fixa e obtenho uma visualizaçãodo resultado (dependendo de suas preferências, R, Python / Pyplot e Matlab facilitam a visualização de alta qualidade de seus conjuntos de dados). Eu posso usar essa visualização para verificar se as coisas não deram muito errado. Da mesma forma, rastrear o progresso do seu software via saída de log pode ser uma técnica de teste manual viável, pelo menos se eu puder selecionar o tipo de evento a ser registrado.

amon
fonte
7

Parece meio estranho para mim começar a escrever testes de unidade para centenas de funções que já estão implementadas.

Você deseja (normalmente) escrever os testes à medida que altera as funções mencionadas. Você não precisa sentar e escrever centenas de testes de unidade para as funções existentes, o que seria (em grande parte) uma perda de tempo. O software (provavelmente) está funcionando como está. O objetivo desses testes é garantir que mudanças futuras não quebrem o comportamento antigo. Se você nunca alterar uma função específica novamente, provavelmente nunca valerá a pena testá-la (uma vez que está funcionando atualmente, sempre funcionou e provavelmente continuará funcionando). Eu recomendo a leitura Trabalhando efetivamente com o código herdadopor Michael Feathers nesta frente. Ele tem ótimas estratégias gerais para testar coisas que já existem, incluindo técnicas de quebra de dependência, testes de caracterização (saída da função copiar / colar no conjunto de testes para garantir a manutenção do comportamento de regressão) e muito mais.

Como você escreve testes de unidade se os parâmetros de função são difíceis de criar?

Idealmente, você não. Em vez disso, você facilita a criação dos parâmetros (e, portanto, facilita o teste do seu design). É certo que as mudanças no design levam tempo e essas refatorações podem ser difíceis em projetos herdados como o seu. TDD (Test Driven Development) pode ajudar com isso. Se os parâmetros forem super difíceis de criar, você terá muitos problemas para escrever testes no estilo primeiro teste.

No curto prazo, use zombarias, mas cuidado com o inferno zombador e os problemas que os acompanham a longo prazo. Entretanto, à medida que cresci como engenheiro de software, percebi que as zombarias quase sempre são um mini cheiro que estão tentando resolver um problema maior e não abordar o problema principal. Eu gosto de me referir a isso como "embalagem de bosta", porque se você colocar um pedaço de papel alumínio em um pouco de cocô de cachorro no tapete, ele ainda fede. O que você precisa fazer é levantar-se, recolher o cocô e jogá-lo no lixo, e depois retirá-lo. Obviamente, isso é mais trabalhoso, e você corre o risco de ter um pouco de matéria fecal em suas mãos, mas melhor para você e sua saúde a longo prazo. Se você continuar embrulhando esses cocô, não vai querer morar em sua casa por muito mais tempo. A zombaria é de natureza semelhante.

Por exemplo, se você tem o Class_Aque é difícil de instanciar porque precisa ler em 700 arquivos, pode simplesmente zombar. A próxima coisa que você sabe, seu simulada fica fora de data, eo verdadeiro Class_A faz algo totalmente diferente do que a simulação, e seus testes ainda estão passando mesmo que eles devem estar falhando. Uma solução melhor é dividir Class_Aem componentes mais fáceis de usar / testar e, em vez disso, testar esses componentes. Talvez escreva um teste de integração que realmente atinja o disco e verifique se Class_Afunciona como um todo. Ou talvez apenas tenha um construtor para o Class_Aqual você possa instanciar com uma string simples (representando seus dados) em vez de precisar ler do disco.

Como escrever testes se os resultados são altamente variáveis?

Algumas dicas:

1) Use inversos (ou mais geralmente, testes baseados em propriedades). Qual é o problema [1,2,3,4,5]? Nenhuma idéia. O que é ifft(fft([1,2,3,4,5]))? Deve ser [1,2,3,4,5](ou próximo a isso, podem ocorrer erros de ponto flutuante).

2) Use afirmações "conhecidas". Se você escrever uma função determinante, pode ser difícil dizer qual é o determinante de uma matriz 100x100. Mas você sabe que o determinante da matriz de identidade é 1, mesmo que seja 100x100. Você também sabe que a função deve retornar 0 em uma matriz não invertível (como um 100x100 cheio de todos os 0s).

3) Use afirmações grosseiras em vez de afirmações exatas . Há algum tempo, escrevi um código que registrava duas imagens, gerando pontos de amarração que criam um mapeamento entre as imagens e fazendo uma distorção entre elas para fazer com que correspondessem. Ele pode se registrar em um nível de sub-pixel. Como você pode testá-lo? Coisas como:

EXPECT_TRUE(reg(img1, img2).size() < min(img1.size(), img2.size()))

como você só pode se registrar em partes sobrepostas, a imagem registrada deve ser menor ou igual à menor imagem) e também:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

como uma imagem registrada deve estar PRÓXIMA, mas você pode experimentar um pouco mais do que erros de ponto flutuante devido ao algoritmo em questão, então verifique se cada pixel está com +/- 5% do intervalo válido (0-255 é um intervalo comum, escala de cinza). Deve ter pelo menos o mesmo tamanho. Você pode até mesmo testar o fumo (ou seja, chamá-lo e certificar-se de que não trava). Em geral, essa técnica é melhor para testes maiores, nos quais o resultado final não pode ser (facilmente) calculado a priori para a execução do teste.

4) Use OU ARMAZENUE uma semente de número aleatório para o seu RNG.

Runs não precisa ser reprodutível. É falso, no entanto, que a única maneira de obter uma execução reproduzível é fornecer uma semente específica a um gerador de números aleatórios. Às vezes, o teste aleatório é valioso . Eu vi um erro no código científico que surge em casos degenerados que foram gerados aleatoriamente . Em vez de sempre chamar sua função com a mesma semente, gere uma semente aleatória , use essa semente e registre o valor da semente. Dessa forma, cada execução tem uma semente aleatória diferente , mas se ocorrer uma falha, você poderá executar novamente o resultado usando a semente que você registrou para depurar. Na verdade, eu usei isso na prática e ele esmagou um bug, então pensei em mencioná-lo.Desvantagem: você precisa registrar suas execuções de teste. De cabeça para baixo: correção e correção de erros.

HTH.

Matt Messersmith
fonte
2
  1. Tipos de teste

    • Parece meio estranho para mim começar a escrever testes de unidade para centenas de funções que já estão implementadas

      Pense no contrário: se um patch que toca em várias funções interrompe um de seus testes de ponta a ponta, como você vai descobrir qual é o problema?

      É muito mais fácil escrever testes de unidade para funções individuais do que para todo o programa. É muito mais fácil garantir que você tenha uma boa cobertura de uma função individual. É muito mais fácil refatorar uma função quando você tem certeza de que os testes de unidade capturam os casos de canto que você quebrou.

      Escrever testes de unidade para funções já existentes é perfeitamente normal para quem trabalhou em uma base de código herdada. Eles são uma boa maneira de confirmar sua compreensão das funções e, uma vez escritos, são uma boa maneira de encontrar mudanças inesperadas de comportamento.

    • Os testes de ponta a ponta também valem a pena. Se eles são mais fáceis de escrever, faça-os primeiro e adicione testes de unidade ad-hoc para cobrir as funções com as quais você está mais preocupado em quebrar. Você não precisa fazer tudo de uma vez.

    • Sim, adicionar CI ao software existente é sensato e normal.

  2. Como escrever testes de unidade

    Se seus objetos são realmente caros e / ou complexos, escreva zombarias. Você pode simplesmente vincular os testes usando zombarias separadamente dos testes usando objetos reais, em vez de usar polimorfismo.

    De qualquer forma, você deve ter alguma maneira fácil de criar instâncias - uma função para criar instâncias fictícias é comum - mas ter testes para o processo de criação real também é sensato.

  3. Resultados variáveis

    Você deve ter alguns invariantes para o resultado. Teste esses, em vez de um único valor numérico.

    Você pode fornecer um gerador de números pseudoaleatórios simulados se o seu código monte carlo o aceitar como parâmetro, o que tornaria os resultados previsíveis pelo menos para um algoritmo conhecido, mas é frágil, a menos que literalmente retorne o mesmo número todas as vezes.

Sem utilidade
fonte
1
  1. Nunca é uma idéia idiota adicionar CI. Por experiência, sei que esse é o caminho a seguir quando você tem um projeto de código aberto no qual as pessoas são livres para contribuir. O IC permite que você impeça as pessoas de adicionar ou alterar código, se o código interromper seu programa, por isso é quase inestimável ter uma base de código em funcionamento.

    Ao considerar testes, você certamente pode fornecer alguns testes de ponta a ponta (acho que é uma subcategoria de testes de integração) para garantir que seu fluxo de código esteja funcionando da maneira que deveria. Você deve fornecer pelo menos alguns testes de unidade básicos para garantir que as funções produzam os valores corretos, pois parte dos testes de integração pode compensar outros erros cometidos durante o teste.

  2. A criação de objetos de teste é algo bastante difícil e trabalhoso, de fato. Você está certo ao querer criar objetos fictícios. Esses objetos devem ter alguns valores padrão, mas com maiúsculas e minúsculas, para os quais você certamente sabe qual deve ser a saída.

  3. O problema com os livros sobre esse assunto é que o cenário do IC (e outras partes dos devops) evolui tão rapidamente que qualquer coisa em um livro provavelmente ficará desatualizada alguns meses depois. Não conheço nenhum livro que possa ajudá-lo, mas o Google deve, como sempre, ser seu salvador.

  4. Você deve executar seus testes várias vezes e fazer análises estatísticas. Dessa forma, você pode implementar alguns casos de teste nos quais você mede a média / média de várias execuções e a compara à sua análise, para saber quais valores estão corretos.

Algumas dicas:

  • Use a integração de ferramentas de IC na sua plataforma GIT para impedir que código quebrado entre na sua base de código.
  • pare de mesclar o código antes que a revisão por pares fosse feita por outros desenvolvedores. Isso torna os erros mais facilmente conhecidos e novamente impede que o código quebrado entre na sua base de código.
Pelicano
fonte
1

Em uma resposta anterior, Amon já mencionou alguns pontos muito importantes. Deixe-me adicionar um pouco mais:

1. Diferenças entre o desenvolvimento de software científico e software comercial

Para software científico, o foco normalmente está no problema científico, é claro. Os problemas estão mais lidando com o embasamento teórico, encontrando o melhor método numérico etc. O software é apenas uma, mais ou menos, pequena parte do trabalho.

Na maioria dos casos, o software é escrito por uma ou apenas algumas pessoas. Geralmente é escrito para um projeto específico. Quando o projeto termina e tudo é publicado, em muitos casos, o software não é mais necessário.

O software comercial é normalmente desenvolvido por grandes equipes por um longo período de tempo. Isso requer muito planejamento para arquitetura, design, testes de unidade, testes de integração etc. Esse planejamento requer uma quantidade substancial de tempo e experiência. Em um ambiente científico, normalmente não há tempo para isso.

Se você deseja converter seu projeto em um software semelhante ao software comercial, verifique o seguinte:

  • Você tem tempo e recursos?
  • Qual é a perspectiva de longo prazo do software? O que acontecerá com o software quando você terminar seu trabalho e sair da universidade?

2. Testes de ponta a ponta

Se o software se tornar cada vez mais complexo e várias pessoas estiverem trabalhando nele, os testes serão obrigatórios. Mas como amon já mencionado, acrescentando testes de unidade para software científico é bastante difícil. Então você tem que usar uma abordagem diferente.

Como seu software obtém sua entrada de um arquivo, como a maioria dos softwares científicos, é perfeito para criar vários arquivos de entrada e saída de amostra. Você deve executar esses testes automaticamente em cada release e comparar os resultados com suas amostras. Este poderia ser um substituto muito bom para testes de unidade. Você também obtém testes de integração dessa maneira.

Claro que, para obter resultados reproduzíveis, você deve usar a mesma semente para o seu gerador de números aleatórios, como amon já escreveu.

Os exemplos devem cobrir os resultados típicos do seu software. Isso também deve incluir casos extremos do espaço de parâmetros e algoritmos numéricos.

Você deve tentar encontrar exemplos que não precisam de muito tempo para serem executados, mas ainda abrangem casos de teste típicos.

3. Integração contínua

Como a execução dos exemplos de teste pode levar algum tempo, acho que a integração contínua não é viável. Você provavelmente terá que discutir as partes adicionais com seus colegas. Por exemplo, eles precisam corresponder aos métodos numéricos usados.

Então eu acho que é melhor fazer a integração de uma maneira bem definida depois de discutir a fundamentação teórica e métodos numéricos, testes cuidadosos, etc.

Não acho que seja uma boa ideia ter algum tipo de automatismo para integração contínua.

A propósito, você está usando um sistema de controle de versão?

4. Testando seus algoritmos numéricos

Se você estiver comparando resultados numéricos, por exemplo, ao verificar suas saídas de teste, não deverá verificar a igualdade de números flutuantes. Sempre pode haver erros de arredondamento. Em vez disso, verifique se a diferença é menor que um limite específico.

Também é uma boa idéia verificar seus algoritmos em relação a diferentes algoritmos ou formular o problema científico de uma maneira diferente e comparar os resultados. Se você obtiver os mesmos resultados usando duas ou mais maneiras independentes, isso é uma boa indicação de que sua teoria e sua implementação estão corretas.

Você pode fazer esses testes no seu código de teste e usar o algoritmo mais rápido para o seu código de produção.

Bernie
fonte
0

Meu conselho seria escolher cuidadosamente como você gasta seus esforços. No meu campo (bioinformática), os algoritmos de última geração mudam tão rapidamente que gastar energia com a prova de erros do seu código pode ser melhor gasto no próprio algoritmo.

Dito isto, o que é valorizado é:

  • é o melhor método da época, em termos de algoritmo?
  • quão fácil é portar para diferentes plataformas de computação (diferentes ambientes HPC, tipos de SO, etc.)
  • robustez - ele roda em MEU conjunto de dados?

Seu instinto de criar uma base de código à prova de balas é nobre, mas vale lembrar que este não é um produto comercial. Torne-o o mais portátil possível, à prova de erros (para o seu tipo de usuário), conveniente para que outros contribuam, depois concentre-se no próprio algoritmo

baiacu
fonte