如何对与 System(或 Android)类交互的方法进行单元测试

5

你如何编写与系统类(例如Android框架类)交互的单元测试?

假设你有这些类:

public class DeviceInfo {
    public final int screenWidth, screenHeight;
    public final String model;

    public DeviceInfo(int screenWidth, int screenHeight, String deviceModel) {
        this.screenWidth = screenWidth;
        this.screenHeight = screenHeight;
        this.model = deviceModel;
    }

}

public class DeviceInfoProvider {
    private final Context context;

    public DeviceInfoProvider(Context context) {
        this.context = context;
    }

    public DeviceInfo getScreenParams() {
        DisplayMetrics metrics = new DisplayMetrics();
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        windowManager.getDefaultDisplay().getMetrics(metrics);
        int screenWidth = metrics.widthPixels;
        int screenHeight = metrics.heightPixels;
        String model= Build.MODEL;
        DeviceInfo params = new DeviceInfo(screenWidth, screenHeight, model);
        return params;
    }
}

我该如何编写测试以验证 DeviceInfoProvider.getScreenParams() 方法的正确行为。

下面的测试可以通过,但它非常丑陋而脆弱:

@Test
public void testGetScreenParams() throws Exception {
    // Setup
    Context context = spy(RuntimeEnvironment.application);
    DeviceInfoProvider deviceInfoProvider = new DeviceInfoProvider(context);

    // Stub
    WindowManager mockWindowManager = mock(WindowManager.class);
    Display mockDisplay = mock(Display.class);
    when(context.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager);
    when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay);
    doAnswer(new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            DisplayMetrics metrics = (DisplayMetrics) invocation.getArguments()[0];
            metrics.scaledDensity = 3.25f;
            metrics.widthPixels = 1081;
            metrics.heightPixels = 1921;
            return null;
        }
    }).when(mockDisplay).getMetrics(any(DisplayMetrics.class));

    // Run
    DeviceInfo deviceInfo = deviceInfoProvider.getScreenParams();

    // Verify
    assertThat(deviceInfo.screenWidth, equalTo(1081));
    assertThat(deviceInfo.screenHeight, equalTo(1921));
    assertThat(deviceInfo.model, equalTo(Build.MODEL));
}

你会如何改进它?

注意:目前我正在使用Robolectric、Mockito和PowerMock。


将特定上下文的代码抽象出来,以便进行单元测试时可以进行模拟。尽量避免模拟您不拥有的类。 - Nkosi
DeviceInfoProvider 是否应该被用作依赖项? - Nkosi
问题是:你为什么想要测试这个?难道你不信任Google的团队吗?你应该首先对*你自己的代码进行单元测试,并模拟你不负责的依赖项。 - Timothy Truckle
1个回答

7

被测试系统与实现相关联过于紧密。尽量避免模拟您不拥有的类。通过接口抽象代码并将责任委托给在运行时接口后面的任何实现。

public interface DisplayProvider {
    public int widthPixels;
    public int heightPixels;
}

public interface BuildProvider {
    public string Model;
}

重构依赖类,使其依赖于抽象而非具体实现(即实现细节)。
public class DeviceInfoProvider {
    private final DisplayProvider display;
    private final BuildProvider build;

    public DeviceInfoProvider(DisplayProvider display, BuildProvider build) {
        this.display = display;
        this.build = build;
    }

    public DeviceInfo getScreenParams() {
        int screenWidth = display.widthPixels;
        int screenHeight = display.heightPixels;
        String model = build.Model;
        DeviceInfo params = new DeviceInfo(screenWidth, screenHeight, model);
        return params;
    }
}

在独立环境中进行单元测试

@Test
public void testGetScreenParams() throws Exception {
    // Arrange
    DisplayProvider mockDisplay = mock(DisplayProvider.class);
    BuildProvider mockBuild = mock(BuildProvider.class);        
    DeviceInfoProvider deviceInfoProvider = new DeviceInfoProvider(mockDisplay, mockBuild);

    when(mockDisplay.widthPixels).thenReturn(1081);
    when(mockDisplay.heightPixels).thenReturn(1921);
    when(mockBuild.Model).thenReturn(Build.MODEL);

    // Act
    DeviceInfo deviceInfo = deviceInfoProvider.getScreenParams();

    // Assert
    assertThat(deviceInfo.screenWidth, equalTo(1081));
    assertThat(deviceInfo.screenHeight, equalTo(1921));
    assertThat(deviceInfo.model, equalTo(Build.MODEL));
}

谢谢你的回答,这是一个很好的建议。但是,如果你想测试新的DisplayProvider和BuildProvider,最后你需要模拟系统类,对吧? - Addev
再次提到那些类将封装已经被其提供者进行可扩展性测试的实现关注点。这也将离开单元测试领域并进入集成测试。 - Nkosi
如何对 Context 进行模拟? - IgorGanapolsky
1
@IgorGanapolsky,你不应该嘲笑Context。这只是一个实现问题,只有派生实现才需要关注。抽象依赖的整个原因是为了让你不必模拟它,而是在其周围进行抽象。 - Nkosi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接