Android: get reference to started Service in instr

2019-04-11 19:43发布

问题:

I'm trying to write instrumentation test for my NetworkMonitorService as described in the official "testing your service" documentation.

Currently I'm stuck because I can't figure out how can I grab a reference to the started service in order to inject mocks into it and assert behavior.

My code:

@RunWith(AndroidJUnit4.class)
@SmallTest
public class NetworkMonitorServiceTest {

    @Rule public final ServiceTestRule mServiceTestRule = new ServiceTestRule();

    @Test
    public void serviceStarted_someEventHappenedInOnStartCommand() {
        try {
            mServiceTestRule.startService(new Intent(
                    InstrumentationRegistry.getTargetContext(),
                    NetworkMonitorService.class));
        } catch (TimeoutException e) {
            throw new RuntimeException("timed out");
        }

        // I need a reference to the started service in order to assert that some event happened
        // in onStartCommand()...
    }
}

The service in question doesn't support binding. I think that if I'd implement support for binding and then use this in test in order to get a reference to the service it could work. However, I don't like writing production code just for sake of supporting test cases...

So, how can I test (instrumentation test) a Service that doesn't support binding?

回答1:

Replace your application with special version "for tests". Do it by providing custom instrumentation test runner. Mock your dependencies it this "app for tests". See for details

Here is a simplified example how "app for test" can be used. Let's assume you want to mock network layer (eg. Api) during tests.

public class App extends Application {
    public Api getApi() {
        return realApi;
    }
}

public class MySerice extends Service {
    private Api api;
    @Override public void onCreate() {
        super.onCreate();
        api = ((App) getApplication()).getApi();
    }
}

public class TestApp extends App {
    private Api mockApi;

    @Override public Api getApi() {
        return mockApi;
    }

    public void setMockApi(Api api) {
        mockApi = api;
    }
}

public class MyTest {
    @Rule public final ServiceTestRule mServiceTestRule = new ServiceTestRule();

    @Before public setUp() {
        myMockApi = ... // init mock Api
        ((TestApp)InstrumentationRegistry.getTargetContext()).setMockApi(myMockApi);
    }

    @Test public test() {
        //start service
        //use mockApi for assertions
    }
}

In the example dependency injection is done via application's method getApi. But you can use Dagger or any others approaches in the same way.



回答2:

I found a very simple way for doing this. You can just perform a binding and you'll get the reference to the already running service, there are no conflicts with service creation because you already started it with onStartCommand, if you check you will see onCreate is called only once so you can be sure it is the same service. Just add the following after your sample:

    Intent serviceIntent =
            new Intent(InstrumentationRegistry.getTargetContext(),
                    NetworkMonitorService.class);

    // Bind the service and grab a reference to the binder.
    IBinder binder = mServiceRule.bindService(serviceIntent);

    // Get the reference to the service
    NetworkMonitorService service =
            ((NetworkMonitorService.LocalBinder) binder).getService();

    // Verify that the service is working correctly however you need
    assertThat(service, is(any(Object.class)));

I hope it helps.



回答3:

this works at least for bound services:

@Test
public void testNetworkMonitorService() throws TimeoutException {

    Intent intent = new Intent(InstrumentationRegistry.getTargetContext(), NetworkMonitorService.class);
    mServiceRule.startService(intent);

    IBinder binder = mServiceRule.bindService(intent);
    NetworkMonitorService service = ((NetworkMonitorService.LocalBinder) binder).getService();

    mServiceRule.unbindService();
}

to access fields, annotate with @VisibleForTesting(otherwise = VisibleForTesting.NONE)