|
| 1 | +# Testing obfuscated build |
| 2 | + |
| 3 | +How to test the closest possible to production build. |
| 4 | + |
| 5 | +## Problem |
| 6 | + |
| 7 | +We work on debug builds and most often see debug builds and our UI tests run on debug builds. But user has release build. It is the same build apart from optimisations we do to reduce binary size and protect our apps identity. Any problems related to these optimisations are very rare but still would be good to catch them even before they hit beta. |
| 8 | + |
| 9 | +## Solution |
| 10 | + |
| 11 | +Run UI tests on obfuscated build. For that we need to use [keeper](https://slackhq.github.io/keeper/) plugin. The reason is that Android Gradle Plugin doesn't include usages from `androidTest` sources and will throw out all code referenced by UI tests. So they won't work. |
| 12 | + |
| 13 | +We can use Keeper in two ways: |
| 14 | + |
| 15 | +### Run UI tests on release build |
| 16 | + |
| 17 | +Can be done by adding following to `build.gradle`: |
| 18 | + |
| 19 | +``` |
| 20 | +android { |
| 21 | + testBuildType = "release" |
| 22 | +} |
| 23 | +``` |
| 24 | +or rather: |
| 25 | +``` |
| 26 | +android { |
| 27 | + if (hasProperty("testingMinimizedBuild")) { |
| 28 | + testBuildType = "release" |
| 29 | + } |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +But this has downside: sometimes you separate code via build type folders. E.g. place dummy implementation in `/debug/` and real implementation into `/release/` source set to make sure debug code never gets into production builds. Or the same principle applied to dependencies like: |
| 34 | + |
| 35 | +``` |
| 36 | +debugImplementation 'com.facebook.flipper:flipper:0.154.0' |
| 37 | +debugImplementation 'com.facebook.soloader:soloader:0.10.1' |
| 38 | +releaseImplementation 'com.facebook.flipper:flipper-noop:0.154.0' |
| 39 | +``` |
| 40 | + |
| 41 | +If you have such configurations, it's not a way to go. |
| 42 | + |
| 43 | +### Run obfuscation on debug build. |
| 44 | + |
| 45 | +Instead of running tests on release build type, we run them as usual on debug, but apply obfuscation to debug build via: |
| 46 | + |
| 47 | +``` |
| 48 | +buildTypes { |
| 49 | + debug { |
| 50 | + ... |
| 51 | + if (hasProperty("testingMinimizedBuild")) { |
| 52 | + isMinifyEnabled = hasProperty("testingMinimizedBuild") |
| 53 | + isShrinkResources = hasProperty("testingMinimizedBuild") |
| 54 | + proguardFiles 'proguard-rules.pro' |
| 55 | + } |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +### Main trick |
| 61 | + |
| 62 | +Once we have build and minimize for tests we need to keep all needed classes. To do so we apply [keeper](https://slackhq.github.io/keeper/) plugin: |
| 63 | + |
| 64 | +``` |
| 65 | +if (hasProperty("testingMinimizedBuild")) { |
| 66 | + apply plugin: "com.slack.keeper" |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +As you noted we do everything if some property(eg `hasProperty("testingMinimizedBuild")`). This way we can run UI tests normally and to run tests on obfuscated build. |
| 71 | + |
| 72 | +To pass param to the build: |
| 73 | + |
| 74 | +``` |
| 75 | +./gradlew assembleDebugAndroidTest -PtestingMinimizedBuild |
| 76 | +``` |
| 77 | + |
| 78 | +### Other tricks |
| 79 | + |
| 80 | +#### R8 repo |
| 81 | + |
| 82 | +Keeper adds R8 repo on project level so if your project uses |
| 83 | + |
| 84 | +``` |
| 85 | +repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) |
| 86 | +``` |
| 87 | + |
| 88 | +it will fail the build. What you need to do is to tell keeper not to add any repos and do it yourself: |
| 89 | + |
| 90 | +``` |
| 91 | +keeper { |
| 92 | + automaticR8RepoManagement = false |
| 93 | +} |
| 94 | +... |
| 95 | +repositories { |
| 96 | + ... |
| 97 | + maven { setUrl("http://storage.googleapis.com/r8-releases/raw") } |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +#### Memory and time |
| 102 | + |
| 103 | +Obviously build will take longer time depending on project size. But you also need to increase heap memory for JVM, otherwise you'll get lots OOMs. |
| 104 | + |
| 105 | +You can either do it in gradle.properties file: |
| 106 | +``` |
| 107 | +org.gradle.jvmargs=-Xmx16G -XX:+UseParallelGC -Dfile.encoding=UTF-8 |
| 108 | +``` |
| 109 | + |
| 110 | +Or to give more memory only for those runs(your final command may look like): |
| 111 | + |
| 112 | +``` |
| 113 | +./gradlew assembleDebugAndroidTest -PtestingMinimizedBuild "-Dorg.gradle.jvmargs=-Xmx16G -XX:+UseParallelGC" -Dfile.encoding=UTF-8 |
| 114 | +``` |
| 115 | + |
| 116 | +*Note*: double quotes for `"-Dorg.gradle.jvmargs=-Xmx16G -XX:+UseParallelGC"` otherwise gradle may be unhappy with incorrect `org.gradle.jvmargs`. |
| 117 | + |
| 118 | +#### Additional Proguard rules |
| 119 | + |
| 120 | +Depending on your UI tests you may want to disable obfuscation of certain classes in addition to you main Proguard rules so that your test code can find needed stuff. |
| 121 | + |
| 122 | +`proguard-debug-r8.pro` |
| 123 | + |
| 124 | +``` |
| 125 | +# Make UI tests able to find needed stuff. |
| 126 | +-keep class org.yaml.** { *; } |
| 127 | +-keep class okreplay.** { *; } |
| 128 | +-keepattributes InnerClasses |
| 129 | +-keep class **.R |
| 130 | +-keep class **.R$* { |
| 131 | + <fields>; |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +And in: |
| 136 | + |
| 137 | +``` |
| 138 | +buildTypes { |
| 139 | + release { |
| 140 | + minifyEnabled true |
| 141 | + proguardFiles 'proguard-rules.pro' |
| 142 | + } |
| 143 | + release { |
| 144 | + if (hasProperty("testingMinimizedBuild")) { |
| 145 | + minifyEnabled true |
| 146 | + proguardFiles 'proguard-rules.pro', 'proguard-debug-r8.pro' // here we extend proguard with our test specific rules file |
| 147 | + } |
| 148 | + } |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +#### AGP version |
| 153 | + |
| 154 | +If your Android Gradle Plugin version is less than `7.1.0` than you need not the latest version of keeper. You need `0.11.2`. |
| 155 | + |
| 156 | +This is because of new gradle API through which you apply the plugin. |
| 157 | + |
| 158 | +Also on different versions of AGP work different R8. If something doesn't work(you see some `PrintUses` stack trace) you may want to try new R8 `TraceReferences` API(worked for us on AGP 7.1.+): |
| 159 | + |
| 160 | +``` |
| 161 | +keeper { |
| 162 | + traceReferences() |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +Otherwise you may want to try different version of R8. Look for tags [here](https://r8.googlesource.com/r8/). More [here](https://slackhq.github.io/keeper/configuration/#custom-r8-behavior). |
| 167 | + |
| 168 | + |
| 169 | +### Further reading: |
| 170 | + |
| 171 | +1. [Keeper advanced configuration](https://slackhq.github.io/keeper/configuration/) and reading source code. |
| 172 | +2. [Testing minimized build at Avito](https://avito-tech.github.io/avito-android/blog/2020/03/testing-a-minimized-build/) |
| 173 | + |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | + |
| 178 | + |
0 commit comments