web-dev-qa-db-de.com

Ein Singleton mit Mockito verspotten

Ich muss etwas Legacy-Code testen, der in einem Methodenaufruf ein Singleton verwendet. Der Test soll sicherstellen, dass der clas sunder-Test die Singletons-Methode aufruft. Ich habe ähnliche Fragen zu SO gesehen, aber alle Antworten erfordern andere Abhängigkeiten (verschiedene Test-Frameworks) - ich bin leider auf die Verwendung von Mockito und JUnit beschränkt, dies sollte jedoch bei einem beliebten Framework durchaus möglich sein.

Der 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";
    }

}

Die zu testende Klasse:

public class DriverSnapshotHandler {

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

}

Der Unit-Test:

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();

    }

}

Die Idee bestand darin, das erwartete Verhalten des gefürchteten Singleton zu konfigurieren, da die getestete Klasse die Methoden getInstance und dann formatTachoIcon aufruft. Leider schlägt dies mit einer Fehlermeldung fehl:

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

Das, was Sie fragen, ist nicht möglich, da Ihr älterer Code auf der statischen Methode getInstance() beruht und Mockito nicht erlaubt, statische Methoden zu überlisten

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

Es gibt zwei Möglichkeiten, dieses Problem zu umgehen:

  1. Verwenden Sie ein anderes Mocking-Tool wie PowerMock, mit dem statische Methoden simuliert werden können. 

  2. Stellen Sie Ihren Code so um, dass Sie sich nicht auf die statische Methode verlassen. Die am wenigsten invasive Möglichkeit, dies zu erreichen, ist das Hinzufügen eines Konstruktors zu DriverSnapshotHandler, der eine FormatterService-Abhängigkeit einführt. Dieser Konstruktor wird nur in Tests verwendet und Ihr Produktionscode verwendet weiterhin die echte Singleton-Instanz. 

    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();
        }
    }
    

Dann sollte Ihr Test so aussehen:

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

Ich denke es ist möglich. Siehe ein Beispiel wie man einen Singleton testet

Vor einem 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);
    }
}

Nach dem Test ist es wichtig, die Klasse aufzuräumen, da andere Tests mit der nachgeahmten Instanz verwechselt werden.

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

Der Test:

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

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

Ihr getInstance Methos ist statisch und kann daher nicht mit Mockito verspottet werden. http://cube-drone.com/media/optimized/172.png . Möglicherweise möchten Sie PowerMockito verwenden, um dies zu tun. Obwohl ich es nicht empfehlen würde, es so zu machen. Ich würde DriverSnapshotHandler über Abhängigkeitseinspritzung testen:

public class DriverSnapshotHandler {

    private FormatterService formatterService;

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

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

}

Der Unit-Test:

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();

    }

}

Möglicherweise möchten Sie den Mock in einer @After-Methode auf null setzen. __ Dies ist imho die Reinigungslösung.

1