Llevamos una serie de post hablando sobre PHPUnit, sobre como hacer asserts y de patrones para hacer mejores test. Hoy vamos a comentar un poco acerca del patrón/antipatrón Singleton.
Según la Wikipedia El patrón de diseño singleton (instancia única) está diseñado para restringir la creación de objetos pertenecientes a una clase o el valor de un tipo a un único objeto.
Básicamente consiste en garantizar que una clase solo tenga una instancia y un punto de acceso.
Con un ejemplo quizás lo veamos más claro:
class Foo
{
static private $instance = NULL;
static public function getInstance()
{
if (self::$instance == NULL) {
self::$instance = new static();
}
return self::$instance;
}
}
Este patrón puede utilizarse clases como Logger, acceso único a un fichero o recurso, para conexiones a BD.
Si queremos testar esta clase, con PHPUnit podemos hacer algo como esto:
class FooTest extends PHPUnit_Framework_TestCase
{
public function testGetInstance()
{
$this->assertInstanceOf('Foo', Foo::getInstance());
}
}
Hasta aquí todo perfecto, el problema viene cuando tenemos que testar los métodos de la clase Singleton. Imagina que tenemos un método que modifica algún valor de la clase en un test. Para mantener los tests separados, debemos estar seguros de que en un 2º test ese valor está como al principio. Creo que lo mejor será verlo con un ejemplo.
Si tenemos por ejemplo, un método que incrementa un contador, en cada test debemos tener el contador a 0.
private $counter = 0;
public function getCounter()
{
return $this->counter;
}
public function incrementCounter()
{
$this->counter++;
}
Y tenemos estos tests
public $foo;
public function setUp()
{
$this->foo = Foo::getInstance();
}
public function testGetCounter();
{
$this->assertEquals(0, $this->foo->getCounter());
}
public function testIncrementCounter();
{
$this->assertEquals(0, $this->getCounter());
$this->foo->incrementCounter();
$this->assertEquals(1, $this->getCounter());
}
¿Por qué el patrón Singleton no es la mejor idea?
Tendremos siempre un bonito test fallido. Quizás esto parezca un ejemplo un poco tonto pero hay algunas razones por las que el patrón singleton no es la mejor idea.
- Introducen un estado global en el código y eso en general no es bueno.
- Introducen algo de acoplamiento con cualquier clase que lo utiliza. Esto hace que no se pueda a volver a utilizar la clase sin reutilizar el singleton.
- Hacer pruebas unitarias es un poco duro cuando hay patrones singleton de por medio.
- En PHP no hay memoria de aplicación, es decir un Singleton creado en una request vive exactamente para esa petición.
¿Cómo solucionar todo esto?
Una de las posibles soluciones es tener un método en la clase que establezca a NULL la única instancia del objeto. Pero ojo, debemos tener cuidado en donde se utiliza este método.
Para terminar el ejemplo solo tenemos que añadir a la clase Foo un método como este:
public statinc function tearDown()
{
static::$instance = NULL;
}
Y en nuestros tests crear un método tearDown para limpiar el escenario cada tests. Así queda nuesto método tearDown en nuestra clase de test
public statinc function tearDown()
{
Foo::tearDown();
}
Conclusiones
Utilizar Singlenton podría considerarse un antipatrón, pero puede que nos encontremos código que hay que refactorizar y el primer paso para refactorizar es tener tests de lo que vamos a refactorizar 🙂
Pero seamos realistas, todo lo que no se utiliza correctamente, es malo. Con esto quiero decir que el patrón Singleton no siempre es malo y que hay veces donde está justificado su uso. Por ejemplo si utilizamos Apis de 3º o para accesos a memcached o cosas así. Así que queda a expensas de nosotros utilizar Dependencia de Inyección, Singleton o cualquier otro patrón.
El post de Gonzalo123 me sirvió de ayuda para poder testar unas clases que utilizan Singleton. Por eso he hecho esta pequeña traducción del artículo http://gonzalo123.com/2012/09/24/the-reason-why-singleton-is-a-problem-with-phpunit/