Testar o desenvolvimento orientado ao implementar uma lista de comprimento flexível

5

De acordo com a estratégia comumente usada do TDD, para implementar algo, você escreve um teste que falha primeiro no código, escreve o código mais simples, refaz e repete. Eu estou tentando imaginar este cenário com a implementação de uma lista de comprimento flexível (por exemplo, List<T> em. net.

Digamos que eu teste primeiro inserindo um item. Provavelmente, a maneira mais simples de conseguir isso é fazendo o backup da lista com um array com comprimento 1 (que passará no teste). Nada para refatorar aqui, então eu vou em frente e escrevo outro teste que insira 2 itens. Vou simplesmente mudar o comprimento do array para 2 e o teste passar novamente. Então escrevo o teste com 3 itens, expanda o array e repito novamente. Eu vou acabar sempre fazendo isso até estar cansado.

Esta é uma exceção à estratégia de teste de falha inicial? Ou estou faltando alguma coisa na estratégia acima?

PS: A implementação real faz o backup da lista com uma matriz que cresce duas vezes maior toda vez que o número de elementos excede o tamanho da matriz.

    
por Louis Rhys 31.01.2013 / 09:18
fonte

7 respostas

3

"Escreva a coisa mais simples que poderia funcionar" é um princípio, mas não é o único, e nem sempre é o mais importante. "Não se repita" é sem dúvida mais importante, e repetir-se da maneira que você descreve definitivamente não é justificado pelo "mais simples". Assim que você perceber que está se repetindo, deverá alternar para uma solução que possa lidar com mais de um tamanho de lista.

Note que esta solução pode ainda ser uma matriz de tamanho fixo se você sabe que não precisará mais do que um determinado número de elementos conforme os requisitos. Alternativamente, pode ser uma estrutura dinâmica que cresce com as demandas feitas sobre ela. Mas em nenhum caso é útil escalar o último de inteiros um por um - não menos porque você não pode alcançar o topo.

    
por 31.01.2013 / 09:30
fonte
2

Ok, você está fazendo o que você acha que é a coisa mais simples que funciona, mas você não definiu seu problema adequadamente.

Inverta o problema e veja o que você tem. "Implementar um clone de lista" tem muitas nuances que precisam ser expandidas e documentadas. Qual é o objetivo deste clone List? Que número de itens é esperado para suportar?

Geralmente respostas para estas e outras perguntas surgem no começo. Por exemplo, pergunte-se por que você está implementando uma substituição de lista? Se é apenas um exercício de codificação, você precisa elaborar um cenário um pouco. No mundo real, você pode estar fazendo isso porque a implementação da Lista existente não exibe certas características desejadas. Pode ser muito lento ou não classificado ou incapaz de lidar com um zilhão de itens ou o que nunca. Isso é o que você está faltando aqui.

Para reafirmar o problema, como você saberá que está feito? Quais são seus critérios de aceitação? O que significa "Trabalhar e completar" para este problema? O TDD pode eliminar problemas como o que acontece se você tentar acessar um item na posição "-3.2", mas na verdade não precisa. O que é suposto fazer é certificar-se de que seu código a) faz o que se espera que ele faça e b) continue a fazer o que se espera dele quando você refatorou ou substituiu o código.

    
por 31.01.2013 / 12:22
fonte
1

Você pode usar for nos testes. Eu criaria um teste que insere um item. E, em seguida, outro teste que insere um número aleatório de itens (em que count > tamanho da realocação).

No entanto, eu nunca começaria com um tamanho de matriz interno de 1. Não faz sentido, é uma lista, não é?

    
por 31.01.2013 / 09:30
fonte
1

"I'll go ahead and write another test that insert 2 items. (…) Then I write test with 3 items, expand the array, and repeat again. I will end up forever doing this until I'm tired."

O problema aqui não é que sua implementação de List<T> seja falha, mas que seus testes de unidade estão especificando o tipo errado de implementação.

Se você considerar que esses testes unitários são uma especificação para sua implementação List<T> , você acabou de especificar algo como o seguinte:

  • Pode inserir 1 item na lista vazia, após o qual conterá 1 item.
  • Pode inserir 2 itens na lista vazia, após o que conterá 2 itens.
  • Pode inserir 3 itens na lista vazia, após o que conterá 3 itens.
  • etc.

E assim que seus testes passarem, sua lista poderá cumprir exatamente essa especificação. Mas isso é realmente o que você quer?

Ou prefere talvez uma especificação mais geral, como a seguinte?

  • Pode inserir 1 item na lista vazia, após o qual conterá 1 item.
  • Pode inserir 1 item em uma lista com itens n , após o qual conterá itens n +1.

Em caso afirmativo, escreva testes de unidade diferentes que codifiquem a essa especificação (e você descobrirá que não precisa mais de muitos testes, mas que, na verdade, é feito depois de dois).

    
por 01.09.2013 / 09:46
fonte
0

Se o comprimento flexível é importante, a melhor maneira que posso pensar é escrever um teste geral como função com argumento o número de entradas. Então a natureza do seu problema deve informar quais números são importantes para testar. Bons candidatos são os números 0 e 1. Em geral, você quer testar o que Robert Martin chama de "Condições de contorno" em seu ótimo livro "Código Limpo", descrito como:

Boundary is what separates the known from the unknown

O outro lado do limite no seu caso seria um grande número de entradas, digamos o maior que você pode esperar, para ver que seu sistema pode lidar com isso.

    
por 01.09.2013 / 05:22
fonte
0

TL; DR

O teste é mais uma arte do que uma ciência. Como construir um teste é, na verdade, menos importante do que saber o que é útil para testar.

Comportamento de teste

Comportamento de teste, não detalhes de implementação. Que comportamento você está realmente tentando testar? Com toda a probabilidade, seu recurso precisa fazer apenas duas coisas: criar uma lista e extrair dados da lista.

De um ponto de vista pragmático, não importa realmente como sua lista é implementada. O que você realmente quer saber é se suas entradas resultam em alguma saída esperada. Por exemplo:

  1. Criação de listas

    Dados dos elementos de dados x, y e z
    Quando eu chamo o método de criação com esses elementos
    Então o método retorna uma lista que corresponde ao meu objeto de fixture.

  2. Extração de listas

    Dada uma lista contendo 50 elementos de fixação | Quando olho para o 43º elemento
    Então eu devo encontrar meu valor esperado.

Na maioria dos casos, requisitos não funcionais também podem ser descritos em termos comportamentais.

Condições de limite de teste

Geralmente, não é útil testar o infinito ou testar exaustivamente todas as entradas possíveis ou valores intermediários em um sistema. Por outro lado, muitas vezes é útil pensar em condições de contorno como:

  • Uma lista sem elementos de dados.
  • Uma lista com um número muito pequeno de elementos de dados.
  • Uma lista com um grande número de elementos de dados.

Um teste é realmente uma hipótese que você está validando, por isso é bom fazer suposições documentadas. Se é improvável que você tenha mais de 500 elementos em sua lista, você pode definir limites nos elementos 499, 500 e 501 que valem a pena testar; no entanto, não há quase nenhum valor em testar explicitamente 13.756 elementos neste cenário, porque ele já é coberto pelos seus testes de limite.

    
por 01.09.2013 / 09:28
fonte
0

Acho que você está confundindo testes de unidade com testes baseados em propriedade em sua pergunta.

Testes de unidade olham para uma classe como se ela fosse uma unidade de processamento em uma cadeia de processamento de sinal , como um circuito amplificador ou um rack de efeitos de guitarra. Acho que Kent Beck usou exatamente essa metáfora em seu trabalho " Desenvolvimento Orientado a Testes: Por Exemplo ", mas neste momento não tenho uma cópia do livro para verificar.

O ponto é explicar que é mais fácil verificar se um certo filtro sempre retira freqüências altas, do que verificar se uma combinação específica de muitos filtros e distorções faz com que você soe exatamente como Jimi Hendrix.

Aplicado ao seu caso, você pode querer testar em um teste de unidade que uma classe List pode add elementos conforme necessário. Sendo rigoroso com a filosofia TDD, você deve providenciar que o List já tenha um número aleatório de elementos antes de adicioná-lo - caso contrário, uma implementação mínima pode saber o número total esperado e retornar .

Por outro lado, o teste baseado em propriedade , verifica que uma função é matematicamente bem definida no sentido de que retorna o que é esperado para todo o domínio onde ela está definida (a definição aqui é meu e pode ser impreciso). Você pode ter uma idéia de como isso funciona lendo em QuickCheck e ScalaCheck .

O conceito central é que, se você passar para a estrutura de teste, uma função que tenha uma assinatura digitada, por exemplo, sum(one: Int, another: Int) , então o framework gera muitos testes percorrendo todas as variações possíveis do tipo . No sum example, Int vai de Int.MinValue a Int.MaxValue : o framework escolhe, digamos, 100 pares de valores e tenta sum deles. Para classes mais complexas, você pode precisar dizer ao framework como criar uma instância Arbitrária de sua classe.

Aplicado ao seu caso, você pode querer mostrar que length do seu List é sempre aumentado em um após a operação add . Defina uma Lista Arbitrária como uma Lista que já contém um número aleatório de itens e defina que, após um add , esse número seja aumentado em 1.

O principal benefício do teste baseado em propriedade é que você obtém um lote de testes escrito automaticamente para você, e você pode até controlar a geração para procurar por casos extremos. O teste baseado em propriedade gerará 100 listas e verificará se a invariante é válida; O teste unitário teria de ser repetido 100 vezes para lhe dar uma garantia. No entanto, o teste baseado em propriedade não é uma bala de prata, é apenas um complemento para testes de unidade em determinadas situações.

    
por 14.04.2014 / 12:43
fonte