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 .


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(...));
  ...
}

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.


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)
             );

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!


+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).


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


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.


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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.