É viável e útil gerar automaticamente algum código de testes unitários?

5

Hoje cedo desenvolvi uma ideia, baseada em um caso de uso real específico, que eu gostaria de verificar quanto à viabilidade e utilidade. Esta questão contará com um bom pedaço de código Java, mas pode ser aplicada a todos os idiomas que estão sendo executados dentro de uma VM, e talvez até mesmo fora dela. Embora exista um código real, ele não usa nada específico do idioma, portanto, leia-o principalmente como pseudo-código.

A ideia
Torne os testes de unidade menos complicados, adicionando de algumas maneiras a autogeração de código com base na interação humana com a base de código. Eu entendo que isso vai contra o princípio do TDD, mas eu não acho que alguém já provou que fazer TDD é melhor do que criar primeiro o código e depois imediatamente após os testes. Isso pode até ser adaptado para se encaixar no TDD, mas esse não é meu objetivo atual.

Para mostrar como ele deve ser usado, vou copiar uma das minhas aulas aqui, para as quais preciso fazer testes unitários.

public class PutMonsterOnFieldAction implements PlayerAction {
    private final int handCardIndex;
    private final int fieldMonsterIndex;

    public PutMonsterOnFieldAction(final int handCardIndex, final int fieldMonsterIndex) {
        this.handCardIndex = Arguments.requirePositiveOrZero(handCardIndex, "handCardIndex");
        this.fieldMonsterIndex = Arguments.requirePositiveOrZero(fieldMonsterIndex, "fieldCardIndex");
    }

    @Override
    public boolean isActionAllowed(final Player player) {
        Objects.requireNonNull(player, "player");
        Hand hand = player.getHand();
        Field field = player.getField();
        if (handCardIndex >= hand.getCapacity()) {
            return false;
        }
        if (fieldMonsterIndex >= field.getMonsterCapacity()) {
            return false;
        }
        if (field.hasMonster(fieldMonsterIndex)) {
            return false;
        }
        if (!(hand.get(handCardIndex) instanceof MonsterCard)) {
            return false;
        }
        return true;
    }

    @Override
    public void performAction(final Player player) {
        Objects.requireNonNull(player);
        if (!isActionAllowed(player)) {
            throw new PlayerActionNotAllowedException();
        }
        Hand hand = player.getHand();
        Field field = player.getField();
        field.setMonster(fieldMonsterIndex, (MonsterCard)hand.play(handCardIndex));
    }
}

Podemos observar a necessidade dos seguintes testes:

  • Teste de construtor com entrada válida
  • Teste de construtor com entradas inválidas
  • isActionAllowed test com entrada válida
  • isActionAllowed test com entradas inválidas
  • performAction test com entrada válida
  • performAction test com entradas inválidas

A minha ideia centra-se principalmente no teste isActionAllowed com entradas inválidas. Escrever esses testes não é divertido, você precisa garantir uma série de condições e verificar se ele realmente retorna false , isso pode ser estendido para performAction , onde uma exceção precisa ser lançada nesse caso.

O objetivo da minha ideia é gerar esses testes, indicando (através da interface gráfica do IDE, esperançosamente) que você deseja gerar testes com base em uma ramificação específica.

A implementação pelo exemplo

  1. O usuário clica em "Gerar código para a filial if (handCardIndex >= hand.getCapacity()) ".
  2. Agora, a ferramenta precisa encontrar um caso em que isso seja válido.

    (Eu não adicionei o código relevante, pois isso pode atrapalhar o post, em última instância)

  3. Para invalidar a ramificação, a ferramenta precisa encontrar handCardIndex e hand.getCapacity() , de modo que a condição >= seja válida.

  4. Ele precisa construir um Player com um Hand que tenha uma capacidade de pelo menos 1.
  5. Ele percebe que o capacity private int de Hand precisa ser pelo menos 1.
  6. Ele procura maneiras de defini-lo como 1. Felizmente, ele encontra um construtor que usa capacity como argumento. Usa 1 para isso.
  7. Mais trabalho precisa ser feito para construir com sucesso uma instância Player , envolvendo a criação de objetos que possuem restrições que podem ser vistas pela inspeção do código-fonte.
  8. Encontrou o hand com a menor capacidade possível e é capaz de construí-lo.
  9. Agora, para invalidar o teste, será necessário definir handCardIndex = 1 .
  10. Ele constrói o teste e afirma que ele é falso (o valor retornado da ramificação)

O que a ferramenta precisa para funcionar?
Para funcionar corretamente, será necessário verificar todos os códigos-fonte (incluindo o código JDK) para descobrir todas as restrições. Opcionalmente, isso pode ser feito através do javadoc, mas isso nem sempre é usado para indicar todas as restrições. Também poderia fazer alguma tentativa e erro, mas praticamente pára se você não puder anexar código-fonte a classes compiladas.

Em seguida, é necessário algum conhecimento básico sobre o que são os tipos primitivos , incluindo matrizes. E precisa ser capaz de construir alguma forma de "árvores de modificação". A ferramenta sabe que precisa alterar uma determinada variável para um valor diferente para obter o testcase correto. Por isso, será necessário listar todas as formas possíveis de alterá-lo, sem usar a reflexão, obviamente.

O que essa ferramenta não substituirá é a necessidade de criar testes unitários personalizados que testem todos os tipos de condições quando um determinado método realmente funciona. É puramente usado para testar métodos quando eles invalidam restrições.

Minhas perguntas :

  • A criação de tal ferramenta é factível ? Alguma vez funcionaria ou existem alguns problemas óbvios?
  • Essa ferramenta seria útil ? É útil mesmo gerar automaticamente esses testes? Poderia ser estendido para fazer coisas ainda mais úteis?
  • Por acaso, tal projeto já existe e eu estaria reinventando a roda?

Se não for útil, mas ainda é possível fazer isso, ainda considerarei isso por diversão. Se é considerado útil, então eu poderia fazer um projeto de código aberto para ele, dependendo da hora.

Para pessoas que pesquisam mais informações básicas sobre as classes Player e Hand usadas no meu exemplo, consulte este repositório . No momento da gravação, o PutMonsterOnFieldAction ainda não foi enviado para o repo, mas isso será feito assim que eu terminar os testes de unidade.

    
por skiwi 29.05.2014 / 15:53
fonte

5 respostas

1

É tudo apenas software, então, com esforço suficiente, é possível ;-). Em uma linguagem que suporta uma maneira decente de fazer análise de código, ela também deve ser viável.

Quanto à utilidade, acho que alguma automação em torno do teste unitário é útil para um determinado nível, dependendo de como ele é implementado. Você precisa ter clareza sobre aonde deseja ir com antecedência e estar bem ciente das limitações desse tipo de ferramental.

No entanto, o fluxo que você descreve tem uma enorme limitação, porque o código que está sendo testado conduz o teste. Isso significa que um erro no raciocínio ao desenvolver o código provavelmente acabará no teste também. O resultado final provavelmente será um teste que basicamente confirma que o código "faz o que ele faz" em vez de confirmar que ele faz o que deve fazer. Isso na verdade não é completamente inútil porque pode ser usado mais tarde para verificar se o código ainda faz o que ele fez anteriormente, mas não é um teste funcional e você não deve considerar seu código testado com base em tal teste. (Você ainda pode pegar alguns bugs superficiais, como manuseio de nulos, etc.) Não é inútil, mas não pode substituir um teste 'real'. Se isso significa que você ainda terá que criar o teste 'real', pode não valer a pena o esforço.

Você pode seguir rotas ligeiramente diferentes com isso. A primeira é apenas lançar dados na API para ver onde ela quebra, talvez depois de definir algumas asserções genéricas sobre o código. Isso é basicamente teste do Fuzz

O outro seria gerar os testes, mas sem as asserções, basicamente ignorando a etapa 10. Portanto, termine com um 'questionário de API' onde suas ferramentas determinam casos de teste úteis e solicita ao testador a resposta esperada dada uma chamada específica . Dessa forma você está realmente testando novamente. Ainda não está completo, se você esquecer um cenário funcional em seu código, a ferramenta não o encontrará magicamente e não removerá todas as suposições. Suponha que o código devesse ter sido if (handCardIndex > hand.getCapacity()) se > =, uma ferramenta como essa nunca descobrirá isso sozinha. Se você quiser voltar para o TDD, poderia sugerir casos de teste baseados apenas na interface, mas isso seria ainda mais funcionalmente incompleto, porque não há nem mesmo um código a partir do qual você possa inferir alguma funcionalidade.

Seus principais problemas sempre serão 1. Carregar erros no código para o teste e 2. Integralidade funcional. Ambas as questões podem ser reprimidas de alguma forma, nunca serem eliminadas. Você sempre terá que voltar aos requisitos e sentar-se para verificar se todos foram realmente testados corretamente. O claro perigo aqui é uma falsa sensação de segurança, porque a cobertura do código mostra 100% de cobertura. Mais cedo ou mais tarde alguém cometerá esse erro, cabe a você decidir se os benefícios superam esse risco. IMHO se resume a um trade-off clássico entre qualidade e velocidade de desenvolvimento.

    
por 01.07.2014 / 12:35
fonte
1

TLDR : É uma má ideia (veja abaixo para ler o porquê).

Primeiro, queria abordar isso:

I understand this goes against the principle of TDD, but I don't think anyone ever proved that doing TDD is better over first creating code and then immediatly therafter the tests. This may even be adapted to be fit into TDD, but that is not my current goal.

O TDD é melhor do que escrever o código primeiro (sim, foi comprovado).

Aqui estão alguns dos benefícios (você também pode ler sobre isso em vários artigos e livros on-line):

  • O TDD garante que você não escreva coisas desnecessárias (por exemplo, "escreva um código mínimo para o teste ser aprovado")
  • O TDD garante que a interface de sua API seja otimizada do ponto de vista do cliente, em vez de do ponto de vista do implementador. O primeiro, garante que o código será mais fácil de usar, para o código do cliente. O segundo tende a poluir a API pública do seu módulo com detalhes de implementação (mesmo quando você toma cuidado para evitar isso).
  • O TDD garante que você escreva código para suas especificações, não para uma ideia abstrata do seu algoritmo
  • com o código escrito primeiro, você encontra muitas situações em que acredita que o código funcionará e "não vê motivo para escrever um teste extra apenas para um caso difícil" (também conhecido como "ociosidade do desenvolvedor", "muito má tradução de YAGNI "e" número de desculpa do programador 8 "). Com o TDD você não tem a desculpa. Pode não parecer muito, até que você trabalhe em equipe e seus colegas lhe dão uma desculpa - ou chamem você para criticá-los.
  • O TDD minimiza o esforço (geralmente, ao escrever o código primeiro, o loop de desenvolvimento é "implementar, escrever testes, modificar / corrigir implementação para testes a serem executados"; com TDD, é "escrever testes, implementar até os testes serem aprovados"). pode ser muito mais curto).

Is creating such a tool feasible? Would it ever work, or are there some obvious problems?

Dados recursos suficientes, é teoricamente possível criar tal ferramenta.

Na prática, é duvidoso que você possa fazer algo assim, que realmente se encaixa em implementações genéricas. É bastante fácil fazer algo para um caso simples, mas considere que você pode testar uma API que chame 25 outras APIs internamente (com cada uma adicionando à complexidade ciclomática de seu algoritmo). A ferramenta teria que fazer muito mais do que os melhores analisadores estáticos no mercado atualmente.

O problema óbvio é que os testes não cobririam os requisitos do seu (cliente), de qualquer forma significativa - eles cobririam todos os casos da implementação.

Um teste unitário correto deve testar se uma unidade do seu código faz o que é suposto , em uma situação. Um teste gerado como você menciona, testaria que o código faz o que é implementado para fazer (e talvez o seu código seja alcançável). Isso não se relaciona de forma alguma com suas especificações.

Would such a tool be useful?

Parcialmente (com funcionalidade limitada). Poderia encurtar o tempo necessário para escrever testes de unidade para casos de canto.

Is it even useful to automatically generate these testcases at all? Could it be extended to do even more useful things?

Não realmente. Eu acho que seria útil para gerar automaticamente um testcase de cada vez, com o usuário escolhendo quais casos ele quer gerar (mas gerando todos os casos seria uma relação sinal / ruído muito baixa - você teria que descartar a maioria dos códigos gerados como irrelevantes) .

    
por 01.07.2014 / 14:22
fonte
1

Com estas questões principais:

Is it even useful to automatically generate these testcases at all? Could it be extended to do even more useful things? Does, by chance, such a project already exist and would I be reinventing the wheel?

Eu declararei ousadamente que sim, gerar testcases pode ser útil e sim, tais projetos já existem e são usados. No entanto, não é tão útil na forma como é perguntado na questão, ou seja, como um substituto TDD. Que não é.

Isso foi dito em uma das outras respostas:

A correct unit test should test that a unit of your code does what it is supposed to do, in a situation. A test generated like you mention, would test that the code does what it is implemented to do (and maybe that your code is reachable). This in no way relates to your specs.

Que eu mais concordo com. É assim que os testes de unidade são geralmente usados e como eles são usados no TDD. No entanto, nem todos os testes são usados para isso.

Assuma uma situação em que você herdou uma base de código grande sem testes de unidade existentes. Você não sabe sobre seu comportamento e certamente não sabe se alguma mudança que você está prestes a introduzir interromperá qualquer comportamento existente. Não seria ótimo ter testes gerados que capturassem o comportamento atual da base de código existente?

Claro que sim. Esses tipos de testes são chamados de testes regressão , e neste caso eu vejo gerando casos de teste como úteis. Ferramentas realmente existem que já fazem isso: talvez o mais popular seja uma ferramenta comercial AgitarOne com seu < um teste de regressão que gera capacidades . Existem outras alternativas também.

Outro caso de uso em que isso seria útil é verificar se alguns contratos comuns não são violados. Contratos comuns podem ser: a) seu código nunca deve lançar uma exceção de ponteiro nulo se você não fornecer nenhum parâmetro nulo; e b) seu código, se implementar o método equals, deve seguir as regras exigidas pelo contrato do método equals . Existem ferramentas para isso também, como a ferramenta gratuita Randoop , que usa geração de teste aleatório assistida por feedback para capturar tais questões.

Estes são alguns exemplos em que vejo utilidade em casos de teste gerados. No entanto, como apontado nas respostas anteriores, não é o mesmo benefício que alguém obtém dos testes em TDD, por exemplo, ou um bom conjunto de testes em geral. Estes são casos de certa forma: no caso geral, você ainda deve escrever os testes você mesmo.

    
por 06.01.2015 / 16:06
fonte
1

Recentemente, fiz algumas pesquisas sobre testes gerados. Parece que existem três abordagens principais para gerar testes para código desconhecido. Todas as três abordagens assumem que o comportamento atual (ou especificação) é intencional ou pelo menos aceito. Por isso, poderia ser caracterizado com um teste.

As três abordagens são:

Nenhuma análise

O código não é analisado de todo. Os métodos são chamados em uma seqüência aleatória e todos os resultados intermediários são verificados em testes de regressão posteriores.

Esta abordagem é implementada por randoop ou evosuite .

Análise estática

Os dados de código e o fluxo de controle são analisados. Algumas ferramentas tentam construir dados de teste que afetam todas as ramificações, outras até verificam se o código está em conformidade com uma determinada especificação. A análise também deve analisar o estado que é alterado por um método com determinados dados de teste. Ambos os resultados da análise podem ser usados para gerar testes (configuração com dados de teste, declaração de mudanças de estado analisadas).

Essa parece ser a abordagem mais conveniente para o usuário. É também o mais próximo do seu problema. O Symbolic Pathfinder parece ser uma ferramenta para essa abordagem, ainda que não esteja pronto para produção.

Análise dinâmica

O código é analisado em tempo de execução (depuração, criação de perfil). Abordagens comuns tentam capturar certas chamadas de método (com estado antes e depois da chamada) e serializá-las para testes.

Eu desenvolvi uma ferramenta para essa abordagem, que é chamada de TestRecorder .

As perguntas

Is creating such a tool feasible? Would it ever work, or are there some obvious problems?

Como algumas respostas acima apontam: Não pode haver uma solução que esteja sempre correta. No entanto, acho que pode haver uma solução que aumente sua eficiência.

Would such a tool be useful? Is it even useful to automatically generate these testcases at all? Could it be extended to do even more useful things?

Isso depende do cenário. Acho que a maneira de escrever testes junto com seu código (TDD) é uma boa maneira de manter o código com qualidade mínima.

Ainda assim, às vezes eu me deparo com grandes bases de código herdadas. Ninguém realmente quer gastar muito tempo com esse código, mas o risco de introduzir um novo código nesse código é bastante alto. Eu acho que poderia ser uma troca válida para usar testes gerados neste cenário.

    
por 11.01.2019 / 08:36
fonte
0

Não é viável, falando com o conhecimento teórico atual em ciência da computação.

A maneira mais fácil de ver isso é colocar em seu código uma conjectura que não foi provada. (Existem muitos deles). Você não sabe se o código pode entrar neste ou naquele ramo, você também não sabe qual valor o deixaria no ramo.

Assim, você não pode realmente gerar casos de teste que entrariam em determinado ramo, mesmo que você tenha acesso ao código-fonte.

E se você acha que é apenas um software, você o obterá se tentar bastante, lembre-se que após um aumento de alguns números na duração da conjetura, o espaço de busca cresce exponencialmente. Então você tem que encontrar outro caminho.

    
por 01.07.2014 / 14:04
fonte