Estou fortemente convencido do valor de usar testes que verificam um programa completo (por exemplo, testes de convergência), incluindo um conjunto automatizado de testes de regressão . Depois de ler alguns livros de programação, tive a sensação incômoda de que "deveria" escrever testes de unidade (ou seja, testes que verificam a correção de uma única função e não significam executar todo o código para resolver um problema) também . No entanto, os testes de unidade nem sempre parecem se encaixar nos códigos científicos e acabam parecendo artificiais ou como uma perda de tempo.
Devemos escrever testes de unidade para códigos de pesquisa?
programming-paradigms
testing
David Ketcheson
fonte
fonte
Respostas:
Por muitos anos, fiquei com a má compreensão de que não tinha tempo suficiente para escrever testes de unidade para o meu código. Quando escrevi testes, eles estavam inchados, coisas pesadas que só me incentivaram a pensar que só deveria escrever testes de unidade quando soubesse que eram necessários.
Então comecei a usar o Test Driven Development e achei uma revelação completa. Agora estou firmemente convencido de que não tenho tempo para não escrever testes de unidade .
Na minha experiência, desenvolvendo com testes em mente, você acaba com interfaces mais limpas, classes e módulos mais focados e, geralmente, mais código SOLID testável.
Toda vez que trabalho com código legado que não possui testes de unidade e preciso testar manualmente algo, fico pensando "isso seria muito mais rápido se esse código já tivesse testes de unidade". Toda vez que tenho que tentar adicionar a funcionalidade de teste de unidade ao código com alto acoplamento, fico pensando "isso seria muito mais fácil se tivesse sido escrito de maneira desacoplada".
Comparando e contrastando as duas estações experimentais que eu apoio. Um já existe há algum tempo e possui uma grande quantidade de código legado, enquanto o outro é relativamente novo.
Ao adicionar funcionalidade ao laboratório antigo, geralmente é necessário ir ao laboratório e passar muitas horas trabalhando com as implicações da funcionalidade de que eles precisam e como posso adicionar essa funcionalidade sem afetar nenhuma outra funcionalidade. O código simplesmente não está configurado para permitir testes off-line, então praticamente tudo precisa ser desenvolvido on-line. Se eu tentasse me desenvolver off-line, acabaria com mais objetos simulados do que seria razoável.
No laboratório mais recente, geralmente posso adicionar funcionalidades desenvolvendo-as off-line na minha mesa, zombando apenas daquilo que é imediatamente necessário e depois passando um curto período de tempo no laboratório, resolvendo os problemas restantes que não foram detectados. -linha.
Para maior clareza, e desde @ naught101 perguntou ...
Como trabalho com software de controle experimental e aquisição de dados, com algumas análises ad hoc, a combinação de TDD com controle de revisão ajuda a documentar as alterações no hardware do experimento subjacente e as alterações nos requisitos de coleta de dados ao longo do tempo.
Mesmo na situação de desenvolvimento de código exploratório, no entanto, eu pude ver um benefício significativo ao codificar suposições, além da capacidade de ver como essas suposições evoluem ao longo do tempo.
fonte
Os códigos científicos tendem a ter constelações de funções interligadas com mais frequência do que os códigos comerciais em que trabalhei, geralmente devido à estrutura matemática do problema. Portanto, não acho que testes de unidade para funções individuais sejam muito eficazes. No entanto, acho que há uma classe de testes de unidade que são eficazes e ainda são bastante diferentes dos testes de programas inteiros, pois eles visam a funcionalidade específica.
Acabei de definir brevemente o que quero dizer com esse tipo de teste. O teste de regressão procura alterações no comportamento existente (validado de alguma forma) quando são feitas alterações no código. O teste de unidade executa um pedaço de código e verifica se ele fornece a saída desejada com base em uma especificação. Eles não são tão diferentes, pois o teste de regressão original era um teste de unidade, pois eu tinha que determinar que a saída era válida.
Dois outros exemplos de testes de unidade, vindos de PyLith , são localização de pontos, uma função simples e fácil de produzir resultados sintéticos e criação de células coesivas de volume zero em uma malha, que envolve várias funções, mas trata de uma parte circunscrita de funcionalidade no código.
Existem muitos testes desse tipo, incluindo testes de conservação e consistência. A operação não é tão diferente da regressão (você executa um teste e verifica a saída em relação a um padrão), mas a saída padrão vem de uma especificação e não de uma execução anterior.
fonte
Desde que li sobre Desenvolvimento Orientado a Testes no Code Complete, 2ª edição , usei uma estrutura de teste de unidadecomo parte da minha estratégia de desenvolvimento, e aumentou drasticamente minha produtividade, reduzindo a quantidade de tempo que gastei na depuração porque os vários testes que escrevo são diagnósticos. Como benefício colateral, estou muito mais confiante em meus resultados científicos e usei meus testes de unidade em várias ocasiões para defender meus resultados. Se houver um erro em um teste de unidade, geralmente consigo descobrir o motivo rapidamente. Se meu aplicativo falhar e todos os meus testes de unidade forem aprovados, faço uma análise de cobertura do código para ver quais partes do meu código não são exercidas, além de percorrer o código com um depurador para identificar a origem do erro. Depois, escrevo um novo teste para garantir que o bug permaneça corrigido.
Muitos dos testes que escrevo não são puros testes de unidade. Estritamente definidos, os testes de unidade devem exercer a funcionalidade de uma função. Quando posso testar facilmente uma única função usando dados simulados, faço isso. Outras vezes, não consigo zombar facilmente dos dados, preciso escrever um teste que exercite a funcionalidade de uma determinada função; portanto, testarei essa função junto com outras pessoas em um teste de integração. Testes de integraçãoteste o comportamento de várias funções ao mesmo tempo. Como Matt ressalta, os códigos científicos costumam ser uma constelação de funções interligadas, mas muitas vezes certas funções são chamadas em sequência e testes de unidade podem ser escritos para testar a saída em etapas intermediárias. Por exemplo, se meu código de produção chamar cinco funções em sequência, escreverei cinco testes. O primeiro teste chamará apenas a primeira função (portanto, é um teste de unidade). O segundo teste chamará a primeira e a segunda funções, o terceiro teste chamará as três primeiras funções, e assim por diante. Mesmo que eu pudesse escrever testes de unidade para todas as funções do meu código, escreveria testes de integração de qualquer maneira, porque erros podem surgir quando várias partes modulares de um programa são combinadas. Por fim, depois de escrever todos os testes de unidade e testes de integração, acho que preciso, Vou embrulhar meus estudos de caso em testes de unidade e usá-los para testes de regressão, porque quero que meus resultados sejam repetíveis. Se eles não são repetíveis e eu recebo resultados diferentes, quero saber o porquê. O fracasso de um teste de regressão pode não ser um problema real, mas me forçará a descobrir se os novos resultados são pelo menos tão confiáveis quanto os antigos.
Também vale a pena, juntamente com o teste de unidade, a análise estática de códigos, depuradores de memória e a compilação com sinalizadores de aviso do compilador para detectar erros simples e códigos não utilizados.
fonte
Na minha experiência, à medida que a complexidade dos códigos de pesquisa científica aumenta, é necessário ter uma abordagem muito modular na programação. Isso pode ser doloroso para códigos com uma base grande e antiga (
f77
alguém?), Mas é necessário avançar. À medida que um módulo é construído em torno de um aspecto específico do código (para aplicativos CFD, pense em Condições de Contorno ou Termodinâmica), o teste de unidade é muito valioso para validar a nova implementação e isolar problemas e outros desenvolvimentos de software.Esses testes de unidade devem estar um nível abaixo da verificação do código (posso recuperar a solução analítica da minha equação de onda?) E 2 níveis abaixo da validação do código (posso prever os valores RMS de pico corretos no meu fluxo de tubulação turbulento), simplesmente garantindo que a programação (os argumentos são passados corretamente, os ponteiros estão apontando para a coisa certa?) e a "matemática" (esta sub-rotina calcula o coeficiente de atrito. Se eu inserir um conjunto de números e calcular a solução manualmente, a rotina produzirá o mesmo resultado?) estão corretos. Basicamente, indo um nível acima do que os compiladores podem detectar, ou seja, erros básicos de sintaxe.
Definitivamente, eu o recomendaria por pelo menos alguns módulos cruciais em seu aplicativo. No entanto, é preciso perceber que é extremamente tedioso e demorado; portanto, a menos que você tenha mão-de-obra ilimitada, eu não o recomendaria por 100% de um código complexo.
fonte
O teste de unidade para códigos científicos é útil por várias razões.
Três em particular são:
Os testes de unidade ajudam outras pessoas a entender as restrições do seu código. Basicamente, os testes de unidade são uma forma de documentação.
Os testes de unidade verificam para garantir que uma única unidade de código esteja retornando resultados corretos e para garantir que o comportamento de um programa não seja alterado quando os detalhes forem modificados.
O uso de testes de unidade facilita a modularização dos códigos de pesquisa. Isso pode ser particularmente importante se você começar a tentar direcionar seu código para uma nova plataforma, por exemplo, estiver interessado em paralelá-lo ou executá-lo em uma máquina GPGPU.
Acima de tudo, os testes de unidade dão a você a confiança de que os resultados da pesquisa que você está produzindo usando seus códigos são válidos e verificáveis.
Observo que você mencionou o teste de regressão em sua pergunta. Em muitos casos, o teste de regressão é realizado através da execução regular e automatizada de testes de unidade e / ou testes de integração (que testam se partes do código funcionam corretamente quando combinadas; na computação científica, isso geralmente é feito comparando a saída com dados experimentais ou o resultados de programas anteriores confiáveis). Parece que você já está usando testes de integração ou teste de unidade no nível de grandes componentes complexos com êxito.
O que eu diria é que, à medida que os códigos de pesquisa se tornam cada vez mais complexos e dependem do código e das bibliotecas de outras pessoas, é importante entender onde ocorre o erro. O teste de unidade permite que o erro seja identificado com muito mais facilidade.
Você pode encontrar a descrição, evidências e referências na Seção 7, "Planejar erros", do artigo que eu co-autor sobre Boas Práticas de Computação Científica, é útil - ele também introduz o conceito complementar de programação defensiva.
fonte
Nas minhas aulas deal.II eu ensino que o software que não tem testes não funciona corretamente (e ir para o estresse que eu propositadamente disse " não não funcionar corretamente", não " pode não funcionar corretamente).
É claro que eu vivo de acordo com o mantra - que é como o deal.II passou a executar 2.500 testes com cada commit ;-)
Mais sério, acho que Matt já define bem as duas classes de testes. Nós escrevemos testes de unidade para o material de nível inferior e, naturalmente, progride para testes de regressão para o material de nível superior. Eu não acho que eu poderia traçar um limite claro que separaria nossos testes de um lado ou de outro, certamente há muitos que seguem a linha em que alguém olhou para a saída e achou que ela era bastante razoável (teste de unidade?) sem ter olhado para o último bit de precisão (teste de regressão?).
fonte
Sim e não. Certamente menos adequado para rotinas fundamentais do conjunto de ferramentas básico que você usa para facilitar sua vida, como rotinas de conversão, mapeamentos de strings, física básica e matemática, etc. você pode preferir testá-los como testes funcionais, em vez de como unidades. Além disso, unittest e estressar muito as classes e entidades cujo nível e uso vão mudar muito (por exemplo, para fins de otimização) ou cujos detalhes internos serão alterados por qualquer motivo. O exemplo mais típico é uma classe que envolve uma matriz enorme, mapeada a partir do disco.
fonte
Absolutamente!
O que não é suficiente para você?
Na programação científica, mais do que qualquer outro tipo, estamos desenvolvendo com base na tentativa de igualar um sistema físico. Como você saberá se fez isso além de testar? Antes mesmo de começar a codificar, decida como você usará seu código e resolva alguns exemplos de execuções. Tente pegar todos os casos de borda possíveis. Faça isso de maneira modular - por exemplo, para uma rede neural, você pode fazer um conjunto de testes para um único neurônio e um conjunto de testes para uma rede neural completa. Dessa forma, quando você começa a escrever o código, pode garantir que seu neurônio funcione antes de começar a trabalhar na rede. Trabalhar em estágios como esse significa que, quando você se deparar com um problema, você só tem o 'estágio' de código mais recente para testar, os estágios anteriores já foram testados.
Além disso, depois de fazer os testes, se você precisar reescrever o código em um idioma diferente (convertendo para CUDA, por exemplo), ou mesmo se estiver apenas atualizando, você já terá os casos de teste e poderá usá-los para criar verifique se as duas versões do seu programa funcionam da mesma maneira.
fonte
Sim.
A idéia de que qualquer código seja escrito sem testes de unidade é um anátema. A menos que você prove seu código correto e, em seguida, prove a prova correta = P.
fonte
Eu abordaria essa questão de forma pragmática e não dogmática. Faça a si mesmo a pergunta: "O que poderia dar errado na função X?" Imagine o que acontece com a saída quando você introduz alguns erros típicos no código: um prefator errado, um índice errado, ... E então escreva testes de unidade que provavelmente detectarão esse tipo de erro. Se para uma determinada função não há como escrever esses testes sem repetir o código da própria função, não o faça - mas pense nos testes no próximo nível superior.
Um problema muito mais importante com testes de unidade (ou de fato quaisquer testes) no código científico é como lidar com as incertezas da aritmética de ponto flutuante. Até onde eu sei, ainda não existem boas soluções gerais.
fonte
Sinto muito por Tangurena - por aqui, o mantra é "Código não testado é código quebrado" e veio do chefe. Em vez de repetir todas as boas razões para fazer testes de unidade, quero apenas adicionar alguns detalhes.
fonte
Eu usei o teste de unidade com bom efeito em vários códigos de pequena escala (ou seja, programador único), incluindo a terceira versão do meu código de análise de dissertação em física de partículas.
As duas primeiras versões entraram em colapso devido ao seu próprio peso e à multiplicação de interconexões.
Outros escreveram que a interação entre os módulos costuma ser o local onde a codificação científica é interrompida, e eles estão certos quanto a isso. Mas é muito mais fácil diagnosticar esses problemas quando você pode mostrar conclusivamente que cada módulo está fazendo o que deve fazer.
fonte
Uma abordagem ligeiramente diferente que eu usei durante o desenvolvimento de um solucionador químico (para domínios geológicos complexos) foi o que você poderia chamar de Teste de Unidade por Copiar e Colar Snippet .
A criação de um equipamento de teste para o código original incorporado em um grande modelador de sistema químico não era viável no prazo.
No entanto, pude elaborar um conjunto cada vez mais complexo de trechos, mostrando como o analisador (Boost Spirit) das fórmulas químicas funcionava, como testes de unidade para diferentes expressões.
O teste de unidade final mais complexo foi muito próximo ao código necessário no sistema, sem a necessidade de alterar esse código para ser ridicularizado. Consegui, assim, copiar todo o código testado em minha unidade.
O que torna isso mais do que apenas um exercício de aprendizado e um verdadeiro conjunto de regressão são dois fatores - os testes de unidade mantidos na fonte principal e executados como parte de outros testes para esse aplicativo (e sim, eles perceberam um efeito colateral do Boost Espírito mudando 2 anos depois) - como o código copiado e colado foi minimamente modificado no aplicativo real, ele poderia ter comentários referentes aos testes de unidade para ajudar alguém a mantê-los sincronizados.
fonte
Para bases de código maiores, testes (não necessariamente testes de unidade) para itens de alto nível são úteis. Os testes de unidade para alguns algoritmos mais simples também são úteis para garantir que seu código não esteja fazendo nenhum disparate porque sua função auxiliar está usando em
sin
vez decos
.Mas, para o código geral de pesquisa, é muito difícil escrever e manter testes. Os algoritmos tendem a ser grandes sem resultados intermediários significativos, que podem ter testes óbvios e geralmente levam muito tempo para serem executados antes que haja um resultado. É claro que você pode testar com execuções de referência que tiveram bons resultados, mas esse não é um bom teste no sentido de teste de unidade.
Os resultados geralmente são aproximações da verdadeira solução. Embora você possa testar suas funções simples se elas forem precisas até alguns epsilon, será muito difícil verificar se, por exemplo, alguma malha de resultado está correta ou não, que foi avaliada por inspeção visual pelo usuário (você) antes.
Nesses casos, os testes automatizados geralmente têm uma relação custo / benefício muito alta. Eu recomendo algo melhor: escreva programas de teste. Por exemplo, escrevi um script python de tamanho médio para criar dados sobre resultados, como histogramas de tamanhos de arestas e ângulos de uma malha, área do maior e menor triângulo e sua proporção, etc.
Eu posso usá-lo para avaliar malhas de entrada e saída durante a operação normal e usá-lo para uma verificação de sanidade depois de alterar o algoritmo. Quando troco o algoritmo, nem sempre sei se o novo resultado é melhor, porque muitas vezes não há uma medida absoluta de qual aproximação é a melhor. Mas, ao gerar essas métricas, posso dizer sobre alguns fatores o que é melhor como "A nova variante acaba por ter uma melhor relação de ângulo, mas uma pior taxa de convergência".
fonte