androidjunitrobolectric

Robolectric does not bind service from another thread


I am using Robolectric 3.4.2 and I need to test the interaction between two services.

In my test I wrote a dummy service:

ShadowApplication.getInstance().setComponentNameAndServiceForBindService(
        new ComponentName(SERVICE.getPackageName(), SERVICE.getClassName()),
            new Binder() {
                @Override
                public IInterface queryLocalInterface(final String descriptor) {
                    return null;
                }
            }
    );

and it works if I invoke BindService directly from my test case, but if the call to bindService is in a different thread (like in the real application), the onServiceConnected() callback is never called.

ServiceConnection connection = new ServiceConnection(){

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        try {
            doSomething();
        } catch (Exception e) {
            Log.e(TAG, "Cannot Do Something", e);
        }
        mContext.unbindService(this);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) { }
};

new Thread(new Runnable() {
    @Override
    public void run() {
        mContext.bindService(SERVICE.createIntent()), connection, Context.BIND_AUTO_CREATE);
    }
}).start();

Am I doing something wrong, or is it expected to work this way?


Solution

  • I have good news. It works!

    To solve the issue let's look at the source code. Let's look bindService method inside ShadowInstrumentation class:

    private boolean bindService(
                  final Intent intent,
                  final ServiceConnection serviceConnection,
                  ServiceCallbackScheduler serviceCallbackScheduler) {
                boundServiceConnections.add(serviceConnection);
                unboundServiceConnections.remove(serviceConnection);
                if (exceptionForBindService != null) {
                  throw exceptionForBindService;
                }
                final Intent.FilterComparison filterComparison = new Intent.FilterComparison(intent);
                final ServiceConnectionDataWrapper serviceConnectionDataWrapper =
                    serviceConnectionDataForIntent.getOrDefault(filterComparison, defaultServiceConnectionData);
                if (unbindableActions.contains(intent.getAction())
                    || unbindableComponents.contains(intent.getComponent())
                    || unbindableComponents.contains(
                        serviceConnectionDataWrapper.componentNameForBindService)) {
                  return false;
                }
                startedServices.add(filterComparison);
                Runnable onServiceConnectedRunnable =
                    () -> {
                      serviceConnectionDataForServiceConnection.put(
                          serviceConnection, serviceConnectionDataWrapper);
                      serviceConnection.onServiceConnected(
                          serviceConnectionDataWrapper.componentNameForBindService,
                          serviceConnectionDataWrapper.binderForBindService);
                    };
            
                if (bindServiceCallsOnServiceConnectedInline) {
                  onServiceConnectedRunnable.run();
                } else {
                  serviceCallbackScheduler.schedule(onServiceConnectedRunnable);
                }
                return true;
              }
    

    So to call callback it should run:

     onServiceConnectedRunnable.run();
    

    For this, we should set a flag bindServiceCallsOnServiceConnectedInline in true:

    shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(true);
    

    So finally code:

    @RunWith(RobolectricTestRunner.class)
    
    public class MyServiceRoboTest {
        private static final String TAG = MyServiceRoboTest.class.getCanonicalName();
        private final Context mContext = ApplicationProvider.getApplicationContext();
    
            @Before
            public void setUp() {
                ShadowLog.stream = System.out;
                Log.i(TAG, "MyServiceRoboTest setUp");
                MockitoAnnotations.initMocks(this);
        
                MyService service = Robolectric.setupService(MyService.class);
                Assert.assertNotNull(service);
        
                final ShadowApplication shadowApplication =
                        Shadows.shadowOf((Application) mContext);
        
                shadowApplication.setComponentNameAndServiceForBindServiceForIntent(
                        new Intent(mContext, MyService.class),
                        new ComponentName(mContext, MyService.class),
                        service.onBind(null));
        
                shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(true);
            }
        
            @Test
            public void testMyMessageManager() {
                MyManager.ServiceConnectListener serviceConnectListener = Mockito.mock(MyManager.ServiceConnectListener.class);
                MyMessageManager myMessageManager = new MyMessageManager(mContext, serviceConnectListener);
    
                verify(serviceConnectListener).onServiceConnected();
               
            }
        }