O que acontece com os testes dos métodos quando esse método se torna privado após o novo design no TDD?

29

Digamos que eu comece a desenvolver um RPG com personagens que atacam outros personagens e esse tipo de coisa.

Aplicando o TDD, faço alguns casos de teste para testar a lógica dentro do Character.receiveAttack(Int)método. Algo assim:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Digamos que eu tenha 10 métodos, receiveAttackmétodo de teste . Agora, adiciono um método Character.attack(Character)(que chama receiveAttackmétodo) e, após alguns ciclos de TDD testá-lo, tomo uma decisão: Character.receiveAttack(Int)deveria private.

O que acontece com os 10 casos de teste anteriores? Devo excluí-los? Devo manter o método public(acho que não)?

Esta questão não é sobre como testar métodos privados, mas como lidar com eles após um novo design ao aplicar o TDD

Hector
fonte
2
Possível duplicado de métodos privados de provas como protegida
mosquito
10
Se é privado, você não o testa, é fácil. Retirar e fazer a dança refactor
kayess
6
Provavelmente estou indo contra a corrente aqui. Mas geralmente evito métodos privados a todo custo. Eu prefiro mais testes do que menos testes. Eu sei o que as pessoas estão pensando "O que, para que você nunca tenha nenhum tipo de funcionalidade que não queira expor ao consumidor?". Sim, tenho muitas coisas que não quero expor. Em vez disso, quando eu tenho um método privado, refatoro-o para sua própria classe e uso a classe da classe original. A nova classe pode ser marcada como internalou equivalente ao seu idioma para impedir que seja exposta. De fato, a resposta de Kevin Cline é esse tipo de abordagem.
user9993
3
@ user9993 você parece tê-lo ao contrário. Se é importante que você faça mais testes, a única maneira de garantir que você não tenha perdido nada importante é executar a análise de cobertura. E para ferramentas de cobertura, não importa se o método é privado ou público ou qualquer outra coisa. Esperando que tornar público material vai de alguma forma compensar a falta de análise de cobertura dá uma falsa sensação de segurança que eu tenho medo
mosquito
2
@gnat Mas eu nunca disse nada sobre "não ter cobertura"? Meu comentário sobre "Prefiro mais testes do que menos testes" deveria ter tornado isso óbvio. Não tenho certeza do que você está recebendo exatamente, é claro que também testo o código que extraí. Esse é o ponto.
user9993

Respostas:

52

No TDD, os testes servem como documentação executável do seu design. Seu design mudou, portanto, obviamente, sua documentação também deve!

Observe que, no TDD, a única maneira pela qual o attackmétodo poderia ter aparecido é o resultado de uma reprovação no teste. O que significa que attackestá sendo testado por algum outro teste. O que significa que indiretamente receiveAttack é coberto pelos attacktestes de. Idealmente, qualquer alteração receiveAttackdeve interromper pelo menos um dos attacktestes.

E, se isso não acontecer, existe uma funcionalidade receiveAttackque não é mais necessária e não deve mais existir!

Portanto, como receiveAttackjá foi testado attack, não importa se você mantém ou não os testes. Se sua estrutura de teste facilitar o teste de métodos particulares, e se você decidir testar métodos particulares, poderá mantê-los. Mas você também pode excluí-los sem perder a cobertura e a confiança do teste.

Jörg W Mittag
fonte
14
Esta é uma boa resposta, exceto "Se sua estrutura de teste facilitar o teste de métodos particulares e se você decidir testar métodos particulares, poderá mantê-los". Métodos particulares são detalhes de implementação e nunca devem ser testados diretamente.
David Arno
20
@ DavidArno: Eu discordo, por esse raciocínio as partes internas de um módulo nunca devem ser testadas. No entanto, as partes internas de um módulo podem ser de grande complexidade e, portanto, ter testes de unidade para cada funcionalidade interna individual pode ser valioso. Os testes de unidade são usados ​​para verificar os invariantes de uma parte da funcionalidade, se um método privado tiver invariantes (pré-condições / pós-condições), um teste de unidade pode ser valioso.
Matthieu M.
8
" por esse raciocínio, as partes internas de um módulo nunca devem ser testadas ". Esses internos nunca devem ser testados diretamente . Todos os testes devem testar apenas APIs públicas. Se um elemento interno estiver inacessível por meio de uma API pública, exclua-o, pois não faz nada.
David Arno
28
@ DavidArno Por essa lógica, se você estiver construindo um executável (e não uma biblioteca), não deverá ter nenhum teste de unidade. - "Chamadas de função não fazem parte da API pública! Somente argumentos de linha de comando são! Se uma função interna do seu programa estiver inacessível por meio de um argumento de linha de comando, exclua-a, pois ela não faz nada." - Embora as funções privadas não façam parte da API pública da classe, elas fazem parte da API interna da classe. E embora você não precise necessariamente testar a API interna de uma classe, é possível, usando a mesma lógica para testar a API interna de um executável.
RM
7
@RM, se eu fosse criar um executável de maneira não modular, seria forçado a escolher entre testes frágeis dos internos, ou apenas usando testes de integração usando as E / S executáveis ​​e de tempo de execução. Portanto, pela minha lógica atual, e não pela sua versão strawman, eu a criaria de forma modular (por exemplo, através de um conjunto de bibliotecas). As APIs públicas desses módulos podem ser testadas de maneira não frágil.
David Arno
23

Se o método for complexo o suficiente para precisar de teste, ele deve ser público em alguma classe. Então você refatora de:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

para:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Mova o teste atual para X.complexity para ComplexityTest. Em seguida, envie um texto para X.something zombando da Complexidade.

Na minha experiência, a refatoração para classes menores e métodos mais curtos traz enormes benefícios. Eles são mais fáceis de entender, mais fáceis de testar e acabam sendo reutilizados mais do que se poderia esperar.

Kevin Cline
fonte
Sua resposta explica muito mais claramente a idéia que eu estava tentando explicar no meu comentário sobre a pergunta do OP. Boa resposta.
user9993
3
Obrigado pela sua resposta. Na verdade, o método receiveAttack é bastante simples ( this.health = this.health - attackDamage). Talvez extraí-lo para outra classe seja uma solução com excesso de engenharia, para este momento.
Héctor
1
Definitivamente, isso é um exagero para o OP - ele quer ir até a loja, não voar para a lua -, mas uma boa solução no caso geral.
Se a função é tão simples, talvez seja uma engenharia excessiva que ela seja definida como uma função em primeiro lugar.
David K
1
pode ser um exagero hoje, mas dentro de seis meses, quando houver muitas alterações nesse código, os benefícios serão claros. E em qualquer IDE decente nos dias de hoje, certamente a extração de algum código para uma classe separada deve ser um par de pressionamentos de tecla, no máximo, uma solução excessivamente projetada, considerando que no binário de tempo de execução tudo se resumirá à mesma.
Stephen Byrne
6

Digamos que eu tenha 10 métodos testando o método receiveAttack. Agora, adiciono um método Character.attack (Character) (que chama o método receiveAttack) e, após alguns ciclos de TDD testá-lo, tomo uma decisão: Character.receiveAttack (Int) deve ser privado.

Algo a ter em mente aqui é que a decisão que você está tomando é remover um método da API . As cortesias de compatibilidade com versões anteriores sugeririam

  1. Se você não precisar removê-lo, deixe-o na API
  2. Se você não precisa removê-lo ainda , em seguida, marque-o como obsoleto e se possível documento quando o fim da vida vai acontecer
  3. Se você precisar removê-lo, terá uma grande alteração de versão

Os testes são removidos / ou substituídos quando sua API não oferece mais suporte ao método. Nesse ponto, o método privado é um detalhe de implementação que você deve poder refatorar.

Nesse ponto, você está de volta à questão padrão de saber se o seu conjunto de testes deve acessar diretamente implementações, em vez de interagir puramente por meio da API pública. Um método privado é algo que poderíamos substituir sem que a suíte de testes atrapalhasse . Então, eu esperaria que os testes em questão desaparecessem - sendo aposentados ou movendo-se com a implementação para um componente testável separadamente.

VoiceOfUnreason
fonte
3
A depreciação nem sempre é uma preocupação. Da pergunta: "Digamos que eu comece a desenvolver ..." se o software ainda não foi lançado, a reprovação é um problema. Além disso: "um jogo de papéis" implica que esta não é uma biblioteca reutilizável, mas um software binário destinado aos usuários finais. Embora alguns softwares de usuário final tenham uma API pública (por exemplo, MS Office), a maioria não. Mesmo o software que faz ter uma API pública só tem uma parte dele exposto para plugins, scripting (por exemplo, jogos com extensão LUA), ou outras funções. Ainda assim, vale a pena trazer a idéia para o caso geral que o OP descreve.