3

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?

Vasiliy
  • 16,221
  • 11
  • 71
  • 127
  • 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 http://stackoverflow.com/a/41393275/2711056 – MyDogTom Jan 02 '17 at 15:21
  • @MyDogTom, I thought about this approach, but I can't see an easy way to mock service's dependencies this way. Could you provide some example in code? – Vasiliy Jan 02 '17 at 15:48

3 Answers3

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.

Community
  • 1
  • 1
MyDogTom
  • 4,486
  • 1
  • 28
  • 41
  • Thanks, but I've already tried this approach. The problem here is that it doesn't allow to inject mocks per-test - you specify mocks for all tests that will be run. This might work alright for small applications, but in large apps (my Dagger 2 object graph contains hundreds of objects in ~10 modules which are used by ~5 components) what you'd like to do is to inject mocks per test. I couldn't find a simple way to do this with this approach. Did you? – Vasiliy Jan 02 '17 at 20:37
  • @Vasiliy I use this approach only for integration test, mocking only the biggest / slowest dependencies (back-end, in app layer). For unit tests I extract logic into separate pojo (or almost pojo) class and write test for this class. In your case your service could use some sort of helper which contains all logic. Cover this helper with a bunch of unit tests. Write a couple of integration tests just to see that everything works together. You don't need to cover all cases at this point. – MyDogTom Jan 03 '17 at 04:31
  • Yeah, this is exactly where I'm standing now - I have unit tests for "helpers" and "managers", but I want to have integration test for the entire service. However I don't see a way to write integration test that is any good without mocking some particular service's dependencies (at a minimum I need to mock Android's ConnectivityManager, EventBus, ServerPingManager). However I don't want to mock these dependencies for all tests - there are tests that should still use non-mocked dependencies. In any case, thanks for the good idea. +1 – Vasiliy Jan 03 '17 at 06:25
0

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.

Alejandro Casanova
  • 3,633
  • 4
  • 31
  • 46
0

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)

Martin Zeitler
  • 1
  • 19
  • 155
  • 216