A tool for context-aware Android application testing
Developed By: Tsiskomichelis Stelios - [email protected] - AUEB (2018 - 2019),
Inspiration/Architectural design: Vassilis Zafeiris
Barista is a tool consisted of the Barista Gradle Plugin and the Barista Library. Both together offer a great framework for customizing the runtime Android environment during traditional instrumentation testing. More specific, it allows the developer to customize various emulator options such as Wifi/Data connectivity, geolocation, battery status, screen preferences etc., using declarations before each test method.
From now on, Barista can act as a fuzzer, taking advantage of Android's Monkey tool and the new context fuzzing that has been implemented.
For this time of being, Barista is tested with the following prerequisites:
Android Gradle Plugin Version 3.2.1
Gradle Version 4.6
Compile Sdk Version API 28
Android Studio 3.5
Android Studio AVD emulators
a) Clone the project from the repository
b) Install the plugin to the local maven repository using the command
gradlew build install
c) At any android project use the follwing .gradle configurations:
Global build.gradle file
buildscript{
...
repositories{
...
mavenLocal()
}
}
...
dependencies{
....
classpath 'gr.aueb.android:barista-plugin:*version*'
}
The above configuration makes sure that gradle will look into the local maven repository to find the previous installed plugin. (Later will be available in global maven repo). It also sets the classpath of the plugin executable as an android build dependency.
In order to use the Barista Plugin just declare the usage of the plugin using the following code in your app specific .gradle file
Module specific build.gradle file (Your main android app module .gradle file)
plugins{
...
id 'gr.aueb.android.barista-plugin'
}
d) Update debug android build type to have test coverage enabled
buildTypes {
...
debug {
...
testCoverageEnabled true
}
}
e) When you run any android test (basically when the assembleDebugAndroidTest gradle task is trigered) the plugin should deploy a HTTP server container right before the execution of tests in the emulator. This HTTP server is alive until all the tests are finished. The default listening port of the server is 8040. In order to set a specific listening port you can use the below configuration structure inside your gradle.build file
...
baristaSettings {
host = "xxx.xxx.xxx.xxx" (IP of computer)
port = 8070
}
...
Normally the http server closes automaticaly after each test build. However there may be cases where this is not happening correctly for various reasons. In such cases you have to shutdown the server manually by using http://localhost:8040/barista/kill request from your browser.
f) In order to get coverage reports working.
Extend your main activity with BaristaActivity:
...
public class YourMainActivity extends BaristaActivity {
...
}
Copy paste the following task for the coverage reports
task jacocoTestReport(type: JacocoReport) {
reports {
xml.enabled = true
html.enabled = true
}
def fileFilter = ['**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'**/*Test*.*',
'android/**/*.*',
'**/data/models/*']
def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/debug", excludes: fileFilter)
def mainSrc = "${project.projectDir}/src/main/java"
sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = fileTree(dir: "$project.rootDir", includes: [
"coverage.exec"
])
}
After successfully deploying the barista plugin this message should appear in the build output of your android project
...
:app:assembleDebugAndroidTest
:app:deployTestDispacherServerTask
Deploying Server at: http://localhost:8040/barista/
Now there is a HTTP Server container ready to accept REST requests from the barista library module.
The Barista library provides the developer a set of commands in the form of annotations that can be used at the instrumentation test in order to execute adb and telnet commands at test time to the connected emulator(s).
-
Clone the library files
-
Install the library to the local maven repository using
gradlew clean build publishToMavenLocal
Later will be available to the global repository.
-
Add the library to the classpath.
Inside the project build.gradle file
buildscript { repositories { .... } dependencies { classpath 'com.android.tools.build:gradle:3.2.1' classpath 'gr.aueb.android:barista-plugin:1.0-SNAPSHOT' classpath 'gr.aueb.android:barista:1.0' } }
-
Assign the Barista Instrumentation Test Listener inside the module specific gradle.build file
android { compileSdkVersion 28 defaultConfig { ... testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArgument "listener", "gr.aueb.android.barista.core.BaristaRunListener" } ... }
-
Add the dependecies needed for Barista Library to work
dependencies { ... //BARISTA DEPENDENCIES START implementation 'gr.aueb.android:barista:2.0' implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.squareup.retrofit2:converter-jackson:2.4.0' implementation 'com.squareup.okhttp3:okhttp:3.6.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.6.0' //BARISTA DEPENDENCIES END ... }
-
Workflow
-
Barista plugin task checks if target project is Android. Check based on imported plugins and packages
-
If it is an Android Project, check in the gradle task graph if there is a task named "assembleDebugAndroidTest" This indicates that a test is about to take place.
-
Just before the instrumentation tests run, The HTTP server is deployed and access tokens are shared among the connected emulators. This way each time a request arrives the server knows to which emulator a command should be executed.
-
At test time the Barista Run Listener parses the barista annotation of each test, constructs an appropriate command requests and sends it to the server.
-
The server receives which command(s) to execute to which emulator and does so.
-
The emulator now has the requested settings and the body of the instrumentation test is executed.
-
After the completion of each test the emulator is reseted to the previous/default settings
-
Step 4 is executed again until all tests are finished.
-
When all tests are finished the server shuts down.
-
-
Host-Emulator connectivity
AVD emulators can communicate with the host-machine by using the default NAT-provided IP 10.0.2.2 (https://developer.android.com/studio/run/emulator-networking). Many factors can affect the communication between the emulator and the host machine such as a local firewall.
-
Workflow
-
Barista plugin task checks if target project is Android. Check based on imported plugins and packages
-
If it is an Android Project, register the following gradle tasks:
- startMonkey, which is a task that calls Android's Monkey tool for UI fuzzing.
- startBaristaFuzzer, which is a task that calls Android's Monkey tool and or own context fuzzing logic.
-
By calling one of those 2 tasks, Barista calls the appropriate adb commands to fuzz the application under test.
-
Gradle captures the options given and launches the appropriate tests.
-
-
Fuzzing
Barista fuzzer injects UI events through the Monkey tool, and changes the context of the emulator with the help of the following commands.
Declaration | Description |
---|---|
--events | The number of events that the fuzzer is gonna inject to the UI. |
--epochs | The number of testing cycles. |
--throttle | The pause time after each UI event is ejected (in ms). |
--config | The *.properties config file that includes the context fuzzing parameters. |
--parallelRun | By default Barista fuzzer runs UI events and context events sequantially. By adding this parameter the events are run in parallel. |
The .properties file contains which context models with be included when the fuzzer will start.
There are six context models that are based on the commands following. These models are:
Model | Description |
---|---|
RandomWalkModel | Changes the location of the device based on street walk model. |
FuzzMovementModel | Changes the location of the device unpredictably. The change can be either small or really large. |
BatteryDrainModel | Drains the battery. |
PoorConnectivityModel | Simulates a poor connection either wifi or data. |
RandomConnectivityModel | Changes wifi and data connection unpredictably. |
FuzzGPSModel | Turns GPS on or off unpredictably. |
An example *.properties file is included under the fuzzer folder. The file must be in the directory of the app under test.
Declaration | Description |
---|---|
@Wifi | Enable or disable the internet connection using WiFi |
@Data | Enable or disable the internet connection using Mobile Data |
@Gps | Enable or disable GPS |
@GeoFix | Set the geographic location of the device (longitude, latitude) |
@Permission | Grant the app package a "dangerous permission" |
@BatteryOptions | Set the battery level and the charging status of the device |
@ScreenSize | Set the screen dimensions (width, height) |
@Density | Set the screen pixel density |
@Orientation | Set the orientation of the device |
-
@ScreenSize ( width = [integerValue], height = [integerValue] )
Set the screen dimensions of the target device. This command is equivalent to:
adb shell wm size [width]x[height]
-
@Density ( density = [integerValue] )
Set the screen density of the target device. This command is equivalent to:
adb shell wm density [density_value]
-
@Orientation ( OrientationOptions )
Set the screen orientation of the target device. This command is equivalent to:
adb shell content insert --uri content://settings/system --bind name:s:user_rotation --bind value:i:[user option]
-
@GeoFix ( lat = [doubleValue] , longt = [doubleValue] )
Set the latitude and longtitude of the emulator using android telnet command:
telnet localhost [target_port] geo fix [longtitude] [latitude]
-
@BatteryOption ( plugged = [booleanValue] )
Set the charging status of the emulator to true or false using the equivalent adb command:
adb shell dumpsys battery set ac 1|0
default value is true.
-
@BatteryOption ( level = [integerValue] )
Set the level of the battery using the equivalent adb command:
adb command dumpsys battery level [1-100]
default value is 100.
-
Both command options can be combined into one
e.x @BatteryOption ( plugged = [booleanValue] , level = [integerValue] )
-
@Wifi ( enabled = [NetworkAdapterStateType] )
Disables or enables the wifi connection of the emulator using the equivalent adb command:
shell svc wifi [enable|disable]
-
@Data ( enabled = [NetworkAdapterStateType] )
Disables or enables the mobile data connection of the emulator using the equivalent adb command:
shell svc data [enable|disable]
-
@Gps ( enabled = [NetworkAdapterStateType] )
Disables or enables the GPS connection of the emulator using the equivalent adb command:
shell settings put secure location_providers_allowed [+gps|-gps]
-
@Permission ( type = [stringValue] )
Grants permission for a specific android permission type. This command is best used for bypassing the user input for granting explicit permissions. The requested permission to grant must also be present into the manifest file of the application
Usage example:
@Permission ( type = "android.permission.BODY_SENSORS" )
The easiest way to understand how to use the Barista Framework is to think of of the state of the emulator before running a test.
If you want to test a feature of your app that depends on a specific state of the device, then you add the appropriate annotations that will build this state for you at test time.
-
@Test @Wifi(NetworkAdapterStateType.ENABLED) public void enterExampleActivityWithWiFiOff() { ... } @Test @Data(NetworkAdapterStateType.ENABLED) public void emableWifiPopupCloses() { ... Barista.setWifiState(NetworkAdapterStateType.ENABLED); ... }
-
gradlew :app:startBaristaFuzzer --events="100" --throttle="100" --epochs="4" --config="fuzzer/fuzzerTest1.properties" --parallelRun --stacktrace
- Barista cannot find the
adb
executable in system path: Ensure that you set the path of android binaries to/etc/profile
or other global configuration files.