How to test an IntentService with Robolectric?

11,726

Solution 1

onHandleIntent is a protected method so it can't be called directly.

my solution was to extend the service class in my test case, override onHandleIntent making it public and calling super.onHandleIntent(intent)

then call onHandleIntent directly from the test case.

Solution 2

Robolectric has a ServiceController that can go thru service lifecycle just like activity. This controller provides all methods to execute corresponding service callbacks (e.g. controller.attach().create().startCommand(0, 0).destroy()).

Theoretically we can expect that IntentService.onStartCommand() will trigger IntentService.onHandleIntent(Intent), via its internal Handler. However this Handler uses a Looper which runs on a background thread, and I have no idea how to make this thread advance to next task. A workaround would be to create TestService that mimics the same behavior, but triggers onHandleIntent(Intent) on main thread (thread used to run tests).

@RunWith(RobolectricGradleTestRunner.class)
public class MyIntentServiceTest {
    private TestService service;
    private ServiceController<TestService> controller;

    @Before
    public void setUp() {
        controller = Robolectric.buildService(TestService.class);
        service = controller.attach().create().get();
    }

    @Test
    public void testWithIntent() {
        Intent intent = new Intent(RuntimeEnvironment.application, TestService.class);
        // add extras to intent
        controller.withIntent(intent).startCommand(0, 0);
        // assert here
    }

    @After
    public void tearDown() {
        controller.destroy();
    }

    public static class TestService extends MyIntentService {
        public boolean enabled = true;

        @Override
        public void onStart(Intent intent, int startId) {
            // same logic as in internal ServiceHandler.handleMessage()
            // but runs on same thread as Service
            onHandleIntent(intent);
            stopSelf(startId);
        }
    }
}

UPDATE: Alternatively, it's quite straightforward to create a similar controller for IntentService, as follows:

public class IntentServiceController<T extends IntentService> extends ServiceController<T> {
    public static <T extends IntentService> IntentServiceController<T> buildIntentService(Class<T> serviceClass) {
        try {
            return new IntentServiceController<>(Robolectric.getShadowsAdapter(), serviceClass);
        } catch (IllegalAccessException | InstantiationException e) {
            throw new RuntimeException(e);
        }
    }

    private IntentServiceController(ShadowsAdapter shadowsAdapter, Class<T> serviceClass) throws IllegalAccessException, InstantiationException {
        super(shadowsAdapter, serviceClass);
    }

    @Override
    public IntentServiceController<T> withIntent(Intent intent) {
        super.withIntent(intent);
        return this;
    }

    @Override
    public IntentServiceController<T> attach() {
        super.attach();
        return this;
    }

    @Override
    public IntentServiceController<T> bind() {
        super.bind();
        return this;
    }

    @Override
    public IntentServiceController<T> create() {
        super.create();
        return this;
    }

    @Override
    public IntentServiceController<T> destroy() {
        super.destroy();
        return this;
    }

    @Override
    public IntentServiceController<T> rebind() {
        super.rebind();
        return this;
    }

    @Override
    public IntentServiceController<T> startCommand(int flags, int startId) {
        super.startCommand(flags, startId);
        return this;
    }

    @Override
    public IntentServiceController<T> unbind() {
        super.unbind();
        return this;
    }

    public IntentServiceController<T> handleIntent() {
        invokeWhilePaused("onHandleIntent", getIntent());
        return this;
    }
}

Solution 3

Robolectric 3.1+

Create an instance of your service

Call onHandleIntent directly

    YourService svc = Robolectric.buildService(YourService.class).get();
    Intent fakeIntent = new Intent();
    fakeIntent.putExtra(YourService.SOME_EXTRA, "debug|fail");

    svc.onHandleIntent(fakeIntent);

    assertThat(something.happened).isEqualTo(true);

Note: use Robolectric.buildService (not .buildIntentService) even if you're testing an IntentService. .buildIntentService is currently not working, as far as I can tell.

Update 2017-05-17:

Issue with buildIntentService will be fixed in future

Solution 4

I think you have approached this the wrong way. You are testing here:

Intent startedIntent = shadowActivity.getNextStartedService();
assertNotNull(startedIntent);

that startService()was called.

You then must assume that Android has implemented their end correctly and that after calling startService() the service will indeed be started.

I think that if you want to test the behavior of onHandleIntent(), you must just call it directly??

I am not sure exactly... but I think the test you want to write would just be testing the Android framework. You should write two tests.

1) Test that startService() is called (as you have in your example).

2) Test the behaviour of onHandleIntent() (by calling it directly).

I have very little experience with Robolectric but hopefully this is helpful.

Cheers

Solution 5

I use this

@Before
public void setup(){
    mainActivity = Robolectric.buildActivity(MainActivity.class)
            .create()
            .start()
            .resume()
            .get();
    context = Robolectric.getShadowApplication().getApplicationContext();
    bt_start = (Button) mainActivity.findViewById(R.id.button);
    bt_stop = (Button) mainActivity.findViewById(R.id.button2);
}

@Test
public void serviceIsOn(){

    bt_start.performClick();
    Intent intent = Robolectric.shadowOf(mainActivity).peekNextStartedService();
    assertEquals(MiService.class.getCanonicalName(),intent.getComponent().getClassName());
}

and then I run the test and all is ok

Share:
11,726
Gal Ben-Haim
Author by

Gal Ben-Haim

Advanced Technology Director @ http://www.augury.com/

Updated on June 06, 2022

Comments

  • Gal Ben-Haim
    Gal Ben-Haim about 2 years

    I'm trying to test the onHandleIntent() method of an IntentService using Robolectric.

    I'm starting the service with:

    Activity activity = new Activity();
    Intent intent = new Intent(activity, MyService.class);
    activity.startService(intent);
    
    ShadowActivity shadowActivity = Robolectric.shadowOf(activity);
    Intent startedIntent = shadowActivity.getNextStartedService();
    assertNotNull(startedIntent);
    

    seems like startedIntent is not null, but onHandleIntent() doesn't seem to be called.

    how should I test it ?

  • rds
    rds about 9 years
    You are not testing the onHandleIntent of the service, as the OP asked. You are testing that the activity calls the service.
  • Juan Mendez
    Juan Mendez almost 9 years
    Hi. I followed your guideline. I didn't have to write the service in my test case, but just included a public method which calls onHandleIntent which also passes my intent.. Rather that having my activity start the service.. I simply instantiate the service class and make the call. I also included a receiver with the intent, and it was successfully received!
  • Jeff Bowman
    Jeff Bowman about 6 years
    Note that the attach() calls are implicit as of Robolectric 3.4, and the methods have been marked private. Instead, services are automatically attached when the controller is created.