No TDD, se eu escrever um caso de teste que passa sem modificar o código de produção, o que isso significa?

17

Estas são as regras de Robert C. Martin para TDD :

  • Você não tem permissão para escrever nenhum código de produção, a menos que seja aprovado no teste de unidade.
  • Você não tem permissão para escrever mais testes de unidade que o suficiente para falhar; e falhas de compilação são falhas.
  • Você não tem permissão para escrever mais código de produção que o suficiente para passar no único teste de unidade com falha.

Quando escrevo um teste que parece valer a pena, mas passa sem alterar o código de produção:

  1. Isso significa que fiz algo errado?
  2. Devo evitar escrever esses testes no futuro, se puder ser ajudado?
  3. Devo deixar esse teste lá ou removê-lo?

Nota: Eu estava tentando fazer esta pergunta aqui: Posso começar com um teste de unidade aprovado? Mas eu não era capaz de articular a questão suficientemente bem até agora.

Daniel Kaplan
fonte
O "Kata do jogo de boliche" vinculado no artigo que você cita na verdade tem um teste de aprovação imediata como sua etapa final.
precisa saber é

Respostas:

21

Ele diz que você não pode escrever código de produção, a menos que seja aprovado em um teste de unidade com falha, não que você não possa escrever um teste que passe desde o início. A intenção da regra é dizer "Se você precisar editar o código de produção, certifique-se de escrever ou alterar um teste primeiro".

Às vezes, escrevemos testes para provar uma teoria. O teste passa e isso desmente a nossa teoria. Não removemos o teste. No entanto, podemos (sabendo que temos o suporte do controle de origem) interromper o código de produção, para ter certeza de que entendemos por que passou quando não esperávamos.

Se for um teste válido e correto, e não estiver duplicando um teste existente, deixe-o lá.

pdr
fonte
Melhorar a cobertura de teste do código existente é outro motivo perfeitamente válido para escrever um (espero) aprovação no teste.
Jack
13

Isso significa que:

  1. Você escreveu o código de produção que atende ao recurso que deseja sem escrever o teste primeiro (uma violação do "TDD religioso") ou
  2. O recurso de que você precisa já foi cumprido pelo código de produção e você está apenas escrevendo outro teste de unidade para cobrir esse recurso.

A última situação é mais comum do que você imagina. Como um exemplo completamente ilusório e trivial (mas ainda ilustrativo), digamos que você escreveu o seguinte teste de unidade (pseudocódigo, porque sou preguiçoso):

public void TestAddMethod()
{
    Assert.IsTrue(Add(2,3) == 5);
}

Porque tudo o que você realmente precisa é o resultado de 2 e 3 somados.

Seu método de implementação seria:

public int add(int x, int y)
{
    return x + y;
}

Mas vamos dizer que agora preciso adicionar 4 e 6:

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

Não preciso reescrever meu método, porque ele já cobre o segundo caso.

Agora, digamos que descobri que minha função Adicionar realmente precisa retornar um número que tenha algum limite, digamos 100. Posso escrever um novo método que testa isso:

public void TestAddMethod3()
{
    Assert.IsTrue(Add(100,100) == 100);
}

E este teste agora falhará. Agora devo reescrever minha função

public int add(int x, int y)
{
    var a = x + y;
    return a > 100 ? 100 : a;
}

para fazer passar.

O senso comum determina que, se

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

passa, você não deliberadamente faz com que seu método falhe, apenas para que você possa ter um teste com falha e escrever um novo código para fazer esse teste passar.

Robert Harvey
fonte
5
Se você seguisse completamente os exemplos de Martin (e ele não sugere necessariamente que você o faça), para add(2,3)passar, você literalmente retornaria 5. Codificado. Em seguida, você escreveria o teste para o add(4,6)qual forçaria a escrever o código de produção que faz com que seja aprovado sem quebrar add(2,3)ao mesmo tempo. Você iria acabar com return x + y, mas você não iria começar com ele. Em teoria. Naturalmente, Martin (ou talvez fosse outra pessoa, não me lembro) gosta de fornecer exemplos de educação, mas não espera que você realmente escreva esse código trivial dessa maneira.
Anthony Pegram
1
@tieTYT, geralmente, se bem me lembro do (s) livro (s) de Martin, o segundo caso de teste normalmente seria suficiente para que você escrevesse a solução geral para um método simples (e, na realidade, você realmente faria isso funcionar da maneira mais adequada). primeira vez). Não há necessidade de um terço.
Anthony Pegram
2
@tieTYT, você continuaria escrevendo testes até o fazer. :)
Anthony Pegram 21/03/2013
4
Há uma terceira possibilidade, e isso vai contra o seu exemplo: você escreveu um teste duplicado. Se você seguir o TDD "religiosamente", um novo teste aprovado será, portanto, sempre uma bandeira vermelha. Após o DRY , você nunca deve escrever dois testes que testam essencialmente a mesma coisa.
precisa saber é o seguinte
1
"Se você seguisse completamente os exemplos de Martin (e ele não sugere necessariamente que você o faça), para adicionar add (2,3), você literalmente retornaria 5. Codificado". - este é o pouco estrito TDD que sempre me irritou, a idéia de que você escreve um código que sabe estar errado na expectativa de um teste futuro que venha a ser provado. E se esse teste futuro nunca for escrito, por algum motivo, e os colegas assumirem que "todos os testes são verdes" implica "todos os códigos estão corretos"?
Julia Hayward
2

Seu teste passou, mas você não está errado. Eu acho que aconteceu porque o código de produção não é TDD desde o início.

Suponhamos TDD canônico (?). Não há código de produção, mas alguns casos de teste (é claro que sempre falham). Nós adicionamos código de produção para passar. Então pare aqui para adicionar mais casos de teste de falha. Adicione novamente o código de produção para passar.

Em outras palavras, seu teste pode ser um tipo de teste de funcionalidade, não um simples teste de unidade TDD. Esses são sempre ativos valiosos para a qualidade do produto.

Pessoalmente, não gosto de regras totalitárias e desumanas;

9dan
fonte
2

Na verdade, o mesmo problema surgiu em um dojo na noite passada.

Eu fiz uma pesquisa rápida sobre isso. Isto é o que eu vim com:

Basicamente, não é proibido explicitamente pelas regras do TDD. Talvez sejam necessários alguns testes adicionais para provar que uma função funciona corretamente para uma entrada generalizada. Nesse caso, a prática de TDD é deixada de lado por pouco tempo. Observe que deixar a prática do TDD em breve não necessariamente quebra as regras do TDD, desde que não haja um código de produção adicionado nesse meio tempo.

Testes adicionais podem ser escritos desde que não sejam redundantes. Uma boa prática seria fazer o teste de particionamento de classe de equivalência. Isso significa que os casos de borda e pelo menos um caso interno para cada classe de equivalência são testados.

Um problema que pode ocorrer com essa abordagem, porém, é que, se os testes forem aprovados desde o início, não será possível garantir que não haja falsos positivos. Significando que pode haver testes que passam porque os testes não foram implementados corretamente e não porque o código de produção está funcionando corretamente. Para evitar isso, o código de produção deve ser ligeiramente alterado para interromper o teste. Se isso fizer com que o teste falhe, é provável que o teste seja implementado corretamente e o código de produção possa ser alterado novamente para que o teste passe novamente.

Se você quiser praticar TDD estrito, talvez não escreva nenhum teste adicional que seja aprovado desde o início. Por outro lado, em um ambiente de desenvolvimento corporativo, na verdade, devemos abandonar a prática de TDD se testes adicionais parecerem úteis.

leifbattermann
fonte
0

Um teste que passa sem modificar o código de produção não é inerentemente ruim e geralmente é necessário para descrever um requisito adicional ou caso limite. Enquanto seu teste "parecer valer a pena", como você diz que o seu faz, mantenha-o.

O problema é quando você escreve um teste que já está passando como um substituto para realmente entender o espaço do problema.

Podemos imaginar em dois extremos: um programador que escreve um grande número de testes "apenas no caso" de um erro; e um segundo programador que analisa cuidadosamente o espaço do problema antes de escrever um número mínimo de testes. Digamos que ambos estão tentando implementar uma função de valor absoluto.

O primeiro programador escreve:

assert abs(-88888) == 88888
assert abs(-12345) == 12345
assert abs(-5000) == 5000
assert abs(-32) == 32
assert abs(46) == 46
assert abs(50) == 50
assert abs(5001) == 5001
assert abs(999999) == 999999
...

O segundo programador escreve:

assert abs(-1) == 1
assert abs(0) == 0
assert abs(1) == 1

A implementação do primeiro programador pode resultar em:

def abs(n):
    if n < 0:
        return -n
    elif n > 0:
        return n

A implementação do segundo programador pode resultar em:

def abs(n):
    if n < 0:
        return -n
    else:
        return n

Todos os testes são aprovados, mas o primeiro programador não apenas escreveu vários testes redundantes (diminuindo desnecessariamente o ciclo de desenvolvimento), como também falhou ao testar um caso de limite ( abs(0)).

Se você se encontra escrevendo testes que passam sem modificar o código de produção, pergunte a si mesmo se seus testes estão realmente agregando valor ou se precisa gastar mais tempo entendendo o espaço do problema.

pensador
fonte
Bem, o segundo programador também foi claramente descuidado com os testes, porque seu colega de trabalho redefiniu abs(n) = n*ne passou.
Eiko
@Eiko Você está absolutamente certo. Escrever muito poucos testes pode te morder da mesma maneira. O segundo programador era excessivamente mesquinho por não pelo menos testar abs(-2). Como em tudo, moderação é a chave.
thinkterry