É correto introduzir métodos que são usados ​​apenas durante testes de unidade?

12

Recentemente, eu fui TDDing um método de fábrica. O método era criar um objeto simples ou um objeto embrulhado em um decorador. O objeto decorado pode ser de um dos vários tipos, todos estendendo a StrategyClass.

No meu teste, eu queria verificar se a classe do objeto retornado é como o esperado. Isso é fácil quando o objeto comum é retornado, mas o que fazer quando ele é embrulhado em um decorador?

Eu codifico em PHP para que eu pudesse usar ext/Reflectionpara descobrir uma classe de objeto empacotado, mas parecia-me complicar demais as coisas e, de certa forma, repetir as regras do TDD.

Em vez I decidiu introduzir getClassName()que voltaria nome da classe do objeto quando chamado de StrategyClass. Quando chamado do decorador, no entanto, retornaria o valor retornado pelo mesmo método no objeto decorado.

Algum código para deixar mais claro:

interface StrategyInterface {
  public function getClassName();
}

abstract class StrategyClass implements StrategyInterface {
  public function getClassName() {
    return \get_class($this);
  }
}

abstract class StrategyDecorator implements StrategyInterface {

  private $decorated;

  public function __construct(StrategyClass $decorated) {
    $this->decorated = $decorated;
  }

  public function getClassName() {
    return $this->decorated->getClassName();
  }

}

E um teste PHPUnit

 /**
   * @dataProvider providerForTestGetStrategy
   * @param array $arguments
   * @param string $expected
   */
  public function testGetStrategy($arguments, $expected) {

    $this->assertEquals(
      __NAMESPACE__.'\\'.$expected,  
      $this->object->getStrategy($arguments)->getClassName()
    )
  }

//below there's another test to check if proper decorator is being used

Meu argumento aqui é: está correto introduzir métodos que não têm outro uso senão facilitar os testes de unidade? De alguma forma, não parece certo para mim.

Mchl
fonte
Possivelmente, esta pergunta trará mais informações sobre sua pergunta, programmers.stackexchange.com/questions/231237/… , acredito que depende do uso e se os métodos ajudarão muito no teste de unidade e na depuração de qualquer aplicativo que você esteja desenvolvendo. .
Clement Mark-Aaba

Respostas:

13

Meu pensamento é não, você não deve fazer nada apenas porque é necessário para a testabilidade. Muitas decisões tomadas pelas pessoas beneficiam a testabilidade e a testabilidade pode até ser o principal benefício, mas deve ser uma boa decisão de design por outros méritos. Isso significa que algumas propriedades desejadas não são testáveis. Outro exemplo é quando você precisa saber o quão eficiente é a rotina, por exemplo, o Hashmap usa um intervalo de valores de hash distribuído igualmente - nada na interface externa pode lhe dizer isso.

Em vez de pensar: "estou recebendo a classe de estratégia correta" acho "a classe que eu obtenho desempenha o que essa especificação está tentando testar?" É bom quando você pode testar o encanamento interno, mas não precisa, basta testar girar o botão e ver se há água quente ou fria.

Jeremy
fonte
O +1 OP está descrevendo o teste de unidade 'caixa limpa', não o teste de recurso TDD #
Steven A. Lowe
1
Eu vejo o seguinte, embora eu esteja um pouco relutante em adicionar testes dos algoritmos StrategyClass quando eu quiser testar se o método de fábrica está fazendo seu trabalho. Este tipo de quebra IMHO isolamento. Outra razão pela qual quero evitar isso é que essas classes específicas operam no banco de dados; portanto, testá-las requer zombaria / stub.
Mchl
Por outro lado, e à luz desta pergunta: programmers.stackexchange.com/questions/86656/… quando distinguimos "testes TDD" de "testes unitários", isso fica perfeitamente correto (ainda assim o encanamento do banco de dados: P)
Mchl
Se você adicionar os métodos, eles se tornarão parte do contrato com seus usuários. Você terminará com codificadores que chamam suas funções somente de teste e ramificam com base nos resultados. Geralmente, prefiro expor o mínimo possível da classe.
BillThor
5

Minha opinião é que, às vezes, você precisa refazer um pouco o código-fonte para torná-lo mais testável. Não é o ideal e não deve ser uma desculpa para desordenar a interface com funções que devem ser usadas apenas para testes, portanto a moderação geralmente é a chave aqui. Você também não quer estar na situação em que os usuários do seu código repentinamente usam as funções da interface de teste para interações normais com seu objeto.

Minha maneira preferida de lidar com isso (e eu tenho que me desculpar por não poder mostrar como fazer isso no PHP, já que codifico principalmente em linguagens de estilo C) é fornecer as funções 'test' de uma maneira que elas não são. exposto ao mundo externo pelo próprio objeto, mas pode ser acessado por objetos derivados. Para fins de teste, derivaria então uma classe que manipularia a interação com o objeto que realmente quero testar e o teste de unidade usaria esse objeto em particular. Um exemplo em C ++ seria algo parecido com isto:

Classe de tipo de produção:

class ProdObj
{
  ...
  protected:
     virtual bool checkCertainState();
};

Classe auxiliar de teste:

class TestHelperProdObj : public ProdObj
{
  ...
  public:
     virtual bool checkCertainState() { return ProdObj::checkCertainState(); }
};

Dessa forma, você pelo menos está em uma posição em que não precisa expor as funções do tipo 'teste' em seu objeto principal.

Timo Geusch
fonte
Uma abordagem interessante. Eu precisaria ver como eu poderia adaptar isso #
24611
3

Poucos meses atrás, quando coloquei minha máquina de lavar louça recém-comprada, saiu muita água da mangueira, percebi que isso provavelmente se deve ao fato de ter sido testado corretamente na fábrica de onde veio. Não é incomum ver furos de montagem e outras coisas nas máquinas que existem apenas para fins de teste em uma linha de montagem.

O teste é importante, se necessário, basta adicionar algo a ele.

Mas tente algumas das alternativas. Sua opção baseada em reflexão não é tão ruim assim. Você pode ter um acessador virtual protegido para o que precisa e criar uma classe derivada para testar e afirmar. Talvez você possa dividir sua classe e testar diretamente uma classe de folha resultante. Ocultar o método de teste com uma variável do compilador no seu código-fonte também é uma opção (eu mal conheço PHP, não tenho certeza se isso é possível no PHP).

No seu contexto, você pode decidir não testar a composição adequada dentro do decorador, mas testar o comportamento esperado que a decoração deve ter. Talvez isso enfoque um pouco mais o comportamento esperado do sistema e não tanto as especificações técnicas (o que o padrão decorador o comprará do ponto de vista funcional?).

Joppe
fonte
1

Eu sou um novato absoluto no TDD, mas parece depender do método que está sendo adicionado. Pelo que entendi do TDD, seus testes devem "impulsionar" a criação da sua API até certo ponto.

Quando estiver tudo bem:

  • Contanto que o método não quebre o encapsulamento e atenda a um propósito alinhado à responsabilidade do objeto.

Quando não estiver tudo bem:

  • Se o método parece algo que nunca seria útil ou não faz sentido em relação a outros métodos de interface, pode ser incoesivo ou confuso. Nesse ponto, isso prejudicaria minha compreensão desse objeto.
  • Exemplo de Jeremy, "... quando você precisa saber o quão eficiente é a rotina, por exemplo, o seu Hashmap usa um intervalo distribuído de valores de hash uniformemente distribuídos - nada na interface externa pode lhe dizer isso."
filhote
fonte