Posted by: Partyk Kosieradzki
See the original article on my website:
https://patrykkosieradzki.com/easy-ui-and-screenshot-testing-on-android/
Ways of testing on Android
Every good Android application should be well tested to minimize the risk of error after releasing it to the world. The most basic tests for any application are Unit Tests. You must write them to ensure that a particular part of the code is working.
On Android we have also Instrumentation Tests — tests that run on physical devices and emulators, and they can take advantage of the Android framework APIs and supporting APIs, such as AndroidX Test. One of many ways of using this mechanism is to test the UI and take screenshots of every screen in our app. I really like this, because if you run this UI/Screenshot Test every time before creating a new pull request, you can easily check if you didn’t change something in the UI by mistake.
How to capture device screenshots?
I’ve worked on a few projects that used screenshot testing and these test were always dependent on external libraries like Karumi Shot or Facebook Screenshot Tests For Android. These libraries were honestly a pain in the ass, completely impossible to set up and use in a long term. One guy from my project spent a few days to manually fix some code inside the library to make it work on Windows 10. I decided that this was too much for a simple operation like taking a device screenshot, so I began to research for a better solution.
What did I find?
Well, first of all, Android 4.3 is a minimum system version we need to have in our project, because we are gonna be using UI Automator under the hood. The solution I found is simply… using Android Support Test Library. This library contains a Screenshot class and capture() method that we’re gonna need.
This is great, because all of the screenshots can be taken, even if, for example, the Activity/Fragment is not currently displayed on the screen or a system dialog is in the foreground.
As I said, we’re gonna need the Screenshot.capture() method:
Creates a ScreenCapture that contains a
Bitmap of the visible screen content for Build.VERSION_CODES.JELLY_BEAN_MR2 and above.
Note: Only use this method if all your tests run on API versions Build.VERSION_CODES.JELLY_BEAN_MR2 or above. If you need to take screenshots on lower API levels, you need to use capture(Activity) or
capture(View) for those versions.
Processing the screenshot
In order to process the screenshot, we have to use two things: ScreenCapture and ScreenCaptureProcessor.
There is a default implementation called BasicScreenCaptureProcessor, which does the following:
A basic ScreenCaptureProcessor for processing a
ScreenCapture.
This will perform basic processing on the given ScreenCapture such as saving to the public Pictures directory, given by android.os.Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES), with a simple name that includes a few characteristics about the device it was saved on followed by a UUID.
This API is currently in beta.
Taking a simple screenshot
At this point, if you were to take a simple screenshot, you’d just have to use a function like this:
private const val TAG = "ScreenshotsUtils" | |
fun takeScreenshot(screenShotName: String) { | |
Log.d(TAG, "Taking screenshot of '$screenShotName'") | |
val screenCapture = Screenshot.capture() | |
try { | |
screenCapture.apply { | |
name = screenShotName | |
process() | |
} | |
Log.d(TAG, "Screenshot taken") | |
} catch (ex: IOException) { | |
Log.e(TAG, "Could not take a screenshot", ex) | |
} | |
} |
WARNING
Make sure you have WRITE_EXTERNAL_STORAGE permission added to the manifest (adding it only for debug build is fine). If you run on API 23+ (Marshmallow), you will also need to have those permissions granted before running the test.
You can “hack” this, by using installOptions in AGP (Android Gradle Plugin). Just put this in your build.gradle file:
android { | |
// The rest of you build.gradle | |
adbOptions { | |
installOptions '-g', '-r' | |
} | |
} |
Custom ScreenCaptureProcessor
If you want to set a custom name for your screenshots and save to a specific location, rather than default “screenshots” folder under “Picture” on the device. Since you could use other apps on the same device, it’ll be better to set up a specific folder on the device. I wanted to make it event better, so I made an implementation that also takes into account flavors and build types:
class MyScreenCaptureProcessor : BasicScreenCaptureProcessor() { | |
init { | |
this.mDefaultScreenshotPath = File( | |
File( | |
getExternalStoragePublicDirectory(DIRECTORY_PICTURES), | |
"${BuildConfig.APPLICATION_ID}/${BuildConfig.BUILD_TYPE}" | |
).absolutePath, | |
SCREENSHOTS_FOLDER_NAME | |
) | |
} | |
override fun getFilename(prefix: String): String = prefix | |
companion object { | |
const val SCREENSHOTS_FOLDER_NAME = "screenshots" | |
} | |
} |
Now, in order to take screenshots we have to update the takeScreenshot(…) function a little:
private const val TAG = "ScreenshotsUtils" | |
fun takeScreenshot(screenShotName: String) { | |
Log.d(TAG, "Taking screenshot of '$screenShotName'") | |
val screenCapture = Screenshot.capture() | |
val processors = setOf(MyScreenCaptureProcessor()) | |
try { | |
screenCapture.apply { | |
name = screenShotName | |
process(processors) | |
} | |
Log.d(TAG, "Screenshot taken") | |
} catch (ex: IOException) { | |
Log.e(TAG, "Could not take a screenshot", ex) | |
} | |
} |
Job Offers
Android Test Orchestrator
The next thing we have to do is to protect our UI tests against test crashes. Normally if you run a lot of tests and one of them crashes it will stop the rest of the remaining tests. If you want to avoid it then you have to use Android Test Orchestrator in your project.
A simple configuration for Android Test Orchestrator looks like this:
android { | |
defaultConfig { | |
... | |
testInstrumentationRunnerArguments clearPackageData: 'true' | |
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | |
} | |
testOptions { | |
execution 'ANDROID_TEST_ORCHESTRATOR' | |
} | |
} | |
dependencies { | |
androidTestImplementation 'com.android.support.test:runner:1.0.1' | |
androidTestUtil 'com.android.support.test:orchestrator:1.0.1' | |
} |
Turning off the animations
Another wise thing to do is to turn off the animations during tests. This setting could prevent unwanted lags and can speed up the tests. To do that, you just have to add animationsDisabled property to the build.gradle:
android { | |
testOptions { | |
... | |
animationsDisabled = true | |
} | |
} |
Retrieve screenshots from the device
If we run our tests now then we should see screenshots saved on the device. Great! But how do we pull them from the device to a specific directory on our PC?
To solve this we have to create a few Gradle Tasks
- First one for creating screenshots directory
- Second one for fetching screenshots
- And the last one for clearing screenshots from the device after fetching
Fetching your screenshots will be done automatically, right after connectedDebugAndroidTest Gradle Task. To make this work, add this to your build.gradle:
def appId = "com.patrykkosieradzki.moviebox" | |
android { | |
... | |
defaultConfig { | |
... | |
applicationId appId | |
} | |
} | |
def projectScreenshotsDirectory = "$projectDir/screenshots" | |
def deviceScreenshotsDirectory = '/sdcard/Pictures/' + appId + '/debug/screenshots' | |
def clearScreenshotsTask = task('clearScreenshots', type: Exec) { | |
println deviceScreenshotsDirectory | |
executable "${android.getAdbExe().toString()}" | |
args 'shell', 'rm', '-r', deviceScreenshotsDirectory | |
} | |
def createScreenshotDirectoryTask = task('createScreenshotDirectory', type: Exec, group: 'reporting') { | |
executable "${android.getAdbExe().toString()}" | |
args 'shell', 'mkdir', '-p', deviceScreenshotsDirectory | |
} | |
def fetchScreenshotsTask = task('fetchScreenshots', type: Exec, group: 'reporting') { | |
executable "${android.getAdbExe().toString()}" | |
args 'pull', deviceScreenshotsDirectory + '/.', projectScreenshotsDirectory | |
finalizedBy { | |
clearScreenshotsTask | |
} | |
dependsOn { | |
createScreenshotDirectoryTask | |
} | |
doFirst { | |
new File(projectScreenshotsDirectory).mkdirs() | |
} | |
} | |
tasks.whenTaskAdded { task -> | |
if (task.name == 'connectedDebugAndroidTest') { | |
task.finalizedBy { | |
fetchScreenshotsTask | |
} | |
} | |
} |
Now, to run tests and fetch screenshots, just execute the following in you project’s directory:
./gradlew connectedDebugAndroidTest
You should see a new folder created right after, called “screenshots”.
If you want to learn more about ADB commands, you learn a lot here: https://developer.android.com/studio/command-line/adb
Show me the Github Repository
To sum it up I’ve created a simple movie app on Github that you can find here:
k0siara/AndroidMovieBoxSimple Android App written in Kotlin, using MVI and Clean Architecture to manage movie info from… |
That’s all folks. Hope this helps you with building a good, well tested Android app. As always, if you have any questions you can either write a comment or message me directly. Bye and happy testing! 😀
Tags: Android Testing, Screenshot Testing, Android, Kotlin, Mobile Development
View original article at: