Vale a pena escrever testes de unidade para códigos de pesquisa científica?


89

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?


2
Essa é uma pergunta em aberto, não é?
qubyte

2
Como em todas as "regras", sempre se aplica uma dose de pensamento crítico. Pergunte a si mesmo se uma determinada rotina tem uma maneira óbvia de ser testada em unidade. Caso contrário, um teste de unidade não faz sentido naquele momento ou o design do código foi ruim. Idealmente, uma rotina executa uma tarefa o mais independente possível de outras rotinas, mas isso deve ser negociado ocasionalmente.
Lagerbaer

Há uma boa discussão em uma linha semelhante sobre uma pergunta sobre stackoverflow .
naught101

Respostas:


85

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.


7
Mark, que tipo de código você está falando aqui? Modelo reutilizável? Acho que esse raciocínio não é realmente aplicável a coisas como código de análise de dados exploratórios, onde você realmente precisa pular bastante e muitas vezes nunca espera reutilizar o código em qualquer outro lugar.
precisa saber é o seguinte

35

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.

hChrrr

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.


4
A Wikipedia diz que "o teste de unidade, também conhecido como teste de componente, refere-se a testes que verificam a funcionalidade de uma seção específica do código, geralmente no nível da função". Testes de convergência em um código de elemento finito claramente não podem ser testes de unidade, pois envolvem muitas funções.
David Ketcheson

Foi por isso que deixei claro no topo do post que considero a visão ampla dos testes de unidade e "geralmente" significa exatamente isso.
precisa saber é o seguinte

Minha pergunta foi feita no sentido da definição mais amplamente aceita de testes de unidade. Agora eu fiz isso completamente explícito na pergunta.
David Ketcheson 03/12/19

Eu ter esclarecido a minha resposta
Matt Knepley

Seus últimos exemplos são relevantes para o que eu pretendia.
David Ketcheson

28

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.



Você consideraria os testes de integração suficientes ou acha que também precisa escrever testes de unidade separados?
siamii

Eu escreveria testes de unidade separados sempre que possível e viável. Facilita a depuração e aplica código desacoplado (que é o que você deseja).
amigos estão dizendo sobre geoff

19

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 ( f77algué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.


Você tem exemplos ou critérios específicos para escolher quais peças realizar o teste de unidade (e quais não)?
David Ketcheson

@DavidKetcheson Minha experiência é limitada pelo aplicativo e pelo idioma que usamos. Portanto, para o nosso código CFD de uso geral, com cerca de 200 mil linhas, principalmente da F90, tentamos ao longo dos últimos dois anos isolar realmente algumas funcionalidades do código. Criar um módulo e usá-lo em todo o código não está conseguindo isso; portanto, é preciso realmente comparar esses módulos e praticamente torná-los bibliotecas. Portanto, apenas muito poucas instruções USE e todas as conexões com o restante do código são feitas por meio de chamadas de rotina. Rotinas que você pode unittest, é claro, assim como o restante da biblioteca.
precisa saber é o seguinte

@ DavidKetcheson Como eu disse na minha resposta, as condições de contorno e a termodinâmica foram dois aspectos do nosso código que conseguimos isolar e, portanto, sem sentido, faziam sentido. De uma maneira mais geral, eu começaria com algo pequeno e tentaria fazê-lo de maneira limpa. Idealmente, este é um trabalho para 2 pessoas. Uma pessoa escreve as rotinas e a documentação que descreve a interface, outra deve escrever o teste de unidade, idealmente sem olhar o código-fonte e seguir apenas a descrição da interface. Dessa forma, a intenção da rotina é testada, mas sei que isso não é uma coisa fácil de organizar.
precisa saber é o seguinte

1
Por que não incluir os outros tipos de teste de software (integração, sistema) além do teste de unidade? Além de tempo e custo, essa não seria a solução técnica mais completa? Minhas referências são 1 (Seção 3.4.2) e 2 (página 5). Em outras palavras, o Código Fonte não deve ser testado pelos níveis tradicionais de teste de software 3 ("Níveis de Teste")?
Ximiki

14

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.


9

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?).


Por que você propõe essa hierarquia (unidade para menor, regressão para maior) versus os níveis tradicionais nos testes de software?
Ximiki

@ ximiki - eu não pretendo. Estou dizendo que os testes existem em um espectro que inclui todas as categorias listadas no seu link.
Wolfgang Bangerth

8

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.


7

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.


+1: "Trabalhar em estágios como esse significa que, quando você se depara com um problema, você só tem o 'estágio' de código mais recente para testar, os estágios anteriores já foram testados."
Ximiki

5

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.


3
... e então você prova que a prova de que a prova está correta, e ... agora é uma toca de coelho profunda.
JM

2
Tartarugas todo o caminho deixa Dijkstra orgulhoso!
aterrel 4/11

2
Apenas resolva o caso geral e faça com que sua prova se prove correta! Toro de tartarugas!
Aesin

5

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.


Se você testar manualmente ou automaticamente usando testes de unidade, você terá exatamente os mesmos problemas com a representação de ponto flutuante. Eu recomendo Richard Harris excelente série de artigos em ACCU 's revista sobrecarga .
Mark Booth

"Se para uma determinada função não há como escrever tais testes sem repetir o código da própria função, então não". Você pode elaborar? Um exemplo deixaria isso claro para mim.
Ximiki

5

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.

  • O uso da memória deve ser testado. Todas as funções que alocam memória devem ser testadas, certificando-se de que as funções que armazenam e recuperam dados nessa memória estejam fazendo a coisa certa. Isso é ainda mais importante no mundo da GPU.
  • Embora brevemente mencionado antes, o teste de casos extremos é extremamente importante. Pense nesses testes da mesma maneira que você testa o resultado de qualquer cálculo. Certifique-se de que o código se comporte nas bordas e falhe normalmente (no entanto, você define isso na sua simulação) quando os parâmetros ou dados de entrada ficarem fora dos limites aceitáveis. O pensamento envolvido na escrita deste tipo de teste ajuda a aprimorar seu trabalho e pode ser um dos motivos pelos quais você raramente encontra alguém que tenha escrito testes de unidade, mas não considera o processo útil.
  • Use uma estrutura de teste (como mencionado por Geoff, que forneceu um bom link). Eu usei a estrutura de teste do BOOST em conjunto com o sistema CTest do CMake e posso recomendá-lo como uma maneira fácil de escrever rapidamente testes de unidade (bem como testes de validação e regressão).

+1: "Verifique se o código se comporta nas bordas e falha com facilidade (no entanto, você define isso na sua simulação) quando os parâmetros ou dados de entrada estiverem fora dos limites aceitáveis".
Ximiki

5

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.


3

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.


2

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 sinvez de cos.

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".

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.