Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test methods that interact with System (or Android) classes

How do you manage to write unit tests that interact with the system classes i.e. Android Framework classes?

Imagine you have those classes:

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

How can I write a test to verify the correct behaviour of the method DeviceInfoProvider.getScreenParams().

The following test passes, but it is very ugly and fragile:

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

How would you improve that?

Note: Currently I'm using Robolectric, Mockito and PowerMock

like image 408
Addev Avatar asked Mar 11 '23 14:03

Addev


1 Answers

The System Under Test is too tightly coupled to implementation concerns. Try to avoid mocking classes you don't own. Abstract code behind interfaces and delegate responsibility to whichever implementation happens to be behind the interface at run time.

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

public interface BuildProvider {
    public string Model;
}

Refactor dependent class to rely on the abstractions and not concretions (implementation concerns).

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

Unit test in isolation

@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));
}
like image 78
Nkosi Avatar answered Apr 26 '23 18:04

Nkosi