web-dev-qa-db-fra.com

Se moquer d'un singleton avec mockito

J'ai besoin de tester du code existant, qui utilise un singleton dans un appel de méthode. Le but du test est de s'assurer que le test de clas sunder appelle la méthode singletons. J'ai vu des questions similaires sur SO, mais toutes les réponses nécessitent d'autres dépendances (différents frameworks de test) - je suis malheureusement limité à utiliser Mockito et JUnit, mais cela devrait être parfaitement possible avec un tel framework populaire.

Le singleton:

public class FormatterService {

    private static FormatterService INSTANCE;

    private FormatterService() {
    }

    public static FormatterService getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new FormatterService();
        }
        return INSTANCE;
    }

    public String formatTachoIcon() {
        return "URL";
    }

}

La classe sous test:

public class DriverSnapshotHandler {

    public String getImageURL() {
        return FormatterService.getInstance().formatTachoIcon();
    }

}

Le test unitaire:

public class TestDriverSnapshotHandler {

    private FormatterService formatter;

    @Before
    public void setUp() {

        formatter = mock(FormatterService.class);

        when(FormatterService.getInstance()).thenReturn(formatter);

        when(formatter.formatTachoIcon()).thenReturn("MockedURL");

    }

    @Test
    public void testFormatterServiceIsCalled() {

        DriverSnapshotHandler handler = new DriverSnapshotHandler();
        handler.getImageURL();

        verify(formatter, atLeastOnce()).formatTachoIcon();

    }

}

L'idée était de configurer le comportement attendu du singleton redouté, car la classe à tester appellera ses méthodes getInstance puis formatTachoIcon. Malheureusement, cela échoue avec un message d'erreur:

when() requires an argument which has to be 'a method call on a mock'.
18
fbielejec

Ce que vous demandez n'est pas possible car votre code hérité repose sur une méthode statique getInstance() et que Mockito ne permet pas de se moquer des méthodes statiques. La ligne suivante ne fonctionnera donc pas.

when(FormatterService.getInstance()).thenReturn(formatter);

Il y a 2 façons de contourner ce problème:

  1. Utilisez un autre outil de moquage, tel que PowerMock, qui permet de se moquer des méthodes statiques. 

  2. Refacturez votre code, de sorte que vous ne comptiez pas sur la méthode statique. Pour y parvenir, la manière la moins invasive possible consiste à ajouter un constructeur à DriverSnapshotHandler qui injecte une dépendance FormatterService. Ce constructeur ne sera utilisé que dans les tests et votre code de production continuera à utiliser l'instance singleton réelle. 

    public static class DriverSnapshotHandler {
    
        private final FormatterService formatter;
    
        //used in production code
        public DriverSnapshotHandler() {
            this(FormatterService.getInstance());
        }
    
        //used for tests
        DriverSnapshotHandler(FormatterService formatter) {
            this.formatter = formatter;
        }
    
        public String getImageURL() {
            return formatter.formatTachoIcon();
        }
    }
    

Ensuite, votre test devrait ressembler à ceci:

FormatterService formatter = mock(FormatterService.class);
when(formatter.formatTachoIcon()).thenReturn("MockedURL");
DriverSnapshotHandler handler = new DriverSnapshotHandler(formatter);
handler.getImageURL();
verify(formatter, atLeastOnce()).formatTachoIcon();
19
noscreenname

Je pense que c'est possible. Voir un exemple comment tester un singleton

Avant un test:

@Before
public void setUp() {
    formatter = mock(FormatterService.class);
    setMock(formatter);
    when(formatter.formatTachoIcon()).thenReturn(MOCKED_URL);
}

private void setMock(FormatterService mock) {
    try {
        Field instance = FormatterService.class.getDeclaredField("instance");
        instance.setAccessible(true);
        instance.set(instance, mock);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Après le test, il est important de nettoyer la classe, car d'autres tests seront confondus avec l'instance simulée.

@After
public void resetSingleton() throws Exception {
   Field instance = FormatterService.class.getDeclaredField("instance");
   instance.setAccessible(true);
   instance.set(null, null);
}

Le test:

@Test
public void testFormatterServiceIsCalled() {
    DriverSnapshotHandler handler = new DriverSnapshotHandler();
    String url = handler.getImageURL();

    verify(formatter, atLeastOnce()).formatTachoIcon();
    assertEquals(MOCKED_URL, url);
}
8
Kyrylo Semenko

Votre getInstance Methos est statique, vous ne pouvez donc pas vous moquer de mockito. http://cube-drone.com/media/optimized/172.png . Vous voudrez peut-être utiliser PowerMockito pour le faire. Bien que je ne recommanderais pas de le faire de cette façon. Je voudrais tester DriverSnapshotHandler via l'injection de dépendance:

public class DriverSnapshotHandler {

    private FormatterService formatterService;

    public DriverSnapshotHandler(FormatterService formatterService) {
        this.formatterService = formatterService;
    }

    public String getImageURL() {
        return formatterService.formatTachoIcon();
    }

}

Le test unitaire:

public class TestDriverSnapshotHandler {

    private FormatterService formatter;

    @Before
    public void setUp() {

        formatter = mock(FormatterService.class);

        when(formatter.formatTachoIcon()).thenReturn("MockedURL");

    }

    @Test
    public void testFormatterServiceIsCalled() {

        DriverSnapshotHandler handler = new DriverSnapshotHandler(formatter);
        handler.getImageURL();

        verify(formatter, times(1)).formatTachoIcon();

    }

}

Vous souhaiterez peut-être définir la valeur de référence sur null dans une méthode @After . C'est à mon sens la solution la plus propre.

1
Florian Biesinger