burlarse de una clase singleton

Recientemente leí que hacer un singleton de clase hace que sea imposible burlarse de los objetos de la clase, lo que dificulta probar a sus clientes. No pude entender de inmediato la razón subyacente. ¿Puede alguien explicar por qué es imposible burlarse de una clase singleton? Además, ¿hay más problemas asociados con hacer una clase singleton?

Por supuesto, podría escribir algo como no usar singleton, son malvados, usar Guice / Spring / lo que sea, pero primero, esto no respondería a su pregunta y segundo, a veces tiene que lidiar con singleton, cuando usa código heredado para ejemplo.

Entonces, no hablemos de lo bueno o malo sobre singleton (hay otra pregunta para esto) pero veamos cómo manejarlos durante la prueba. Primero, veamos una implementación común del singleton:

public class Singleton { private Singleton() { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } public String getFoo() { return "bar"; } } 

Aquí hay dos problemas de prueba:

  1. El constructor es privado, por lo que no podemos extenderlo (y no podemos controlar la creación de instancias en las pruebas, pero bueno, ese es el punto de los singletons).

  2. El getInstance es estático, por lo que es difícil inyectar un falso en lugar del objeto singleton en el código con el singleton .

Para burlarse de los marcos basados ​​en la herencia y el polymorphism, ambos puntos son obviamente grandes problemas. Si tiene el control del código, una opción es hacer que su singleton sea “más comprobable” agregando un setter que permita ajustar el campo interno como se describe en Aprenda a dejar de preocuparse y amar al Singleton (ni siquiera necesita una burla) marco en ese caso). Si no lo hace, los marcos de burla modernos basados ​​en la interceptación y los conceptos de AOP permiten superar los problemas mencionados anteriormente.

Por ejemplo, Mocking Static Method Calls muestra cómo simular un Singleton usando JMockit Expectations .

Otra opción sería usar PowerMock , una extensión de Mockito o JMock que permite simular cosas que normalmente no son modificables como métodos estáticos, finales, privados o de construcción. También puedes acceder a las partes internas de una clase.

La mejor manera de burlarse de un singleton es no usarlos en absoluto, o al menos no en el sentido tradicional. Algunas prácticas que quizás desee consultar son:

  • progtwigción a las interfaces
  • dependency injection
  • Inversión de control

Entonces, en lugar de tener solo, accedes así:

 Singleton.getInstance().doSometing(); 

… defina su “singleton” como una interfaz y haga que otra cosa administre su ciclo de vida e inyéctelo donde lo necesite, por ejemplo como una variable de instancia privada:

 @Inject private Singleton mySingleton; 

Luego, cuando esté probando la clase / componentes / etc que depende del singleton, puede inyectar fácilmente una versión falsa.

La mayoría de los contenedores de dependency injection le permitirán marcar un componente como ‘singleton’, pero depende del contenedor administrarlo.

El uso de las prácticas anteriores hace que resulte mucho más fácil probar el código por unidad y le permite enfocarse en su lógica funcional en lugar de lógica de cableado. También significa que su código realmente comienza a volverse verdaderamente orientado a objetos, ya que cualquier uso de métodos estáticos (incluidos los constructores) es debatiblemente procesal. Por lo tanto, sus componentes comienzan a volverse verdaderamente reutilizables.

Echa un vistazo a Google Guice como titular para 10:

http://code.google.com/p/google-guice/

También puede mirar Spring y / u OSGi que pueden hacer este tipo de cosas. Hay muchas cosas de IOC / DI por ahí. 🙂

Un Singleton, por definición, tiene exactamente una instancia. Por lo tanto, su creación está estrictamente controlada por la clase misma. Normalmente es una clase concreta, no una interfaz, y debido a su constructor privado no es subclassable. Además, sus clientes lo encuentran activamente (llamando a Singleton.getInstance() o un equivalente), por lo que no puede usar fácilmente, por ejemplo, Dependency Injection para reemplazar su instancia “real” con una instancia simulada:

 class Singleton { private static final myInstance = new Singleton(); public static Singleton getInstance () { return myInstance; } private Singleton() { ... } // public methods } class Client { public doSomething() { Singleton singleton = Singleton.getInstance(); // use the singleton } } 

Para los simulacros, lo ideal es que necesites una interfaz que pueda subclasificarse libremente y cuya implementación concreta se proporcione a sus clientes mediante dependency injection.

Puede relajar la implementación Singleton para que sea comprobable por

  • proporcionando una interfaz que puede ser implementada por una subclase falsa así como la “real”
  • agregando un método setInstance para permitir el reemplazo de la instancia en pruebas unitarias

Ejemplo:

 interface Singleton { private static final myInstance; public static Singleton getInstance() { return myInstance; } public static void setInstance(Singleton newInstance) { myInstance = newInstance; } // public method declarations } // Used in production class RealSingleton implements Singleton { // public methods } // Used in unit tests class FakeSingleton implements Singleton { // public methods } class ClientTest { private Singleton testSingleton = new FakeSingleton(); @Test public void test() { Singleton.setSingleton(testSingleton); client.doSomething(); // ... } } 

Como puede ver, solo puede probar la unidad de código que usa Singleton al comprometer la “limpieza” del Singleton. Al final, es mejor no usarlo en absoluto si puede evitarlo.

Actualización: Y aquí está la referencia obligatoria a Trabajar eficazmente con código heredado de Michael Feathers.

Depende mucho de la implementación de singleton. Pero sobre todo porque tiene un constructor privado y, por lo tanto, no puede ampliarlo. Pero tienes la siguiente opción

  • hacer una interfaz – SingletonInterface
  • hacer que su clase singleton implemente esa interfaz
  • dejar que Singleton.getInstance() devuelva SingletonInterface
  • proporcionar una implementación simulada de SingletonInterface en sus pruebas
  • configurarlo en el campo private static en Singleton usando reflexión.

Pero será mejor que evites singletons (que representan un estado global). Esta conferencia explica algunos conceptos de diseño importantes desde el punto de vista de la capacidad de prueba.

No es que el patrón Singleton sea en sí mismo un mal puro, sino que se usa de forma masiva incluso en situaciones en las que no es aceptable. Muchos desarrolladores piensan “Oh, probablemente solo necesite uno de estos, así que conviértalo en un singleton”. De hecho, debería pensar: “Probablemente solo necesite uno de estos, así que construyamos uno al comienzo de mi progtwig y pase las referencias donde sea necesario”.

El primer problema con singleton y pruebas no es tanto por el singleton sino por la pereza. Debido a la conveniencia de obtener un singleton, la dependencia del objeto singleton a menudo se integra directamente en los métodos, lo que hace que sea muy difícil cambiar el singleton a otro objeto con la misma interfaz pero con una implementación diferente (por ejemplo, un objeto simulado )

En lugar de:

 void foo() { Bar bar = Bar.getInstance(); // etc... } 

preferir:

 void foo(IBar bar) { // etc... } 

Ahora puede probar la función foo con un objeto de bar burlado que puede controlar. Has eliminado la dependencia para que puedas probar foo sin probar la bar .

El otro problema con los singletons y las pruebas es cuando se prueba el singleton en sí. Un singleton es (por diseño) muy difícil de reconstruir, por lo que, por ejemplo, solo puedes probar el contructor singleton una vez. También es posible que la instancia única de Bar conserve el estado entre las pruebas, lo que provoca el éxito o el fracaso dependiendo del orden en que se ejecutan las pruebas.

Hay una manera de burlarse de Singleton. Utilice powermock para simular el método estático y use Whitebox para invocar al constructor YourClass mockHelper = Whitebox .invokeConstructor(YourClass.class); Whitebox.setInternalState(mockHelper, "yourdata",mockedData); PowerMockito.mockStatic(YourClass.class); Mockito.when(YourClass.getInstance()).thenReturn(mockHelper); YourClass mockHelper = Whitebox .invokeConstructor(YourClass.class); Whitebox.setInternalState(mockHelper, "yourdata",mockedData); PowerMockito.mockStatic(YourClass.class); Mockito.when(YourClass.getInstance()).thenReturn(mockHelper);

Lo que está sucediendo es que el código de bytes Singleton está cambiando en tiempo de ejecución.

disfrutar