Práticas recomendadas para testar métodos protegidos com PHPUnit

287

Eu achei a discussão em Você testa o método privado informativo.

Eu decidi que, em algumas classes, quero ter métodos protegidos, mas testá-los. Alguns desses métodos são estáticos e curtos. Como a maioria dos métodos públicos os utiliza, provavelmente poderei remover os testes com segurança mais tarde. Mas, para começar com uma abordagem TDD e evitar a depuração, eu realmente quero testá-los.

Pensei no seguinte:

  • Método Objeto conforme recomendado em uma resposta parece ser um exagero para isso.
  • Comece com métodos públicos e quando a cobertura do código for fornecida por testes de nível superior, proteja-os e remova os testes.
  • Herdar uma classe com uma interface testável tornando públicos métodos protegidos

Qual é a melhor prática? Mais alguma coisa?

Parece que o JUnit altera automaticamente os métodos protegidos para se tornar público, mas não tive uma visão mais aprofundada. O PHP não permite isso via reflexão .

GrGr
fonte
Duas perguntas: 1. por que você deveria se preocupar em testar a funcionalidade que sua classe não expõe? 2. Se você deve testá-lo, por que é privado?
nad2000
2
Talvez ele queira testar se uma propriedade privada está sendo definido corretamente ea única maneira de testar usando apenas a função de levantadora é fazer com que o público propriedade privada e verificando os dados
AntonioCS
4
E, portanto, esse é o estilo de discussão e, portanto, não é construtivo. Novamente :)
mlvljr
72
Você pode chamá-lo contra as regras do site, mas apenas chamá-lo de "não construtivo" é ... é um insulto.
Andy V
1
@Visser, isso é uma ofensa a si mesmo;)
Pacerier

Respostas:

417

Se você estiver usando PHP5 (> = 5.3.2) com PHPUnit, poderá testar seus métodos privados e protegidos usando reflexão para defini-los como públicos antes de executar seus testes:

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}
uckelman
fonte
27
Para citar o link para o blog sebastians: "Então: Só porque o teste de atributos e métodos protegidos e privados é possível não significa que isso é uma" coisa boa "." - Apenas para manter isso em mente
edorian
10
Eu contestaria isso. Se você não precisa que seus métodos protegidos ou privados funcionem, não os teste.
uckelman
10
Apenas para esclarecer, você não precisa usar o PHPUnit para que isso funcione. Também funcionará com o SimpleTest ou o que for. Não há nada sobre a resposta que dependa do PHPUnit.
Ian Dunn
84
Você não deve testar membros protegidos / privados diretamente. Eles pertencem à implementação interna da classe e não devem ser associados ao teste. Isso impossibilita a refatoração e, eventualmente, você não testa o que precisa ser testado. Você precisa testá-los indiretamente usando métodos públicos. Se você achar isso difícil, tenha quase certeza de que há um problema com a composição da classe e precisará separá-lo em classes menores. Lembre-se de que sua turma deve ser uma caixa preta para o seu teste - você joga alguma coisa e recebe algo de volta, e isso é tudo!
Gphilip
24
@ gphilip Para mim, o protectedmétodo também faz parte da API pública, porque qualquer classe de terceiros pode estendê-lo e usá-lo sem nenhuma mágica. Então, acho que apenas privatemétodos se enquadram na categoria de métodos a não serem diretamente testados. protectede publicdeve ser testado diretamente.
Filip Halaxa
48

Você parece já estar ciente, mas eu vou reafirmar de qualquer maneira; É um mau sinal, se você precisar testar métodos protegidos. O objetivo de um teste de unidade é testar a interface de uma classe, e métodos protegidos são detalhes de implementação. Dito isto, há casos em que faz sentido. Se você usar herança, poderá ver uma superclasse como fornecendo uma interface para a subclasse. Então, aqui, você teria que testar o método protegido (mas nunca privado ). A solução para isso é criar uma subclasse para fins de teste e usar isso para expor os métodos. Por exemplo.:

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

Observe que você sempre pode substituir herança por composição. Ao testar o código, geralmente é muito mais fácil lidar com o código que usa esse padrão; portanto, convém considerar essa opção.

Troelskn
fonte
2
Você pode apenas implementar diretamente o material () como público e retornar parent :: stuff (). Veja minha resposta. Parece que estou lendo as coisas muito rapidamente hoje.
Michael Johnson
Você está certo; É válido alterar um método protegido para público.
troelskn
Portanto, o código sugere minha terceira opção e "Observe que você sempre pode substituir herança por composição". vai na direção da minha primeira opção ou refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr
34
Não concordo que seja um mau sinal. Vamos fazer a diferença entre TDD e testes de unidade. O teste de unidade deve testar métodos privados, já que são unidades e se beneficiaria da mesma maneira que os métodos públicos de teste de unidade se beneficiarão do teste de unidade.
Koen
36
Métodos protegidos fazem parte da interface de uma classe, não são apenas detalhes de implementação. O objetivo dos membros protegidos é que subclassers (usuários por direito próprio) possam usar esses métodos protegidos dentro de classes. Aqueles claramente precisam ser testados.
BT
40

o teastburn tem a abordagem correta. Ainda mais simples é chamar o método diretamente e retornar a resposta:

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

Você pode chamar isso simplesmente em seus testes:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
robert.egginton
fonte
1
Este é um ótimo exemplo, obrigado. O método deve ser público em vez de protegido, não deveria?
Valk 26/12
Bom ponto. Na verdade, eu uso esse método na minha classe base da qual estendo minhas classes de teste; nesse caso, isso faz sentido. O nome da classe estaria errado aqui.
robert.egginton
Eu fiz a mesma peça exata do código baseado em xD teastburn
Nebulosar
23

Eu gostaria de propor uma pequena variação para getMethod () definido na resposta de uckelman .

Essa versão altera o getMethod () removendo valores codificados e simplificando um pouco o uso. Eu recomendo adicioná-lo à sua classe PHPUnitUtil como no exemplo abaixo ou à sua classe de extensão PHPUnit_Framework_TestCase (ou, suponho, globalmente ao seu arquivo PHPUnitUtil).

Como o MyClass está sendo instanciado de qualquer maneira e o ReflectionClass pode usar uma string ou um objeto ...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

Também criei uma função de apelido getProtectedMethod () para ser explícito o que é esperado, mas isso depende de você.

Felicidades!

queima de chá
fonte
+1 para usar a API da classe de reflexão.
Bill Ortell
10

Acho troelskn está perto. Eu faria isso:

class ClassToTest
{
   protected function testThisMethod()
   {
     // Implement stuff here
   }
}

Em seguida, implemente algo como isto:

class TestClassToTest extends ClassToTest
{
  public function testThisMethod()
  {
    return parent::testThisMethod();
  }
}

Você então executa seus testes no TestClassToTest.

Deveria ser possível gerar automaticamente essas classes de extensão analisando o código. Eu não ficaria surpreso se o PHPUnit já oferecer esse mecanismo (embora eu não tenha verificado).

Michael Johnson
fonte
Heh ... parece que eu estou dizendo, use a sua terceira opção :)
Michael Johnson
2
Sim, essa é exatamente a minha terceira opção. Tenho certeza de que o PHPUnit não oferece esse mecanismo.
GrGr 3/11/08
Isso não vai funcionar, você não pode substituir uma função protegida por uma função pública com o mesmo nome.
Koen.
Posso estar errado, mas não acho que essa abordagem possa funcionar. O PHPUnit (tanto quanto eu o usei) requer que sua classe de teste estenda outra classe que forneça a funcionalidade de teste real. A menos que haja uma maneira de contornar isso, não tenho certeza de que posso ver como essa resposta pode ser usada. phpunit.de/manual/current/en/…
Cypher
FYI funciona este onl para protegidas métodos, não para os privados
SliQ
5

Vou jogar meu chapéu no ringue aqui:

Eu usei o __call hack com graus variados de sucesso. A alternativa que eu criei foi usar o padrão Visitor:

1: gerar uma classe personalizada ou stdClass (para aplicar o tipo)

2: prepare isso com o método e argumentos necessários

3: verifique se o seu SUT possui um método acceptVisitor que executará o método com os argumentos especificados na classe visitante

4: injete na classe que você deseja testar

5: SUT injeta o resultado da operação no visitante

6: aplique suas condições de teste ao atributo de resultado do Visitante

sunwukung
fonte
1
+1 para uma solução interessante
jsh
5

Você pode realmente usar __call () de maneira genérica para acessar métodos protegidos. Para poder testar esta classe

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

você cria uma subclasse em ExampleTest.php:

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

Observe que o método __call () não faz referência à classe de forma alguma, portanto você pode copiar o acima para cada classe com os métodos protegidos que deseja testar e apenas alterar a declaração da classe. Você pode colocar essa função em uma classe base comum, mas eu não tentei.

Agora, o caso de teste em si difere apenas no local em que você constrói o objeto a ser testado, trocando ExampleExposed por Example.

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

Acredito que o PHP 5.3 permita que você use a reflexão para alterar diretamente a acessibilidade dos métodos, mas presumo que você precise fazer isso para cada método individualmente.

David Harkness
fonte
1
A implementação __call () funciona muito bem! Tentei votar, mas cancelei meu voto até depois de testar esse método e agora não tenho permissão para votar devido a um limite de tempo no SO.
Adam Franco
A call_user_method_array()função está obsoleta a partir do PHP 4.1.0 ... use em seu call_user_func_array(array($this, $method), $args)lugar. Note que se você estiver usando PHP 5.3.2+ você pode usar o reflexo para ter acesso a protegidas métodos / privadas e atributos
nuqqsa
@nuqqsa - Obrigado, atualizei minha resposta. Desde então, escrevi um Accessiblepacote genérico que usa reflexão para permitir que testes acessem propriedades e métodos privados / protegidos de classes e objetos.
David Harkness
Este código não funciona para mim no PHP 5.2.7 - o método __call não é chamado para métodos que a classe base define. Não consigo encontrá-lo documentado, mas acho que esse comportamento foi alterado no PHP 5.3 (onde confirmei que funciona).
Russell Davis
@ Russell - __call()só é chamado se o chamador não tiver acesso ao método. Como a classe e suas subclasses têm acesso aos métodos protegidos, as chamadas para eles não serão atendidas __call(). Você pode postar seu código que não funciona no 5.2.7 em uma nova pergunta? Eu usei o acima em 5.2 e só mudei para usar reflexão com 5.3.2.
David Harkness
2

Sugiro que você siga a solução alternativa para a solução / idéia de "Henrik Paul" :)

Você conhece nomes de métodos particulares de sua classe. Por exemplo, eles são como _add (), _edit (), _delete () etc.

Portanto, quando você quiser testá-lo a partir do aspecto do teste de unidade, basta chamar métodos privados prefixando e / ou sufixando alguma palavra comum (por exemplo, _addPhpunit) para que quando o método __call () for chamado (já que o método _addPhpunit () não existe) da classe do proprietário, basta colocar o código necessário no método __call () para remover as palavras com prefixo / sufixo (Phpunit) e depois chamar esse método privado deduzido a partir daí. Este é outro bom uso de métodos mágicos.

Experimente.

Anirudh Zala
fonte