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