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

Unit Test your code, even your UI

Stan Kocken
November 10, 2015

Unit Test your code, even your UI

Some examples of JUnit testing for Android.
Talk given for the DroidCon Paris in November 2015.

Stan Kocken

November 10, 2015
Tweet

More Decks by Stan Kocken

Other Decks in Programming

Transcript

  1. Unit Test your code,
    even your UI
    @stan_kocken

    View Slide

  2. Testing
    Instrumentation (on Android Device)
    Unit Tests (on JVM)

    View Slide

  3. Unit Testing on JVM
    Run quickly
    Allow TDD (Test Driven Development)
    Easy to add on your build process (CI)

    View Slide

  4. app
    ↳ src
    ↳ main
    ↳ test
    ↳ java
    ↳ com.skocken.junittest
    ↳ ExampleUnitTest
    Test hierarchy

    View Slide

  5. Build Variants
    Android Studio
    Configure
    View > Tools Windows >
    Test Artifact: Android Instrumentation Tests
    Unit Tests

    View Slide

  6. Android Studio
    Execute
    app
    ↳ src
    ↳ main
    ↳ test
    ↳ java
    ↳ com.skocken.junittest
    ↳ ExampleUnitTest
    Right click
    > Run ‘All tests’
    (or Run ‘Junit’)

    View Slide

  7. Gradle
    Execute
    ./gradlew testDebugUnitTest

    BUILD SUCCESSFUL
    Total time: 2.057 secs
    test{flavor}{build_type}UnitTest

    View Slide

  8. app
    ↳ src
    ↳ main
    ↳ test
    ↳ java
    ↳ com.skocken.junittest
    ↳ ExampleUnitTest
    public class ExampleUnitTest {

    @Test

    public void addition_isCorrect() throws Exception {

    assertEquals(4, 2 + 2);

    }

    }
    OK

    View Slide

  9. TDD
    Add a test
    Run (should fail)
    Write code to fix it
    Run (should succeed)
    Refactor code
    (Test Driven Development)

    View Slide

  10. Basic class
    public class Plane {


    private final String mName;


    public Plane(String name) {

    mName = name;

    }


    }
    Alt + Enter
    > Create Test
    > JUnit4

    View Slide

  11. Basic test
    public class PlaneTest {



    }

    @Test

    public void testShouldCompareName() {

    Plane plane = new Plane("A380");

    assertTrue(plane.sameAs("A380"));

    assertFalse(plane.sameAs("B747"));

    }
    Alt + Enter
    > Create method ‘sameAs’

    View Slide

  12. Add ‘sameAs’
    public class Plane {


    private final String mName;


    public Plane(String name) {

    mName = name;

    }


    }

    public boolean sameAs(String name) {

    return false;

    }


    View Slide

  13. Run Test
    public class PlaneTest {

    @Test

    public void testShouldCompareName() {

    Plane plane = new Plane("A380");

    assertTrue(plane.sameAs("A380"));

    assertFalse(plane.sameAs("B747"));

    }
    }
    !

    View Slide

  14. Fix ‘sameAs’
    public class Plane {


    private final String mName;


    public Plane(String name) {

    mName = name;

    }

    public boolean sameAs(String name) {

    return false;

    }


    }
    // return false;
    return name != null && name.equals(mName);

    View Slide

  15. Run Test
    public class PlaneTest {

    @Test

    public void testShouldCompareName() {

    Plane plane = new Plane("A380");

    assertTrue(plane.sameAs("A380"));

    assertFalse(plane.sameAs("B747"));

    }
    }
    OK

    View Slide

  16. Add some Android logs
    public class Plane {


    private final String mName;


    public Plane(String name) {

    mName = name;

    }

    public boolean sameAs(String name) {

    return name != null && name.equals(mName);

    }


    }
    Log.v("Plane", "compare with: " + name);

    View Slide

  17. Run Test
    public class PlaneTest {

    @Test

    public void testShouldCompareName() {

    Plane plane = new Plane("A380");

    assertTrue(plane.sameAs("A380"));

    assertFalse(plane.sameAs("B747"));

    }
    }
    !
    java.lang.RuntimeException: Method v in android.util.Log not mocked.
    at android.util.Log.v(Log.java)
    at com.skocken.junittest.Plane.sameAs(Plane.java:14)
    at com.skocken.junittest.PlaneTest.testShouldCompareName(PlaneTest.java:13)

    View Slide

  18. Mock android.jar - build.gradle
    android {


    testOptions {

    unitTests.returnDefaultValues = true

    }

    }

    View Slide

  19. Run Test
    public class PlaneTest {

    @Test

    public void testShouldCompareName() {

    Plane plane = new Plane("A380");

    assertTrue(plane.sameAs("A380"));

    assertFalse(plane.sameAs("B747"));

    }
    }
    OK

    View Slide

  20. Run Test, with “null”
    public class PlaneTest {


    @Test

    public void testShouldCompareName() { … }


    @Test

    public void testShouldCompareNameNull() {

    Plane plane = new Plane(null);

    assertTrue(plane.sameAs(null));

    assertFalse(plane.sameAs("B747"));

    }


    }
    !

    View Slide

  21. Fix ‘sameAs’
    public class Plane {


    private final String mName;


    public Plane(String name) {

    mName = name;

    }

    public boolean sameAs(String name) {
    Log.v("Plane", "compare with: " + name);

    return name != null && name.equals(mName);

    }


    }
    // return name != null && name.equals(mName);
    return TextUtils.equals(mName, name);

    View Slide

  22. Run Test
    public class PlaneTest {


    @Test

    public void testShouldCompareName() {
    Plane plane = new Plane("A380");

    assertTrue(plane.sameAs("A380"));

    assertFalse(plane.sameAs(“B747"));
    }


    @Test

    public void testShouldCompareNameNull() {

    Plane plane = new Plane(null);

    assertTrue(plane.sameAs(null));

    assertFalse(plane.sameAs("B747"));

    }


    }
    !

    View Slide

  23. build.gradle
    android {


    testOptions {

    unitTests.returnDefaultValues = true

    }

    }
    public static boolean equals(CharSequence a, CharSequence b) {

    …

    }
    public static boolean equals(CharSequence a, CharSequence b) {

    return false;

    }
    TextUtils.java
    Mock of TextUtils.java

    View Slide

  24. Solutions
    TextUtils → custom method
    (or library like Apache Common)
    Need to replace every calls
    More code “duplicate”
    “just because” of the test
    Tests clean, no change

    View Slide

  25. Solutions
    Robolectric
    Not “exactly” Android
    A bit slower (~ +3sec)
    No change on code to test

    View Slide

  26. Example

    View Slide

  27. Board
    play(Player player, int x, int y)
    Player getPlayer(int x, int y)
    int getPlayLeft()
    Player getWinner()

    View Slide

  28. Tests
    public class GameBoardTest extends TestCase {

    private GameBoard mSubject;

    @Before

    public void setUp() throws Exception {

    mSubject = new GameBoard();

    }

    @Test

    public void testSetPlayer() throws Exception {
    assertEquals(GameBoard.Player.EMPTY, mSubject.getPlayer(0, 0));
    mSubject.play(GameBoard.Player.J1, 0, 0);
    assertEquals(GameBoard.Player.J1, mSubject.getPlayer(0, 0));

    }

    }
    Fix your tests now

    View Slide

  29. Tests
    public class GameBoardTest extends TestCase {


    private GameBoard mSubject;


    @Before

    public void setUp() throws Exception {

    mSubject = new GameBoard();

    }


    @Test

    public void testSetPlayer() throws Exception { … }


    @Test

    public void testGetNbPlayLeft() throws Exception {

    assertEquals(9, mSubject.getNbPlayLeft());

    mSubject.play(GameBoard.Player.J1, 2, 2);

    assertEquals(8, mSubject.getNbPlayLeft());

    mSubject.play(GameBoard.Player.J1, 1, 1);

    assertEquals(7, mSubject.getNbPlayLeft());

    }


    }
    Fix your tests now

    View Slide

  30. Tests
    public class GameBoardTest extends TestCase {


    private GameBoard mSubject;


    @Before

    public void setUp() throws Exception {

    mSubject = new GameBoard();

    }


    …


    @Test

    public void testGetWinnerInCol() throws Exception {

    mSubject.play(GameBoard.Player.J1, 0, 0);

    mSubject.play(GameBoard.Player.J1, 0, 1);

    assertNull(mSubject.getWinner());

    mSubject.play(GameBoard.Player.J1, 0, 2);

    assertEquals(mSubject.getWinner(), GameBoard.Player.J1);

    }


    }
    Fix your tests now

    View Slide

  31. AI
    constructor: (GameBoard board, Player aiPlayer)
    playNext()

    View Slide

  32. Tests
    public class AITest extends TestCase {


    private static final GameBoard.Player PLAYER_AI = GameBoard.Player.J2;


    private GameBoard mGameBoard;


    private AI mAI;


    @Before

    public void setUp() throws Exception {

    mGameBoard = new GameBoard();

    mAI = new AI(mGameBoard, PLAYER_AI);

    }


    }

    View Slide

  33. Tests
    public class AITest extends TestCase {

    …

    Fix your tests now
    @Test

    public void testPlayNextWithEmpty() throws Exception {

    GameBoard.Player[][] initBoard =

    {

    {__, __, __},

    {__, __, __},

    {__, __, __}


    };

    mGameBoard.setBoard(initBoard);

    mAI.playNext();
    assertEquals(PLAYER_AI, mGameBoard.getPlayer(1, 1));

    }


    }

    View Slide

  34. Tests
    @Test

    public void testPlayWinnerMove() throws Exception {

    GameBoard.Player[][] initBoard =

    {

    {J1, IA, J1},

    {IA, IA, J1},

    {J1, __, __}


    };

    mGameBoard.setBoard(initBoard);


    mIA.playNext();

    assertEquals(IA, mGameBoard.getPlayer(1, 2));

    assertEquals(IA, mGameBoard.getWinner());

    }


    }
    Fix your tests now
    public class AITest extends TestCase {

    …


    View Slide

  35. Go Further
    Improve your AI
    Increase board size

    View Slide

  36. Android UI

    View Slide

  37. Activity

    View Slide

  38. BaseActivity:1600+ lines
    A base activity that handles common
    functionality in the app. This includes the
    navigation drawer, login and authentication,
    Action Bar tweaks, amongst others.

    View Slide

  39. God Object

    View Slide

  40. How to do differently?
    View Proxy
    Touch Android View
    No business logic
    DataProvider
    Access Data
    Store state
    Presenter
    Data to ViewProxy

    View Slide

  41. Architecture
    Presenter
    DataProvider ViewProxy
    Activity View
    Strong Reference
    Weak Reference
    Other “Strong” Reference on DataProvider, Presenter or
    ViewProxy forbidden. Use WeakReference.
    OK

    View Slide

  42. Example

    View Slide

  43. v1: Activity
    public class OriginalActivity extends Activity

    implements View.OnClickListener {


    private final GameBoard mGameBoard = new GameBoard();


    private final GameBoard.Player mCurrentPlayer = GameBoard.Player.J1;


    private final AI mAI = new AI(mGameBoard, GameBoard.Player.J2);


    private ViewGroup mBoxesLayout;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    mBoxesLayout = (ViewGroup) findViewById(R.id.boxes_layout);

    }


    @Override

    protected void onResume() {

    super.onResume();

    refreshBoard();

    }


    @Override

    public void onClick(View v) {

    Object x = v.getTag(R.id.tag_x);

    Object y = v.getTag(R.id.tag_y);

    if (x instanceof Integer && y instanceof Integer) {

    play((int) x, (int) y);

    }

    }


    private void play(int x, int y) {

    mGameBoard.play(mCurrentPlayer, x, y);

    mAI.playNext();

    refreshBoard();

    }
    private void refreshBoard() {

    int size = mGameBoard.getSize();

    for (int y = 0; y < size; y++) {

    for (int x = 0; x < size; x++) {

    GameBoard.Player player = mGameBoard.getPlayer(x, y);


    int boxType;

    switch (player) {

    case EMPTY:

    boxType = BoardDef.BOX_EMPTY;

    break;

    case J1:

    boxType = BoardDef.BOX_CROSS;

    break;

    case J2:

    boxType = BoardDef.BOX_ROUND;

    break;

    default:

    continue;

    }

    setBoxValue(x, y, boxType);

    }

    }

    }


    private void setBoxValue(int x, int y, int boxType) {

    ViewGroup rowLayout = (ViewGroup) mBoxesLayout.getChildAt(y);

    View boxView = rowLayout.getChildAt(x);


    int bgColor;

    switch (boxType) {

    case BoardDef.BOX_CROSS:

    bgColor = Color.RED;

    boxView.setOnClickListener(null);

    break;

    case BoardDef.BOX_ROUND:

    bgColor = Color.BLUE;

    boxView.setOnClickListener(null);

    break;

    default:

    bgColor = Color.GRAY;

    boxView.setOnClickListener(this);

    break;

    }

    boxView.setBackgroundColor(bgColor);

    boxView.setTag(R.id.tag_x, x);

    boxView.setTag(R.id.tag_y, y);

    }

    }

    View Slide

  44. DataProvider Presenter ViewProxy
    v2: Creation
    refreshBoard()
    getGameBoard() setBoxValue(
    int x, int y,
    int boxType);

    View Slide

  45. DataProvider Presenter ViewProxy
    v2: User click box
    onSelectBox(int x, int y)
    play(int x, int y)
    refreshBoard()

    View Slide

  46. v2: Definition
    public interface BoardDef {


    int BOX_EMPTY = 0;


    int BOX_CROSS = 1;


    int BOX_ROUND = 2;


    interface IPresenter extends BaseDef.IPresenter {


    void refreshBoard();


    void onSelectBox(int x, int y);

    }


    interface IDataProvider extends BaseDef.IDataProvider {


    GameBoard getGameBoard();


    void play(int x, int y);

    }


    interface IView extends BaseDef.IView {


    void setBoxValue(int x, int y, int boxType);

    }

    }

    View Slide

  47. v2: DataProvider
    public class BoardDataProvider

    extends BaseDataProvider

    implements BoardDef.IDataProvider {


    private final GameBoard mGameBoard = new GameBoard();


    private final GameBoard.Player mCurrentPlayer =
    GameBoard.Player.J1;


    private final AI mAI = new AI(mGameBoard,
    GameBoard.Player.J2);


    @Override

    public GameBoard getGameBoard() {

    return mGameBoard;

    }


    @Override

    public void play(int x, int y) {

    mGameBoard.play(mCurrentPlayer, x, y);

    mAI.playNext();

    getPresenter().refreshBoard();

    }

    }


    View Slide

  48. public class BoardPresenter 

    extends BasePresenter

    implements BoardDef.IPresenter {


    public BoardPresenter(Activity activity,

    BoardDef.IDataProvider provider, BoardDef.IView view) {

    super(activity, provider, view);

    }


    @Override

    public void onResume() {

    super.onResume();

    refreshBoard();

    }


    @Override

    public void onSelectBox(int x, int y) {

    getProvider().play(x, y);

    }


    @Override

    public void refreshBoard() {

    GameBoard gameBoard = getProvider().getGameBoard();

    int size = gameBoard.getSize();

    for (int y = 0; y < size; y++) {

    for (int x = 0; x < size; x++) {

    setBoxValue(gameBoard, y, x);

    }

    }

    }
    v2: Presenter

    View Slide

  49. public class BoardPresenter 

    extends BasePresenter

    implements BoardDef.IPresenter {


    public BoardPresenter(Activity activity,

    BoardDef.IDataProvider provider, BoardDef.IView view) {

    super(activity, provider, view);

    }


    @Override

    public void onResume() {

    super.onResume();

    refreshBoard();

    }


    @Override

    public void onSelectBox(int x, int y) {

    getProvider().play(x, y);

    }


    @Override

    public void refreshBoard() {

    GameBoard gameBoard = getProvider().getGameBoard();

    int size = gameBoard.getSize();

    for (int y = 0; y < size; y++) {

    for (int x = 0; x < size; x++) {

    setBoxValue(gameBoard, y, x);

    }

    }

    }
    v2: Presenter
    private void setBoxValue(GameBoard gameBoard, int y, int x) {

    GameBoard.Player player = gameBoard.getPlayer(x, y);

    int boxValue;

    switch (player) {

    case EMPTY:

    boxValue = BoardDef.BOX_EMPTY;

    break;

    case J1:

    boxValue = BoardDef.BOX_CROSS;

    break;

    case J2:

    boxValue = BoardDef.BOX_ROUND;

    break;

    default:

    return;

    }

    getView().setBoxValue(x, y, boxValue);

    }

    }

    View Slide

  50. public class BoardViewProxy extends BaseViewProxy

    implements BoardDef.IView, View.OnClickListener {


    public BoardViewProxy(Activity activity) {

    super(activity);

    }


    @Override

    public void onClick(View v) {

    int x = (Integer) v.getTag(R.id.tag_x);

    int y = (Integer) v.getTag(R.id.tag_y);

    getPresenter().onSelectBox(x, y);

    }
    v2: ViewProxy
    @Override

    public void setBoxValue(int x, int y, int boxType) {

    ViewGroup boxLayout = findViewByIdEfficient(R.id.boxes_layout);

    ViewGroup rowLayout = (ViewGroup) boxLayout.getChildAt(y);

    View boxView = rowLayout.getChildAt(x);


    int bgColor;

    switch (boxType) {

    case BoardDef.BOX_CROSS:

    bgColor = Color.RED;

    boxView.setOnClickListener(null);

    break;

    case BoardDef.BOX_ROUND:

    bgColor = Color.BLUE;

    boxView.setOnClickListener(null);

    break;


    View Slide

  51. public class BoardViewProxy extends BaseViewProxy

    implements BoardDef.IView, View.OnClickListener {


    public BoardViewProxy(Activity activity) {

    super(activity);

    }


    @Override

    public void onClick(View v) {

    int x = (Integer) v.getTag(R.id.tag_x);

    int y = (Integer) v.getTag(R.id.tag_y);

    getPresenter().onSelectBox(x, y);

    }
    v2: ViewProxy
    @Override

    public void setBoxValue(int x, int y, int boxType) {

    ViewGroup boxLayout = findViewByIdEfficient(R.id.boxes_layout);

    ViewGroup rowLayout = (ViewGroup) boxLayout.getChildAt(y);

    View boxView = rowLayout.getChildAt(x);


    int bgColor;

    switch (boxType) {

    case BoardDef.BOX_CROSS:

    bgColor = Color.RED;

    boxView.setOnClickListener(null);

    break;

    case BoardDef.BOX_ROUND:

    bgColor = Color.BLUE;

    boxView.setOnClickListener(null);

    break;

    default:

    bgColor = Color.GRAY;

    boxView.setOnClickListener(this);

    break;

    }

    boxView.setBackgroundColor(bgColor);

    boxView.setTag(R.id.tag_x, x);

    boxView.setTag(R.id.tag_y, y);

    }
    }

    View Slide

  52. Improve clarity
    To test it easily!
    Why split my god object
    Activity?

    View Slide

  53. Tests: setup
    public class BoardDataProviderTest extends TestCase {


    private BoardDef.IPresenter mPresenter;


    private BoardDataProvider mDataProvider;


    @Before

    public void setUp() throws Exception {

    mPresenter = Mockito.mock(BoardDef.IPresenter.class);


    mDataProvider = new BoardDataProvider();

    mDataProvider.setPresenter(mPresenter);

    }

    View Slide

  54. Tests: setup
    public class BoardPresenterTest extends TestCase {


    private BoardDef.IDataProvider mDataProvider;


    private BoardDef.IView mBoardView;


    private BoardPresenter mBoardPresenter;


    @Before

    public void setUp() throws Exception {

    mDataProvider = Mockito.mock(BoardDef.IDataProvider.class);

    mBoardView = Mockito.mock(BoardDef.IView.class);


    mBoardPresenter = new BoardPresenter(mDataProvider, mBoardView);

    }

    View Slide

  55. Tests: setup
    public class BoardViewProxyTest extends TestCase {


    private View mContentView;


    private BoardDef.IPresenter mPresenter;


    private BoardViewProxy mViewProxy;


    @Before

    public void setUp() throws Exception {

    mPresenter = Mockito.mock(BoardDef.IPresenter.class);


    mContentView = Mockito.mock(View.class);

    Activity mockActivity = Mockito.mock(Activity.class);

    when(mockActivity.findViewById(android.R.id.content))

    .thenReturn(mContentView);


    mViewProxy = new BoardViewProxy(mockActivity);

    mViewProxy.setPresenter(mPresenter);

    }

    View Slide

  56. Test the UI
    Android stuffs
    TextView
    ImageView

    View Proxy
    Already Tested by
    framework team

    View Slide

  57. Test the UI
    View Proxy Mock
    Mock
    Mock

    View Slide

  58. Tests ViewProxy
    @Test

    public void testSetBoxValue() throws Exception {
    ViewGroup boxesViewGroup = Mockito.mock(ViewGroup.class);
    when(mContentView.findViewById(R.id.boxes_layout)).thenReturn(boxesViewGroup);

    ViewGroup firstRowViewGroup = Mockito.mock(ViewGroup.class);
    ViewGroup secondRowViewGroup = Mockito.mock(ViewGroup.class);
    ViewGroup thirdRowViewGroup = Mockito.mock(ViewGroup.class);
    when(boxesViewGroup.getChildAt(0)).thenReturn(firstRowViewGroup);
    when(boxesViewGroup.getChildAt(1)).thenReturn(secondRowViewGroup);
    when(boxesViewGroup.getChildAt(2)).thenReturn(thirdRowViewGroup);

    View firstRowFirstColView = Mockito.mock(View.class);

    View secondRowSecondColView = Mockito.mock(View.class);

    View thirdRowSecondColView = Mockito.mock(View.class);

    when(firstRowViewGroup.getChildAt(0)).thenReturn(firstRowFirstColView);

    when(secondRowViewGroup.getChildAt(1)).thenReturn(secondRowSecondColView);

    when(thirdRowViewGroup.getChildAt(1)).thenReturn(thirdRowSecondColView);

    mViewProxy.setBoxValue(0, 0, BoardDef.BOX_CROSS);

    mViewProxy.setBoxValue(1, 1, BoardDef.BOX_ROUND);

    mViewProxy.setBoxValue(1, 2, BoardDef.BOX_EMPTY);

    assertBackgroundColor(Color.RED, firstRowFirstColView);

    assertBackgroundColor(Color.BLUE, secondRowSecondColView);

    assertBackgroundColor(Color.GRAY, thirdRowSecondColView);

    }

    private void assertBackgroundColor(int expectedColor, View view) {

    ArgumentCaptor colorArg = ArgumentCaptor.forClass(Integer.class);

    verify(view).setBackgroundColor(colorArg.capture());

    assertEquals("The background expectedColor is not correct",

    expectedColor, (int) colorArg.getValue());

    }

    View Slide

  59. Code Coverage
    Highlight part of code not tested
    The percentage is (quite) useless
    Display in Android Studio

    View Slide

  60. Android Studio
    Execute
    app
    ↳ src
    ↳ main
    ↳ test
    ↳ java
    ↳ com.skocken.junittest
    ↳ ExampleUnitTest
    Right click
    > Run ‘All tests’ with Coverage
    (or Run ‘Junit’ with Coverage)

    View Slide

  61. Singleton
    Goal: be able to mock it

    View Slide

  62. Common Singleton patterns
    public class MyManager {


    private static class SingletonHolder {


    private static final MyManager INSTANCE = new MyManager();

    }


    private MyManager() {

    }


    public static MyManager getInstance() {

    return SingletonHolder.INSTANCE;

    }


    public void myMethod() {


    }

    }
    Testable?
    // or
    public enum MyManager {

    INSTANCE;


    public void myMethod() {

    }

    }

    View Slide

  63. My testable Singleton pattern
    public class MyManager {


    private MyManager() {

    }


    public static MyManager getInstance() {

    return Singleton.getInstance(MyManager.class);

    }


    public void myMethod() {

    }

    }

    View Slide

  64. Test
    public class GameBoard {


    ...


    public void myMethod() {

    MyManager.getInstance().myMethod();

    }


    }
    public class GameBoardTest extends TestCase {

    ...


    @Test

    public void testShouldCallMockSingleton() {
    MyManager managerMock = Mockito.mock(MyManager.class);
    Singleton.setInstance(MyManager.class, managerMock);

    mSubject.myMethod();
    verify(managerMock, times(1)).myMethod();

    }


    }

    View Slide

  65. Credits
    @stan_kocken
    Presentation pattern: github.com/StanKocken/Presentation
    TicTacToe Example: github.com/StanKocken/TicTacToe
    Singleton: github.com/StanKocken/39d604bcf8c814a48859
    Leak Canary: github.com/square/leakcanary
    Presentation pattern co-writer: Marc Olory

    View Slide