用 mockito 嘲笑单例

2022-08-31 20:14:01

我需要测试一些遗留代码,这些代码在方法调用中使用单一实例。测试的目的是确保 clas sunder 测试调用单例方法。我在SO上看到了类似的问题,但所有的答案都需要其他依赖项(不同的测试框架) - 不幸的是,我仅限于使用Mockito和JUnit,但这在这种流行的框架中应该是完全可能的。

单例:

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

}

被测类:

public class DriverSnapshotHandler {

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

}

单元测试:

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

    }

}

这个想法是配置可怕的单例的预期行为,因为被测试的类将调用它的getInstance,然后格式化TachoIcon方法。不幸的是,这将失败,并显示一条错误消息:

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

答案 1

你所要求的是不可能的,因为你的旧代码依赖于静态方法,而Mockito不允许模拟静态方法,所以下面的行将不起作用。getInstance()

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

有两种方法可以解决此问题:

  1. 使用不同的模拟工具,如 PowerMock,它允许模拟静态方法。

  2. 重构代码,以便不依赖于静态方法。我能想到的实现此目的的侵入性最小的方法是向注入依赖项的构造函数添加一个构造函数。此构造函数将仅用于测试,您的生产代码将继续使用真正的单例实例。DriverSnapshotHandlerFormatterService

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

然后,您的测试应如下所示:

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

答案 2

我认为这是可能的。查看如何测试单例的示例

测试前:

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

测试后 - 清理类很重要,因为其他测试将与模拟实例混淆。

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

测试:

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

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

推荐