Como você escreve testes de unidade para código com resultados difíceis de prever?

124

Frequentemente, trabalho com programas numéricos / matemáticos, nos quais é difícil prever com antecedência o resultado exato de uma função.

Ao tentar aplicar o TDD com esse tipo de código, geralmente acho mais fácil escrever o código em teste do que escrever testes de unidade para esse código, porque a única maneira de saber o resultado esperado é aplicar o próprio algoritmo (seja no meu no papel ou no computador). Parece errado, porque estou efetivamente usando o código em teste para verificar meus testes de unidade, e não o contrário.

Existem técnicas conhecidas para escrever testes de unidade e aplicar TDD quando o resultado do código sob teste é difícil de prever?

Um exemplo (real) de código com resultados difíceis de prever:

Função weightedTasksOnTimeque, dada a quantidade de trabalho realizado por dia workPerDayno intervalo (0, 24], o horário atual initialTime> 0 e uma lista de tarefas taskArray; cada uma com um tempo para concluir a propriedade time> 0, data de vencimento duee valor de importância importance; retorna um valor normalizado no intervalo [0, 1] que representa a importância das tarefas que podem ser concluídas antes da duedata se cada tarefa for concluída na ordem dada por taskArray, começando em initialTime.

O algoritmo para implementar esta função é relativamente simples: repita as tarefas no taskArray. Para cada tarefa, adicione timea initialTime. Se o novo horário < due, adicione importancea um acumulador. O tempo é ajustado pelo workPerDay inverso. Antes de devolver o acumulador, divida pela soma das importâncias da tarefa para normalizar.

function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time * (24 / workPerDay)
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator / totalImportance(taskArray)
}

Acredito que o problema acima pode ser simplificado, mantendo o núcleo, removendo workPerDaye o requisito de normalização, para fornecer:

function weightedTasksOnTime(initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator
}

Esta pergunta aborda situações em que o código em teste não é uma reimplementação de um algoritmo existente. Se o código é uma reimplementação, é intrinsecamente fácil prever os resultados, porque as implementações confiáveis ​​existentes do algoritmo agem como um oráculo de teste natural.

PaintingInAir
fonte
4
Você pode fornecer um exemplo simples de uma função cujo resultado é difícil de prever?
Robert Harvey
62
FWIW, você não está testando o algoritmo. Presumivelmente, isso está correto. Você está testando a implementação. Trabalhar manualmente é geralmente bom como uma construção paralela.
Kristian H
7
Há situações em que um algoritmo não pode ser razoavelmente testado em unidade - por exemplo, se o tempo de execução for vários dias / meses. Isso pode acontecer ao resolver problemas de NP. Nesses casos, pode ser mais viável fornecer uma prova formal de que o código está correto.
Hulk
12
Algo que eu já vi em códigos numéricos muito complicados é tratar testes de unidade apenas como testes de regressão. Escreva a função, execute-a para vários valores interessantes, valide os resultados manualmente e, em seguida, escreva o teste de unidade para capturar regressões do resultado esperado. Codificação de horror? Curioso o que os outros pensam.
Chuu

Respostas:

251

Há duas coisas que você pode testar em código difícil de testar. Primeiro, os casos degenerados. O que acontece se você não tiver elementos em sua matriz de tarefas, ou apenas um ou dois, mas um já tiver vencido, etc. Qualquer coisa que seja mais simples que o seu problema real, mas ainda razoável para calcular manualmente.

O segundo são os testes de sanidade. Essas são as verificações que você faz onde não sabe se uma resposta está certa , mas você definitivamente saberia se está errada . São coisas como o tempo deve avançar, os valores devem estar em um intervalo razoável, as porcentagens devem somar 100, etc.

Sim, isso não é tão bom quanto um teste completo, mas você ficaria surpreso com a frequência com que você estraga as verificações de sanidade e os casos degenerados, o que revela um problema no seu algoritmo completo.

Karl Bielefeldt
fonte
54
Pense que este é um conselho muito bom. Comece escrevendo esse tipo de teste de unidade. À medida que você desenvolve o software, se encontrar erros ou respostas incorretas - adicione-os como testes de unidade. Faça o mesmo, até certo ponto, quando encontrar respostas definitivamente corretas. Edificá-los ao longo do tempo, e você (finalmente) vai ter um conjunto muito completo de testes de unidade apesar de ter começado a não saber o que eles estavam indo para ser ...
Algy Taylor
21
Outra coisa que pode ser útil em alguns casos (embora talvez não seja este) é escrever uma função inversa e testar se, quando encadeadas, suas entradas e saídas são as mesmas.
Cyberspark
7
A verificação de integridade geralmente é um bom alvo para testes baseados em propriedades com algo como QuickCheck
jk.
10
a outra categoria de testes que eu recomendo são algumas para verificar alterações não intencionais na saída. Você pode 'trapacear' usando o próprio código para gerar o resultado esperado, pois a intenção é ajudar os mantenedores sinalizando que algo destinado como uma alteração neutra na saída afetou involuntariamente o comportamento algorítmico.
Dan Neely
5
@ iFlo Não tenho certeza se você estava brincando, mas o inverso inverso já existe. Worth percebendo que o teste não pode ser um problema na função inversa embora
lucidbrot
80

Eu costumava escrever testes para software científico com resultados difíceis de prever. Fizemos muito uso de relações metamórficas. Essencialmente, há coisas que você sabe sobre como seu software deve se comportar, mesmo que você não saiba as saídas numéricas exatas.

Um possível exemplo para o seu caso: se você diminuir a quantidade de trabalho que pode realizar todos os dias, a quantidade total de trabalho que você poderá realizar permanecerá na mesma, mas provavelmente diminuirá. Portanto, execute a função para vários valores de workPerDaye verifique se a relação é válida.

James Elderfield
fonte
32
Relações metamórficas um exemplo específico de teste baseado em propriedade , que é, em geral, uma ferramenta útil para situações como estas
Dannnno
38

As outras respostas têm boas idéias para o desenvolvimento de testes para casos de borda ou erro. Para os outros, o uso do algoritmo em si não é ideal (obviamente), mas ainda é útil.

Ele detectará se o algoritmo (ou dados dos quais depende) mudou

Se a alteração for um acidente, você poderá reverter uma confirmação. Se a alteração foi deliberada, você precisa revisitar o teste de unidade.

user949300
fonte
6
E, para que conste, esse tipo de teste costuma ser chamado de "teste de regressão", de acordo com sua finalidade, e é basicamente uma rede de segurança para qualquer modificação / refatoração.
Pac0
21

Da mesma maneira que você escreve testes de unidade para qualquer outro tipo de código:

  1. Encontre alguns casos de teste representativos e teste-os.
  2. Encontre casos extremos e teste-os.
  3. Encontre condições de erro e teste-as.

A menos que seu código envolva algum elemento aleatório ou não seja determinístico (ou seja, não produzirá a mesma saída com a mesma entrada), é testável por unidade.

Evite efeitos colaterais ou funções afetadas por forças externas. As funções puras são mais fáceis de testar.

Robert Harvey
fonte
2
Para algoritmos não determinísticos pode guardar semente de RNG ou zombar-lo usando quer utilizando sequência fixa ou série determinitistic baixo discrepância por exemplo Halton sequência
Wondra
14
@PaintingInAir Se for impossível verificar a saída do algoritmo, o algoritmo pode estar incorreto?
9188 WolfgangGroiss #
5
Unless your code involves some random elementO truque aqui é transformar seu gerador de números aleatórios em uma dependência injetada, para que você possa substituí-lo por um gerador de números que forneça o resultado exato que você deseja. Isso permite que você teste com precisão novamente - contando os números gerados como parâmetros de entrada. not deterministic (i.e. it won't produce the same output given the same input)Como um teste de unidade deve começar a partir de uma situação controlada , só pode ser não determinístico se tiver um elemento aleatório - que você poderá injetar. Não consigo pensar em outras possibilidades aqui.
Flater
3
@PaintingInAir: Ou. Meu comentário se aplica à execução rápida ou à gravação rápida de teste. Se você demorar três dias para calcular um único exemplo manualmente (vamos supor que você use o método mais rápido disponível que não esteja usando o código) - serão necessários três dias. Se, em vez disso, você basear o resultado esperado do teste no próprio código, o teste estará se comprometendo. É como fazer if(x == x), é uma comparação inútil. Você precisa que seus dois resultados ( reais : provêm do código; esperado : provêm de seu conhecimento externo) sejam independentes um do outro.
Flater
2
Ainda é testável por unidade, mesmo que não seja determinístico, desde que esteja em conformidade com as especificações e que a conformidade possa ser medida (por exemplo, distribuição e distribuição aleatória). Pode ser necessário apenas um grande número de amostras para eliminar o risco de anomalia.
Mckenzm 9/1018
17

Atualização devido a comentários postados

A resposta original foi removida por uma questão de brevidade - você pode encontrá-la no histórico de edições.

PaintingInAir Por contexto: como empreendedor e acadêmico, a maioria dos algoritmos que eu desenho não é solicitada por ninguém além de mim. O exemplo dado na pergunta faz parte de um otimizador sem derivativos para maximizar a qualidade de uma ordem de tarefas. Em termos de como descrevi internamente a necessidade da função de exemplo: "Preciso de uma função objetiva para maximizar a importância das tarefas concluídas no prazo". No entanto, ainda parece haver uma grande lacuna entre essa solicitação e a implementação de testes de unidade.

Primeiro, um TL; DR para evitar uma resposta demorada:

Pense da seguinte maneira:
um cliente entra no McDonald's e pede um hambúrguer com alface, tomate e sabonete como coberturas. Essa ordem é dada ao cozinheiro, que faz o hambúrguer exatamente conforme solicitado. O cliente recebe esse hambúrguer, come-o e depois reclama ao cozinheiro que este não é um hambúrguer saboroso!

Isso não é culpa do cozinheiro - ele está apenas fazendo o que o cliente pediu explicitamente. Não é tarefa do cozinheiro verificar se o pedido solicitado é realmente saboroso . O cozinheiro simplesmente cria aquilo que o cliente pede. É da responsabilidade do cliente encomendar algo que ache saboroso .

Da mesma forma, não é tarefa do desenvolvedor questionar a correção do algoritmo. O único trabalho deles é implementar o algoritmo conforme solicitado.
O teste de unidade é uma ferramenta do desenvolvedor. Confirma que o hambúrguer corresponde à ordem (antes de sair da cozinha). Não tenta (nem deve) tentar confirmar que o hambúrguer encomendado é realmente saboroso.

Mesmo se você é o cliente e o cozinheiro, ainda há uma distinção significativa entre:

  • Não preparei esta refeição adequadamente, não estava saborosa (= erro de cozinheira). Um bife queimado nunca vai ser bom, mesmo se você gosta de bife.
  • Preparei a refeição adequadamente, mas não gosto (= erro do cliente). Se você não gosta de bife, nunca vai gostar de comer bife, mesmo que tenha cozinhado com perfeição.

O principal problema aqui é que você não está fazendo uma separação entre o cliente e o desenvolvedor (e o analista - embora essa função também possa ser representada por um desenvolvedor).

Você precisa distinguir entre testar o código e testar os requisitos de negócios.

Por exemplo, o cliente deseja que ele funcione assim [isso] . No entanto, o desenvolvedor entende mal, e ele escreve um código que faz isso .

O desenvolvedor, portanto, escreverá testes de unidade que testam se [isso] funciona conforme o esperado. Se ele desenvolveu o aplicativo corretamente, seus testes de unidade serão aprovados, mesmo que o aplicativo não faça isso , o que o cliente esperava.

Se você deseja testar as expectativas do cliente (os requisitos de negócios), isso precisa ser feito em uma etapa separada (e posterior).

Um fluxo de trabalho de desenvolvimento simples para mostrar quando esses testes devem ser executados:

  • O cliente explica o problema que deseja resolver.
  • O analista (ou desenvolvedor) anota isso em uma análise.
  • O desenvolvedor escreve um código que faz o que a análise descreve.
  • O desenvolvedor testa seu código (testes de unidade) para ver se ele seguiu a análise corretamente
  • Se os testes de unidade falharem, o desenvolvedor voltará a desenvolver. Isso faz um loop indefinidamente, até que a unidade teste todos os resultados.
  • Agora, com uma base de código testada (confirmada e aprovada), o desenvolvedor cria o aplicativo.
  • O aplicativo é entregue ao cliente.
  • O cliente agora testa se o aplicativo que ele recebe realmente resolve o problema que ele procurava resolver (testes de controle de qualidade) .

Você pode se perguntar qual é o sentido de fazer dois testes separados quando o cliente e o desenvolvedor são um e o mesmo. Como não há "entrega" do desenvolvedor para o cliente, os testes são executados um após o outro, mas ainda são etapas separadas.

  • Os testes de unidade são uma ferramenta especializada que ajuda a verificar se o estágio de desenvolvimento está concluído.
  • Os testes de controle de qualidade são feitos usando o aplicativo .

Se você deseja testar se o seu algoritmo está correto, isso não faz parte do trabalho do desenvolvedor . Essa é a preocupação do cliente, e o cliente testará isso usando o aplicativo.

Como empreendedor e acadêmico, você pode estar perdendo uma distinção importante aqui, que destaca as diferentes responsabilidades.

  • Se o aplicativo não seguir o que o cliente solicitou inicialmente, as alterações subseqüentes no código geralmente são feitas gratuitamente ; pois é um erro de desenvolvedor. O desenvolvedor cometeu um erro e deve pagar o custo de corrigi-lo.
  • Se o aplicativo faz o que o cliente solicitou inicialmente, mas agora o cliente mudou de idéia (por exemplo, você decidiu usar um algoritmo diferente e melhor), as alterações na base de código são cobradas do cliente , uma vez que não é o erro do desenvolvedor que o cliente pediu algo diferente do que agora deseja. É responsabilidade do cliente (custo) mudar de idéia e, portanto, fazer com que os desenvolvedores se esforcem mais para desenvolver algo que não foi previamente acordado.
Flater
fonte
Eu ficaria feliz em ver mais detalhes sobre a situação "Se você criou o algoritmo", pois acho que essa é a situação com maior probabilidade de apresentar problemas. Especialmente em situações em que nenhum exemplo "se A então B, senão C" é fornecido. (ps Eu não sou o voto negativo) #
PaintingInAir
@PaintingInAir: Mas eu realmente não posso elaborar isso, pois depende da sua situação. Se você decidiu criar esse algoritmo, obviamente fez isso para fornecer um recurso específico. Quem pediu para você fazer isso? Como eles descreveram sua solicitação? Eles disseram o que eles precisavam para acontecer em determinados cenários? (essa informação é a que me refiro como "a análise" em minha resposta) Qualquer explicação que você recebeu (que o levou a criar o algoritmo) pode ser usada para testar se o algoritmo funciona conforme solicitado. Em resumo, qualquer coisa , menos o código / algoritmo auto-criado, pode ser usado.
Flater
2
@PaintingInAir: É perigoso acoplar firmemente o cliente, analista e desenvolvedor; como você está propenso a pular etapas essenciais, como definir o problema desde o início . Eu acredito que é isso que você está fazendo aqui. Você parece querer testar a correção do algoritmo, e não se ele foi implementado corretamente. Mas não é assim que você faz. O teste da implementação pode ser feito usando testes de unidade. Testar o próprio algoritmo é uma questão de usar seu aplicativo (testado) e verificar seus resultados - esse teste real está fora do escopo da sua base de código (como deveria ser ).
Flater
4
Essa resposta já é enorme. É altamente recomendável tentar encontrar uma maneira de reformular o conteúdo original, para que você possa integrá-lo à nova resposta, se não quiser jogá-lo fora.
jpmc26
7
Além disso, eu discordo de sua premissa. Os testes podem e absolutamente devem revelar quando o código gera uma saída incorreta de acordo com a especificação. É válido para testes validar as saídas para alguns casos de teste conhecidos. Além disso, o cozinheiro deve saber melhor do que aceitar o "sabonete para as mãos" como um ingrediente válido de hambúrguer, e o empregador quase certamente ensinou o cozinheiro sobre quais ingredientes estão disponíveis.
jpmc26
9

Teste de propriedade

Às vezes, as funções matemáticas são mais bem servidas pelo "Teste de propriedade" do que pelos testes de unidade tradicionais baseados em exemplos. Por exemplo, imagine que você está escrevendo testes de unidade para algo como uma função inteira "multiplicar". Embora a função em si possa parecer muito simples, se é a única maneira de se multiplicar, como você a testará completamente sem a lógica da própria função? Você pode usar tabelas gigantes com entradas / saídas esperadas, mas isso é limitado e propenso a erros.

Nesses casos, você pode testar propriedades conhecidas da função, em vez de procurar resultados esperados específicos. Para multiplicação, você deve saber que multiplicar um número negativo e um número positivo deve resultar em um número negativo, e que multiplicar dois números negativos deve resultar em um número positivo, etc. Usando valores aleatórios e verificando se essas propriedades são preservadas para todos testar valores é uma boa maneira de testar essas funções. Você geralmente precisa testar mais de uma propriedade, mas geralmente pode identificar um conjunto finito de propriedades que, juntos, validam o comportamento correto de uma função sem necessariamente conhecer o resultado esperado para cada caso.

Uma das melhores introduções ao teste de propriedades que eu já vi é essa em F #. Esperamos que a sintaxe não seja um obstáculo para entender a explicação da técnica.

Aaron M. Eshbach
fonte
1
Eu sugeriria talvez adicionar algo um pouco mais específico em seu exemplo de multiplicação, como gerar quartetos aleatórios (a, b, c) e confirmar que (ab) (cd) produz (ac-ad) - (bc-bd). Uma operação de multiplicação pode ser bastante interrompida e ainda manter a regra (negativo vezes negativo produz positivo), mas a regra distributiva prevê resultados específicos.
Supercat
4

É tentador escrever o código e depois ver se o resultado "parece certo", mas, como você corretamente intui, não é uma boa idéia.

Quando o algoritmo é difícil, você pode fazer várias coisas para facilitar o cálculo manual do resultado.

  1. Use o Excel. Configure uma planilha que faça parte ou todo o cálculo para você. Mantenha-o simples o suficiente para poder ver as etapas.

  2. Divida seu método em métodos testáveis ​​menores, cada um com seus próprios testes. Quando tiver certeza de que as peças menores funcionam, use-as para trabalhar manualmente na próxima etapa.

  3. Use propriedades agregadas para verificar a sanidade. Por exemplo, digamos que você tenha uma calculadora de probabilidade; talvez você não saiba quais devem ser os resultados individuais, mas sabe que todos eles precisam somar 100%.

  4. Força bruta. Escreva um programa que gere todos os resultados possíveis e verifique se nenhum deles é melhor do que o que o seu algoritmo gera.

Ewan
fonte
Para 3., permita alguns erros de arredondamento aqui. É possível que seu valor total chegue a 100,000001% ou valores semelhantes próximos, mas não exatos.
Flater
2
Não tenho muita certeza sobre 4. Se você é capaz de gerar o resultado ideal para todas as combinações de entradas possíveis (que você usa para confirmar os testes), já é inerentemente capaz de calcular o resultado ideal e, portanto, não Não é necessário esse segundo pece de código que você está tentando testar. Nesse ponto, seria melhor usar o gerador de resultados ideal existente, pois já está comprovado que ele funciona. (e se ainda não estiver comprovado que funcione, não será possível confiar no resultado para verificar seus testes de fato).
Flater
6
@flater normalmente você tem outros requisitos, além de correção que a força bruta não atende. por exemplo, desempenho.
Ewan
1
@flater Eu odiaria usar seu tipo, caminho mais curto, mecanismo de xadrez, etc., se você acredita nisso. Mas id totalmente jogar no seu erro de arredondamento permitido casino o dia todo
Ewan
3
@flater você renuncia quando chega a um jogo de final de peão rei? só porque o jogo inteiro não pode ser brutalmente forçado não significa uma posição individual. Só porque você brute force o caminho mais curto correto para um não rede significa que você sabe o caminho mais curto em todas as redes
Ewan
2

TL; DR

Vá para a seção "testes comparativos" para obter conselhos que não estão em outras respostas.


Começos

Comece testando os casos que devem ser rejeitados pelo algoritmo (zero ou negativo workPerDay, por exemplo) e os casos que são triviais (por exemplo, tasksmatriz vazia ).

Depois disso, você deseja testar os casos mais simples primeiro. Para a tasksentrada, precisamos testar comprimentos diferentes; deve ser suficiente testar 0, 1 e 2 elementos (2 pertence à categoria "muitos" para este teste).

Se você pode encontrar entradas que podem ser calculadas mentalmente, é um bom começo. Uma técnica que às vezes uso é começar do resultado desejado e voltar ao trabalho (nas especificações) para as entradas que devem produzir esse resultado.

Teste comparativo

Às vezes, a relação da saída com a entrada não é óbvia, mas você tem uma relação previsível entre diferentes saídas quando uma entrada é alterada. Se entendi o exemplo corretamente, a adição de uma tarefa (sem alterar outras entradas) nunca aumentará a proporção do trabalho realizado dentro do prazo, para que possamos criar um teste que chame a função duas vezes - uma vez com e uma vez sem a tarefa extra - e afirma a desigualdade entre os dois resultados.

Fallbacks

Às vezes, tive que recorrer a um longo comentário mostrando um resultado calculado manualmente nas etapas correspondentes à especificação (esse comentário geralmente é mais longo que o caso de teste). O pior caso é quando você precisa manter a compatibilidade com uma implementação anterior em um idioma diferente ou em um ambiente diferente. Às vezes, você apenas precisa rotular os dados de teste com algo parecido /* derived from v2.6 implementation on ARM system */. Isso não é muito satisfatório, mas pode ser aceitável como teste de fidelidade ao transportar ou como muleta de curto prazo.

Lembretes

O atributo mais importante de um teste é sua legibilidade - se as entradas e saídas são opacas ao leitor, o teste tem um valor muito baixo, mas se o leitor for ajudado a entender as relações entre eles, o teste terá dois propósitos.

Não se esqueça de usar um "aproximadamente igual a" apropriado para resultados inexatos (por exemplo, ponto flutuante).

Evite o excesso de teste - adicione apenas um teste se ele cobrir algo (como um valor limite) que não é atingido por outros testes.

Toby Speight
fonte
2

Não há nada de muito especial nesse tipo de função difícil de testar. O mesmo se aplica ao código que usa interfaces externas (por exemplo, uma API REST de um aplicativo de terceiros que não está sob seu controle e certamente não pode ser testado pelo seu conjunto de testes; ou usando uma biblioteca de terceiros em que você não tem certeza sobre o formato exato de bytes dos valores de retorno).

É uma abordagem bastante válida para simplesmente executar seu algoritmo para alguma entrada sã, ver o que ele faz, garantir que o resultado esteja correto e encapsular a entrada e o resultado como um caso de teste. Você pode fazer isso em alguns casos e, assim, obter várias amostras. Tente tornar os parâmetros de entrada o mais diferentes possível. No caso de uma chamada de API externa, você faria algumas chamadas no sistema real, rastreou-as com alguma ferramenta e depois zombou delas em seus testes de unidade para ver como o programa reage - o que é o mesmo que escolher algumas executa seu código de planejamento de tarefas, verificando-os manualmente e depois codificando o resultado em seus testes.

Obviamente, traga casos extremos como (no seu exemplo) uma lista vazia de tarefas; coisas assim.

Talvez sua suíte de testes não seja tão boa quanto para um método em que você possa prever facilmente resultados; mas ainda 100% melhor do que nenhuma suíte de testes (ou apenas um teste de fumaça).

Se o seu problema, porém, é que você acha difícil decidir se um resultado está correto, esse é um problema completamente diferente. Por exemplo, digamos que você tenha um método que detecte se um número arbitrariamente grande é primo. Você dificilmente pode lançar qualquer número aleatório e, em seguida, apenas "olhar" se o resultado estiver correto (supondo que você não possa decidir o que é primordial em sua cabeça ou em um pedaço de papel). Nesse caso, há muito pouco que você pode fazer - você precisaria obter resultados conhecidos (por exemplo, alguns números primos grandes) ou implementar a funcionalidade com um algoritmo diferente (talvez até uma equipe diferente - a NASA parece gostar de isso) e espero que, se alguma implementação for com erros, pelo menos o bug não leve aos mesmos resultados errados.

Se esse é um caso regular para você, é necessário conversar bem com os engenheiros de requisitos. Se eles não puderem formular seus requisitos de uma maneira fácil (ou possível) de verificar, então quando você sabe se terminou?

AnoE
fonte
2

Outras respostas são boas, então tentarei abordar alguns pontos que eles coletivamente perderam até agora.

Eu escrevi (e testei exaustivamente) um software para processar imagens usando o Radar de Abertura Sintética (SAR). É de natureza científica / numérica (há muita geometria, física e matemática envolvidas).

Algumas dicas (para testes científicos / numéricos gerais):

1) Use inversos. Qual é o fftde [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). O mesmo vale para o caso 2D.

2) Use afirmações conhecidas. Se você escrever uma função determinante, pode ser difícil dizer qual é o determinante de uma matriz 100x100 aleatória. 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 . Eu escrevi um código para o referido processamento SAR que registraria duas imagens, gerando pontos de ligação que criam um mapeamento entre as imagens e, em seguida, fazendo uma distorção entre elas para fazer com que correspondessem. Ele pode se registrar em um nível de sub-pixel. A priori, é difícil dizer algo sobre como pode ser o registro de duas imagens. Como você pode testá-lo? Coisas como:

EXPECT_TRUE(register(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 à sua 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. Verifique se cada pixel está dentro de +/- 5% do intervalo que os pixels podem assumir (0-255 é em escala de cinza, comum no processamento de imagens). O resultado deve ter pelo menos o mesmo tamanho da entrada.

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 / ouvi sobre bugs no código científica que surgem em casos degenerados que foram gerados aleatoriamente (em algoritmos complicados, pode ser difícil ver o que o caso degenerado mesmo é) 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. É certo que isso aconteceu apenas uma vez, e tenho certeza de que nem sempre vale a pena fazer, então use essa técnica com prudência. Aleatório com a mesma semente é sempre seguro, no entanto. Desvantagem (ao contrário de usar sempre a mesma semente o tempo todo): é necessário registrar suas execuções de teste. De cabeça para baixo: correção e correção de erros.

Seu caso particular

1) Teste se um vazio taskArray retorna 0 (afirmação conhecida).

2) Gerar aleatória de entrada de tal modo que task.time > 0 , task.due > 0, e task.importance > 0 para todos task s, e afirmar o resultado é maior do que 0 (afirmar áspera, entrada aleatória) . Você não precisa enlouquecer e gerar sementes aleatórias; seu algoritmo simplesmente não é complexo o suficiente para justificá-lo. Há cerca de 0 chances de valer a pena: basta manter o teste simples.

3) Teste se task.importance == 0 para todos os task s, então o resultado é 0 (afirmação conhecida)

4) Outras respostas abordaram isso, mas pode ser importante para o seu caso em particular : se você estiver criando uma API para ser consumida por usuários fora da sua equipe, será necessário testar os casos degenerados. Por exemplo, se workPerDay == 0, certifique-se de gerar um erro adorável que informa ao usuário que é entrada inválida. Se você não está criando uma API e é apenas para você e sua equipe, provavelmente pode pular esta etapa e recusar chamá-la com o caso degenerado.

HTH.

Matt Messersmith
fonte
1

Incorpore o teste de asserção ao seu conjunto de testes de unidade para testar o algoritmo com base em propriedades. Além de escrever testes de unidade que verificam saída específica, escreva testes projetados para falhar, acionando falhas de asserção no código principal.

Muitos algoritmos contam com suas provas de correção na manutenção de certas propriedades ao longo dos estágios do algoritmo. Se você puder verificar essas propriedades de maneira sensata, observando a saída de uma função, apenas o teste de unidade é suficiente para testar suas propriedades. Caso contrário, o teste baseado em asserção permite testar se uma implementação mantém uma propriedade toda vez que o algoritmo a assume.

O teste baseado em asserção expõe falhas de algoritmo, erros de codificação e falhas de implementação devido a problemas como instabilidade numérica. Muitas linguagens possuem mecanismos que extraem asserções em tempo de compilação ou antes que o código seja interpretado, de modo que, quando executadas no modo de produção, as asserções não incorrem em uma penalidade de desempenho. Se o seu código passar nos testes de unidade, mas falhar em um caso da vida real, você poderá ativar as asserções novamente como uma ferramenta de depuração.

Tobias Hagge
fonte
1

Algumas das outras respostas aqui são muito boas:

  • Casos de base de teste, borda e canto
  • Executar verificações de sanidade
  • Realizar testes comparativos

... Eu acrescentaria algumas outras táticas:

  • Decomponha o problema.
  • Prove o algoritmo fora do código.
  • Teste se o algoritmo [externamente comprovado] foi implementado conforme projetado.

A decomposição permite garantir que os componentes do seu algoritmo façam o que você espera que eles façam. E uma decomposição "boa" permite também garantir que elas sejam coladas corretamente. Uma ótima decomposição generaliza e simplifica o algoritmo na medida em que você pode prever os resultados (do (s) algoritmo (s) genérico (s) simplificado)) manualmente, o suficiente para escrever testes completos.

Se você não puder se decompor nessa extensão, prove o algoritmo fora do código por qualquer meio que seja suficiente para satisfazer você e seus colegas, partes interessadas e clientes. E então, apenas se decomponha o suficiente para provar que sua implementação corresponde ao design.

svidgen
fonte
0

Isso pode parecer uma resposta idealista, mas ajuda a identificar diferentes tipos de teste.

Se respostas estritas são importantes para a implementação, exemplos e respostas esperadas realmente devem ser fornecidos nos requisitos que descrevem o algoritmo. Esses requisitos devem ser revisados ​​em grupo e, se você não obtiver os mesmos resultados, o motivo precisa ser identificado.

Mesmo se você estiver desempenhando o papel de analista e implementador, você deve realmente criar requisitos e revisá-los muito antes de escrever testes de unidade; nesse caso, você conhecerá os resultados esperados e poderá escrever seus testes de acordo.

Por outro lado, se esta é uma parte que você está implementando que não faz parte da lógica de negócios ou suporta uma resposta da lógica de negócios, convém executar o teste para ver quais são os resultados e modificá-lo para esperar esses resultados. Os resultados finais já são verificados em relação aos seus requisitos, portanto, se estiverem corretos, todo o código que alimenta esses resultados finais deve estar numericamente correto e, nesse ponto, seus testes de unidade são mais para detectar casos de falha de borda e futuras alterações de refatoração do que para provar que um determinado algoritmo produz resultados corretos.

Bill K
fonte
0

Eu acho que é perfeitamente aceitável em algumas ocasiões seguir o processo:

  • projetar um caso de teste
  • use seu software para obter a resposta
  • verifique a resposta manualmente
  • escreva um teste de regressão para que versões futuras do software continuem a dar essa resposta.

Essa é uma abordagem razoável em qualquer situação em que verificar a exatidão de uma resposta manualmente seja mais fácil do que calcular a resposta manualmente a partir dos primeiros princípios.

Conheço pessoas que escrevem software para renderizar páginas impressas e têm testes para verificar se exatamente os pixels corretos estão definidos na página impressa. A única maneira sensata de fazer isso é escrever o código para renderizar a página, verificar visualmente que ela fica bem e capturar o resultado como um teste de regressão para versões futuras.

Só porque você lê em um livro que uma metodologia específica incentiva a escrever os casos de teste primeiro, não significa que você sempre deve fazer dessa maneira. Regras existem para serem quebradas.

Michael Kay
fonte
0

As respostas de outras respostas já possuem técnicas para a aparência de um teste quando o resultado específico não pode ser determinado fora da função testada.

O que faço adicionalmente e que não vi nas outras respostas é gerar automaticamente testes de alguma forma:

  1. Entradas 'aleatórias'
  2. Iteração entre intervalos de dados
  3. Construção de casos de teste a partir de conjuntos de limites
  4. Todos acima.

Por exemplo, se a função usar três parâmetros, cada um com faixa de entrada permitida [-1,1], teste todas as combinações de cada parâmetro, {-2, -1,01, -1, -0,99, -0,5, -0,01, 0,0,01 , 0,5,0,99,1,1,01,2, um pouco mais aleatório em (-1,1)}

Em resumo: Às vezes, a má qualidade pode ser subsidiada pela quantidade.

Keith
fonte