¿Cómo se está probando el patrón de registro o singleton en PHP?

¿Por qué es difícil probar singletons o el patrón de registro en un lenguaje como PHP que es impulsado por las solicitudes?

Puede escribir y ejecutar pruebas aparte de la ejecución real del progtwig, para que pueda afectar libremente el estado global del progtwig y ejecutar algunos desgloses e inicialización para cada función de prueba para llevarlo al mismo estado para cada prueba.

¿Me estoy perdiendo de algo?

Si bien es cierto que “puede escribir y ejecutar pruebas aparte de la ejecución real del progtwig para que pueda afectar libremente el estado global del progtwig y ejecutar algunos desgloses e inicialización por cada función de prueba para ponerlo en el mismo estado para cada uno prueba.” , es tedioso hacerlo Desea probar el TestSubject de forma aislada y no perder tiempo recreando un entorno de trabajo.

Ejemplo

class MyTestSubject { protected $registry; public function __construct() { $this->registry = Registry::getInstance(); } public function foo($id) { return $this->doSomethingWithResults( $registry->get('MyActiveRecord')->findById($id) ); } } 

Para que esto funcione, debes tener el Registry concreto. Está codificado, y es un Singleton. Esto último significa prevenir cualquier efecto secundario de una prueba previa. Tiene que restablecerse para cada prueba que ejecutará en MyTestSubject. Podría agregar un método Registry::reset() y llamarlo en setup() , pero agregar un método solo para poder probar parece feo. Supongamos que necesita este método de todos modos, por lo que termina con

 public function setup() { Registry::reset(); $this->testSubject = new MyTestSubject; } 

Ahora todavía no tiene el objeto ‘MyActiveRecord’ que se supone que debe devolver en foo . Como le gusta el Registro, su MyActiveRecord en realidad se parece a esto

 class MyActiveRecord { protected $db; public function __construct() { $registry = Registry::getInstance(); $this->db = $registry->get('db'); } public function findById($id) { … } } 

Hay otra llamada al Registro en el constructor de MyActiveRecord. La prueba debe asegurarse de que contenga algo; de lo contrario, la prueba fallará. Por supuesto, nuestra clase de base de datos también es Singleton y debe reiniciarse entre pruebas. Doh!

 public function setup() { Registry::reset(); Db::reset(); Registry::set('db', Db::getInstance('host', 'user', 'pass', 'db')); Registry::set('MyActiveRecord', new MyActiveRecord); $this->testSubject = new MyTestSubject; } 

Entonces, con los que finalmente se configuraron, puedes hacer tu prueba

 public function testFooDoesSomethingToQueryResults() { $this->assertSame('expectedResult', $this->testSubject->findById(1)); } 

y darse cuenta de que tiene otra dependencia: su base de datos de pruebas físicas aún no estaba configurada. Mientras configuraba la base de datos de prueba y la llenaba de datos, su jefe se acercó y le dijo que ahora está en SOA y que todas estas llamadas a la base de datos tienen que ser reemplazadas por llamadas al servicio web .

Hay una nueva clase MyWebService para eso, y tienes que hacer que MyActiveRecord lo use en su lugar. Genial, justo lo que necesitabas. Ahora debe cambiar todas las pruebas que usan la base de datos. Maldita sea, piensas. ¿Toda esa mierda solo para asegurarse de que doSomethingWithResults funcione como se espera? MyTestSubject realmente no le importa de dónde provienen los datos.

Presentación de simulacros

La buena noticia es que puedes reemplazar todas las dependencias anotándolos o burlándolos de ellos. Un doble de prueba pretende ser real.

 $mock = $this->getMock('MyWebservice'); $mock->expects($this->once()) ->method('findById') ->with($this->equalTo(1)) ->will($this->returnValue('Expected Unprocessed Data')); 

Esto creará un doble para un servicio web que espera ser llamado una vez durante la prueba con el primer argumento del método findById siendo 1. findById datos predefinidos.

Después de poner eso en un método en su TestCase, su setup convierte

 public function setup() { Registry::reset(); Registry::set('MyWebservice', $this->getWebserviceMock()); $this->testSubject = new MyTestSubject; } 

Estupendo. Ya no tiene que preocuparse por configurar un entorno real ahora. Bueno, excepto por el Registro. ¿Qué tal burlarse de eso también? Pero cómo hacer eso. Está codificado de manera que no hay forma de reemplazarlo en el tiempo de ejecución de la prueba. ¡Mierda!

Pero espere un segundo, ¿no acabamos de decir que a MyTestClass no le importa de dónde provienen los datos? Sí, solo le importa que pueda llamar al método findById . Ojalá piense ahora: ¿por qué el Registro está allí? Y correcto eres. Vamos a cambiar todo a

 class MyTestSubject { protected $finder; public function __construct(Finder $finder) { $this->finder = $finder; } public function foo($id) { return $this->doSomethingWithResults( $this->finder->findById($id) ); } } 

Byebye Registry. Ahora estamos inyectando la dependencia MyWebSe … err … Finder ?! Sí. Nos importa el método findById , entonces estamos usando una interfaz ahora

 interface Finder { public function findById($id); } 

No te olvides de cambiar el simulacro en consecuencia

 $mock = $this->getMock('Finder'); $mock->expects($this->once()) ->method('findById') ->with($this->equalTo(1)) ->will($this->returnValue('Expected Unprocessed Data')); 

y setup () se convierte

 public function setup() { $this->testSubject = new MyTestSubject($this->getFinderMock()); } 

Voila! Agradable y fácil y. Podemos concentrarnos en probar MyTestClass ahora.

Mientras hacía eso, su jefe volvió a llamar y dijo que quiere que regrese a una base de datos porque SOA es solo una palabra de moda usada por consultores con precios excesivos para que se sienta productivo. Esta vez, no te preocupes, porque no tienes que volver a cambiar las pruebas. Ya no dependen del medio ambiente.

Por supuesto, aún debes asegurarte de que MyWebservice y MyActiveRecord implementen la interfaz del Finder para tu código real, pero como asumimos que ya tienen estos métodos, solo es cuestión de implements Finder en la clase.

Y eso es. Espero que haya ayudado.

Recursos adicionales:

Puede encontrar información adicional sobre otros inconvenientes al probar Singletons y tratar con el estado global en

  • Código de prueba que usa Singletons

Esto debería ser de gran interés, ya que es del autor de PHPUnit y explica las dificultades con ejemplos reales en PHPUnit.

También de interés son:

  • TotT: Usar la dependency injection para evitar Singletons
  • Singletons son mentirosos patológicos
  • Defecto: Estado global frágil y Singletons

Los Singleton (en todos los lenguajes de progtwigción orientada a objetos, no solo PHP) dificultan un tipo particular de depuración llamado prueba unitaria por la misma razón que las variables globales. Introducen estado global en un progtwig, lo que significa que no puede probar ningún módulo de su software que dependa del singleton de forma aislada. Las pruebas unitarias deben incluir solo el código bajo prueba (y sus superclases).

Los singletons son esencialmente estados globales, y si bien tener estado global puede tener sentido en ciertas circunstancias, se debe evitar a menos que sea necesario.

Al finalizar una prueba de PHP, puede vaciar una instancia de singleton como esta:

 protected function tearDown() { $reflection = new ReflectionClass('MySingleton'); $property = $reflection->getProperty("_instance"); $property->setAccessible(true); $property->setValue(null); }