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

Diving into the inner workings of OkReplay

Diving into the inner workings of OkReplay

In this talk we covered the internals of the OkReplay library for recording and replaying Espresso tests.

felipecsl

May 18, 2018
Tweet

More Decks by felipecsl

Other Decks in Programming

Transcript

  1. Diving into the inner workings of OkReplay FELIPE LIMA /

    @FELIPECSL / MAY 17, 2018 / DROIDCHELLA
  2. • Yaml file where a list of http “interactions” is

    recorded • Each test method corresponds to a tape file • Interactions are recorded in request-response pairs • Includes method, URL, status, headers and response body • Modes: read-only, read-write, write-only, etc. • Read from instrumentation APK “assets” directory • Writes to device’s external storage directory Tape WHERE INTERACTIONS HAPPEN
  3. Tape file !tape name: test league interactions: - recorded: 2017-07-31T03:46:40.477Z

    request: method: GET uri: http://elifut.com/nations.json response: status: 200 headers: Cache-Control: max-age=0, private, must-revalidate Connection: keep-alive Content-Type: application/json; charset=utf-8 Date: Mon, 31 Jul 2017 03:47:07 GMT Server: nginx/1.10.3 + Phusion Passenger 5.1.5 Status: 200 OK Transfer-Encoding: chunked X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-Powered-By: Phusion Passenger 5.1.5 X-Runtime: '0.012716' X-XSS-Protection: 1; mode=block body: '<redacted>'
  4. Core OKHTTP INTERCEPTOR @Override public Response intercept(Chain chain) { Request

    request = chain.request(); if (isRunning) { Request recordedRequest = OkHttpRequestAdapter.adapt(request); if (tape.isReadable() && tape.seek(recordedRequest)) { return replayTape(request, tape, recordedRequest); } else { return recordResponse(chain, request, tape, recordedRequest); } } else { return chain.proceed(request); } }
  5. Core OKHTTP INTERCEPTOR @Override public Response intercept(Chain chain) { Request

    request = chain.request(); if (isRunning) { Request recordedRequest = OkHttpRequestAdapter.adapt(request); if (tape.isReadable() && tape.seek(recordedRequest)) { return replayTape(request, tape, recordedRequest); } else { return recordResponse(chain, request, tape, recordedRequest); } } else { return chain.proceed(request); } }
  6. Core OKHTTP INTERCEPTOR @Override public Response intercept(Chain chain) { Request

    request = chain.request(); if (isRunning) { Request recordedRequest = OkHttpRequestAdapter.adapt(request); if (tape.isReadable() && tape.seek(recordedRequest)) { return replayTape(request, tape, recordedRequest); } else { return recordResponse(chain, request, tape, recordedRequest); } } else { return chain.proceed(request); } }
  7. Core OKHTTP INTERCEPTOR @Override public Response intercept(Chain chain) { Request

    request = chain.request(); if (isRunning) { Request recordedRequest = OkHttpRequestAdapter.adapt(request); if (tape.isReadable() && tape.seek(recordedRequest)) { return replayTape(request, tape, recordedRequest); } else { return recordResponse(chain, request, tape, recordedRequest); } } else { return chain.proceed(request); } }
  8. Core OKHTTP INTERCEPTOR @Override public Response intercept(Chain chain) { Request

    request = chain.request(); if (isRunning) { Request recordedRequest = OkHttpRequestAdapter.adapt(request); if (tape.isReadable() && tape.seek(recordedRequest)) { return replayTape(request, tape, recordedRequest); } else { return recordResponse(chain, request, tape, recordedRequest); } } else { return chain.proceed(request); } }
  9. Core OKHTTP INTERCEPTOR @Override public Response intercept(Chain chain) { Request

    request = chain.request(); if (isRunning) { Request recordedRequest = OkHttpRequestAdapter.adapt(request); if (tape.isReadable() && tape.seek(recordedRequest)) { return replayTape(request, tape, recordedRequest); } else { return recordResponse(chain, request, tape, recordedRequest); } } else { return chain.proceed(request); } }
  10. Replaying Responses private okhttp3.Response replayResponse( okhttp3.Request request, Tape tape, Request

    recordedRequest ) { Response recordedResponse = tape.play(recordedRequest); okhttp3.Response okhttpResponse = OkHttpResponseAdapter.adapt(request, recordedResponse); okhttpResponse = setOkReplayHeader(okhttpResponse, "PLAY"); okhttpResponse = setViaHeader(okhttpResponse); return okhttpResponse; }
  11. Replaying Responses abstract class MemoryTape implements Tape { @Override public

    Response play(final Request request) { if (mode.isSequential()) { Integer nextIndex = orderedIndex.getAndIncrement(); RecordedInteraction nextInteraction = interactions.get(nextIndex).toImmutable(); return nextInteraction.response(); } else { int position = findMatch(request); if (position < 0) { throw new IllegalStateException("no matching recording found"); } else { return interactions.get(position).toImmutable().response(); } } } }
  12. Replaying Responses abstract class MemoryTape implements Tape { @Override public

    Response play(final Request request) { if (mode.isSequential()) { Integer nextIndex = orderedIndex.getAndIncrement(); RecordedInteraction nextInteraction = interactions.get(nextIndex).toImmutable(); return nextInteraction.response(); } else { int position = findMatch(request); if (position < 0) { throw new IllegalStateException("no matching recording found"); } else { return interactions.get(position).toImmutable().response(); } } } }
  13. Replaying Responses abstract class MemoryTape implements Tape { @Override public

    Response play(final Request request) { if (mode.isSequential()) { Integer nextIndex = orderedIndex.getAndIncrement(); RecordedInteraction nextInteraction = interactions.get(nextIndex).toImmutable(); return nextInteraction.response(); } else { int position = findMatch(request); if (position < 0) { throw new IllegalStateException("no matching recording found"); } else { return interactions.get(position).toImmutable().response(); } } }
 }
  14. x Match Rules public enum MatchRules implements MatchRule { method

    { @Override public boolean isMatch(Request a, Request b) { return a.method().equalsIgnoreCase(b.method()); } }, uri { @Override public boolean isMatch(Request a, Request b) { return a.url().equals(b.url()); } }, host { @Override public boolean isMatch(Request a, Request b) { return a.url().url().getHost().equals(b.url().url().getHost()); } }, path { @Override public boolean isMatch(Request a, Request b) { return a.url().url().getPath().equals(b.url().url().getPath()); } }, port { @Override public boolean isMatch(Request a, Request b) { return a.url().url().getPort() == b.url().url().getPort(); } } }
  15. Test Rule class RecorderRule( configuration: OkReplayConfig ) : Recorder(configuration), TestRule

    { override fun apply(statement: Statement, description: Description): Statement { val annotation = description.getAnnotation(OkReplay::class.java) if (annotation != null) { return object : Statement() { override fun evaluate() { try { val tapeName = annotation.tape val tapeMode = annotation.mode val matchRules = annotation.match val matchRule = ComposedMatchRule.of(*matchRules) start(tapeName, tapeMode.toOptional(), matchRule) statement.evaluate() } catch (e: Exception) { throw e } finally { stop() } } } } else { return statement } } }
  16. Test Rule class RecorderRule( configuration: OkReplayConfig ) : Recorder(configuration), TestRule

    { override fun apply(statement: Statement, description: Description): Statement { val annotation = description.getAnnotation(OkReplay::class.java) if (annotation != null) { return object : Statement() { override fun evaluate() { try { val tapeName = annotation.tape val tapeMode = annotation.mode val matchRules = annotation.match val matchRule = ComposedMatchRule.of(*matchRules) start(tapeName, tapeMode.toOptional(), matchRule) statement.evaluate() } catch (e: Exception) { throw e } finally { stop() } } } } else { return statement } } }
  17. Test Rule class RecorderRule( configuration: OkReplayConfig ) : Recorder(configuration), TestRule

    { override fun apply(statement: Statement, description: Description): Statement { val annotation = description.getAnnotation(OkReplay::class.java) if (annotation != null) { return object : Statement() { override fun evaluate() { try { val tapeName = annotation.tape val tapeMode = annotation.mode val matchRules = annotation.match val matchRule = ComposedMatchRule.of(*matchRules) start(tapeName, tapeMode.toOptional(), matchRule) statement.evaluate() } catch (e: Exception) { throw e } finally { stop() } } } } else { return statement } } }
  18. Test Rule class RecorderRule( configuration: OkReplayConfig ) : Recorder(configuration), TestRule

    { override fun apply(statement: Statement, description: Description): Statement { val annotation = description.getAnnotation(OkReplay::class.java) if (annotation != null) { return object : Statement() { override fun evaluate() { try { val tapeName = annotation.tape val tapeMode = annotation.mode val matchRules = annotation.match val matchRule = ComposedMatchRule.of(*matchRules) start(tapeName, tapeMode.toOptional(), matchRule) statement.evaluate() } catch (e: Exception) { throw e } finally { stop() } } } } else { return statement } } }
  19. Test Rule class RecorderRule( configuration: OkReplayConfig ) : Recorder(configuration), TestRule

    { override fun apply(statement: Statement, description: Description): Statement { val annotation = description.getAnnotation(OkReplay::class.java) if (annotation != null) { return object : Statement() { override fun evaluate() { try { val tapeName = annotation.tape val tapeMode = annotation.mode val matchRules = annotation.match val matchRule = ComposedMatchRule.of(*matchRules) start(tapeName, tapeMode.toOptional(), matchRule) statement.evaluate() } catch (e: Exception) { throw e } finally { stop() } } } } else { return statement } } }
  20. Test Rule class RecorderRule( configuration: OkReplayConfig ) : Recorder(configuration), TestRule

    { override fun apply(statement: Statement, description: Description): Statement { val annotation = description.getAnnotation(OkReplay::class.java) if (annotation != null) { return object : Statement() { override fun evaluate() { try { val tapeName = annotation.tape val tapeMode = annotation.mode val matchRules = annotation.match val matchRule = ComposedMatchRule.of(*matchRules) start(tapeName, tapeMode.toOptional(), matchRule) statement.evaluate() } catch (e: Exception) { throw e } finally { stop() } } } } else { return statement } } }
  21. Recorder Rule STARTING THE TAPE public class Recorder { public

    void start( String tapeName, Optional<TapeMode> mode, Optional<MatchRule> matchRule ) { tape = getTapeLoader().loadTape(tapeName); tape.setMode(mode.or(configuration.getDefaultMode())); tape.setMatchRule(matchRule.or(configuration.getDefaultMatchRule())); configuration.interceptor().start(configuration, tape); } }
  22. Usage @RunWith(AndroidJUnit4.class) public class ExampleInstrumentationTest { private final ActivityTestRule<MainActivity> activityTestRule

    = new ActivityTestRule<>(MainActivity.class); private final OkReplayConfig configuration = new OkReplayConfig.Builder() .tapeRoot(new AndroidTapeRoot(getContext(), getClass())) .defaultMode(TapeMode.READ_WRITE) .interceptor(new OkReplayInterceptor()) .defaultMatchRules(MatchRules.host, MatchRules.path, MatchRules.method) .build(); @Rule public final TestRule testRule = new OkReplayRuleChain(configuration, activityTestRule).get(); @Test @OkReplay public void bar() { onView(withId(R.id.foo)).perform(click()); onView(withId(R.id.bar)).check(matches(withText(containsString("bar")))); } }
  23. Usage @RunWith(AndroidJUnit4.class) public class ExampleInstrumentationTest { private final ActivityTestRule<MainActivity> activityTestRule

    = new ActivityTestRule<>(MainActivity.class); private final OkReplayConfig configuration = new OkReplayConfig.Builder() .tapeRoot(new AndroidTapeRoot(getContext(), getClass())) .defaultMode(TapeMode.READ_WRITE) .interceptor(new OkReplayInterceptor()) .defaultMatchRules(MatchRules.host, MatchRules.path, MatchRules.method) .build(); @Rule public final TestRule testRule = new OkReplayRuleChain(configuration, activityTestRule).get(); @Test @OkReplay public void bar() { onView(withId(R.id.foo)).perform(click()); onView(withId(R.id.bar)).check(matches(withText(containsString("bar")))); } }
  24. Usage @RunWith(AndroidJUnit4.class) public class ExampleInstrumentationTest { private final ActivityTestRule<MainActivity> activityTestRule

    = new ActivityTestRule<>(MainActivity.class); private final OkReplayConfig configuration = new OkReplayConfig.Builder() .tapeRoot(new AndroidTapeRoot(getContext(), getClass())) .defaultMode(TapeMode.READ_WRITE) .interceptor(new OkReplayInterceptor()) .defaultMatchRules(MatchRules.host, MatchRules.path, MatchRules.method) .build(); @Rule public final TestRule testRule = new OkReplayRuleChain(configuration, activityTestRule).get(); @Test @OkReplay public void bar() { onView(withId(R.id.foo)).perform(click()); onView(withId(R.id.bar)).check(matches(withText(containsString("bar")))); } }
  25. Usage @RunWith(AndroidJUnit4.class) public class ExampleInstrumentationTest { private final ActivityTestRule<MainActivity> activityTestRule

    = new ActivityTestRule<>(MainActivity.class); private final OkReplayConfig configuration = new OkReplayConfig.Builder() .tapeRoot(new AndroidTapeRoot(getContext(), getClass())) .defaultMode(TapeMode.READ_WRITE) .interceptor(new OkReplayInterceptor()) .defaultMatchRules(MatchRules.host, MatchRules.path, MatchRules.method) .build(); @Rule public final TestRule testRule = new OkReplayRuleChain(configuration, activityTestRule).get(); @Test @OkReplay public void bar() { onView(withId(R.id.foo)).perform(click()); onView(withId(R.id.bar)).check(matches(withText(containsString("bar")))); } }
  26. • Granting WRITE_EXTERNAL_STORAGE permission • Handling secrets • Re-recording tapes

    • If your test fails, you’ll need to re-record the tapes • Switching between replaying/recording modes Pitfalls
  27. • Writing stable Espresso tests is hard • A lot

    of cool concepts can be brought in from other platforms • Naming is also hard • Writing stable and reproducible Android UI tests is possible Conclusion FINAL REMARKS