O que fazer quando os testes TDD revelam novas funcionalidades necessárias e que também precisam de testes?

13

O que você faz quando está escrevendo um teste e chega ao ponto em que precisa fazer o teste passar e percebe que precisa de uma parte adicional da funcionalidade que deve ser separada em sua própria função? Essa nova função também precisa ser testada, mas o ciclo do TDD diz: Faça um teste falhar, faça-o passar e depois refatorar. Se estou na etapa em que estou tentando fazer meu teste passar, não devo sair e iniciar outro teste com falha para testar a nova funcionalidade que preciso implementar.

Por exemplo, estou escrevendo uma classe de ponto que tem uma função WillCollideWith ( LineSegment ) :

public class Point {
    // Point data and constructor ...

    public bool CollidesWithLine(LineSegment lineSegment) {
        Vector PointEndOfMovement = new Vector(Position.X + Velocity.X,
                                               Position.Y + Velocity.Y);
        LineSegment pointPath = new LineSegment(Position, PointEndOfMovement);
        if (lineSegment.Intersects(pointPath)) return true;
        return false;
    }
}

Eu estava escrevendo um teste para CollidesWithLine quando percebi que precisaria de uma função LineSegment.Intersects ( LineSegment ) . Mas devo parar o que estou fazendo no meu ciclo de teste para criar essa nova funcionalidade? Isso parece quebrar o princípio "Vermelho, Verde, Refatorador".

Devo apenas escrever o código que detecta esse lineSegments Intersect dentro da função CollidesWithLine e refatorá- lo depois que ele estiver funcionando? Isso funcionaria neste caso, pois eu posso acessar os dados do LineSegment , mas e nos casos em que esse tipo de dados é privado?

Joshua Harris
fonte

Respostas:

14

Apenas comente seu teste e seu código recente (ou coloque-o em um esconderijo) para que você realmente volte o relógio para o início do ciclo. Então comece com o LineSegment.Intersects(LineSegment)teste / código / refator. Quando terminar, remova o comentário do código / teste anterior (ou retire-o do stash) e continue com o ciclo.

Javier
fonte
Como isso é diferente, basta ignorá-lo e voltar mais tarde?
18713 Joshua Harris
1
apenas pequenos detalhes: não há teste extra de "ignore-me" nos relatórios e, se você usar stashes, o código será indistinguível do caso "limpo".
9608 Javier
O que é um esconderijo? isso é como o controle de versão?
21475 Joshua Harris
1
alguns VCS o implementam como um recurso (pelo menos Git e Fossil). Ele permite que você remova uma alteração, mas salve-a para reaplicar algum tempo depois. Não é difícil de fazer manualmente, basta salvar um diff e reverter para o último estado. Mais tarde, você reaplicará o diff e continuará.
9608 Javier
6

No ciclo TDD:

Na fase "faça o teste passar", você deve escrever a implementação mais simples que fará o teste passar . Para fazer seu teste passar, você decidiu criar um novo colaborador para lidar com a lógica ausente, porque talvez tenha sido muito trabalho colocar em sua classe de ponto para fazer seu teste passar. É aí que reside o problema. Suponho que o teste que você estava tentando passar foi um grande passo . Portanto, acho que o problema está no seu teste, você deve excluir / comentar esse teste e descobrir um teste mais simples que permita dar um pequeno passo sem introduzir a parte LineSegment.Intersects (LineSegment). Se você passar no teste, poderá refatorarseu código (aqui você aplicará o princípio SRP) movendo essa nova lógica para um método LineSegment.Intersects (LineSegment). Seus testes ainda serão aprovados porque você não alterou nenhum comportamento, mas apenas mudou alguns códigos.

Na sua solução de design atual

Mas para mim, você tem um problema de design mais profundo aqui: está violando o Princípio de Responsabilidade Única . O papel de um Ponto é .... ser um ponto, é tudo. Não há inteligência em ser um ponto, é apenas um valor de xey. Pontos são tipos de valor . É o mesmo para segmentos, segmentos são tipos de valor compostos por dois pontos. Eles podem conter um pouco de "inteligência", por exemplo, para calcular seu comprimento com base na posição dos pontos. Mas é isso.

Agora, decidir se um ponto e um segmento estão colidindo é uma responsabilidade por si só. E certamente é muito trabalho para um ponto ou segmento lidar sozinho. Ele não pode pertencer à classe Point, porque, caso contrário, o Points saberá sobre os segmentos. E não pode pertencer a Segmentos porque os segmentos já têm a responsabilidade de cuidar dos pontos dentro do segmento e talvez também calcular a extensão do próprio segmento.

Portanto, essa responsabilidade deve ser de propriedade de outra classe, como por exemplo um "PointSegmentCollisionDetector", que teria um método como:

bool AreInCollision (ponto p, segmentos)

E isso é algo que você testaria separadamente a partir de pontos e segmentos.

O bom desse design é que agora você pode ter uma implementação diferente do seu detector de colisão. Portanto, seria fácil, por exemplo, avaliar o seu mecanismo de jogo (presumo que você esteja escrevendo um jogo: p) alternando seu método de detecção de colisão em tempo de execução. Ou fazer algumas verificações / experimentos visuais em tempo de execução entre diferentes estratégias de detecção de colisões.

No momento, ao colocar essa lógica em sua classe point, você está bloqueando as coisas e pressionando muita responsabilidade na classe Point.

Espero que faça sentido,

foobarcode
fonte
Está certo que eu estava tentando teste muito grande de uma mudança e eu acho que você está certo sobre separando isso em uma classe de colisão, mas que me faz uma pergunta totalmente novo que você pode ser capaz de me ajudar com: Devo usar uma interface quando os métodos são semelhantes? .
Joshua Harris
2

A coisa mais fácil de se fazer no estilo TDD seria extrair uma interface para o LineSegment e alterar o parâmetro do método para incluir a interface. Em seguida, você pode simular o segmento de linha de entrada e codificar / testar o método Intersect independentemente.

Dan Lyons
fonte
1
Eu sei que esse é o método TDD que eu mais ouço, mas um ILineSegment não faz sentido. Uma coisa é fazer interface com um recurso externo ou algo que pode vir de várias formas, mas não vejo um motivo para anexar alguma funcionalidade a outra coisa que não um segmento de linha.
18713 Joshua Harris
0

Com o jUnit4, você pode usar a @Ignoreanotação para os testes que deseja adiar.

Adicione a anotação a cada método que você deseja adiar e continue escrevendo testes para a funcionalidade necessária. Volte para refatorar os casos de teste mais antigos posteriormente.

bakoyaro
fonte