Mejores prácticas para probar métodos protegidos con PHPUnit

287

Encontré el debate sobre ¿Prueba método privado informativo.

He decidido que en algunas clases quiero tener métodos protegidos, pero probarlos. Algunos de estos métodos son estáticos y cortos. Debido a que la mayoría de los métodos públicos los utilizan, probablemente pueda eliminar las pruebas de forma segura más adelante. Pero para comenzar con un enfoque TDD y evitar la depuración, realmente quiero probarlos.

Pensé en lo siguiente:

  • Método Objeto como se aconseja en una respuesta parece ser excesivo para esto.
  • Comience con métodos públicos y cuando la cobertura del código esté dada por pruebas de nivel superior, proteja y elimine las pruebas.
  • Herede una clase con una interfaz comprobable que haga públicos los métodos protegidos

¿Cuál es la mejor práctica? ¿Hay algo mas?

Parece que JUnit cambia automáticamente los métodos protegidos para que sean públicos, pero no tuve una mirada más profunda. PHP no permite esto a través de la reflexión .

GrGr
fuente
Dos preguntas: 1. ¿Por qué debería molestarse en probar la funcionalidad que su clase no expone? 2. Si debe probarlo, ¿por qué es privado?
nad2000
2
Tal vez quiera probar si una propiedad privada se está configurando correctamente y la única forma de probar usando solo la función de establecimiento es hacer pública la propiedad privada y verificar los datos
AntonioCS
44
Y este es un estilo de discusión y, por lo tanto, no constructivo. De nuevo :)
mlvljr
72
Puede llamarlo en contra de las reglas del sitio, pero llamarlo "no constructivo" es ... es insultante.
Andy V
1
@Visser, se está insultando a sí mismo;)
Pacerier

Respuestas:

417

Si está usando PHP5 (> = 5.3.2) con PHPUnit, puede probar sus métodos privados y protegidos utilizando la reflexión para configurarlos para que sean públicos antes de ejecutar sus pruebas:

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
fuente
27
Para citar el enlace al blog de Sebastián: "Entonces: el hecho de que sea posible probar atributos y métodos privados y protegidos no significa que esto sea" algo bueno "". - Solo para tenerlo en cuenta
Edorian
10
Yo contestaría eso. Si no necesita sus métodos protegidos o privados para trabajar, no los pruebe.
uckelman
10
Solo para aclarar, no necesita usar PHPUnit para que esto funcione. También funcionará con SimpleTest o lo que sea. No hay nada en la respuesta que dependa de PHPUnit.
Ian Dunn el
84
No debe probar los miembros protegidos / privados directamente. Pertenecen a la implementación interna de la clase, y no deben combinarse con la prueba. Esto hace que la refactorización sea imposible y, finalmente, no prueba lo que necesita ser probado. Debe probarlos indirectamente utilizando métodos públicos. Si encuentra esto difícil, casi seguro de que hay un problema con la composición de la clase y necesita separarlo en clases más pequeñas. Tenga en cuenta que su clase debe ser un recuadro negro para su examen: arroja algo y recupera algo, ¡y eso es todo!
gphilip
24
@gphilip Para mí, el protectedmétodo también es parte de la API pública porque cualquier clase de terceros puede extenderlo y usarlo sin ningún tipo de magia. Así que creo que solo los privatemétodos entran en la categoría de métodos que no se deben probar directamente. protectedy publicdebe ser probado directamente.
Filip Halaxa
48

Parece que ya lo sabes, pero lo repetiré de todos modos; Es una mala señal, si necesita probar métodos protegidos. El objetivo de una prueba unitaria es probar la interfaz de una clase, y los métodos protegidos son detalles de implementación. Dicho esto, hay casos en los que tiene sentido. Si usa la herencia, puede ver una superclase como una interfaz para la subclase. Entonces, aquí, tendrías que probar el método protegido (pero nunca un método privado uno ). La solución a esto es crear una subclase para fines de prueba y usarla para exponer los métodos. P.ej.:

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

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

Tenga en cuenta que siempre puede reemplazar la herencia con la composición. Cuando se prueba el código, generalmente es mucho más fácil manejar el código que usa este patrón, por lo que es posible que desee considerar esa opción.

Troelskn
fuente
2
Puede implementar directamente cosas () como público y devolver parent :: stuff (). Mira mi respuesta. Parece que hoy estoy leyendo las cosas demasiado rápido.
Michael Johnson el
Tienes razón; Es válido cambiar un método protegido por uno público.
troelskn 01 de
Entonces, el código sugiere mi tercera opción y "Tenga en cuenta que siempre puede reemplazar la herencia con la composición". va en la dirección de mi primera opción o refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr
34
No estoy de acuerdo con que sea una mala señal. Hagamos una diferencia entre TDD y Unit Testing. Las pruebas unitarias deberían probar métodos privados imo, ya que estas son unidades y se beneficiarían de la misma manera que las pruebas unitarias se benefician de los métodos públicos de las pruebas unitarias.
koen
36
Los métodos protegidos son parte de la interfaz de una clase, no son simplemente detalles de implementación. El objetivo de los miembros protegidos es que los subclases (usuarios por derecho propio) puedan usar esos métodos protegidos dentro de las clases. Esos claramente necesitan ser probados.
BT
40

la quemadura tiene el enfoque correcto. Aún más simple es llamar al método directamente y devolver la respuesta:

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

Puede llamar a esto simplemente en sus pruebas:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
robert.egginton
fuente
1
Este es un gran ejemplo, gracias. El método debería ser público en lugar de protegido, ¿no?
valk
Buen punto. De hecho, uso este método en mi clase base desde la que extiendo mis clases de prueba, en cuyo caso esto tiene sentido. Sin embargo, el nombre de la clase estaría mal aquí.
robert.egginton
Hice exactamente el mismo código basado en Teastburn xD
Nebulosar
23

Me gustaría proponer una ligera variación para getMethod () definido en la respuesta de uckelman .

Esta versión cambia getMethod () eliminando valores codificados y simplificando un poco el uso. Recomiendo agregarlo a su clase PHPUnitUtil como en el ejemplo a continuación o a su clase que amplía PHPUnit_Framework_TestCase (o, supongo, globalmente a su archivo PHPUnitUtil).

Como MyClass se está instanciando de todos modos y ReflectionClass puede tomar una cadena o un 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
}

También creé una función de alias getProtectedMethod () para ser explícito lo que se espera, pero eso depende de usted.

¡Salud!

quemadura
fuente
+1 por usar la API Reflection Class.
Bill Ortell
10

Creo que troelskn está cerca. Yo haría esto en su lugar:

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

Luego, implemente algo como esto:

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

Luego ejecuta sus pruebas contra TestClassToTest.

Debería ser posible generar automáticamente tales clases de extensión analizando el código. No me sorprendería si PHPUnit ya ofrece dicho mecanismo (aunque no lo he comprobado).

Michael Johnson
fuente
Je ... parece que estoy diciendo, usa tu tercera opción :)
Michael Johnson
2
Sí, esa es exactamente mi tercera opción. Estoy bastante seguro de que PHPUnit no ofrece ese mecanismo.
GrGr
Esto no funcionará, no puede anular una función protegida con una función pública con el mismo nombre.
Koen
Podría estar equivocado, pero no creo que este enfoque pueda funcionar. PHPUnit (hasta donde lo he usado) requiere que su clase de prueba extienda otra clase que proporcione la funcionalidad de prueba real. A menos que haya una forma de evitarlo, no estoy seguro de poder ver cómo se puede usar esta respuesta. phpunit.de/manual/current/en/…
Cypher
Para su información, esto solo funciona para los métodos protegidos , no para los privados
Sliq
5

Voy a tirar mi sombrero al ring aquí:

He usado el hack de __call con diversos grados de éxito. La alternativa que se me ocurrió fue usar el patrón Visitor:

1: generar una clase stdClass o personalizada (para imponer el tipo)

2: imprima eso con el método y los argumentos requeridos

3: asegúrese de que su SUT tenga un método acceptVisitor que ejecutará el método con los argumentos especificados en la clase visitante

4: inyectarlo en la clase que desea evaluar

5: SUT inyecta el resultado de la operación en el visitante

6: aplique sus condiciones de prueba al atributo de resultado del visitante

sunwukung
fuente
1
+1 para una solución interesante
jsh
5

De hecho, puede usar __call () de forma genérica para acceder a métodos protegidos. Para poder probar esta clase

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

creas una subclase en 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);
    }
}

Tenga en cuenta que el método __call () no hace referencia a la clase de ninguna manera, por lo que puede copiar lo anterior para cada clase con los métodos protegidos que desea probar y simplemente cambiar la declaración de la clase. Es posible que pueda colocar esta función en una clase base común, pero no la he probado.

Ahora, el caso de prueba solo difiere en el lugar donde construye el objeto que se probará, intercambiando en ExampleExposed por Example.

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

Creo que PHP 5.3 le permite usar la reflexión para cambiar la accesibilidad de los métodos directamente, pero supongo que tendría que hacerlo para cada método individualmente.

David Harkness
fuente
1
¡La implementación de __call () funciona muy bien! Traté de votar, pero desestimé mi voto hasta después de probar este método y ahora no se me permite votar debido a un límite de tiempo en SO.
Adam Franco
La call_user_method_array()función está en desuso a partir de PHP 4.1.0 ... use call_user_func_array(array($this, $method), $args)en su lugar. Tenga en cuenta que si está utilizando PHP 5.3.2+ se puede utilizar la reflexión para obtener acceso a métodos protegidos / privadas y atributos
nuqqsa
@nuqqsa - Gracias, actualicé mi respuesta. Desde entonces, he escrito un Accessiblepaquete genérico que utiliza la reflexión para permitir que las pruebas accedan a propiedades privadas / protegidas y métodos de clases y objetos.
David Harkness
Este código no funciona para mí en PHP 5.2.7: el método __call no se invoca para los métodos que define la clase base. No puedo encontrarlo documentado, pero supongo que este comportamiento cambió en PHP 5.3 (donde he confirmado que funciona).
Russell Davis
@Russell: __call()solo se invoca si la persona que llama no tiene acceso al método. Como la clase y sus subclases tienen acceso a los métodos protegidos, las llamadas no se realizarán __call(). ¿Puedes publicar tu código que no funciona en 5.2.7 en una nueva pregunta? Usé lo anterior en 5.2 y solo pasé a usar la reflexión con 5.3.2.
David Harkness
2

Sugiero la siguiente solución para la solución / idea de "Henrik Paul" :)

Conoces nombres de métodos privados de tu clase. Por ejemplo, son como _add (), _edit (), _delete () etc.

Por lo tanto, cuando desee probarlo desde el aspecto de las pruebas unitarias, simplemente llame a métodos privados con el prefijo y / o sufijo algunos comunes palabra (por ejemplo, _addPhpunit) para que cuando se llame al método __call () (ya que el método _addPhpunit () no existe) de la clase de propietario, solo debe poner el código necesario en el método __call () para eliminar la palabra / s con prefijo / sufijo (Phpunit) y luego llamar a ese método privado deducido desde allí. Este es otro buen uso de los métodos mágicos.

Pruébalo.

Anirudh Zala
fuente