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

I See CI

I See CI

From Droidcon Dubai 2018 by Ahmed El-Helw and Oubai Abbasi

Investment in your CI infrastructure pays back many fold because it makes all members of your team more productive and increases code quality. In this talk, we’ll go through some of the things Careem does for our CI

Avatar for Oubai Abbasi

Oubai Abbasi

April 13, 2018

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'
  44. › 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
  45. > 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
  46. > 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
  47. 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
  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 :alpha do gradle(task: "clean assembleDebug test android") aws_s3 slack({

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

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

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

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

    analysis tools • Optimize for code quality and stability Summary