Over the past few years, Appium has become one of the most widely used mobile automation tools. While there are certain advantages of using Appium, there are obviously some limitations, given the fact that the test code is totally disconnected from the application code. In this post, I will show how you can give a superpower to your automation code and solve the trickiest problems of Android UI automation.
Problem #1: Flaky App
Imagine a random pop up from your app shows up during your test run. What if the logic behind displaying this popup is not under tester’s control? How can tests be reliable in such cases?Or, imagine you want to click a moving banner in your App, and the test failed because the position of banner had changed by the time Appium could click it!
There are so many examples of Apps behaving unpredictably under testing, which makes the test fail.
Imagine if your tests could tell your app => “I am currently testing you, so please disable Rate Our App popup for now.”
Problem #2: Appium doesn’t support everything
We have a feature in our App with which a user can ‘Undo last No vote’ by shaking the device.
How do you automate ‘Shake’ on an Android device using Appium? There is a shake endpoint (POST /session/:session_id/appium/device/shake), but it’s not implemented for the UiAutomator2 driver.
Imagine if your test could tell your app => “Can you please assume/mock that the device has been shaken?”
Solution: Let your app help by providing Backdoors
Backdoor is a way of calling methods defined in Android Application code from test automation code.
Let’s understand this with a simple example. Assume that you have a method foo() in an activity, say the LoginActivity class of your App as shown below:
In method foo() we can write code to disable a random pop up OR mock Shake functionality OR do anything which will set up your Application state. Imagine, we can call this method from the automation codebase by doing something like this:
This will solve our problem of Imagine if your test could tell your app …
Bad News: This is not supported by Appium as is
Good News: This can be implemented in Appium!
Before jumping to the solution, I want to give some real examples where we use backdoors in our automation:
- Changing the Backend URL
- Changing App locale
- Selecting some specific variant of the Client Side A/B test
- Mocking a sim card for an App
- Getting current session ID
- Getting analytics data back from an app
PS: The process of doing it yourself can be a bit complex. If you don’t care how I did it but just want a quick-start guide, then jump to “Quick Start with Backdoor In Your Project”
Implementing Backdoors in Appium
Uiautomator2 server As-Is architecture
- APK-1 => appium-uiautomator2-server-debug-androidTest.apk
- APK-2 => appium-uiautomator2-server.apk
The APK-1 is an instrumentation package for the APK-2
The APK-1 has an instrumentation test. This test is started using the “adb shell am instrument…” command by the driver. The sole purpose of this test is to start a Http Server which is defined in APK-2.
Both apk files run in a single process, highlighted by green box in image above.
Once the server is up and running, the client can send JSON HTTP requests and the server will execute them.
We can take full advantage of the android instrumentation framework by modifying this architecture to something like this:
Modified uiautomator2 server architecture:
Here, we have merged the code of APK-2 into APK-1. Therefore, the instrumented test and http server both are now in a single apk. Let’s call this “Merged Server APK” for future reference.
We now change Merged Server APK’s Manifest.xml such that its instrumentation target package becomes the package name of the App Under test as shown below:
This is done easily by using the ruby gem called appium_instrumenter (It is a subset of calabash-android gem).
We then sign “Merged Server APK” with same keystore as that of the App under test. Once that is done, we have a custom instrumentation server ready, which runs in the same process as our App Under Test (highlighted by the green line).
Now, we have instrumented our App Under Test with “Merged Server APK”, therefore the “Merged Server APK” can access the Context of our App under Test.
This means, we can tell “Merged Server APK” to call methods defined in our Application under test!
We created an endpoint in “Merged Server APK” to tell it which application method to invoke:
We can post the name of the method we want to invoke to this endpoint. The “Merged Server APK” gets hold of the context of the App Under Test and invokes this method using Java Reflection APIs.
The entire code of this merged Appium Uiautomator2 Server with backdoor endpoint is available at this GitHub repo, in the ‘single_apk’ branch.
Next, replace the apk which comes bundled with the appium_uiautomator2_driver node module with our custom apk.
All the tricky part is now done. This is a one-time job.
Finally, we created a helper method in our test automation code, to call backdoor methods. Here is an example in ruby:
Voilà! We have the ultimate powers! We can call any public method defined in the Application class or current Activity class by simply doing something like this:
What’s more, it also supports getting return values back to the test code!
Quick Start with Backdoor In Your Project
- Generate an Appium Uiautomator Server apk for your App:
Install appium_instrumenter gem. Even if your Appium tests are in Java, you need to do this one-time step because I have made this utility in ruby only.
gem install appium_instrumenter
appium_instrumenter instrument app-debug.apk
This will create a ./test_servers folder in your current directory as:
test_servers├── appium-uiautomator2-server-debug-androidTest.apk└── appium-uiautomator2-server-v0.3.0.apk
Install both of the above apk to your device:
adb install test_servers/appium-uiautomator2-server-debug-androidTest.apkadb install test_servers/appium-uiautomator2-server-v0.3.0.apk
2. Now, Install appium_uiautomator2_driver Node module from my fork:
npm install “rajdeepv/appium-uiautomator2-driver#adb_host”
3. Define a backdoor() method:
Define a method which posts data to backdoor endpoint below:
Please see the example above for this method implemented in ruby. You can implement a similar method if you are using Java or any other language bindings.
4. Enjoy your ultimate powers!
Staying out of trouble:
“With Great Power Comes Great Responsibility”.
Backdoor is an extremely powerful tool. However, if misused, it might change the entire purpose of testing. If we use backdoor to completely change the app logic, then we will put more risk into our test than the value we get out of it.
In summary, backdoors make impossible things possible and they augment the capability of tests. In some cases, this is the only way to go!
PS: Given the complexity of the topic, I might not have given you enough explanation about some of the points. If you need more info, feel free to comment below.