I have decided that one of the testing criteria for my application tests with Google's Espresso is:
Test should maintain Activity state after screen orientation rotation
How do I rotate the screen when using Espresso?
I have tried the following Robotium code (Yes I placed Robotium code in my Espresso test so sue me)
solo.setActivityOrientation(solo.LANDSCAPE);
solo.setActivityOrientation(solo.PORTRAIT);
but It crashes the application when I run it within my Espresso test.
Is there any way to do this?
Thanks in advance for any help
If you have the only Activity in your test case, you can do:
1. Declare you test Rule.
#Rule
public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(TestActivity.class);
2. Get you Activity and apply a screen rotation.
mActivityTestRule.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
mActivityTestRule.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
That's a piece of pie!
You can do it with uiautomator library
dependencies {
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
ui automator require min sdk version 18 so if your app has a lower min sdk you need to create a new AndroidManifest.xml in androidTest folder
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:tools="http://schemas.android.com/tools"
package="your.package.name">
<uses-sdk tools:overrideLibrary="android.support.test.uiautomator.v18"/>
</manifest>
and then in your test
UiDevice device = UiDevice.getInstance(getInstrumentation());
device.setOrientationLeft();
device.setOrientationNatural();
device.setOrientationRight();
This more complete solution creates a custom Espresso ViewActionand works well. It shows how to get the Activity (even when it is an AppCompatActivity) before calling its setRequestedOrientation() method. It also has a clean caller interface:
onView(isRoot()).perform(orientationLandscape());
onView(isRoot()).perform(orientationPortrait());
I follow each orientation change with a 100 ms delay, though you may not need it.
How to rotate the screen:
public static void rotateScreen(Activity activity) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
final int orientation = InstrumentationRegistry.getTargetContext()
.getResources()
.getConfiguration()
.orientation;
final int newOrientation = (orientation == Configuration.ORIENTATION_PORTRAIT) ?
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE :
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
activity.setRequestedOrientation(newOrientation);
getInstrumentation().waitForIdle(new Runnable() {
#Override
public void run() {
countDownLatch.countDown();
}
});
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException("Screen rotation failed", e);
}
}
The Activity can be obtained from the ActivityRule.
You can't mix Robotium and Espresso tests. The best way sometimes to solve any issue is to check source code of desired but not comaptible method.
I'm pretty sure that you have already setUp() method, which has code like:
myActivity = this.getActivity();
Use this to change your screen orientation change:
myActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
or
myActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
You may also need to use myActivity.getInstrumentation().waitForIdleSync(); or Thread.sleep(milliseconds); in order to wait for the rotation end because it is performed in Async manner. The second methods depends on emulator/device so choose it wisely.
Hope it help.
After my own troubles with testing of the orientation I want to add that while lelloman's advice to use UiDevice is correct from the documentation standpoint - unfortunately it doesn't work as expected in some cases.
I found that at least on the API 23 emulator rotation could stuck after the test's crash:
Do uiDevice.setOrientationLeft()
Application crashes;
Rotation is stuck;
3.1 Forcing rotation through UiDevice works but when test ends rotation is back the wrong one until you manually change rotation to "Auto-rotate";
3.2 uiDevice.unfreezeRotation() helps while test is running but after the test's end rotation is back to the wrong one.
I don't have this issue on API 28.
So I found that using setRequestedOrientation is the only solution for me.
In addition, if you are using ActivityTestRule in order to access your activity, the documentation states that this class is deprecated and you are better off using ActivityScenarioRule.
Instead of directly accessing properties of the activity, you need to put your interactions with the activity inside a runnable callback.
So the screen rotation example may look something like this:
#get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
private fun rotate() {
activityRule.scenario.onActivity {
it.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
}
P.S. not sure about the proper constant to use, but ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE(or USER_PORTRAIT) was the one that worked for me
Related
For tests I use Espresso and Barista
I have a test in which I need to open another screen by pressing a button. How can I check if this screen opens? Did the screen I need open?
Can I somehow check the chain of screens? To understand that the screens open in the order I need?
If someone throws links to good tutorials on UI tests in Android, I will be very grateful.
An easy solution would be to just check for an element of the new screen to be shown like this:
onView(withId(R.id.id_of_element_in_your_new_screen)).check(matches(isDisplayed()))
If you really want to check out for the current activity that is shown, you could try something like this:
Gather the current activity via InstrumentationRegistry and check for the activity in stage RESUMED.
fun getTopActivity(): Activity? {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
if (resumedActivities.iterator().hasNext()) {
resumedActivities.iterator().next()?.let {
activity = it
}
}
}
return activity
}
You could then check this in a test like this:
#Test
fun checkForActivity() {
val currentActivity = getTopActivity()
assertTrue(currentActivity?.javaClass == YourActivityToCheckAgainst::class.java)
}
I personally use intended(hasComponent(YourActivityToCheckAgainst::class.java.name)), which checks if the last intent was done with a desired activity, set as its component.
I also wrote an extensive Android UI testing tutorial using Espresso + Barista libraries.
The Code A is from CameraX project, you can see source code.
Android Studio will display "only be called from with the same library group" when I remove #SuppressLint("RestrictedApi"), you can see Image 1.
Why can't I remove #SuppressLint("RestrictedApi") in Code A ? What deos a restriction API mean?
Code A
#SuppressLint("RestrictedApi")
private fun updateCameraUi() {
...
// Listener for button used to switch cameras
controls.findViewById<ImageButton>(R.id.camera_switch_button).setOnClickListener {
lensFacing = if (CameraX.LensFacing.FRONT == lensFacing) {
CameraX.LensFacing.BACK
} else {
CameraX.LensFacing.FRONT
}
try {
// Only bind use cases if we can query a camera with this orientation
CameraX.getCameraWithLensFacing(lensFacing)
// Unbind all use cases and bind them again with the new lens facing configuration
CameraX.unbindAll()
bindCameraUseCases()
} catch (exc: Exception) {
// Do nothing
}
}
}
Image 1
There have been breaking changes in the library since the tutorial was made.
Reverting the package version to 1.0.0-alpha06, same as the tutorial, solves the problem.
These are issues with the library that don't affect your code.
In several code examples using these APIs there is often a #SuppressLint("RestrictedApi") in the file hiding the warning.
The projects should still compile and run as they should, although you must make sure that you are using the correct dependency version. The APIs are changing quite frequently still, and if you're referencing an example it might be using an older version which has since changed.
Your best bet is to look directly at the source code and if the method you are calling is declared as public then you probably won't have a problem.
While writing a Robolectric unit test, I noticed my getVisibility() call returned 0 (VISIBLE) after calling fab.hide(), so I assumed it was due to animation and to test it out, added a delayed check. Surprisingly it has also returned VISIBLE. On the actual device it works as expected and returns the correct values.
EDIT: just to clarify I'm using the FAB from the design support library.
My test code is really simple:
fab.performClick();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertThat(fab.isShown()).isFalse();
Code under test:
mActionBunnot.hide();
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
boolean shown = mActionBunnot.isShown();
Log.d(TAG,""+shown);
}
},2000);
When run through Robolectric, both here and in the test, isShown returns true
You can also .isShown() method to get the visibility.
I know it is a bit late, but maybe useful for other people.
Robolectric executes all operations on one single thread. In the past this happened synchronously. Since version 4.3 Robolectric has the Looper PAUSED mode, which improves this behaviour as described in this blog: http://robolectric.org/blog/2019/06/04/paused-looper/
I just started to try Robolectric and as I am currently playing around with the Dynamic Single/Dualpane-Fragment example from the google developer page I thought integrating it there and doing some basic tests.
The first thing I wanted to test is does the single/dualpane handling work correctly.
So its basically down to:
small device & portrait -> single-pane
large device & langscape -> dual-pane
As the code for the example is online (and as it is a standard template in Android-Studio) I am not gonna copy it here again. Just one thing: On launch the activity determines if its single- or dual pane by checking:
if (findViewById(R.id.exercise_detail_container) != null) [..]
It seems that for Robolectric its always dualpane.
So my singlepane test is pretty straight forward:
#Config(emulateSdk = 18)
#RunWith(RobolectricTestRunner.class)
public class SinglePaneTest {
private ExerciseListActivity activity;
private FragmentManager fragmentManager;
#Before
#Config(qualifiers = "port-small")
public void setup() {
this.activity = Robolectric.buildActivity(ExerciseListActivity.class).create().resume().get();
this.fragmentManager = activity.getFragmentManager();
}
#Test
#Config(qualifiers = "port-small")
public void testSinglePane() {
assertNull(activity.findViewById(R.id.exercise_detail_container));
}
}
But the test fails.
Can somebody tell me why? This should be working perfectly fine, shouldn't it?
Just for the record: Yep, in the emulator everything is working fine.
Try reversing order: small-port Don't know if will fix, but it is true that they must appear in the order in the table: http://developer.android.com/guide/topics/resources/providing-resources.html
if you use multiple qualifiers for a resource directory, you must add them to the directory name in the order they are listed in the table.
Robolectric example supports this ordering:
Qualifiers for the resource resolution, such as "fr-normal-port-hdpi".
This seems to be fixed with version 2.4 of robolectric!
I'm writing test to my project with Robolectric-2.3.
I'd like to test my UI properties such as Views visibility. The actions of showing/hiding views are wrapped into Animations. How to test it ?
I tried to use ShadowSystemClock.sleep() method to wait until animation ends but it doesn't seem to work as I expected.
#Test
public void testHideSearch() throws Exception {
mListFragment.hideSearch(); //<--- animation launched here
sleep(1000);
View searchEditText = mListFragment.getView().findViewById(R.id.filterEditText);
assertFalse(searchEditText.getVisibility() == View.VISIBLE);
}
What is the correct approach to the issue ?
Try using this instead of sleep:
Robolectric.getUiThreadScheduler.advanceBy(1000);
If you use Animator's your approach should work, but if you use Animations, you'll need to use ShadowAnimation instead. Assuming the actual visibility of your view is changed in onAnimationEnd(), something like the following should work:
ShadowAnimation animation = Robolectric.shadowOf(searchEditText.getAnimation());
animation.invokeEnd();