2008-10-30 10 views
221

Encontré la discusión en Do you test private method informativa.Mejores prácticas para probar métodos protegidos con PHPUnit

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 usan, 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:

  • Method Object como aconsejó en an answer parece ser excesiva para esto.
  • Comience con los métodos públicos y cuando la cobertura del código esté dada por pruebas de nivel superior, gírelas protegidas y elimine las pruebas.
  • heredar una clase con una interfaz comprobable métodos de toma protegidas públicas

¿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 lo he examinado más detenidamente. PHP no permite esto a través del reflection.

+3

Y lo que esta es la discusión de estilo y por lo tanto no constructiva. De nuevo :) – mlvljr

+58

Puedes llamarlo en contra de las reglas del sitio, pero simplemente llamarlo "no constructivo" es ... es insultante. – Visser

+0

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

Respuesta

346

Si está utilizando PHP5 (> = 5.3.2) con PHPUnit, usted puede probar sus métodos privados y protegidos mediante el uso de la reflexión para ponerlos a ser público antes de ejecutar las 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(...)); 
    ... 
} 
+0

Simple y elegante, me gusta. – Drew

+1

Buena solución; no es necesario cambiar el código que se está probando, me gusta. – bouke

+4

Muy buena solución. Puede agregar sin embargo que esto es php> = 5.3.2 solamente;) – flungabunga

37

Parece que ya lo sabe, pero lo repito 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 los detalles de implementación. Dicho esto, hay casos en los que tiene sentido. Si usa herencia, puede ver una superclase como una interfaz para la subclase. Así que aquí, tendría que probar el método protegido (pero nunca uno privado uno). La solución a esto, es crear una subclase para fines de prueba, y usar esto para exponer los métodos. Ejemplo:

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 tratar el código que usa este patrón, por lo que es posible que desee considerar esa opción.

+2

Puede directamente implementar cosas() como public y return parent :: stuff(). Ver mi respuesta. Parece que estoy leyendo cosas demasiado rápido hoy. –

+0

Tienes razón; Es válido cambiar un método protegido por uno público. – troelskn

+0

Así que 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 http://www.refactoring.com/catalog/replaceInheritanceWithDelegation.html – GrGr

7

Creo que troelskn está cerca. Me gustaría hacer esto en su lugar:

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

A continuación, poner en práctica algo como esto:

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

A continuación, ejecutar las pruebas contra TestClassToTest.

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

+0

Heh ... parece que estoy diciendo, use su tercera opción :) –

+1

Sí, esa es exactamente mi tercera opción. Estoy bastante seguro de que PHPUnit no ofrece ese mecanismo. – GrGr

+0

Esto no funcionará, no puede anular una función protegida con una función pública con el mismo nombre. –

1

que sugieren siguiente solución para "Henrik Paul" 's solución/idea :)

Usted sabe los nombres de los métodos privados de su clase. Por ejemplo, son como _add(), _edit(), _delete() etc.

De ahí que cuando se quiere probar desde los aspectos de la unidad de pruebas, a llamar a los métodos privados con el prefijo y/o sufijo algunos común palabra (por ejemplo _addPhpunit) de manera que cuando se llama __call() método (puesto que el método _addPhpunit() no existe) de la clase propietaria, usted acaba de poner el código necesario en el método __call() para eliminar la palabra/s prefijada/sufijada (Phpunit) y luego llamar a ese método privado deducido desde allí. Este es otro buen uso de los métodos mágicos.

Pruébelo.

2

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

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

se crea una subclase de 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 alguna manera para que pueda copiar el más arriba para cada clase con métodos protegidos que desea probar y simplemente cambie la declaración de clase. Es posible que pueda colocar esta función en una clase base común, pero no lo he intentado.

Ahora el caso de prueba solo difiere en donde se construye el objeto que se va a probar, intercambiándolo en Ejemplo expuesto para Ejemplo.

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

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

+0

¡La implementación de __call() funciona muy bien! Traté de votar, pero desactivé mi voto hasta después de que probé este método y ahora no tengo permitido votar debido a un límite de tiempo en SO. –

+0

La función 'call_user_method_array()' está obsoleta desde PHP 4.1.0 ... use 'call_user_func_array (array ($ this, $ method), $ args)' en su lugar. Tenga en cuenta que si usa PHP 5.3.2+ puede usar Reflection para [obtener acceso a métodos y atributos protegidos/privados] (http://sebastian-bergmann.de/archives/881-Testing-Your-Privates.html) – nuqqsa

+0

@nuqqsa - Gracias, actualicé mi respuesta.Desde entonces he escrito un paquete genérico 'Accesible' que usa la reflexión para permitir que las pruebas accedan a propiedades privadas y protegidas y a métodos de clases y objetos. –

2

Voy a lanzar mi sombrero al ring aquí:

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

1: generar un stdClass o clase personalizada (para hacer cumplir tipo)

2: Primer que con el método y los argumentos requeridos

3 : asegurarse de que el SUT tiene un método acceptVisitor que ejecutará el método con los argumentos especificados en la clase visitar

4: inyectarlo en la clase que desea probar

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

6: aplicar sus condiciones de prueba a consecuencia de Visitantes atribuir

+1

+1 para una solución interesante – jsh

15

me gustaría proponer una ligera variación a getMethod() definido en uckelman's answer.

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

Desde MiClase se crea una instancia 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 que uno de hasta tú.

¡Salud!

+0

+1 para usar la clase de Reflection API. –

26

teastburn tiene el enfoque correcto. Aún más simple es llamar al método directo 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); 
    } 
} 

Usted puede llamar a esto simplemente en sus pruebas por:

$returnVal = PHPUnitUtil::callMethod(
       $this->object, 
       '_nameOfProtectedMethod', 
       array($arg1, $arg2) 
      ); 
+1

Este es un gran ejemplo, gracias. El método debe ser público en lugar de estar protegido, ¿no es así? – valk

+0

Buen punto. De hecho, uso este método en mi clase base de la que extiendo mis clases de prueba, en cuyo caso esto tiene sentido. El nombre de la clase sería incorrecto aquí sin embargo. –

Cuestiones relacionadas