AGile ANDROID

Godfrey Nolan

RIIS LLC

ANDROID Agenda

ANDROID Agenda

WHY

  • Catch more mistakes
  • Confidently make more changes
  • Built in regression testing
  • Extend the life of your codebase
  • Predictability
  • Reliability

Unit Testing intro

public double add(double firstOperand, double secondOperand) {
    return firstOperand + secondOperand;
}


            @Test
            public void calculator_CorrectAdd_ReturnsTrue() {
                assertEquals(7, add(3,4);
            }

            @Test
            public void calculator_CorrectAdd_ReturnsTrue() {
                assertEquals("Addition is broken", 7, add(3,4);
            }

UNIT TESTING 101

  • Command line
  • Setup and Teardown
  • Assertions
  • Parameters
  • Code Coverage
C:\Users\godfrey\AndroidStudioProjects\BasicSample>gradlew test --continue

Downloading https://services.gradle.org/distributions/gradle-2.2.1-all.zip
................................................................................
..................................................
Unzipping C:\Users\godfrey\.gradle\wrapper\dists\gradle-2.2.1-all\6dibv5rcnnqlfbq9klf8imrndn\gradle-2.2.1-all.zip 
to C:\Users\godfrey\.gradle\wrapper\dists\gradle-2.2.1-all\6dibv5rcnnqlfbq9klf8imrndn
Download https://jcenter.bintray.com/com/google/guava/guava/17.0/guava-17.0.jar
Download https://jcenter.bintray.com/com/android/tools/lint/lint-api/24.2.3/lint-api-24.2.3.jar
Download https://jcenter.bintray.com/org/ow2/asm/asm-analysis/5.0.3/asm-analysis-5.0.3.jar
Download https://jcenter.bintray.com/com/android/tools/external/lombok/lombok-ast/0.2.3/lombok-ast-0.2.3.jar
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
.
.
.
:app:compileReleaseUnitTestSources
:app:assembleReleaseUnitTest
:app:testRelease
:app:test

BUILD SUCCESSFUL

Total time: 3 mins 57.013 secs
public class CalculatorTest {

    private Calculator mCalculator;

    @Before
    public void setUp() {
        mCalculator = new Calculator();
    }

    @Test
    public void calculator_CorrectAdd_ReturnsTrue() {
        double resultAdd = mCalculator.add(3, 4);
        assertEquals(7, resultAdd,0);
    }

    @After
    public void tearDown() {
        mCalculator = null;
    }
}

UNIT TESTING 101

  • assertEquals
  • assertTrue
  • assertFalse
  • assertNull
  • assertNotNull
  • assertSame
  • assertNotSame
  • assertThat
  • fail
@RunWith(Parameterized.class)
public class CalculatorParamTest {

    private int mOperandOne, mOperandTwo, mExpectedResult;
    private Calculator mCalculator;

    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                {3, 4, 7}, {4, 3, 7}, {8, 2, 10}, {-1, 4, 3}, {3256, 4, 3260}
        });
    }

    public CalculatorParamTest(int mOperandOne, int mOperandTwo, int mExpectedResult) {
        this.mOperandOne = mOperandOne;
        this.mOperandTwo = mOperandTwo;
        this.mExpectedResult = mExpectedResult;
    }

    @Before
    public void setUp() { mCalculator = new Calculator(); }

    @Test
    public void testAdd_TwoNumbers() {
        int resultAdd = mCalculator.add(mOperandOne, mOperandTwo);
        assertEquals(mExpectedResult, resultAdd, 0);
    }
}

API TESTING 

API TESTING 

GUI TeSTING

  • Espresso
  • Recorded
  • Roll your own
    • OnView
    • OnData
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {
 
  @Rule
  public ActivityTestRule<MainActivity> activityTestRule
    = new ActivityTestRule<> (MainActivity.class);
  @Test
  public void helloWorldTest() {
    onView(withId(R.id.hello_world))
      .check(matches(withText(R.string.hello_world)));
 
  }
}
@Test
public void helloWorldButtonTest(){
 
  onView(withId(R.id.button))
    .perform(click())
    .check(matches(isEnabled()));
 
}
public class CalculatorAddTest extends ActivityInstrumentationTestCase2<CalculatorActivity> {

    public static final String THREE = "3";
    public static final String FOUR = "4";
    public static final String RESULT = "7.0";


    public CalculatorAddTest() {
        super(CalculatorActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        getActivity();
    }

    public void testCalculatorAdd() {

        onView(withId(R.id.operand_one_edit_text)).perform(typeText(THREE));
        onView(withId(R.id.operand_two_edit_text)).perform(typeText(FOUR));
        onView(withId(R.id.operation_add_btn)).perform(click());
        onView(withId(R.id.operation_result_text_view)).check(matches(withText(RESULT)));
   }
}

MORE TOOLS - BUT WHY??

  • F(ast)
  • I(solated)
  • R(epeatable)
  • S(elf-verifying)
  • T(imely) i.e. TDD not TAD

MockiTO TEMPLATE

    @Test
    public void test() throws Exception {

        // Arrange, prepare behavior
        Helper aMock = mock(Helper.class);
        when(aMock.isCalled()).thenReturn(true);

        // Act
        testee.doSomething(aMock);

        // Assert - verify interactions
        verify(aMock).isCalled();
    
    }
    when(methodIsCalled).thenReturn(aValue);
apply plugin: 'jacoco'

task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {

    reports {
        xml.enabled = true
        html.enabled = true
    }

    def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
    def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter)
    def mainSrc = "${project.projectDir}/src/main/java"

    sourceDirectories = files([mainSrc])
    classDirectories = files([debugTree])
    executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec")
}

android {
    //...

    testOptions {
        unitTests.all {
            jacoco {
                includeNoLocationClasses = true
            }
        }
    }
}
String routesTest = "[{\"company\":{\"id\":1,\"name\":\"SmartBus\",\"brandcolor\":\"#BC0E29\",
\"busImgURL\":\"http://ec2-204-236-211-33.compute1.amazonaws.com:8080/assets/images/SmartBus.png\",
\"logoImgURL\":null},\"companyID\":1,\"routeID\":\"125\",\"routeName\":\"FORT ST-EUREKA RD\",
\"routeNumber\":\"125\",\"direction1\":\"Northbound\",\"direction2\":\"Southbound\",
\"daysActive\":\"Weekday,Saturday,Sunday\",\"id\":1},{\"company\":{\"id\":1,\"name\":\"SmartBus\",\"brandcolor\":\"#BC0E29\",
\"busImgURL\":\"http://ec2-204-236-211-33.compute-1.amazonaws.com:8080/assets/images/Smart-Bus.png\",
\"logoImgURL\":null},\"companyID\":1,\"routeID\":\"140\",\"routeName\":\"SOUTHSHORE\",
\"routeNumber\":\"140\",\"direction1\":\"Northbound\",\"direction2\":\"Southbound\",
\"daysActive\":\"Weekday\",\"id\":2}]";

when(jsonFetcher.fetchUrl("http://ec2-204-236-211-33.compute-1.amazonaws.com:8080/companies/1/routes")).thenReturn(routesTest);
    when(methodIsCalled).thenReturn(aValue);

MockiTO

String routesTest = "[{\"company\":{\"id\":1,\"name\":\"SmartBus\",\"brandcolor\":\"#BC0E29\",
\"busImgURL\":\"http://ec2-204-236-211-33.compute1.amazonaws.com:8080/assets/images/SmartBus.png\",
\"logoImgURL\":null},\"companyID\":1,\"routeID\":\"125\",\"routeName\":\"FORT ST-EUREKA RD\",
\"routeNumber\":\"125\",\"direction1\":\"Northbound\",\"direction2\":\"Southbound\",
\"daysActive\":\"Weekday,Saturday,Sunday\",\"id\":1},{\"company\":{\"id\":1,\"name\":\"SmartBus\",\"brandcolor\":\"#BC0E29\",
\"busImgURL\":\"http://ec2-204-236-211-33.compute-1.amazonaws.com:8080/assets/images/Smart-Bus.png\",
\"logoImgURL\":null},\"companyID\":1,\"routeID\":\"140\",\"routeName\":\"SOUTHSHORE\",
\"routeNumber\":\"140\",\"direction1\":\"Northbound\",\"direction2\":\"Southbound\",
\"daysActive\":\"Weekday\",\"id\":2}]";

when(jsonFetcher.fetchUrl("http://ec2-204-236-211-33.compute-1.amazonaws.com:8080/companies/1/routes")).thenReturn(routesTest);
    when(methodIsCalled).thenReturn(aValue);

MockiTO

@Test
public void testParseRoutes() throws Exception {
    ArrayList<String> temp = new ArrayList<>();
    temp.add("125");
    temp.add("140");
    assertEquals(parser.parseRoutes(
        jsonFetcher.fetchUrl("http://ec2-204-236-211-33.compute-1.amazonaws.com:8080/companies/1/routes")), 
        temp);
}

TROUBLE SHOOTING

  • run gradlew build from the command line
  • Add sdk.dir to local.properties
  • sdk.dir=/home/godfrey/android/sdk

TEST DRIVEN DEVELOPMENT (TDD)

  • Unit testing vs TDD
  • Why TDD
  • Sample app
  • Lessons learned

TEST DRIVEN DEVELOPMENT 

  • Write test first
  •  See it fail
  •  Write simplest possible solution
    to get test to pass
  •  Refactor
  •  Wash, Rinse, Repeat 

 

TEST DRIVEN DEVELOPMENT 

  • Built in regression testing
  • Longer life for your codebase 
  • YAGNI feature development
  • Red/Green/Refactor helps
    kill procrastination

 

TDD

You can't TDD w/o unit testing

TDD means writing the tests before the code

TDD is more painless than classic unit testing

 

Unit TESTING

You can unit test w/o TDD

Unit tests don't mandate when you write the tests

Unit tests are often written at the end of a coding cycle

STEPS

  • Introduce Continuous Integration to build code
  • Configure android projects for TDD
  • Add minimal unit tests based on existing tests, add to CI
  • Show team how to create unit tests
  • Add testing code coverage metrics to CI, expect 5-10%
  • Add Espresso tests
  • Unit test new features or sprouts, mock existing objects
  • Wrap or ring fence existing code, remove unused code
  • Refactor wrapped code to get code coverage to 60-70%
    (New refactoring in Android Studio)

RESOURCES

http://riis.com/blog

https://www.getpostman.com/

https://github.com/postmanlabs/newman

https://jenkins.io

https://github.com/gnolanltu/SimpleETA

https://play.google.com/store/apps/details?id=com.riis.etadetroit

https://sonarqube.org

https://slides.com/godfreynolan/agileandroid

https://medium.com/@rafael_toledo/setting-up-an-unified-coverage-report-in-android-with-jacoco-robolectric-and-espresso-ffe239aaf3fa

http://www.cimgf.com/2015/05/26/setting-up-jenkins-ci-on-a-mac-2/

CONTACT INFO

godfrey@riis.com

http://bit.ly/AgileAndroidWorkshop

AgileAndroid

By godfreynolan

AgileAndroid

  • 1,061