Effective Java a la déclaration suivante sur les singletons de tests unitaires
Faire d’une classe un singleton peut rendre difficile le test de ses clients, car il est impossible de substituer une implémentation factice à un singleton sans implémenter une interface qui en soit le type.
Quelqu'un peut-il expliquer la raison pour laquelle il en est ainsi?
Vous pouvez utiliser la réflexion pour réinitialiser votre objet singleton afin d’empêcher les tests de s’affecter.
@Before
public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field instance = MySingleton.class.getDeclaredField("instance");
instance.setAccessible(true);
instance.set(null, null);
}
Les simulacres nécessitent des interfaces, car vous remplacez le comportement sous-jacent réel par un imposteur qui imite ce dont vous avez besoin pour le test. Comme le client ne traite qu'avec un type de référence d'interface, il n'a pas besoin de savoir quelle est l'implémentation.
Vous ne pouvez pas vous moquer d'une classe concrète sans interface, car vous ne pouvez pas remplacer le comportement sans que le client de test ne le sache. C'est une classe complètement nouvelle dans ce cas.
C'est vrai pour toutes les classes, Singleton ou pas.
Je pense que cela dépend en fait de la mise en œuvre du modèle d'accès singleton.
Par exemple
MySingleton.getInstance()
Pourrait être très difficile à tester tout en
MySingletonFactory mySingletonFactory = ...
mySingletonFactory.getInstance() //this returns a MySingleton instance or even a subclass
Ne fournit aucune information sur le fait qu'il utilise un singleton. Vous pouvez donc remplacer librement votre usine.
NOTE: un singleton est défini comme n'étant qu'une instance de cette classe dans une application, mais la manière dont il est obtenu ou stocké ne doit pas nécessairement être statique.
Le problème n'est pas de tester les singletons eux-mêmes; le livre dit que si une classe que vous essayez de tester dépend de un singleton, vous aurez probablement des problèmes.
À moins que vous (1) demandiez au singleton d'implémenter une interface et (2) l'injectiez à votre classe à l'aide de cette interface.
Par exemple, les singletons sont généralement instanciés directement comme ceci:
public class MyClass
{
private MySingleton __s = MySingleton.getInstance() ;
...
}
MyClass
peut maintenant être très difficile à tester automatiquement. Par exemple, comme le note @ Boris Pavlović dans sa réponse, si le comportement du singleton est basé sur l'heure du système, vos tests dépendent désormais également de l'heure du système et vous ne pourrez peut-être pas tester des cas qui dépendent du jour de la semaine.
Cependant, si votre singleton "implémente une interface qui lui sert de type", vous pouvez toujours utiliser un singleton implementation de cette interface, tant que vous le transmettez:
public class SomeSingleton
implements SomeInterface
{
...
}
public class MyClass
{
private SomeInterface __s ;
public MyClass( SomeInterface s )
{
__s = s ;
}
...
}
...
MyClass m = new MyClass( SomeSingleton.getInstance() ) ;
Dans la perspective de tester MyClass
, vous ne vous souciez plus de savoir si SomeSingleton
est un singleton ou non: vous pouvez également passer toute autre implémentation de votre choix, y compris celle de singleton, mais vous utiliserez très probablement un modèle factice que vous contrôlez. de vos tests.
BTW, ce n'est pas la façon de le faire:
public class MyClass
{
private SomeInterface __s = SomeSingleton.getInstance() ;
public MyClass()
{
}
...
}
Cela fonctionne toujours de la même manière au moment de l'exécution, mais pour les tests, vous devez maintenant à nouveau dépendre de SomeSingleton
.
Les objets Singleton sont créés sans aucun contrôle de l'extérieur. Dans l'un des autres chapitres du même livre, Bloch suggère d'utiliser enum
s comme implémentation Singleton par défaut. Voyons un exemple
public enum Day {
MON(2), TUE(3), WED(4), THU(5), FRI(6), SAT(7), Sun(1);
private final int index;
private Day(int index) {
this.index = index;
}
public boolean isToday() {
return index == new GregorianCalendar().get(Calendar.DAY_OF_WEEK);
}
}
Disons que nous avons un code qui ne devrait être exécuté que le week-end:
public void leisure() {
if (Day.SAT.isToday() || Day.Sun.isToday()) {
haveSomeFun();
return;
}
doSomeWork();
}
Tester la méthode de loisirs va être assez difficile. Son exécution dépendra du jour où elle sera exécutée. S'il s'exécute en semaine, doSomeWork()
sera appelé et le week-end haveSomeFun()
.
Pour ce cas, nous aurions besoin d’utiliser des outils lourds tels que PowerMock pour intercepter le constructeur GregorianCalendar
, renvoyer un modèle qui renverra un index correspondant à un jour de la semaine ou à un week-end dans deux scénarios de test testant les deux chemins d’exécution de la méthode leisure
.
Autant que je sache, une classe implémentant un Singleton ne peut pas être étendue (le constructeur de la super-classe est toujours appelé implicitement et le constructeur d'un Singleton est privé). Si vous voulez vous moquer d'une classe, vous devez l'étendre. Comme vous le voyez dans ce cas, cela ne serait pas possible.
C'est possible, voir l'exemple
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import Java.lang.reflect.Field;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class DriverSnapshotHandlerTest {
private static final String MOCKED_URL = "MockedURL";
private FormatterService formatter;
@SuppressWarnings("javadoc")
@Before
public void setUp() {
formatter = mock(FormatterService.class);
setMock(formatter);
when(formatter.formatTachoIcon()).thenReturn(MOCKED_URL);
}
/**
* Remove the mocked instance from the class. It is important, because other tests will be confused with the mocked instance.
* @throws Exception if the instance could not be accessible
*/
@After
public void resetSingleton() throws Exception {
Field instance = FormatterService.class.getDeclaredField("instance");
instance.setAccessible(true);
instance.set(null, null);
}
/**
* Set a mock to the {@link FormatterService} instance
* Throws {@link RuntimeException} in case if reflection failed, see a {@link Field#set(Object, Object)} method description.
* @param mock the mock to be inserted to a class
*/
private void setMock(FormatterService mock) {
Field instance;
try {
instance = FormatterService.class.getDeclaredField("instance");
instance.setAccessible(true);
instance.set(instance, mock);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Test method for {@link com.example.DriverSnapshotHandler#getImageURL()}.
*/
@Test
public void testFormatterServiceIsCalled() {
DriverSnapshotHandler handler = new DriverSnapshotHandler();
String url = handler.getImageURL();
verify(formatter, atLeastOnce()).formatTachoIcon();
assertEquals(MOCKED_URL, url);
}
}
Le problème avec les singletons (et aussi avec les méthodes statiques) est qu’il est difficile de remplacer le code réel par une implémentation simulée.
Par exemple considérons le code suivant
public class TestMe() {
public String foo(String data) {
boolean isFeatureFlag = MySingletonConfig.getInstance().getFeatureFlag();
if (isFeatureFlag)
// do somethine with data
else
// do something else with the data
return result;
}
}
Il n'est pas facile d'écrire un test unitaire pour la méthode foo et de vérifier si le comportement correct est exécuté .. C'est parce que vous ne pouvez pas facilement modifier la valeur de retour de getFeatureFlag
.
Le même problème existe pour les méthodes statiques - il n'est pas facile de remplacer la méthode de la classe cible réelle par un comportement fictif.
Bien sûr, il existe des solutions de contournement telles que powermock , ou une dépendance à la méthode, ou une réflexion dans les tests . Mais il est bien préférable de ne pas utiliser des singletons au départ