Using Robolectric, how would one go about testing an IntentService that broadcasts intents as a response?
Assuming the following class:
class MyService extends IntentService {
#Override
protected void onHandleIntent(Intent intent) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("action"));
}
}
In my test case, I'm attempting to do something like this:
#RunWith(RobolectricTestRunner.class)
public class MyServiceTest{
#Test
public void testPurchaseHappyPath() throws Exception {
Context context = new Activity();
// register broadcast receiver
BroadcastReceiver br = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
// test logic to ensure that this is called
}
};
context.registerReceiver(br, new IntentFilter("action"));
// This doesn't work
context.startService(new Intent(context, MyService.class));
}
}
MyService is never started using this approach. I'm relatively new to Robolectric, so I'm probably missing something obvious. Is there some sort of binding I have to do before calling startService? I've verified that broadcasting works by just calling sendBroadcast on the context. Any ideas?
You can't test the service initialization like you're trying to do. When you create a new activity under Robolectric, the activity you get back is actually a ShadowActivity (kind of). That means when you call startService, the method that actually gets executed is this one, which just calls into ShadowApplication#startService. This is the contents of that method:
#Implementation
#Override
public ComponentName startService(Intent intent) {
startedServices.add(intent);
return new ComponentName("some.service.package", "SomeServiceName-FIXME");
}
You'll notice that it doesn't actually try to start your service at all. It just notes that you attempted to start the service. This is useful for the case that some code under test should start the service.
If you want to test the actual service, I think you need to simulate the service lifecycle for the initialization bit. Something like this might work:
#RunWith(RobolectricTestRunner.class)
public class MyServiceTest{
#Test
public void testPurchaseHappyPath() throws Exception {
Intent startIntent = new Intent(Robolectric.application, MyService.class);
MyService service = new MyService();
service.onCreate();
service.onStartCommand(startIntent, 0, 42);
// TODO: test test test
service.onDestroy();
}
}
I'm not familiar with how Robolectric treats BroadcastReceivers, so I left it out.
EDIT: It might make even more sense to do the service creation/destruction in JUnit #Before/#After methods, which would allow your test to only contain the onStartCommand and "test test test" bits.
Related
I tried to find the answer, how to inject fields into the BroadcastReceiver, but everything I find is like this code in the onReceive():
((MyApplication) context.getApplicationContext()).getAppComponent().inject(this);
I test BroadcastReceiver with
MyReceiver receiver = new MyReceiver();
receiver.onReceive(context, testIntent);
In this case, it's impossible to inject mocks into the Receiver. How do you manage this situation? I tried to do something like this:
#Override public void onReceive(Context context, Intent intent) {
// injecting is here
onPostInjectReceive(context, intent);
}
void onPostInjectReceive(Context context, Intent intent) {
}
But even with this workaround it's impossible to make view tests with Espresso, only unit testing. How am I able to inject before I get the callback?
Really I can store BroadcastReceiver component on the App level, but it looks like dirty solution.
I had to implement a feature to this app which consists of an Activity and a Service working on the background (it implements Service, not IntentService).
I went through a few tutorials on the Internet that are supposed to work, and they all use LocalBroadcastManager, which by the way is the recommended by Android:
If you don't need to send broadcasts across applications, consider
using this class with LocalBroadcastManager instead of the more
general facilities described below.
I literally lost a day to find out the problem why it wouldn't work for me: it only works if I use Context.sendBroadcast(). and Context.registerReceiver() instead of the LocalBroadcastManager methods.
Now my app is working, but I feel I am going against the best practices, and I don't know why.
Any ideas why it could be happening?
EDIT:
After I wrote this question I went further on the problem. LocalBroadcastManager works through a Singleton, as we should call LocalBroadcastManager.getInstance(this).method(). I logged both instances (in the Activity and in the Service) and they have different memory addresses.
Now I came to another question, shouldn't a Service have the same Context as the Activity that called it? From this article a Service runs on the Main Thread, hence I'd think the Context would be
the same.
Any thoughts on that? (sorry for the long post)
Code samples:
MyService
public class MyService extends Service {
...
// When an event is triggered, sends a broadcast
Intent myIntent = new Intent(MainActivity.MY_INTENT);
myIntent.putExtra("myMsg","msg");
sendBroadcast(myIntent);
// Previously I was trying:
// LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(myIntent);
}
MyActivity
public class MainActivity {
...
private BroadcastReceiver messageReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
Log.d("onReceive", "received!");
// TODO something
}
};
#Override
protected void onResume() {
super.onResume();
registerReceiver(messageReceiver, new IntentFilter(MY_INTENT));
// Previously I was trying:
// LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(messageReceiver, new IntentFilter(MY_INTENT));
}
}
I've never used LocalBroadcastManager, but it sounds like you have to register your receiver on there (i.e. lbm.registerReceiver(...), not mycontext.registerReceiver(...)). Are you doing that?
Now I came to another question, shouldn't a Service have the same Context as the Activity that called it? From this article a Service runs on the Main Thread, hence I'd think the Context would be the same.
The Context class is not related to threads. In fact, both Service and Activity are (indirect) subclasses of Context -- so they're their own Contexts! That's why you can use "this" as a Context.
But regardless of which context you send into LocalBroadcastManager.getInstance(), you should be getting the exact same LBM instance out. I can't think of any reason that you wouldn't -- except if you're running the Activity and Service in different processes?
Declaration:
private BroadcastReceiver receiver;
Initialization:
receiver = new BroadcastReceiver()
{
#Override
public void onReceive(Context context, Intent intent)
{
//todo
}
};
Registration:
LocalBroadcastManager.getInstance(context).registerReceiver(receiver, new IntentFilter("RECEIVER_FILTER"));
context can be any type of Context, you can use the application context.
Unregister:
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
Broadcast:
Intent intent = new Intent("RECEIVER_FILTER");
intent.putExtra("EXTRA", someExtra);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
check out if your Service and Activity are run in different process, LocalBroadcastManager can't apply in different process.(you should see it in AndroidManifest.xml file)
I had setup AlarmManager in my MainActivity class.
A class called AlarmReceiver gets fired up for every set interval of time.
I have to perform an operation when that class is fired up. That code is in in another class Parsing.java
Now in AlarmReceiver.java, I'm doing this :
public class AlarmReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
Parsing obj = new Parsing(context);
obj.execute();
}
}
I cannot write the code directly in AlarmReceiver.java, because AlarmReceiver.java is already extending BroadcastReceiverand my code which is Parsing.java is extending another class.
So, I'm creating an object for Parsing class and calling that method.
Is my approach correct?
I'll furnish further information in case needed.
Please let me know if my approach is correct?
Thanks in advance!
EDIT:
Parsing.java
public class Parsing extends AsyncTask<Void, Void, Void> {
//some code
}
Starting an AsyncTask from a BroadcastReceiver is wrong for two reasons:
1. The thread on which onReceive() runs is terminated after the method returns, effectively ending any long-running task which may have been started from there. To quote the official docs:
A BroadcastReceiver object is only valid for the duration of the
call to onReceive(Context, Intent). Once your code returns from this
function, the system considers the object to be finished and no longer
active ..... anything that requires asynchronous operation is not
available, because you will need to return from the function to handle
the asynchronous operation, but at that point the BroadcastReceiver
is no longer active and thus the system is free to kill its process
before the asynchronous operation completes.
2. The Context instance that onReceive() provides is not the same as
the Context of an Activity or Service, i.e. Activity.this or
Service.this. You need that proper Context for performing many of
the common useful operations that we usually do from an Activity or
Service. So, for example, the correct way to start a Service in
onReceive() is:
#Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(context.getApplicationContext(), ParsingService.class);
context.getApplicationContext().startService(i);
}
and not
#Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(context, ParsingService.class);
context.startService(i);
}
I don't know how you wrote your Parsing.java, it looks fine but remember this
This method is always called within the main thread of its process, unless you explicitly asked for it to be scheduled on a different thread using registerReceiver. When it runs on the main thread you should never perform long-running operations in it (there is a timeout of 10 seconds that the system allows before considering the receiver to be blocked and a candidate to be killed). You cannot launch a popup dialog in your implementation of onReceive()
To me, i think it's a better way to handle this is calling another service inside onReceive method, like this
#Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(context, ParsingService.class);
context.startService(i);
}
First, I get a spied instance of my class under test:
TestedClass testedClass = spy(new TestedClass(Robolectric.buildActivity(Activity.class).create().get());
Then, some changes happen to the tested class:
testedClass.someString = "whatever"
Then, I simulate sending an intent to a broadcast receiver registered in the tested class:
ShadowApplication shadowApplication = Robolectric.getShadowApplication();
Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
List<BroadcastReceiver> broadcastReceivers = shadowApplication.getReceiversForIntent(intent);
broadcastReceivers.get(0).onReceive(Robolectric.application, intent);
The BroadcastReceiver is found in the list, so I can call onReceive and onReceive event is fired:
public void onReceive(Context context, Intent intent) {
Log.i(tag, someString);
}
However, the TestedClass object that appears is an uninitialized version, this is: someString == ""
It's not the same object that was being spied.
Maybe, it's the normal behaviour, but I would like to get the same instance I created for the test, not a mocked one created parallel.
It depends on the way you instance the BroadcastReceiver.
If you instance the BroadcastReceiver with a default value, the simulated calls to the BroadcastReceiver won't be executed
//Class member myBroadcastReceiver
public BroadcastReceiver myBroadcastReceiver = new MyBroadcastReceiver();
However, if you instance the BroadcastReceiver later, it works properly:
public myBroadcastReceiver;
public initializeBroadcastReceiver() {
myBroadcastReceiver = new MyBroadcastReceiver();
}
These are the facts, nonetheless, I still don't know if the first case is expected behaviour or it is a flaw of Robolectric.
I'm currently writing unit tests for an android application and stumbled into the following issue:
I use the ServiceTestCase to test an IntentService like this:
#Override
public void setUp() throws Exception {
super.setUp();
}
public void testService()
{
Intent intent = new Intent(getSystemContext(), MyIntentService.class);
super.startService(intent);
assertNotNull(getService());
}
However I noticed that my IntentService is created (means that onCreate is called) but I never receive a call into onHandleIntent(Intent intent)
Has anyone already tested an IntentService with the ServiceTestCase class?
Thanks!
This is a bit late, but I just struggled with this. You could solve this by creating a class that simply overrides the onStart of you service so it calls onHandleIntent directly. So for instance, if you have a LocationUpdaterService, you could create a fake class that overrides the onStart function like this:
public class LocationUpdaterServiceFake extends LocationUpdaterService {
#Override
public void onStart(Intent intent, int startId) {
onHandleIntent(intent);
stopSelf(startId);
}
LocationUpdaterService is a subclass of IntentService, so when you write your tests, just use the LocationUpdaterServiceFake class like this
public class LocationUpdateServiceTest extends ServiceTestCase<LocationUpdaterServiceFake> {
public LocationUpdateServiceTest()
{
super(LocationUpdaterServiceFake.class);
}
public void testNewAreaNullLocation()
{
Intent intent = new Intent();
intent.setAction(LocationUpdaterService.ACTION_NEW_AREA);
startService(intent);
}
}
Now whenever you call startService, it will bypass the threading code in IntentService and just call your onHandleIntent function
I just got started into testing my own IntentService and it's proving to be a bit of a headache.
Still trying to work things out but for the scenario where it seems that you do not receive a call to your method onHandleIntent(), (I'm not very good with the technicalities behind junit so forgive my use of terminology) it should be because the test framework, based on your code, actually tears down or end the test method once your call to startService returns. There is insufficient time for onHandleIntent to be triggered.
I verified the above theory by adding an infinite loop within my test case - only then can I see my log statements in onHandleIntent logged.
You just have to add a:
Thread.sleep(XXXXXXX);
Choose the XXXX after the startService, then it will let the thread go into the onHandleIntent method.
In Android Studio 1.1, when running tests using the Run/Debug Configuration | Android Tests facility on any unit under test code (UUT) that extends IntentService, the ServiceTestCase.java (JUnit?) code does not call onHandleIntent(Intent intent) method in the UUT. ServiceTestCase only calls onCreate so the problem is in the test code.
protected void startService(Intent intent) {
if (!mServiceAttached) {
setupService();
}
assertNotNull(mService);
if (!mServiceCreated) {
mService.onCreate();
mServiceCreated = true;
}
mService.onStartCommand(intent, 0, mServiceId);
mServiceStarted = true;
}
In my file smSimulatorTest.java:
public class smSimulatorTest extends ServiceTestCase<smSimulator>
At this point, I'm looking for other solutions in the testing framework that test UUTs through Intents since this is how IntentService is instantiated.
http://developer.android.com/reference/android/app/IntentService.html - To use it, extend IntentService and implement onHandleIntent(Intent). IntentService will receive the Intents, launch a worker thread, and stop the service as appropriate.
I, like others, put my code in the onHandleintent() as directed by the above documentation, however, ServiceTestCase only tests onStart and onStartCommand has shown above.
This is my approach for now:
The start Intent that invokes the service specifies the Service method to test
public void test_can_do_the_work() {
Intent startIntent = new Intent();
startIntent.putExtra("IN_TEST_MODE", "TEST_SPECIFIC_METHOD");
startIntent.setClass(getContext(), MyServiceToTest.class);
startService(startIntent);
assertNotNull(getService()); // Your assertion Service specific assertion
}
In the service onStart, we check for the specific Extra passed and call the method to test. This won't execute when Handle intent fired.
#Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
String in_test_mode = intent.getStringExtra("TEST_SPECIFIC_METHOD");
if(in_test_mode != null){
doServiceWork();
}
}