Upgrade to Pro — share decks privately, control downloads, hide ads and more …

I See Continuous Improvement

I See Continuous Improvement

How setting up CI for your Android projects leads to continuous improvement of the codebase.

Ahmed El-Helw

April 13, 2018
Tweet

More Decks by Ahmed El-Helw

Other Decks in Programming

Transcript

  1. 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
  2. “Checkstyle is a development tool to help programmers write Java

    code that adheres to a coding standard.”
  3. “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.”
  4. • Avoid import java.util.*; • Import order • Modifier order

    • Unnecessary parentheses Other Stylistic Issues
  5. 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); } }
  6. object Caller { fun whoYouGonnaCall(context: Context) { val intent =

    Intent(Intent.ACTION_CALL, Uri.parse("tel:" + "GHOSTBUSTERS")) context.startActivity(intent) } }
  7. class BoringActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.demo) val button: ImageButton = findViewById(R.id.button) button.setOnClickListener { } } }
  8. 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 }
  9. ~/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)
  10. “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.”
  11. ~/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)
  12. ~/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");'?
  13. ~/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)
  14. public class PostApocolypse { private final Set<Human> theLastOfUs = World.getRemainingHumans();

    private volatile int zombies = 9001; public void fightZombies() { zombies--; } }
  15. ~/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
  16. 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; }
  17. ~/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
  18. public class TemplarBase { protected Assassin ezio; public Creed getCreed()

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

    { return ezio.getCreed(); } } public class TemplarCastle extends TemplarBase { protected Assassin ezio; }
  20. ~/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
  21. 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(); } }
  22. 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(); } }
  23. 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. }
  24. Hitting the real APIs • Unreliable • Can’t test edge

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

    cases • How to test payment related scenarios
  26. 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 } }
  27. 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 } }
  28. 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 } }
  29. 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 } }
  30. 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 } }
  31. We save them as a Postman Collection • Nice GUI

    • Easy to edit responses and search requests • Supports placeholders for path, headers or body
  32. fun mount(collectionName: String, fileName: String) { cachedCollections[collectionName] = file.item.firstOrNull {

    collection -> collection.name==collectionName }?: throw IllegalArgumentException("Collection not found: $collectionName") }
  33. 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") }
  34. 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") }
  35. 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) } } })
  36. 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) } } })
  37. 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) } } })
  38. 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) } } })
  39. @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); }
  40. @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); }
  41. 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
  42. Making it cross platform • Share API data • Run

    a python mock server instead of OkHttp • Share screenshot diff-ing logic/UI
  43. › 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
  44. > 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
  45. > 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
  46. 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
  47. lane :alpha do gradle(task: "clean assembleDebug test android") aws_s3 slack({

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

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

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

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

    message: "new version uploaded to S3", channel: “#qa" }) end
  52. lane :release do gradle(task: "clean assembleRelease test android") supply slack({

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

    message: "new version released", channel: “#announcements" }) end
  54. Photo by Evan Kirby on Unsplash Gotchas • Flaky Tests

    • Tool version updates • Unstable environment • Crashing tests
  55. • Invest in improving build times • Set up static

    analysis tools • Optimize for code quality and stability Summary