An overview of the
I, however, am an ordinary Android developer who, in addition to my day job, likes to open source and share widgets I write on Github. One of my personal favorites is RxImagePicker, a responsive image picker for Android that I implemented in my spare time.
Github.com/qingmei2/Rx…
With the support of some friends, the picture selector was gradually applied in some projects. As more and more people used it, I felt more and more pressure. At least, I needed to ensure that every new version would avoid low-level mistakes, or at least not crash when using it.
I quickly realized that I was in a bind — even with a minor update to the library, I needed to keep the basic functionality of every interface in the library available, and after the release, I needed to rely on the latest version on Jcenter and run and manually test its various interfaces.
In this way, I insisted on several versions of the iteration, overlapping, OVERLAPPING, I can not move.
I wasn’t sure how much longer I could stick to manual scratching and testing, which meant that automated testing of the UI was imperative, so I took the opportunity to do so. The result: automated testing of the UI was applied to my project.
Now, for each release, I only need to run it with one click, avoiding low-level bugs and avoiding the tedious manual testing of each interface:
All I had to do was have a nice cup of tea, wait for the results of the automated test, and soon, I got the following results:
There wasn’t a lot of testing code, but it did take me a lot of free time to learn about Espresso UI automation, but I think it was worth it.
, of course, because the Android UI test automation is not widely used in home from (in fact not only the UI test automation, unit test, personally, I guess, anxious state of mind has universality and domestic), the convenient learning materials are very few, I’m more or less stepped on some pit, I decided to share my practice experience, Hope to some friends who want to learn Espresso have certain help.
To prepare
This article assumes that the reader has some understanding of the basic concepts of AndroidUI automated testing, and has a preliminary grasp of the use of the Espresso tool library.
If you are not familiar with these basic apis, please refer to this article by the authors:
Free hands: UI Automation Testing for Android Developers
In addition, it would be nice if the reader had some testing related foundations, including JUnit4, the concept of Rule, and Kotlin’s basic syntax.
All of the test code for the examples in this article comes from this project:
Github.com/qingmei2/Rx…
If you think it’s a good library, you’re welcome to star or fork it as I write this article for my own selfish reasons…
Step by step, problems encountered in practice
1. Dependency and configuration
AndroidJUnitRunner: AndroidJUnitRunner: AndroidJUnitRunner: AndroidJUnitRunner: AndroidJUnitRunner: AndroidJUnitRunner:
android {
defaultConfig {
// ...
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
}
dependencies {
/ /...
androidTestImplementation "Com. Android. Support. Test. Espresso: espresso - core: 3.0.2." "
androidTestImplementation "Com. Android. Support. Test. Espresso: espresso - contrib: 3.0.2." "
androidTestImplementation "Com. Android. Support. Test. Espresso: espresso - idling - resource: 3.0.2." "
androidTestImplementation "Com. Android. Support. Test. Espresso: espresso - the intents: 3.0.2." "
androidTestImplementation 'com. Android. Support. Test: runner: 1.0.2'
androidTestImplementation 'com. Android. Support. Test: rules: 1.0.2'
}
Copy the code
Groovy is a very useful language, and in my personal projects, every time I release a new version, I need to have SAMPLE rely on the remote version of Jcenter; Development relies on the Module in project by adding a configuration variable that can be used as a switch for version control:
Now you can start your own UI automation tests in the androidTest package under Module:
Let’s start with a simple one, sample’s home page:
2. Test the Intent
The main page is very simple, 3 buttons, jump to 3 different picture selection interface, for a single button test code is as follows:
@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityTest {
val TEST_PACKAGE_NAME = "com.qingmei2.sample"
@Rule
@JvmField
var tasksActivityTestRule = IntentsTestRule<MainActivity>(MainActivity::class.java)
@Test
fun testJump2SystemActivity(a) {
// Check for click events -- click the SystemTheme button to jump to the system selector interface
checkScreenJumpEvent({ R.id.btn_system_picker },
{ ".system.SystemActivity"})}private fun checkScreenJumpEvent(buttonId: () -> Int,
shortName: () -> String,
packageName: () -> String = { TEST_PACKAGE_NAME }) {
// Click the corresponding button
onView(withId(buttonId())).perform(click()).check(doesNotExist())
// Whether a corresponding intent was generated
intending(allOf(
toPackage(packageName()), / / package path
hasComponent(hasShortClassName(shortName())) / / class shortClassName
))
// Click the back button to check whether you are back to the current interface
pressBack()
onView(withId(buttonId())).check(matches(isDisplayed()))
}
}
Copy the code
For page jump tests, Espresso provides IntentsTestRule instead of ActivityTestRule, and it provides a mechanism for checking for Intent jump behavior on interface elements.
Therefore, we only need to replace ActivityTestRule with IntentsTestRule for the detection of interface jumps, without unnecessary configuration.
Of course, the real reason for this simplicity is that IntentsTestRule itself inherits ActivityTestRule:
public class IntentsTestRule<T extends Activity> extends ActivityTestRule<T> {}
Copy the code
3. Test the permission request
The next picture selection interface test, taking wechat theme as an example, the interface is as follows:
As I did myself, I needed to add test code that simulated the user opening the camera and the user opening the album, respectively.
In fact, this test is not difficult to write, take opening the wechat theme album interface as an example, the test code is as follows:
@RunWith(AndroidJUnit4::class)
@LargeTest
class WechatActivityTest {
// To open the album, of course you need to call startActivityForResult to get the result
// Hence the activityResult of the Mock success (based on the parameters in the actual project)
private val successActivityResult: Instrumentation.ActivityResult =
with(Intent()) {
putExtra(BasePreviewActivity.EXTRA_RESULT_BUNDLE, EXTRA_BUNDLE)
putExtra(BasePreviewActivity.EXTRA_RESULT_APPLY, EXTRA_RESULT_APPLY)
Instrumentation.ActivityResult(Activity.RESULT_OK, this)}@Rule
@JvmField
var systemActivityTestRule = IntentsTestRule<WechatActivity>(WechatActivity::class.java)
@Test
fun testPickGallery(a) {
intending(allOf(
toPackage("com.qingmei2.rximagepicker_extension_wechat"),
hasComponent(".ui.WechatImagePickerActivity")
)).respondWith(successActivityResult)
onView(withId(R.id.imageView)).check(matches(isDisplayed()))
onView(withId(R.id.fabGallery)).perform(click())
onView(withId(R.id.imageView)).check(doesNotExist())
}
companion object Mock {
private const val EXTRA_BUNDLE = "123"
private const val EXTRA_RESULT_APPLY = "456"}}Copy the code
There seems to be no problem here. We run the Espresso code directly to simulate the button clicking event, and the result is not the expected interface jump, because:
Before entering the album screen, the system pops up a permission request window.
My UI interface logic is that if the user does not grant permission, then the next interface will not jump, and the permission popup is system-level, we cannot find the corresponding Button through Espresso to confirm the permission.
Relying on Google, I discovered that in newer versions Espresso provides a GrantPermissionRule that automatically assigns permissions to pop-ups:
@Rule
@JvmField
var grantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
Copy the code
I added permission Rule WRITE_EXTERNAL_STORAGE to the test class, and sure enough, in the following test, there was no test failure caused by permission popup.
4. The Application & Library?
I’m going to talk about a problem that has bothered me for a long time, but before I talk about it, let me show you a picture:
As you can see, this is the structure of the project, the sample as the upper caller of tool library, the underlying theme of different image selection UI is placed in the library of the module (such as the above WechatImagePickerActivity WeChat theme of the show interface).
I think WechatImagePickerActivity UI test should also be under the library – this seems to be taken for granted, but when I write good relevant test code at run time, I found a problem, photo album interface did not show any pictures, as if the phone doesn’t have any photos.
I thought long and hard and finally found the key to the problem. I found that when I put the Activity’S UI test code under the Library package, my custom album interface could not find any image resources. If I put the Activity’s UI test code under the Application package, my custom photo album interface will display normally.
I don’t know exactly why this happened, but it was enough that I put all the UI test code in the androidTest directory of sample for the time being.
Use dependency injection before testing the UI
Not all interface can be directly tested, and some of the Activity at the time of launch is in need of some additional dependent, for example, my projects WeChat subject interface WechatImagePickerActivity onCreate (), require an object like this:
class SelectionSpec private constructor() : ICustomPickerConfiguration {
/ /... Various configurations, such as maximum number of options, themeId, etc
var themeId: Int = 0
var orientation: Int = 0
var countable: Boolean = false
var maxSelectable: Int = 0
var maxImageSelectable: Int = 0
var maxVideoSelectable: Int = 0
var filters: ArrayList<Filter>? = null
var capture: Boolean = false
var captureStrategy: CaptureStrategy? = null
var spanCount: Int = 0
var gridExpectedSize: Int = 0
var thumbnailScale: Float = 0.toFloat()
// Builder is no longer displayed
}
class WechatImagePickerActivity : AppCompatActivity() {
// In the Activity onCreate(), the object that needs SelectionSpec will crash if it is empty
override fun onCreate(savedInstanceState: Bundle?).{ setTheme(SelectionSpec.instance!! .themeId)super.onCreate(savedInstanceState)
setContentView(R.layout.activity_picker_wechat)
}
// ...
}
Copy the code
Thus, in some cases, UI testing cannot be done at all without adding the necessary dependencies to the interface in advance.
These objects may be parsed and initialized inside the library during API calls, but they are not necessarily initialized for individual UI tests. As you can see, the Activity in this article throws a NullPointException in this case.
The instantiation of dependencies depends on the architectural design of the project or library, and once it’s done, it’s just a matter of injecting dependencies with the beforeActivityLaunched() provided by ActivityTestRule, which is executed before the Activity starts:
@Rule
@JvmField
val tasksActivityTestRule =
object : IntentsTestRule<WechatImagePickerActivity>(WechatImagePickerActivity::class.java) {
override fun beforeActivityLaunched(a) {
super.beforeActivityLaunched()
// Inject the ICustomPickerConfiguration
SelectionSpec.instance = WechatConfigrationBuilder(MimeType.ofImage(), false)
.maxSelectable(9)
.countable(true)
.spanCount(3)
.build()
}
}
Copy the code
Now that we’ve configured the required dependencies before the Activity starts and run the test code, NullPointExceptions no longer occur.
6. Test the operation of RecyclerView
Espresso added RecyclerViewAction in the new version (like 2.2+), which corresponds to the operation of RecyclerView we want to use, which is very convenient for developers to use (remember there was no support for RecyclerView in the earlier version, So if we want to operate on item, we must rely on onData()).
RecyclerViewAction is very powerful, but I won’t talk too much about how to use it. Here’s a simple example. When we want to operate on a particular View in an item, we can customize the ViewAction to do so:
fun clickRecyclerChildWithId(id: Int): ViewAction =
object : ViewAction {
override fun getDescription(a): String =
"Click on a child view with specified id."
override fun getConstraints(a): Matcher<View>? =
null
override fun perform(uiController: UiController, view: View) {
view.findViewById<View>(id).apply {
performClick()
}
}
}
fun ViewInteraction.clickRecyclerChildWithId(itemPosition: Int,
viewId: Int) =
perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
itemPosition, clickRecyclerChildWithId(viewId)
))
Copy the code
Take personal project as an example, wechat album interface, I want to click the CheckView of the specified Position Item to select an image, the test code can be like this:
// select image
onView(withId(R.id.recyclerview))
.perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(
1, clickRecyclerChildWithId(R.id.check_view) // The top-level function
))
Copy the code
Kotlin’s top-level function is very useful, and the extension function makes the above test code much simpler:
// Click Poisition = 1
onView(withId(R.id.recyclerview))
.clickRecyclerChildWithId(1, R.id.check_view) // extend the function
Copy the code
7. Test whether the Activity has finished
In addition to the system’s Back button, many interfaces have a Back button design, and even other interface elements that cause the current Activity to close. How to test this?
I searched through the official documentation of Espresso, but could not find the CheckAPI for shutting down the Activity or not, and I was surprised to find that neither Baidu nor Google had any discussion about this situation.
I was at a loss for a moment. I don’t think engineers have ever thought about this kind of test Case. How did they deal with it?
I have a different perspective on why Espresso doesn’t provide such an API to developers — unless, of course, the API already exists.
My final solution was with ActivityTestRule and JUnit4.
ActivityTestRule itself provides activities under test:
fun ActivityTestRule<out Activity>.isFinished(a): Boolean = activity.isFinishing
Copy the code
After that, with JUnit4’s own assertion, it’s perfectly possible to verify that the Activity is closed. I just call it like this:
Assert.assertTrue(activityTestRule.isFinished())
Copy the code
This simple implementation made me laugh and cry. Fortunately, it took a while, but I got the result I wanted.
summary
Although experienced a variety of strange problems (step on the hole), fortunately, narrowly escaped danger, successfully landing, on the Android test related literature (code demo) has been very few, I hope this article is learning UI automation test peers to provide some feasible suggestions and guidance.
Project Address:
Github.com/qingmei2/Rx…
I also hope that this article can let some friends experience the benefits of UI automation testing, even if the process of coverage implementation is very tortuous, but when it is really implemented, you will really love it, just as the so-called:
Jinfeng Yulu a meet, they win but countless world.