Espresso : test startActivityForResult return RESULT_OK - android

I got an Android app that requires authentication to be used. As the project grows up, I want to add unit testing in my app.
To do that, I use Espresso 2.1
The thing is:
My LoginActivity is called by startActivityForResult
It shows the form
a. If the credentials are wrong, it stays on the LoginActivity
b. If the credentials are ok, it finish with a RESULT_OK.
So in my espresso test class, I have some unit tests. Everything is fine with the not ok cases, the problem is on the ok case.
I need to check that the result is RESULT_OK but as the Activity finishes, espresso failed with a
android.support.test.espresso.NoActivityResumedException: No activities in stage RESUMED. Did you forget to launch the activity. (test.getActivity() or similar)?
Here is my questions:
Is there a way to test the setResult of the Activity ?
Is there a workaround (I'd like to not use any of them, but at least...) to be able to test that ?
I've read things about Espresso-Intents but I can't figure out an example on how I can start an activity of my own package and check the result is a RESULT_OK.

Very short answer:
yes, it is possible to set the result by doing
Intent resultData = new Intent();
resultData.setData(...);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
intending(toPackage("package.containing.login.activity")).respondWith(result);
You would asses that the RESULT_OK was received by checking that one of the views contains the desired returned information.
If that is not clear enough, post any questions in the comments and will try to help.
how I can start an activity of my own package
How would the user do it? Probably by clicking on a button or any other view interaction, right?

Do you really need Espresso for this test? Robolectric ActivityShadow has getResultCode() and getResultIntent() methods.
final Intent startIntent = new Intent();
final ResultActivity activity = Robolectric.buildActivity(ResultActivity.class)
.withIntent(startIntent)
.create()
.get();
final String data = "data";
activity.onEventMainThread(new CompletedEvent(data));
assertEquals(RESULT_OK, shadowOf(activity).getResultCode());
final Intent resultIntent = shadowOf(activity).getResultIntent();
assertEquals(data, resultIntent.getStringExtra(DATA_KEY));
ResultActivity sets result and finishes after receiving a CompletedEvent. In this test the event handler is called directly to simulate the event.

Related

Instrumentation testing an Android activity with intents in isolation

I am attempting to instrumentation test an Activity in isolation however I'm running into issues because part of the testing requires that I verify that the Activity under test launches another Activity via an Intent.
What I'm looking for is some way to intercept an Intent so that I can verify that the isolated Activity actually attempted to launch the next Activity but without the next Activity actually launching.
The issue I'm running into is that when the next Activity launches it crashes because I'm unable to mock a few critical things that it requires. It would be perfect if there was a way to intercept the Intent during testing so that the next Activity never launches.
Is what I'm looking for even possible?
Originally I tried to use Espresso's intended() and intending() methods in order to verify that Intents were being sent without actually starting an Activity (as described here: https://collectiveidea.com/blog/archives/2015/08/11/stub-your-android-intents
However I did not have luck making that work. What I eventually resorted to was using ActivityMonitor to do the job.
Here's an example:
private void registerActivityMonitorAndStartActivity(String name) {
Instrumentation.ActivityMonitor am = new
Instrumentation.ActivityMonitor(name, null, true);
InstrumentationRegistry.getInstrumentation().addMonitor(am);
mActivityTestRule.launchActivity(new Intent());
int count = 0;
while(!InstrumentationRegistry.getInstrumentation().checkMonitorHit(am, 1) && count < 50000) {
count++;
}
Timber.d("Count = " + String.valueOf(count));
assertTrue(InstrumentationRegistry.getInstrumentation().checkMonitorHit(am, 1));
}
This basically has an activity monitor watch for an intent sent to an activity that you specify by name. A while loop runs until the activity monitor sees a hit and then breaks or breaks if a timeout is hit.

Clear Intents between instrumentation tests

One of my Espresso tests is apparently failing because a broadcast intent triggered by the previous test is arriving part way through the subsequent test. At this point, the application is in an inconsistent state and missing some SharedPreferences which are expected by the intent handler.
Is there a way to make sure that all waiting intents have been processed or cancelled before starting an Espresso test?
I don't think there is a way to clear intents that are already dispatched. However you can get around this by tweaking your "previous test".
If you want to fully take advantage of Espresso you have to reduce the scope of your test as small as possible. So instead of broadcasting an intent and verifying what happens next, try stubbing your intents.
Use IntentsTestRule instead of ActivityTestRule and stub out all internal intents (sometimes even the external ones) using the below code:
intending(isInternal()).respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));
Now in your "previous test", check if the right intent is getting broadcasted using something like:
intended(hasComponent("com.example.something.SomeActivity"));
This way, you can test your functionality without actually broadcasting any intents. I always stub my Internal and External intents and verify if the right intent is getting triggered in all my tests. This way the next activity doesn't get launched and my Espresso tests are faster and stable. Always ensure to test only what you are want to test, and keep the scope really small.
Read more: https://google.github.io/android-testing-support-library/docs/espresso/intents/
I had the same problem testing two intents,
one to share text and another to share audio.
I managed to do it by calling between the intents.
Intents.init()
//
Intents.release()
I don't know if it is the right way but is working on the question.
Let say I have to check two Intents with action CHOOSER.
Even my intents are ACTION_SEND, I have a CHOOSER in my code to send intent.
Intents.init()
testIntent1
Intents.release()
Intents.init()
testIntent2
Intents.release()
testIntent1() {
Matcher<Intent> expectedIntent = getIntentMatcherChooser();
String textInET = "share this text";
replaceTextCloseKeyboard(R.id.et_main, text);
openDrawerClickItem(R.id.nav_item_share_text);
intended(expectedIntent);
}
getIntentMatcherChooser(){
Matcher<Intent> expectedIntent = allOf(hasAction(Intent.ACTION_CHOOSER));
intending(expectedIntent).respondWith(new Instrumentation.ActivityResult(0, null));
}

How can I check the expected intent sent without actually launching activity in Espresso?

I have a UI test which clicks a button, and then launch a new Activity in its onClickListener. The test checks whether expected intent is sent or not.
My problem is, I want to test whether expected intent is sent without actually launching the activity. Because I found that new activity initializes its state, and it makes subsequent tests flaky.
I know there are two Espresso Intents API, which are intended and intending, but both fail to meet my needs. intended API actually launches the target activity, and intending API doesn't launch the activity, but it calls onActivityResult callback which I don't want either. Because I'm afraid that code inside onActivityResult may cause another flakiness.
Also intending doesn't assert whether matching intent is sent. It just calls onActivityResult callback when matching intent is found, which means I have to check whether onActivityResult is called or not!
Is there a clean way to achieve what I want?
If you want to test whether expected intent is sent without actually launching the activity you can do it by capturing the intent with an activityResult and then catching the activity :
Intent intent = new Intent();
ActivityResult intentResult = new ActivityResult(Activity.RESULT_OK,intent);
intending(anyIntent()).respondWith(intentResult);
onView(withId(R.id.view_id_to_perform_clicking)).check(matches(isDisplayed())).perform(click());
intended(allOf(hasComponent(ActivityToBeOpened.class.getName())));
This would catch any attempt of launching ActivityToBeOpened. If you want to be more specific you can also catch an intent with Extras:
intended(allOf(hasComponent(ActivityToBeOpened.class.getName()), hasExtra("paramName", "value")));
Hope that helps.
Espresso's Intents class is a concise and handy api, but when it doesn't meet your needs, there is an alternative. If you use AndroidJUnit4 test runner, you can get Instrumentaion instance using InstrumentationRegistry.getInstrumentation(), and then you can add Instrumentation.ActivityMonitor instance.
Instrumentation.ActivityMonitor am = new Instrumentation.ActivityMonitor("YOUR_ACTIVITY", null, true);
InstrumentationRegistry.getInstrumentation().addMonitor(am);
onView(withId(R.id.view_id_to_perform_clicking)).check(matches(isDisplayed())).perform(click());
assertTrue(InstrumentationRegistry.getInstrumentation().checkMonitorHit(am, 1));
The third parameter of ActivityMonitor constructor tells we want to block activity launching. Note that this approach has its limitation. In contrast to Espresso Intents' rich Matcher support, You can not set multiple condition for ActivityMonitor.
You can find several samples in ApiDemos, especially in ContactsSelectInstrumentation class.
Actually, you can block any intent to launch an external or your own activity but still use the rich Espresso Intents API:
Instrumentation.ActivityMonitor soloMonitor = solo.getActivityMonitor();
instrumentation.removeMonitor(soloMonitor);
IntentFilter filter = null;
// Block any intent
Instrumentation.ActivityMonitor monitor = instrumentation.addMonitor(filter, null, true);
instrumentation.addMonitor(soloMonitor);
// User action that results in an external browser activity being launched.
user.clickOnView(system.getView(R.id.callButton));
instrumentation.waitForIdleSync();
Intents.intended(Matchers.allOf(
IntentMatchers.hasAction(Matchers.equalTo(Intent.ACTION_VIEW)),
IntentMatchers.hasData(Matchers.equalTo(Uri.parse(url))),
IntentMatchers.toPackage(chromePackage)));
instrumentation.removeMonitor(monitor);
You able to do that because Espresso Intents still records every Intent with IntentMonitor callback even if you block them. Look at the source code of Espresso Intents on how they do that.
If you use Robotium Solo framework you need to move your own ActivityMonitor before their one. Otherwise just skip the lines related to this.

android launched activity exit callback function

I am launching the android setting activity, from an android service.
Intent LaunchIntent = getPackageManager().getLaunchIntentForPackage("com.android.settings");
startActivity(LaunchIntent);
I am searching, how I can detect if the setting activity is closed,As I need some callback method.
If there is a callback method to know the settings or any other app like browsers,if launched in this method to know if the launched activity is exit its own.
Since settings and browsers are general code we can't put broadcast code in these activities.
Use startActivityForResult to launch the settings activity like so:
Intent LaunchIntent =
getPackageManager().getLaunchIntentForPackage("com.android.settings");
startActivityForResult(LaunchIntent, 42);
Usually, you would use a specific request code as the second argument, but in this case, you have no control over what the settings Activity could return as a result, and you only want to know when it finishes, so you can essentially make up a request code. It must be greater than 0, however. The docs state this here:
requestCode If >= 0, this code will be returned in onActivityResult() when the activity exits.
Then, you can override the onActivityResult method to handle what happens when the settings activity closes:
#Override
protected void onActivityResult (int requestCode, int resultCode, Intent data){
// Do whatever you would like to do
}
If you had used a specific request code when you started the Activity, this is where you would check if the result code exists, but since we aren't expecting any real result, the result code will likely be equal to RESULT_CANCELLED, but that's okay since you at least know that the Activity was cancelled.

Question regarding account creation and sync on Android

I've been reading the sample code from the dev docs on Android's site, specifically this:
http://developer.android.com/resources/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.html
Which is the sole activity of the sample app. It refers to an intent in the onCreate method. I don't understand where this intent is coming from, or what it should contain if this is the only activity the app utilizes.
Log.i(TAG, "loading data from Intent");
final Intent intent = getIntent();
mUsername = intent.getStringExtra(PARAM_USERNAME);
mAuthtokenType = intent.getStringExtra(PARAM_AUTHTOKEN_TYPE);
mRequestNewAccount = mUsername == null;
mConfirmCredentials = intent.getBooleanExtra(PARAM_CONFIRM_CREDENTIALS, false);
That's the block of code working with the intent. Why would you have an intent for the only activity in the app? Is this app called in an unusual way? The Manifest does not include an intent filter for the activity... I guess I'm just a bit lost on this whole thing! If someone could set me straight that'd be great, thanks.
Why would you have an intent for the only activity in the app?
getIntent() gets you the intent that started this activity.
Is this app called in an unusual way?
I guess this activity is called programmatically from another app or activity, since it has been passed some extra data: getStringExtra() is used to extract some data from the intent that started it. putExtra.. and getExtra.. is a way to pass data between activities when they are started.
In that specific example, the intent is sent from the addAccount method in Authenticator.java. That method is called by the OS when you click the Add Account button in the Accounts & sync settings screen and choose your account type.

Categories

Resources