Unit Test your code, even your UI

30ec7b2d4dae5107b36c52fff7a29894?s=47 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.

30ec7b2d4dae5107b36c52fff7a29894?s=128

Stan Kocken

November 10, 2015
Tweet

Transcript

  1. 3.

    Unit Testing on JVM Run quickly Allow TDD (Test Driven

    Development) Easy to add on your build process (CI)
  2. 4.

    app ↳ src ↳ main ↳ test ↳ java ↳

    com.skocken.junittest ↳ ExampleUnitTest Test hierarchy
  3. 5.

    Build Variants Android Studio Configure View > Tools Windows >

    Test Artifact: Android Instrumentation Tests Unit Tests
  4. 6.

    Android Studio Execute app ↳ src ↳ main ↳ test

    ↳ java ↳ com.skocken.junittest ↳ ExampleUnitTest Right click > Run ‘All tests’ (or Run ‘Junit’)
  5. 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
  6. 9.

    TDD Add a test Run (should fail) Write code to

    fix it Run (should succeed) Refactor code (Test Driven Development)
  7. 10.

    Basic class public class Plane {
 
 private final String

    mName;
 
 public Plane(String name) {
 mName = name;
 }
 
 } Alt + Enter > Create Test > JUnit4
  8. 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’
  9. 12.

    Add ‘sameAs’ public class Plane {
 
 private final String

    mName;
 
 public Plane(String name) {
 mName = name;
 } 
 
 } 
 public boolean sameAs(String name) {
 return false;
 }

  10. 13.

    Run Test public class PlaneTest { 
 @Test
 public void

    testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } } !
  11. 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);
  12. 15.

    Run Test public class PlaneTest { 
 @Test
 public void

    testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } } OK
  13. 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);
  14. 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)
  15. 19.

    Run Test public class PlaneTest { 
 @Test
 public void

    testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } } OK
  16. 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"));
 }
 
 } !
  17. 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);
  18. 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"));
 }
 
 } !
  19. 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
  20. 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
  21. 26.
  22. 27.

    Board play(Player player, int x, int y) Player getPlayer(int x,

    int y) int getPlayLeft() Player getWinner()
  23. 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
  24. 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
  25. 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
  26. 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);
 }
 
 }
  27. 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));
 }
 
 }
  28. 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 { 
 …

  29. 37.
  30. 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.
  31. 40.

    How to do differently? View Proxy Touch Android View No

    business logic DataProvider Access Data Store state Presenter Data to ViewProxy
  32. 41.

    Architecture Presenter DataProvider ViewProxy Activity View Strong Reference Weak Reference

    Other “Strong” Reference on DataProvider, Presenter or ViewProxy forbidden. Use WeakReference. OK
  33. 42.
  34. 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);
 }
 }
  35. 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);
 }
 }
  36. 47.

    v2: DataProvider public class BoardDataProvider
 extends BaseDataProvider<BoardDef.IPresenter>
 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();
 }
 }

  37. 48.

    public class BoardPresenter 
 extends BasePresenter<BoardDef.IDataProvider, BoardDef.IView>
 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
  38. 49.

    public class BoardPresenter 
 extends BasePresenter<BoardDef.IDataProvider, BoardDef.IView>
 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);
 }
 }
  39. 50.

    public class BoardViewProxy extends BaseViewProxy<BoardDef.IPresenter>
 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;

  40. 51.

    public class BoardViewProxy extends BaseViewProxy<BoardDef.IPresenter>
 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);
 } }
  41. 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);
 }
  42. 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);
 }
  43. 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);
 }
  44. 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<Integer> colorArg = ArgumentCaptor.forClass(Integer.class);
 verify(view).setBackgroundColor(colorArg.capture());
 assertEquals("The background expectedColor is not correct",
 expectedColor, (int) colorArg.getValue());
 }
  45. 59.

    Code Coverage Highlight part of code not tested The percentage

    is (quite) useless Display in Android Studio
  46. 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)
  47. 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() {
 }
 }
  48. 63.

    My testable Singleton pattern public class MyManager {
 
 private

    MyManager() {
 }
 
 public static MyManager getInstance() {
 return Singleton.getInstance(MyManager.class);
 }
 
 public void myMethod() {
 }
 }
  49. 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();
 }
 
 }