Slide 1

Slide 1 text

I see Continuous Improvement Oubai Abbasi and Ahmed El-Helw

Slide 2

Slide 2 text

How do we know the stability of a given build?

Slide 3

Slide 3 text

How do we maintain code quality as the team grows?

Slide 4

Slide 4 text

How do we quickly find bugs before they make their way into production?

Slide 5

Slide 5 text

How do we make builds available more widely?

Slide 6

Slide 6 text

How do we uncover hidden changes in our apps?

Slide 7

Slide 7 text

Why? • Maintain a stylistically consistent code base • Catch common bugs and design flaws early on • Encourage a test driven culture • Build trust in the code quality

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

Static Analysis Tools Catching issues early

Slide 10

Slide 10 text

“Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard.”

Slide 11

Slide 11 text

“It automates the process of checking Java code to spare humans of this boring (but important) task. This makes it ideal for projects that want to enforce a coding standard.”

Slide 12

Slide 12 text

package ae.droidcon.checkstyle; class Example { static int add(int first, int second) { return first + second; } }

Slide 13

Slide 13 text

[ant:checkstyle] [WARN] ~/code/experimentation/src/ main/java/ae/droidcon/checkstyle/Example.java:5:1: Line contains a tab character. [FileTabCharacter]

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

package ae.droidcon.checkstyle; public class Example< T> { }

Slide 16

Slide 16 text

[ant:checkstyle] [WARN] ~/code/experimentation/src/ main/java/ae/droidcon/checkstyle/Example.java:3:32: GenericWhitespace '<' is followed by whitespace. [GenericWhitespace]

Slide 17

Slide 17 text

package ae.droidcon.checkstyle; class Example { static void awesome (int a, int b) { System.out.println(a + ", " + b); } }

Slide 18

Slide 18 text

[ant:checkstyle] [WARN] ~/code/experimentation/src/ main/java/ae/droidcon/checkstyle/Example.java:4:23: '(' is preceded with whitespace. [MethodParamPad]

Slide 19

Slide 19 text

• Avoid import java.util.*; • Import order • Modifier order • Unnecessary parentheses Other Stylistic Issues

Slide 20

Slide 20 text

“It can find class design problems, method design problems.”

Slide 21

Slide 21 text

class Example { int useless(int value) { value *= 2; return value; } }

Slide 22

Slide 22 text

[ant:checkstyle] [WARN] ~/code/experimentation/src/ main/java/ae/droidcon/checkstyle/Example.java:5:11: Assignment of parameter 'value' is not allowed. [ParameterAssignment]

Slide 23

Slide 23 text

package ae.droidcon.checkstyle; class Example { void play() { int fantasy = 7; int offset = 0; System.out.println(fantasy + offset); offset += 3; System.out.println(fantasy + offset); } }

Slide 24

Slide 24 text

[ant:checkstyle] [WARN] ~/code/experimentation/src/ main/java/ae/droidcon/checkstyle/Example.java:5:9: Variable 'fantasy' should be declared final. [FinalLocalVariable]

Slide 25

Slide 25 text

package ae.droidcon.checkstyle; class Util { static void op() {} static void secondOp() {} }

Slide 26

Slide 26 text

[ant:checkstyle] [WARN] ~/code/experimentation/src/ main/java/ae/droidcon/checkstyle/Util.java:3:1: Utility classes should not have a public or default constructor. [HideUtilityClassConstructor]

Slide 27

Slide 27 text

Lint Photo by Andrew Neel on Unsplash

Slide 28

Slide 28 text

object Caller { fun whoYouGonnaCall(context: Context) { val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:" + "GHOSTBUSTERS")) context.startActivity(intent) } }

Slide 29

Slide 29 text

~/code/experiments/app/src/main/kotlin/ae/droidcon/ lint/Caller.kt:12: Error: Missing permissions required by intent Intent.ACTION_CALL: android.permission.CALL_PHONE [MissingPermission] context.startActivity(intent)

Slide 30

Slide 30 text

class BoringActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.demo) val button: ImageButton = findViewById(R.id.button) button.setOnClickListener { } } }

Slide 31

Slide 31 text

~/code/experiments/app/src/main/java/ae/droidcon/lint/ BoringActivity.kt:14: Error: Unexpected implicit cast to ImageButton: layout tag was Button [WrongViewCast] val button: ImageButton = findViewById(R.id.button)

Slide 32

Slide 32 text

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.demo) val button: Button = findViewById(R.id.button) button.setOnClickListener { } val drawable = ColorDrawable( R.color.accent_material_dark) button.background = drawable }

Slide 33

Slide 33 text

~/code/experiments/app/src/main/java/ae/droidcon/ lint/BoringActivity.kt:20: Error: Call requires API level 16 (current min is 14): android.view.View#setBackground [NewApi] button.background = drawable

Slide 34

Slide 34 text

~/code/experiments/app/src/main/java/ae/droidcon/ lint/BoringActivity.kt:19: Error: Should pass resolved color instead of resource id here: getResources().getColor(R.color.accent_material_dark) [ResourceAsColor] R.color.accent_material_dark)

Slide 35

Slide 35 text

• WrongThread • LongLogTag • SQLiteString • … and many more Additional Checks

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

Error Prone

Slide 38

Slide 38 text

“Using Error Prone to augment the compiler’s type analysis, you can catch more mistakes before they cost you time, or end up as bugs in production.”

Slide 39

Slide 39 text

public Collection sort(Collection foos) { Collections.sort(new ArrayList<>(foos)); return foos; }

Slide 40

Slide 40 text

~/code/experimentation/src/main/java/ae/droidcon/ errorprone/Util.java:9: error: [UnusedCollectionModifiedInPlace] Collection is modified in place, but the result is not used Collections.sort(new ArrayList<>(foos)); ^ (see http://errorprone.info/bugpattern/ UnusedCollectionModifiedInPlace)

Slide 41

Slide 41 text

public class Carbon { public SimpleDateFormat dating() { return new SimpleDateFormat("YYYY-MM-dd"); } }

Slide 42

Slide 42 text

~/code/experimentation/src/main/java/ae/droidcon/ errorprone/Carb.java:7: error: [MisusedWeekYear] Use of "YYYY" (week year) in a date pattern without "ww" (week in year). You probably meant to use "yyyy" (year) instead. return new SimpleDateFormat("YYYY-MM-dd"); ^ (see http://errorprone.info/bugpattern/ MisusedWeekYear) Did you mean 'return new SimpleDateFormat("yyyy- MM-dd");'?

Slide 43

Slide 43 text

public class Finder { public boolean findWaldo(Set values) { return (values.contains(1)); } }

Slide 44

Slide 44 text

~/code/experimentation/src/main/java/ae/droidcon/ errorprone/Finder.java:7: error: [CollectionIncompatibleType] Argument '1' should not be passed to this method; its type int is not compatible with its collection's type argument Long return (values.contains(1)); ^ (see http://errorprone.info/bugpattern/ CollectionIncompatibleType)

Slide 45

Slide 45 text

public class PostApocolypse { private final Set theLastOfUs = World.getRemainingHumans(); private volatile int zombies = 9001; public void fightZombies() { zombies--; } }

Slide 46

Slide 46 text

~/code/experimentation/src/main/java/ae/droidcon/ errorprone/PostApocolypse.java:9: warning: [NonAtomicVolatileUpdate] This update of a volatile variable is non-atomic zombies--; ^ (see http://errorprone.info/bugpattern/ NonAtomicVolatileUpdate) 1 warning

Slide 47

Slide 47 text

public String getFaction(int version) { String result; switch (version) { case FALLOUT_2: case FALLOUT_NEW_VEGAS: result = "New California Rangers"; break; case FALLOUT_3: result = "The Enclave"; break; case FALLOUT_4: result = "Institute"; default: result = "War never changes"; } return result; }

Slide 48

Slide 48 text

~/code/experimentation/src/main/java/ae/droidcon/ errorprone/Fallout.java:17: warning: [FallThrough] Execution may fall through from the previous case; add a `// fall through` comment before this line if it was deliberate default: ^ (see http://errorprone.info/bugpattern/ FallThrough) 1 warning

Slide 49

Slide 49 text

public class TemplarBase { protected Assassin ezio; public Creed getCreed() { return ezio.getCreed(); } } public class TemplarCastle extends TemplarBase { protected Assassin ezio; }

Slide 50

Slide 50 text

public class TemplarBase { protected Assassin ezio; public Creed getCreed() { return ezio.getCreed(); } } public class TemplarCastle extends TemplarBase { protected Assassin ezio; }

Slide 51

Slide 51 text

~/code/experimentation/src/main/java/ae/droidcon/ errorprone/TemplarCastle.java:4: warning: [HidingField] Hiding fields of superclasses may cause confusion and errors. This field is hiding a field of the same name in superclass: TemplarBase protected Assassin ezio; ^ (see http://errorprone.info/bugpattern/ HidingField) 1 warning

Slide 52

Slide 52 text

• MissingOverride • ClassCanBeStatic • RemoveUnusedImports • SelfAssignment Additional Checks

Slide 53

Slide 53 text

• InsecureCryptoUsage • FragmentInjection • UseBinds Security/Perf Checks

Slide 54

Slide 54 text

Infer

Slide 55

Slide 55 text

class Persona { private final Mementos mementos = new Mementos(); Shadow getShadowForPerson(String person) { if (mementos.hasShadowFor(person)) { return new Shadow(person); } return null; } void destroyShadow(String person) { final Shadow shadow = getShadowForPerson(person); shadow.unshadow(); } }

Slide 56

Slide 56 text

class Persona { private final Mementos mementos = new Mementos(); Shadow getShadowForPerson(String person) { if (mementos.hasShadowFor(person)) { return new Shadow(person); } return null; } void destroyShadow(String person) { final Shadow shadow = getShadowForPerson(person); shadow.unshadow(); } }

Slide 57

Slide 57 text

Found 1 issue Persona.java:13: error: NULL_DEREFERENCE object `shadow` last assigned on line 12 could be null and is dereferenced at line 13. 11. void destroyShadow(String person) { 12. final Shadow shadow = getShadowForPerson(person); 13. > shadow.unshadow(); 14. } 15. }

Slide 58

Slide 58 text

• Context leaks • Resource leaks • Unsafe @GuardedBy access Additional Checks

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

Testing

Slide 61

Slide 61 text

How to UI test with network calls

Slide 62

Slide 62 text

Hitting the real APIs • Unreliable • Can’t test edge cases • How to test payment related scenarios

Slide 63

Slide 63 text

Hitting the real APIs • Unreliable • Can’t test edge cases • How to test payment related scenarios

Slide 64

Slide 64 text

Building a mock interface •Reliable •Requires more coding, maintenance and testing

Slide 65

Slide 65 text

Building a mock interface •Reliable •Requires more coding, maintenance and testing

Slide 66

Slide 66 text

Mock web server •No extra code •Still hard to maintain and prepare

Slide 67

Slide 67 text

class RecordCallsInterceptor constructor(context: Context, val gson: Gson) : Interceptor { ... override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) ... val recordModel = ApiRecordModel( request = ApiRecordRequestModel( path = request.url().encodedPath(), method = request.method(), headers = requestHeaders, body = requestBody ), response = ApiRecordResponseModel( code = response.code(), headers = responseHeaders, body = responseBody ) ) ... dumpToServer(recordModel) return response } }

Slide 68

Slide 68 text

class RecordCallsInterceptor constructor(context: Context, val gson: Gson) : Interceptor { ... override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) ... val recordModel = ApiRecordModel( request = ApiRecordRequestModel( path = request.url().encodedPath(), method = request.method(), headers = requestHeaders, body = requestBody ), response = ApiRecordResponseModel( code = response.code(), headers = responseHeaders, body = responseBody ) ) ... dumpToServer(recordModel) return response } }

Slide 69

Slide 69 text

class RecordCallsInterceptor constructor(context: Context, val gson: Gson) : Interceptor { ... override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) ... val recordModel = ApiRecordModel( request = ApiRecordRequestModel( path = request.url().encodedPath(), method = request.method(), headers = requestHeaders, body = requestBody ), response = ApiRecordResponseModel( code = response.code(), headers = responseHeaders, body = responseBody ) ) ... dumpToServer(recordModel) return response } }

Slide 70

Slide 70 text

class RecordCallsInterceptor constructor(context: Context, val gson: Gson) : Interceptor { ... override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) ... val recordModel = ApiRecordModel( request = ApiRecordRequestModel( path = request.url().encodedPath(), method = request.method(), headers = requestHeaders, body = requestBody ), response = ApiRecordResponseModel( code = response.code(), headers = responseHeaders, body = responseBody ) ) ... dumpToServer(recordModel) return response } }

Slide 71

Slide 71 text

class RecordCallsInterceptor constructor(context: Context, val gson: Gson) : Interceptor { ... override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) ... val recordModel = ApiRecordModel( request = ApiRecordRequestModel( path = request.url().encodedPath(), method = request.method(), headers = requestHeaders, body = requestBody ), response = ApiRecordResponseModel( code = response.code(), headers = responseHeaders, body = responseBody ) ) ... dumpToServer(recordModel) return response } }

Slide 72

Slide 72 text

What do we do with dumped requests?

Slide 73

Slide 73 text

We save them as a Postman Collection

Slide 74

Slide 74 text

We save them as a Postman Collection • Nice GUI • Easy to edit responses and search requests • Supports placeholders for path, headers or body

Slide 75

Slide 75 text

Now what?

Slide 76

Slide 76 text

@Test public void loginTest_success() { mount("defaultCollection"); onView(withId(R.id.email)).perform(typeText("[email protected]")); onView(withId(R.id.edt_password)).perform(typeText("Password"), closeSoftKeyboard()); onView(withId(R.id.btn_login_with_fb)).perform(click()); }

Slide 77

Slide 77 text

@Test public void loginTest_success() { mount("defaultCollection"); onView(withId(R.id.email)).perform(typeText("[email protected]")); onView(withId(R.id.password)).perform(typeText("Password"), closeSoftKeyboard()); onView(withId(R.id.btn_login)).perform(click()); }

Slide 78

Slide 78 text

@Test public void loginTest_success() { mount("defaultCollection"); onView(withId(R.id.email)).perform(typeText("[email protected]")); onView(withId(R.id.edt_password)).perform(typeText("Password"), closeSoftKeyboard()); onView(withId(R.id.btn_login_with_fb)).perform(click()); }

Slide 79

Slide 79 text

fun mount(collectionName: String, fileName: String) { cachedCollections[collectionName] = file.item.firstOrNull { collection -> collection.name==collectionName }?: throw IllegalArgumentException("Collection not found: $collectionName") }

Slide 80

Slide 80 text

fun mount(collectionName: String, fileName: String) { val file = PostmanLoader.load(fileName) cachedCollections[collectionName] = file.item.firstOrNull { collection -> collection.name == collectionName }?: throw IllegalArgumentException("Collection not found: $collectionName") }

Slide 81

Slide 81 text

fun mount(collectionName: String, fileName: String) { val file = PostmanLoader.load(fileName) cachedCollections[collectionName] = file.item.firstOrNull { collection -> collection.name == collectionName }?: throw IllegalArgumentException("Collection not found: $collectionName") }

Slide 82

Slide 82 text

MockWebServer().apply { setDispatcher(object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { return mountedCollections .firstOrNull { requestsMatch(request, it.request) }?.let { createResponseFromPostman(it.response[0]) } ?: MockResponse().apply { setResponseCode(404) } } })

Slide 83

Slide 83 text

MockWebServer().apply { setDispatcher(object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { return mountedCollections .firstOrNull { requestsMatch(request, it.request) }?.let { createResponseFromPostman(it.response[0]) } ?: MockResponse().apply { setResponseCode(404) } } })

Slide 84

Slide 84 text

MockWebServer().apply { setDispatcher(object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { return mountedCollections .firstOrNull { requestsMatch(request, it.request) }?.let { createResponseFromPostman(it.response[0]) } ?: MockResponse().apply { setResponseCode(404) } } })

Slide 85

Slide 85 text

MockWebServer().apply { setDispatcher(object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { return mountedCollections .firstOrNull { requestsMatch(request, it.request) }?.let { createResponseFromPostman(it.response[0]) } ?: MockResponse().apply { setResponseCode(404) } } })

Slide 86

Slide 86 text

How to confirm the UI looks right?

Slide 87

Slide 87 text

@Test public void loginTest_success() { mount("defaultCollection"); screenshot("loginTest", "success", "emptyFields", lang); onView(withId(R.id.email)).perform(typeText("[email protected]")); onView(withId(R.id.edt_password)).perform(typeText("Password"), closeSoftKeyboard()); screenshot("loginTest", "success", "filledFields", lang); onView(withId(R.id.btn_login_with_fb)).perform(click()); screenshot("loginTest", "success", "loading", lang); }

Slide 88

Slide 88 text

@Test public void loginTest_success() { mount("defaultCollection"); screenshot("loginTest", "success", "emptyFields", lang); onView(withId(R.id.email)).perform(typeText("[email protected]")); onView(withId(R.id.edt_password)).perform(typeText("Password"), closeSoftKeyboard()); screenshot("loginTest", "success", "filledFields", lang); onView(withId(R.id.btn_login_with_fb)).perform(click()); screenshot("loginTest", "success", "loading", lang); }

Slide 89

Slide 89 text

What to do with screenshot? • Upload them to your server • Tag them with configuration, test, scenario, date • Run image diffing (ie: http://www.imagemagick.org/ Usage/compare/) • Flag different/new screenshots for manual verification

Slide 90

Slide 90 text

Making it cross platform • Share API data • Run a python mock server instead of OkHttp • Share screenshot diff-ing logic/UI

Slide 91

Slide 91 text

Hidden Changes

Slide 92

Slide 92 text

Photo from LifeWithCats

Slide 93

Slide 93 text

Permissions

Slide 94

Slide 94 text

aapt d permissions ./app/build/outputs/apk/debug/app-debug.apk uses-permission: name='android.permission.INTERNET' uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE' uses-permission: name='android.permission.ACCESS_NETWORK_STATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='android.permission.READ_EXTERNAL_STORAGE'

Slide 95

Slide 95 text

dependencies { implementation deps.helpbug.helpbug // more dependencies }

Slide 96

Slide 96 text

aapt d permissions ./app/build/outputs/apk/debug/app-debug.apk

Slide 97

Slide 97 text

› diff before after 6a7,9 > uses-permission: name='android.permission.ACCESS_WIFI_STATE' > uses-permission: name='android.permission.RECORD_AUDIO' > uses-permission: name='android.permission.MODIFY_AUDIO_SETTINGS' Picture from principedifino

Slide 98

Slide 98 text

Method Count

Slide 99

Slide 99 text

> Task :app:countDebugDexMethods Total methods in app-debug.apk: 52673 (80.37% used) Total fields in app-debug.apk: 30053 (45.86% used) Total classes in app-debug.apk: 6962 (10.62% used) Methods remaining in app-debug.apk: 12862 Fields remaining in app-debug.apk: 35482 Classes remaining in app-debug.apk: 58573

Slide 100

Slide 100 text

No content

Slide 101

Slide 101 text

dependencies { implementation deps.helpbug.helpbug // more dependencies }

Slide 102

Slide 102 text

> Task :app:countDebugDexMethods Total methods in app-debug.apk: 69802 (106.51% used) Total fields in app-debug.apk: 42438 (64.76% used) Total classes in app-debug.apk: 9291 (14.18% used) Methods remaining in app-debug.apk: 0 Fields remaining in app-debug.apk: 23097 Classes remaining in app-debug.apk: 56244

Slide 103

Slide 103 text

Delta methods: +17129 Delta fields: +12385 Delta classes: +2329

Slide 104

Slide 104 text

APK Size

Slide 105

Slide 105 text

What else can we do? •Upload apk to S3 bucket for internal distribution •Automatically upload builds to Google Play •Post on slack about new builds

Slide 106

Slide 106 text

lane :alpha do gradle(task: "clean assembleDebug test android") aws_s3 slack({ message: "new version uploaded to S3", channel: “#qa" }) end

Slide 107

Slide 107 text

lane :alpha do gradle(task: "clean assembleDebug test android") aws_s3 slack({ message: "new version uploaded to S3", channel: “#qa" }) end

Slide 108

Slide 108 text

lane :alpha do gradle(task: "clean assembleDebug test android") aws_s3 slack({ message: "new version uploaded to S3", channel: “#qa" }) end

Slide 109

Slide 109 text

lane :alpha do gradle(task: "clean assembleDebug test android") aws_s3 slack({ message: "new version uploaded to S3", channel: “#qa" }) end

Slide 110

Slide 110 text

lane :alpha do gradle(task: "clean assembleDebug test android") aws_s3 slack({ message: "new version uploaded to S3", channel: “#qa" }) end

Slide 111

Slide 111 text

lane :release do gradle(task: "clean assembleRelease test android") supply slack({ message: "new version released", channel: “#announcements" }) end

Slide 112

Slide 112 text

lane :release do gradle(task: "clean assembleRelease test android") supply slack({ message: "new version released", channel: “#announcements" }) end

Slide 113

Slide 113 text

Code Coverage

Slide 114

Slide 114 text

No content

Slide 115

Slide 115 text

No content

Slide 116

Slide 116 text

No content

Slide 117

Slide 117 text

No content

Slide 118

Slide 118 text

Photo by Robert Zunikoff on Unsplash

Slide 119

Slide 119 text

Photo by Annie Spratt on Unsplash Where?

Slide 120

Slide 120 text

Compilation time Pull request time Commit time Post-commit time Photo by Ales Krivec on Unsplash

Slide 121

Slide 121 text

Photo by Evan Kirby on Unsplash Gotchas • Flaky Tests • Tool version updates • Unstable environment • Crashing tests

Slide 122

Slide 122 text

Test Orchestrator

Slide 123

Slide 123 text

• Invest in improving build times • Set up static analysis tools • Optimize for code quality and stability Summary

Slide 124

Slide 124 text

No content

Slide 125

Slide 125 text

Photo by Bryan Minear on Unsplash

Slide 126

Slide 126 text

References • https://speakerdeck.com/kaskasi/i-am-on-a-fastlane-to-hell • https://medium.com/square-corner-blog/surfacing-hidden- change-to-pull-requests-6a371266e479 • https://github.com/KeepSafe/dexcount-gradle-plugin • https://github.com/JakeWharton/dex-method-list • http://www.imagemagick.org/Usage/compare/ • https://android.github.io/kotlin-guides/interop.html