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?
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();
}
}