Como fazer TDD para algo com muitas permutações?


15

Ao criar um sistema como uma IA, que pode seguir muitos caminhos diferentes muito rapidamente, ou realmente qualquer algoritmo que possua várias entradas diferentes, o conjunto de resultados possível pode conter um grande número de permutações.

Que abordagem deve ser adotada para usar o TDD ao criar um sistema que produz muitas e diferentes permutações de resultados?


1
A qualidade geral do sistema de IA é geralmente medida pelo teste Precision-Recall com um conjunto de entradas de referência. Este teste está aproximadamente no mesmo nível dos "testes de integração". Como outros já mencionaram, é mais como "pesquisa de algoritmo orientado a teste" em vez de " design orientado a teste ".
rwong

Por favor, defina o que você quer dizer com "AI". É um campo de estudo mais do que qualquer tipo específico de programa. Para determinadas implementações de IA, geralmente você não pode testar alguns tipos de coisas (ou seja: comportamento emergente) via TDD.
Steven Evers

@SnOrfus Eu falo no sentido mais geral e rudimentar, uma máquina de tomada de decisão.
22411 Nicole

Respostas:


7

Adotando uma abordagem mais prática da resposta do pdr . TDD é tudo sobre design de software, e não testes. Você usa testes de unidade para verificar seu trabalho à medida que avança.

Portanto, em um nível de teste de unidade, é necessário projetar as unidades para que possam ser testadas de maneira completamente determinística. Você pode fazer isso pegando qualquer coisa que torne a unidade não determinística (como um gerador de números aleatórios) e abstraindo-a. Digamos que temos um exemplo ingênuo de um método que decide se uma jogada é boa ou não:

class Decider {

  public boolean decide(float input, float risk) {

      float inputRand = Math.random();
      if (inputRand > input) {
         float riskRand = Math.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider();
d.decide(0.1337f, 0.1337f);

Esse método é muito difícil de testar e a única coisa que você realmente pode verificar nos testes de unidade são seus limites ... mas isso exige muitas tentativas para chegar aos limites. Então, em vez disso, vamos abstrair a parte aleatória criando uma interface e uma classe concreta que agrupa a funcionalidade:

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

   public float random() {
      return Math.random();
   }

}

A Deciderclasse agora precisa usar a classe concreta através de sua abstração, ou seja, a Interface. Essa maneira de fazer as coisas é chamada injeção de dependência (o exemplo abaixo é um exemplo de injeção de construtor, mas você pode fazer isso com um setter também):

class Decider {

  IRandom irandom;

  public Decider(IRandom irandom) { // constructor injection
      this.irandom = irandom;
  }

  public boolean decide(float input, float risk) {

      float inputRand = irandom.random();
      if (inputRand > input) {
         float riskRand = irandom.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider(new ConcreteRandom);
d.decide(0.1337f, 0.1337f);

Você pode se perguntar por que esse "inchaço do código" é necessário. Bem, para iniciantes, agora você pode zombar do comportamento da parte aleatória do algoritmo porque o Decideragora possui uma dependência que segue o IRandom"contrato" do s. Você pode usar uma estrutura de simulação para isso, mas este exemplo é simples o suficiente para se codificar:

class MockedRandom() implements IRandom {

    public List<Float> floats = new ArrayList<Float>();
    int pos;

   public void addFloat(float f) {
     floats.add(f);
   }

   public float random() {
      float out = floats.get(pos);
      if (pos != floats.size()) {
         pos++;
      }
      return out;
   }

}

A melhor parte é que isso pode substituir completamente a implementação concreta "real". O código se torna fácil de testar assim:

@Before void setUp() {
  MockedRandom mRandom = new MockedRandom();

  Decider decider = new Decider(mRandom);
}

@Test
public void testDecisionWithLowInput_ShouldGiveFalse() {

  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandButLowRiskRand_ShouldGiveFalse() {

  mRandom.addFloat(1f);
  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandAndHighRiskRand_ShouldGiveTrue() {

  mRandom.addFloat(1f);
  mRandom.addFloat(1f);

  assertTrue(decider.decide(0.1337f, 0.1337f));
}

Espero que isso lhe dê idéias sobre como projetar seu aplicativo para que as permutações possam ser forçadas, para que você possa testar todos os casos extremos e outros enfeites.


3

O TDD estrito tende a quebrar um pouco em sistemas mais complexos, mas isso não importa muito em termos práticos - uma vez que você vai além de poder isolar entradas individuais, basta escolher alguns casos de teste que fornecem cobertura razoável e os utilizam.

Isso requer algum conhecimento do que a implementação fará bem, mas isso é mais uma preocupação teórica - é altamente improvável que você construa uma IA que foi especificada em detalhes por usuários não técnicos. Está na mesma categoria que passar os testes codificados para os casos de teste - oficialmente o teste é a especificação e a implementação é a solução correta e a mais rápida possível, mas na verdade nunca acontece.


2

TDD não é sobre testes, é sobre design.

Longe de desmoronar com a complexidade, é excelente nessas circunstâncias. Isso levará você a considerar o problema maior em peças menores, o que levará a um design melhor.

Não tente testar todas as permutações do seu algoritmo. Apenas crie teste após teste, escreva o código mais simples para fazer o teste funcionar, até que você tenha suas bases cobertas. Você deve entender o que quero dizer sobre resolver o problema, porque você será incentivado a fingir partes do problema enquanto estiver testando outras partes, para evitar ter que escrever 10 bilhões de testes para 10 bilhões de permutações.

Editar: eu queria adicionar um exemplo, mas não tinha tempo antes.

Vamos considerar um algoritmo de classificação no local. Poderíamos ir em frente e escrever testes que cobrem a extremidade superior da matriz, a extremidade inferior da matriz e todos os tipos de combinações estranhas no meio. Para cada um, teríamos que construir uma matriz completa de algum tipo de objeto. Isso levaria tempo.

Ou podemos resolver o problema em quatro partes:

  1. Atravesse a matriz.
  2. Compare os itens selecionados.
  3. Trocar itens.
  4. Coordene os três acima.

A primeira é a única parte complicada do problema, mas, abstraindo-o do resto, você o tornou muito, muito mais simples.

O segundo é quase certamente tratado pelo próprio objeto, pelo menos opcionalmente, em muitas estruturas de tipo estático, haverá uma interface para mostrar se essa funcionalidade está implementada. Então você não precisa testar isso.

O terceiro é incrivelmente fácil de testar.

O quarto apenas lida com dois ponteiros, solicita à classe de passagem que os mova, pede uma comparação e, com base no resultado dessa comparação, pede que os itens sejam trocados. Se você falsificou os três primeiros problemas, pode testá-lo com muita facilidade.

Como levamos a um design melhor aqui? Digamos que você tenha simplificado e implementado uma classificação de bolha. Funciona, mas, quando você vai para a produção e precisa lidar com um milhão de objetos, é muito lento. Tudo o que você precisa fazer é escrever uma nova funcionalidade transversal e trocá-la. Você não precisa lidar com a complexidade de lidar com os outros três problemas.

Você encontrará a diferença entre testes de unidade e TDD. O testador de unidade dirá que isso tornou seus testes frágeis, que se você tivesse testado entradas e saídas simples, agora não precisaria escrever mais testes para sua nova funcionalidade. O TDDer dirá que eu separei as preocupações adequadamente para que cada classe que eu faça faça uma coisa e uma coisa bem.


1

Não é possível testar todas as permutações de um cálculo com muitas variáveis. Mas isso não é novidade, sempre foi verdade para qualquer programa acima da complexidade dos brinquedos. O objetivo dos testes é verificar a propriedade da computação. Por exemplo, classificar uma lista com 1000 números exige algum esforço, mas qualquer solução individual pode ser verificada com muita facilidade. Agora, embora existam 1000! possíveis (classes de) entradas para esse programa e você não pode testá-las todas, é completamente suficiente gerar apenas 1000 entradas aleatoriamente e verificar se a saída está realmente classificada. Por quê? Como é quase impossível escrever um programa que classifique de forma confiável 1000 vetores gerados aleatoriamente sem também estar correto em geral (a menos que você o monte deliberadamente para manipular determinadas entradas mágicas ...)

Agora, em geral, as coisas são um pouco mais complicadas. Há realmente ter sido erros onde um mailer não iria entregar e-mails aos usuários se eles têm um 'f' em seu nome de usuário e o dia da semana é sexta-feira. Mas considero um esforço desperdiçado tentar antecipar tal estranheza. Seu conjunto de testes deve fornecer uma confiança constante de que o sistema faz o que você espera nas entradas que você espera. Se funcionar em certos casos, você notará logo depois de experimentar o primeiro caso e poderá escrever um teste especificamente contra esse caso (que geralmente também abrange uma classe inteira de casos semelhantes).


Como você gera 1000 entradas aleatoriamente, como você testa as saídas? Certamente esse teste envolverá alguma lógica, que por si só não é testada. Então você testa o teste? Quão? A questão é que você deve testar a lógica usando transições de estado - dada a entrada X, a saída deve ser Y. Um teste que envolve a lógica é propenso a erros, tanto quanto a lógica que ela testa. Em termos lógicos, justificar um argumento por outro argumento o coloca no caminho de regressão cético - você deve fazer algumas afirmações. Essas afirmações são seus testes.
Izhaki

0

Veja os casos extremos mais alguma entrada aleatória.

Para pegar o exemplo de classificação:

  • Classifique algumas listas aleatórias
  • Pegue uma lista que já está classificada
  • Faça uma lista que está na ordem inversa
  • Faça uma lista quase ordenada

Se funcionar rápido para eles, você pode ter certeza de que funcionará para todas as entradas.

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.