ANDROID TDDANDROID TDD
AGENDAAGENDA
Unit Testing intro
Up and Running
Unit testing 101
Tools of the trade
Mocking
TDD
Espresso
Adding TDD to existing projects
UNIT TESTING INTROUNIT TESTING INTRO
Hello World
Benefits
Android Testing Pyramid
Activity Testing
UNIT TESTING INTROUNIT TESTING INTRO
Catch more mistakes
Confidently make more changes
Built in regression testing
Extend the life of your codebase
UNIT TESTING INTROUNIT 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);
}
UP AND RUNNINGUP AND RUNNING
Gradle version
build.gradle changes
Build Variant
Sample app
dependencies {
// Unit testing dependencies.
testCompile 'junit:junit:4.12'
}
UNIT TESTING 101UNIT TESTING 101
Command line
Setup and Teardown
Grouping
Parameters
Assertions
Code Coverage
C:UsersgodfreyAndroidStudioProjectsBasicSample>gradlew test --continue
Downloading https://services.gradle.org/distributions/gradle-2.2.1-all.zip
................................................................................
..................................................
Unzipping C:Usersgodfrey.gradlewrapperdistsgradle-2.2.1-all6dibv5rcnnqlfbq9klf8imrndngr
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-
: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;
}
}
import android.test.suitebuilder.annotation.SmallTest;
@SmallTest
public void calculator_CorrectSub_Small_ReturnsTrue() {
assertEquals(mCalculator.sub(4, 3),1,0);
}
android {
//....
defaultConfig {
//....
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArgument "size", "small"
}
}
@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);
}
}
UNIT TESTING 101UNIT TESTING 101
assertEquals
assertTrue
assertFalse
assertNull
assertNotNull
assertSame
assertNotSame
assertThat
fail
TOOLS OF THE TRADETOOLS OF THE TRADE
Hamcrest
Mockito
Robolectic
Jenkins
dependencies {
// Unit testing dependencies.
testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-library:1.3'
}
@Test
public void calculator_CorrectHamAdd_ReturnsTrue() {
assertThat(both(greaterThan(6)).and(lessThan(8)), mCalculator.add(3, 4));
}
dependencies {
// Unit testing dependencies.
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
}
@SmallTest
public class DatabaseTest {
private User joeSmith = new User("Joe", "Smith");
private final int USER_ID = 1;
@Test
public void testMockUser() {
//mock SQLHelper
SQLHelper dbHelper = Mockito.mock(SQLHelper.class);
//have mockito return joeSmith when calling dbHelper getUser
Mockito.when(dbHelper.getUser(USER_ID)).thenReturn(joeSmith);
//Assert joeSmith is returned by getUser
Assert.assertEquals(dbHelper.getUser(USER_ID), joeSmith);
}
}
dependencies {
// Unit testing dependencies.
testCompile 'junit:junit:4.12'
testCompile "org.robolectric:robolectric:3.0"
}
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class RobolectricUnitTest {
@Test
public void shouldHaveHappySmiles() throws Exception {
String hello = new MainActivity().getResources().getString(R.string.hello_world);
assertThat(hello, equalTo("Hello world!"));
}
}
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class ZodiacUnitTest {
private ListView listView;
private String[] zodiacSigns;
@Before
public void setUp() {
MainActivity mainActivity = Robolectric.buildActivity(MainActivity.class).create().get();
assertNotNull("Main Activity not setup", mainActivity);
listView=(ListView) mainActivity.findViewById(R.id.list_of_signs);
zodiacSigns = RuntimeEnvironment.application.getResources().getStringArray(R.array.zodiac_a
}
@Test
public void listLoaded() throws Exception {
assertThat("should be a dozen star signs", zodiacSigns.length, equalTo(listView.getCount())
}
}
MOCKINGMOCKING
Shared Prefs
Database
System Properties
Web Services
MOCKING TEMPLATEMOCKING 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);
@RunWith(MockitoJUnitRunner.class)
public class UserPreferencesTest {
// Use Mockito to initialize UserPreferences
public UserPreferences tUserPreferences = Mockito.mock(UserPreferences.class);
private Activity tActivity;
@Before
public void setUp() {
// Use Mockito to declare the return value of getSharedPreferences()
Mockito.when(tUserPreferences.getSharedPreferences(tActivity)).thenReturn("true");
}
@Test
public void sharedPreferencesTest_ReturnsTrue() {
// Test
Assert.assertThat(tUserPreferences.getSharedPreferences(tActivity), is("true"));
}
}
@RunWith(MockitoJUnitRunner.class)
public class DatabaseTest {
private User joeSmith = new User("Joe", "Smith");
private final int USER_ID = 1;
@Test
public void testMockUser() {
//mock SQLHelper
SQLHelper dbHelper = Mockito.mock(SQLHelper.class);
//have mockito return joeSmith when calling dbHelper getUser
Mockito.when(dbHelper.getUser(USER_ID)).thenReturn(joeSmith);
//Assert joeSmith is returned by getUser
Assert.assertEquals(dbHelper.getUser(USER_ID), joeSmith);
}
}
@RunWith(MockitoJUnitRunner.class)
public class AudioHelperTest {
private final int MAX_VOLUME = 100;
@Test
public void maximizeVolume_Maximizes_Volume() {
// Create an AudioManager object using Mockito
AudioManager audioManager = Mockito.mock(AudioManager.class);
// Inform Mockito what to return when audioManager.getStreamMaxVolume is called
Mockito.when(audioManager.getStreamMaxVolume(AudioManager.STREAM_RING)).thenReturn(MAX_VOL
new AudioHelper().maximizeVolume(audioManager);
// verify with Mockito that setStreamVolume to 100 was called.
Mockito.verify(audioManager).setStreamVolume(AudioManager.STREAM_RING, MAX_VOLUME, 0);
}
}
@RunWith(MockitoJUnitRunner.class)
public class TowerRetrieverTest {
private static final String SUCCESS_STRING = "success";
@Test
public void towerRetrievalTest()
{
// Use Mockito to initialize the WebService
TowerRetriever towerRetriever = Mockito.mock(TowerRetriever.class);
// Use Mockito to declare the return value as SUCCESS_STRING
Mockito.when(towerRetriever.send("0", "0", "0")).thenReturn(SUCCESS_STRING);
// Test
Assert.assertEquals(SUCCESS_STRING, towerRetriever.send("0", "0", "0"));
}
}
TEST DRIVEN DEVELOPMENT (TDD)TEST DRIVEN DEVELOPMENT (TDD)
Unit testing vs TDD
Why TDD
Sample app
Lessons learned
TEST DRIVEN DEVELOPMENTTEST DRIVEN DEVELOPMENT
Write test first
See it fail
Write simplest possible solution
to get test to pass
Refactor
Wash, Rinse, Repeat
TEST DRIVEN DEVELOPMENTTEST DRIVEN DEVELOPMENT
Built in regression testing
Longer life for your codebase
YAGNI feature development
Red/Green/Refactor helps
kill procrastination
TDDTDD
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 TESTINGUNIT 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
SAMPLE APP - DAILY HOROSCOPESAMPLE APP - DAILY HOROSCOPE
Display each star sign
Display information about each star sign
Display horoscope for star sign
@Before
public void setUp() {
MainActivity mainActivity = Robolectric.buildActivity(MainActivity.class).create().get();
assertNotNull("Main Activity not setup", mainActivity);
listView=(ListView) mainActivity.findViewById(R.id.list_of_signs);
zodiacSigns = RuntimeEnvironment.application.getResources().getStringArray(R.array.zodiac_array);
}
@Test
public void listLoaded() throws Exception {
assertThat("should be a dozen star signs", zodiacSigns.length, equalTo(numSigns));
}
<resources>
<string name="app_name">Horoscope</string>
<string-array name="zodiac_array">
<item>Aries</item>
<item>Taurus</item>
<item>Gemini</item>
<item>Cancer</item>
<item>Leo</item>
<item>Virgo</item>
<item>Libra</item>
<item>Scorpio</item>
<item>Sagittarius</item>
<item>Capricorn</item>
<item>Aquarius</item>
<item>Pisces</item>
</string-array>
</resources>
@Test
public void listContentCheck() {
ListAdapter listViewAdapter = listView.getAdapter();
assertEquals(zodiacSigns[0], listViewAdapter.getItem(0));
assertEquals(zodiacSigns[1], listViewAdapter.getItem(1));
assertEquals(zodiacSigns[2], listViewAdapter.getItem(2));
assertEquals(zodiacSigns[3], listViewAdapter.getItem(3));
assertEquals(zodiacSigns[4], listViewAdapter.getItem(4));
assertEquals(zodiacSigns[5], listViewAdapter.getItem(5));
assertEquals(zodiacSigns[6], listViewAdapter.getItem(6));
assertEquals(zodiacSigns[7], listViewAdapter.getItem(7));
assertEquals(zodiacSigns[8], listViewAdapter.getItem(8));
assertEquals(zodiacSigns[9], listViewAdapter.getItem(9));
assertEquals(zodiacSigns[10], listViewAdapter.getItem(10));
assertEquals(zodiacSigns[11], listViewAdapter.getItem(11));
}
public static final Zodiac[] signs = {
new Zodiac("Aries","Enterprising, Incisive, Spontaneous, Daring, Active, Courageous and Energetic, the Aries are the proverbial infants, guileless and op
new Zodiac("Taurus","Known for being reliable, practical, ambitious and sensual, the people born under the Zodiac Sign Taurus have an eye for beauty.", "
new Zodiac("Gemini","Gemini-born are clever and intellectual people but they can also be tense and restless.", "Twins", "June"),
new Zodiac("Cancer"," The otherwise tenacious, loyal, sympathetic and strong Crabs are vulnerable in many ways.", "Crab", "July"),
new Zodiac("Leo","Warm, action-oriented and driven by the desire to be loved and admired, the Leo have an air royalty about them.", "Lion", "August"),
new Zodiac("Virgo","Methodical, meticulous, analytical and mentally astute, the Virgo natives are perfectionists to the core, or at least, they like to b
new Zodiac("Libra","Librans are famous for maintaining balance and harmony.", "Scales", "October"),
new Zodiac("Scorpio","The Scorpio-born are strong willed and mysterious, and they know how to effortlessly grab the limelight, as they possess what it ta
new Zodiac("Sagittarius","Sagittarians are born adventurers. They tend to get bored with things easily and move on with life", "Archer", "December"),
new Zodiac("Capricorn","The Capricorn-born people are the most determined of the entire Zodiac.", "Goat", "January"),
new Zodiac("Aquarius","The Aquarius-born people are humanitarians to the core", "Water Bearer", "February"),
new Zodiac("Pisces","Pisces or the Fish is considered as the proverbial dreamers of the Zodiac.", "Fish", "March")
};
@Before
public void setUp() {
Intent intent = new Intent(RuntimeEnvironment.application, ZodiacDetailActivity.class);
intent.putExtra(ZodiacDetailActivity.EXTRA_SIGN, ARIES_SIGN_INDEX);
zodiacDetailActivity = Robolectric.buildActivity(ZodiacDetailActivity.class).withIntent(intent).create().get();
assertNotNull("Zodiac Detail Activity not setup", zodiacDetailActivity);
}
@Test
public void zodiacSymbolTest() throws Exception {
TextView symbolTextView = (TextView) zodiacDetailActivity.findViewById(R.id.symbol);
assertEquals(Zodiac.signs[ARIES_SIGN_INDEX].getSymbol(), symbolTextView.getText().toString());
}
public class ZodiacDetailActivity extends Activity {
public static final String EXTRA_SIGN = "ZodiacSign";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_zodiac_detail);
int signNum = (Integer)getIntent().getExtras().get(EXTRA_SIGN);
Zodiac zodiac = Zodiac.signs[signNum];
TextView name = (TextView)findViewById(R.id.name);
name.setText(zodiac.getName());
TextView description = (TextView)findViewById(R.id.description);
description.setText(zodiac.getDescription());
TextView symbol = (TextView)findViewById(R.id.symbol);
symbol.setText(zodiac.getSymbol());
TextView month = (TextView)findViewById(R.id.month);
month.setText(zodiac.getMonth());
}
}
public class AsyncTaskParseJson extends AsyncTask<String, String, String> {
String yourJsonStringUrl = "http://a.knrz.co/horoscope-api/current/";
String horoscope = "";
public AsyncTaskParseJson(Zodiac sign) {
yourJsonStringUrl += sign.getName().toLowerCase();
}
@Override
protected void onPreExecute() {}
@Override
protected String doInBackground(String... arg0) {
try {
// instantiate our json parser
JsonParser jParser = new JsonParser();
// get json string from url
JSONObject json = jParser.getJSONFromUrl(yourJsonStringUrl);
horoscope = json.getString("prediction");
horoscope = URLDecoder.decode(horoscope);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(String strFromDoInBg) {
TextView display = (TextView) findViewById(R.id.daily);
display.setText(horoscope);
}
}
@Test
public void zodiacDailyTest() {
TextView dailyTextView = (TextView) zodiacDetailActivity.findViewById(R.id.daily);
assertEquals("This week try wearing less make-up when you leave the house, " +
"even if it means angering the other members of KISS.", dailyTextView.getText().toString());
}
LESSONS LEARNEDLESSONS LEARNED
What worked
No longer need emulator
Not so much
Android Activities don't work well with TDD
Robolectric is your friend
ESPRESSOESPRESSO
GUI Testing
OnView
OnData
gradlew connectedCheck
@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()));
}
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> activityTestRule
= new ActivityTestRule<>(MainActivity.class);
@Test
public void toDoListTest(){
onData(anything())
.inAdapterView(withId(R.id.list_of_todos)).atPosition(4)
.perform(click());
onView(withId(R.id.txt_selected_item))
.check(matches(withText("go to the gym")));
}
}
EXISTING PROJECTSEXISTING PROJECTS
Way more common
Essential Steps
Lessons Learned
STEPSSTEPS
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%
package alexandria.israelferrer.com.libraryofalexandria;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RatingBar;
import android.widget.TextView;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.squareup.picasso.Picasso;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
public class MainActivity extends Activity {
private static final String PACKAGE = "com.israelferrer.alexandria";
private static final String KEY_FAVS = PACKAGE + ".FAVS";
private List<ArtWork> artWorkList;
private ArtWorkAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.listView);
InputStream stream = getResources().openRawResource(R.raw.artwork);
Type listType = new TypeToken<List<ArtWork>>() {
}.getType();
artWorkList = new Gson().fromJson(new InputStreamReader(stream), listType);
final SharedPreferences preferences = getSharedPreferences(getPackageName()
, Context.MODE_PRIVATE);
for (ArtWork artWork : artWorkList) {
artWork.setRating(preferences.getFloat(PACKAGE + artWork.getId(), 0F));
}
adapter = new ArtWorkAdapter();
listView.setAdapter(adapter);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.filter) {
adapter.orderMode();
return true;
}
return super.onOptionsItemSelected(item);
}
private class ArtWorkAdapter extends BaseAdapter {
private boolean isOrder;
private final List<ArtWork> orderedList;
public ArtWorkAdapter() {
super();
orderedList = new LinkedList<ArtWork>();
}
@Override
public int getCount() {
return artWorkList.size();
}
@Override
public Object getItem(int position) {
return artWorkList.get(position);
}
@Override
public long getItemId(int position) {
return Long.valueOf(artWorkList.get(position).getId());
}
public void orderMode() {
isOrder = !isOrder;
if (isOrder) {
orderedList.clear();
orderedList.addAll(artWorkList);
Collections.sort(orderedList);
notifyDataSetChanged();
} else {
notifyDataSetChanged();
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ArtWork artWork;
if (isOrder) {
artWork = orderedList.get(position);
} else {
artWork = artWorkList.get(position);
}
View row;
switch (artWork.getType()) {
case ArtWork.QUOTE:
row = getLayoutInflater().inflate(R.layout.text_row, null);
TextView quote = (TextView) row.findViewById(R.id.quote);
TextView author = (TextView) row.findViewById(R.id.author);
quote.setText(""" + artWork.getText() + """);
author.setText(artWork.getAuthor());
break;
case ArtWork.PAINTING:
final SharedPreferences preferences = getSharedPreferences(getPackageName()
, Context.MODE_PRIVATE);
final HashSet<String> favs = (HashSet<String>) preferences
.getStringSet(KEY_FAVS,
new HashSet<String>());
row = getLayoutInflater().inflate(R.layout.painting_row, null);
ImageView image = (ImageView) row.findViewById(R.id.painting);
TextView painter = (TextView) row.findViewById(R.id.author);
painter.setText(artWork.getTitle() + " by " + artWork.getAuthor());
Picasso.with(MainActivity.this).load(artWork.getContentUrl()).fit()
.into(image);
RatingBar rating = (RatingBar) row.findViewById(R.id.rate);
rating.setRating(artWork.getRating());
rating.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() {
@Override
public void onRatingChanged(RatingBar ratingBar, float rating,
boolean fromUser) {
preferences.edit().putFloat(PACKAGE + artWork.getId(), rating).apply();
artWork.setRating(rating);
}
});
CheckBox fav = (CheckBox) row.findViewById(R.id.fav);
fav.setChecked(favs.contains(artWork.getId()));
fav.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
final HashSet<String> favs = new HashSet<String>((HashSet<String>)
preferences
.getStringSet(KEY_FAVS,
new HashSet<String>()));
if (isChecked) {
favs.add(artWork.getId());
} else {
favs.remove(artWork.getId());
}
preferences.edit().putStringSet(KEY_FAVS,
favs).apply();
}
});
break;
case ArtWork.MOVIE:
case ArtWork.OPERA:
row = new ViewStub(MainActivity.this);
break;
default:
row = getLayoutInflater().inflate(R.layout.text_row, null);
}
return row;
}
}
}
apply plugin: 'com.android.application'
apply plugin: 'jacoco'
apply plugin: 'sonar-runner'
sonarRunner{
sonarProperties{
property "sonar.host.url", "http://localhost:9000"
property "sonar.jdbc.url", "jdbc:mysql://localhost:3306/sonar?useUnicode=true&charac
property "sonar.jdbc.driverClassName","com.mysql.jdbc.Driver"
property "sonar.jdbc.username","root"
property "sonar.jdbc.password","root"
property "sonar.projectKey", "RIIS:CropCompare"
property "sonar.projectVersion", "2.0"
property "sonar.projectName","CropCompare"
property "sonar.java.coveragePlugin", "jacoco"
property "sonar.sources","srcmain"
property "sonar.tests", "srctest"
property "sonar.jacoco.reportPath", "buildjacocojacocoTest.exec"
property "sonar.java.binaries", "build"
property "sonar.dynamicAnalysis", "resuseReports"
}
}
LESSONS LEARNEDLESSONS LEARNED
Look at different architectures
MVP, MVVM w/data binding
What worked
Take baby steps, Metrics should evolve….
Not so much
Don’t be driven by metrics
Remember….
You don’t need anyone’s permission to start
RESOURCESRESOURCES
http://www.code-labs.io/codelabs/android-testing
https://developer.android.com/training/testing/unit-
testing/local-unit-tests.html
http://tools.android.com/tech-docs/unit-testing-support
http://riis.com/blog
https://github.com/rallat/libraryofalexandria
https://codio.com/godfreynolan/AnDevCon-Agile-Android
CONTACT INFOCONTACT INFO
godfrey@riis.com
@godfreynolan
http://slideshare.net/godfreynolan

Android TDD

  • 1.
  • 2.
    AGENDAAGENDA Unit Testing intro Upand Running Unit testing 101 Tools of the trade Mocking TDD Espresso Adding TDD to existing projects
  • 3.
    UNIT TESTING INTROUNITTESTING INTRO Hello World Benefits Android Testing Pyramid Activity Testing
  • 4.
    UNIT TESTING INTROUNITTESTING INTRO Catch more mistakes Confidently make more changes Built in regression testing Extend the life of your codebase
  • 5.
    UNIT TESTING INTROUNITTESTING 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); }
  • 10.
    UP AND RUNNINGUPAND RUNNING Gradle version build.gradle changes Build Variant Sample app
  • 12.
    dependencies { // Unittesting dependencies. testCompile 'junit:junit:4.12' }
  • 15.
    UNIT TESTING 101UNITTESTING 101 Command line Setup and Teardown Grouping Parameters Assertions Code Coverage
  • 16.
    C:UsersgodfreyAndroidStudioProjectsBasicSample>gradlew test --continue Downloadinghttps://services.gradle.org/distributions/gradle-2.2.1-all.zip ................................................................................ .................................................. Unzipping C:Usersgodfrey.gradlewrapperdistsgradle-2.2.1-all6dibv5rcnnqlfbq9klf8imrndngr 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- :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
  • 17.
    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; } }
  • 18.
    import android.test.suitebuilder.annotation.SmallTest; @SmallTest public voidcalculator_CorrectSub_Small_ReturnsTrue() { assertEquals(mCalculator.sub(4, 3),1,0); } android { //.... defaultConfig { //.... testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArgument "size", "small" } }
  • 19.
    @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); } }
  • 20.
    UNIT TESTING 101UNITTESTING 101 assertEquals assertTrue assertFalse assertNull assertNotNull assertSame assertNotSame assertThat fail
  • 22.
    TOOLS OF THETRADETOOLS OF THE TRADE Hamcrest Mockito Robolectic Jenkins
  • 23.
    dependencies { // Unittesting dependencies. testCompile 'junit:junit:4.12' testCompile 'org.hamcrest:hamcrest-library:1.3' } @Test public void calculator_CorrectHamAdd_ReturnsTrue() { assertThat(both(greaterThan(6)).and(lessThan(8)), mCalculator.add(3, 4)); }
  • 24.
    dependencies { // Unittesting dependencies. testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' } @SmallTest public class DatabaseTest { private User joeSmith = new User("Joe", "Smith"); private final int USER_ID = 1; @Test public void testMockUser() { //mock SQLHelper SQLHelper dbHelper = Mockito.mock(SQLHelper.class); //have mockito return joeSmith when calling dbHelper getUser Mockito.when(dbHelper.getUser(USER_ID)).thenReturn(joeSmith); //Assert joeSmith is returned by getUser Assert.assertEquals(dbHelper.getUser(USER_ID), joeSmith); } }
  • 25.
    dependencies { // Unittesting dependencies. testCompile 'junit:junit:4.12' testCompile "org.robolectric:robolectric:3.0" } @RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml") public class RobolectricUnitTest { @Test public void shouldHaveHappySmiles() throws Exception { String hello = new MainActivity().getResources().getString(R.string.hello_world); assertThat(hello, equalTo("Hello world!")); } }
  • 26.
    @RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class,sdk = 21, manifest = "src/main/AndroidManifest.xml") public class ZodiacUnitTest { private ListView listView; private String[] zodiacSigns; @Before public void setUp() { MainActivity mainActivity = Robolectric.buildActivity(MainActivity.class).create().get(); assertNotNull("Main Activity not setup", mainActivity); listView=(ListView) mainActivity.findViewById(R.id.list_of_signs); zodiacSigns = RuntimeEnvironment.application.getResources().getStringArray(R.array.zodiac_a } @Test public void listLoaded() throws Exception { assertThat("should be a dozen star signs", zodiacSigns.length, equalTo(listView.getCount()) } }
  • 30.
  • 31.
    MOCKING TEMPLATEMOCKING TEMPLATE @Test publicvoid 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);
  • 32.
    @RunWith(MockitoJUnitRunner.class) public class UserPreferencesTest{ // Use Mockito to initialize UserPreferences public UserPreferences tUserPreferences = Mockito.mock(UserPreferences.class); private Activity tActivity; @Before public void setUp() { // Use Mockito to declare the return value of getSharedPreferences() Mockito.when(tUserPreferences.getSharedPreferences(tActivity)).thenReturn("true"); } @Test public void sharedPreferencesTest_ReturnsTrue() { // Test Assert.assertThat(tUserPreferences.getSharedPreferences(tActivity), is("true")); } }
  • 33.
    @RunWith(MockitoJUnitRunner.class) public class DatabaseTest{ private User joeSmith = new User("Joe", "Smith"); private final int USER_ID = 1; @Test public void testMockUser() { //mock SQLHelper SQLHelper dbHelper = Mockito.mock(SQLHelper.class); //have mockito return joeSmith when calling dbHelper getUser Mockito.when(dbHelper.getUser(USER_ID)).thenReturn(joeSmith); //Assert joeSmith is returned by getUser Assert.assertEquals(dbHelper.getUser(USER_ID), joeSmith); } }
  • 34.
    @RunWith(MockitoJUnitRunner.class) public class AudioHelperTest{ private final int MAX_VOLUME = 100; @Test public void maximizeVolume_Maximizes_Volume() { // Create an AudioManager object using Mockito AudioManager audioManager = Mockito.mock(AudioManager.class); // Inform Mockito what to return when audioManager.getStreamMaxVolume is called Mockito.when(audioManager.getStreamMaxVolume(AudioManager.STREAM_RING)).thenReturn(MAX_VOL new AudioHelper().maximizeVolume(audioManager); // verify with Mockito that setStreamVolume to 100 was called. Mockito.verify(audioManager).setStreamVolume(AudioManager.STREAM_RING, MAX_VOLUME, 0); } }
  • 35.
    @RunWith(MockitoJUnitRunner.class) public class TowerRetrieverTest{ private static final String SUCCESS_STRING = "success"; @Test public void towerRetrievalTest() { // Use Mockito to initialize the WebService TowerRetriever towerRetriever = Mockito.mock(TowerRetriever.class); // Use Mockito to declare the return value as SUCCESS_STRING Mockito.when(towerRetriever.send("0", "0", "0")).thenReturn(SUCCESS_STRING); // Test Assert.assertEquals(SUCCESS_STRING, towerRetriever.send("0", "0", "0")); } }
  • 36.
    TEST DRIVEN DEVELOPMENT(TDD)TEST DRIVEN DEVELOPMENT (TDD) Unit testing vs TDD Why TDD Sample app Lessons learned
  • 37.
    TEST DRIVEN DEVELOPMENTTESTDRIVEN DEVELOPMENT Write test first See it fail Write simplest possible solution to get test to pass Refactor Wash, Rinse, Repeat
  • 38.
    TEST DRIVEN DEVELOPMENTTESTDRIVEN DEVELOPMENT Built in regression testing Longer life for your codebase YAGNI feature development Red/Green/Refactor helps kill procrastination
  • 39.
    TDDTDD You can't TDDw/o unit testing TDD means writing the tests before the code TDD is more painless than classic unit testing UNIT TESTINGUNIT 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
  • 40.
    SAMPLE APP -DAILY HOROSCOPESAMPLE APP - DAILY HOROSCOPE Display each star sign Display information about each star sign Display horoscope for star sign
  • 42.
    @Before public void setUp(){ MainActivity mainActivity = Robolectric.buildActivity(MainActivity.class).create().get(); assertNotNull("Main Activity not setup", mainActivity); listView=(ListView) mainActivity.findViewById(R.id.list_of_signs); zodiacSigns = RuntimeEnvironment.application.getResources().getStringArray(R.array.zodiac_array); } @Test public void listLoaded() throws Exception { assertThat("should be a dozen star signs", zodiacSigns.length, equalTo(numSigns)); } <resources> <string name="app_name">Horoscope</string> <string-array name="zodiac_array"> <item>Aries</item> <item>Taurus</item> <item>Gemini</item> <item>Cancer</item> <item>Leo</item> <item>Virgo</item> <item>Libra</item> <item>Scorpio</item> <item>Sagittarius</item> <item>Capricorn</item> <item>Aquarius</item> <item>Pisces</item> </string-array> </resources> @Test public void listContentCheck() { ListAdapter listViewAdapter = listView.getAdapter(); assertEquals(zodiacSigns[0], listViewAdapter.getItem(0)); assertEquals(zodiacSigns[1], listViewAdapter.getItem(1)); assertEquals(zodiacSigns[2], listViewAdapter.getItem(2)); assertEquals(zodiacSigns[3], listViewAdapter.getItem(3)); assertEquals(zodiacSigns[4], listViewAdapter.getItem(4)); assertEquals(zodiacSigns[5], listViewAdapter.getItem(5)); assertEquals(zodiacSigns[6], listViewAdapter.getItem(6)); assertEquals(zodiacSigns[7], listViewAdapter.getItem(7)); assertEquals(zodiacSigns[8], listViewAdapter.getItem(8)); assertEquals(zodiacSigns[9], listViewAdapter.getItem(9)); assertEquals(zodiacSigns[10], listViewAdapter.getItem(10)); assertEquals(zodiacSigns[11], listViewAdapter.getItem(11)); }
  • 43.
    public static finalZodiac[] signs = { new Zodiac("Aries","Enterprising, Incisive, Spontaneous, Daring, Active, Courageous and Energetic, the Aries are the proverbial infants, guileless and op new Zodiac("Taurus","Known for being reliable, practical, ambitious and sensual, the people born under the Zodiac Sign Taurus have an eye for beauty.", " new Zodiac("Gemini","Gemini-born are clever and intellectual people but they can also be tense and restless.", "Twins", "June"), new Zodiac("Cancer"," The otherwise tenacious, loyal, sympathetic and strong Crabs are vulnerable in many ways.", "Crab", "July"), new Zodiac("Leo","Warm, action-oriented and driven by the desire to be loved and admired, the Leo have an air royalty about them.", "Lion", "August"), new Zodiac("Virgo","Methodical, meticulous, analytical and mentally astute, the Virgo natives are perfectionists to the core, or at least, they like to b new Zodiac("Libra","Librans are famous for maintaining balance and harmony.", "Scales", "October"), new Zodiac("Scorpio","The Scorpio-born are strong willed and mysterious, and they know how to effortlessly grab the limelight, as they possess what it ta new Zodiac("Sagittarius","Sagittarians are born adventurers. They tend to get bored with things easily and move on with life", "Archer", "December"), new Zodiac("Capricorn","The Capricorn-born people are the most determined of the entire Zodiac.", "Goat", "January"), new Zodiac("Aquarius","The Aquarius-born people are humanitarians to the core", "Water Bearer", "February"), new Zodiac("Pisces","Pisces or the Fish is considered as the proverbial dreamers of the Zodiac.", "Fish", "March") }; @Before public void setUp() { Intent intent = new Intent(RuntimeEnvironment.application, ZodiacDetailActivity.class); intent.putExtra(ZodiacDetailActivity.EXTRA_SIGN, ARIES_SIGN_INDEX); zodiacDetailActivity = Robolectric.buildActivity(ZodiacDetailActivity.class).withIntent(intent).create().get(); assertNotNull("Zodiac Detail Activity not setup", zodiacDetailActivity); } @Test public void zodiacSymbolTest() throws Exception { TextView symbolTextView = (TextView) zodiacDetailActivity.findViewById(R.id.symbol); assertEquals(Zodiac.signs[ARIES_SIGN_INDEX].getSymbol(), symbolTextView.getText().toString()); } public class ZodiacDetailActivity extends Activity { public static final String EXTRA_SIGN = "ZodiacSign"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_zodiac_detail); int signNum = (Integer)getIntent().getExtras().get(EXTRA_SIGN); Zodiac zodiac = Zodiac.signs[signNum]; TextView name = (TextView)findViewById(R.id.name); name.setText(zodiac.getName()); TextView description = (TextView)findViewById(R.id.description); description.setText(zodiac.getDescription()); TextView symbol = (TextView)findViewById(R.id.symbol); symbol.setText(zodiac.getSymbol()); TextView month = (TextView)findViewById(R.id.month); month.setText(zodiac.getMonth()); } }
  • 44.
    public class AsyncTaskParseJsonextends AsyncTask<String, String, String> { String yourJsonStringUrl = "http://a.knrz.co/horoscope-api/current/"; String horoscope = ""; public AsyncTaskParseJson(Zodiac sign) { yourJsonStringUrl += sign.getName().toLowerCase(); } @Override protected void onPreExecute() {} @Override protected String doInBackground(String... arg0) { try { // instantiate our json parser JsonParser jParser = new JsonParser(); // get json string from url JSONObject json = jParser.getJSONFromUrl(yourJsonStringUrl); horoscope = json.getString("prediction"); horoscope = URLDecoder.decode(horoscope); } catch (Exception e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(String strFromDoInBg) { TextView display = (TextView) findViewById(R.id.daily); display.setText(horoscope); } } @Test public void zodiacDailyTest() { TextView dailyTextView = (TextView) zodiacDetailActivity.findViewById(R.id.daily); assertEquals("This week try wearing less make-up when you leave the house, " + "even if it means angering the other members of KISS.", dailyTextView.getText().toString()); }
  • 45.
    LESSONS LEARNEDLESSONS LEARNED Whatworked No longer need emulator Not so much Android Activities don't work well with TDD Robolectric is your friend
  • 46.
  • 49.
    @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())); }
  • 50.
    @RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityTest{ @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class); @Test public void toDoListTest(){ onData(anything()) .inAdapterView(withId(R.id.list_of_todos)).atPosition(4) .perform(click()); onView(withId(R.id.txt_selected_item)) .check(matches(withText("go to the gym"))); } }
  • 51.
    EXISTING PROJECTSEXISTING PROJECTS Waymore common Essential Steps Lessons Learned
  • 52.
    STEPSSTEPS Introduce Continuous Integrationto 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%
  • 53.
    package alexandria.israelferrer.com.libraryofalexandria; import android.app.Activity; importandroid.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.ListView; import android.widget.RatingBar; import android.widget.TextView; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.squareup.picasso.Picasso; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; public class MainActivity extends Activity { private static final String PACKAGE = "com.israelferrer.alexandria"; private static final String KEY_FAVS = PACKAGE + ".FAVS"; private List<ArtWork> artWorkList; private ArtWorkAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView = (ListView) findViewById(R.id.listView); InputStream stream = getResources().openRawResource(R.raw.artwork); Type listType = new TypeToken<List<ArtWork>>() { }.getType(); artWorkList = new Gson().fromJson(new InputStreamReader(stream), listType); final SharedPreferences preferences = getSharedPreferences(getPackageName() , Context.MODE_PRIVATE); for (ArtWork artWork : artWorkList) { artWork.setRating(preferences.getFloat(PACKAGE + artWork.getId(), 0F)); } adapter = new ArtWorkAdapter(); listView.setAdapter(adapter); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.filter) { adapter.orderMode(); return true; } return super.onOptionsItemSelected(item); } private class ArtWorkAdapter extends BaseAdapter { private boolean isOrder; private final List<ArtWork> orderedList; public ArtWorkAdapter() { super(); orderedList = new LinkedList<ArtWork>(); } @Override public int getCount() { return artWorkList.size(); } @Override public Object getItem(int position) { return artWorkList.get(position); } @Override public long getItemId(int position) { return Long.valueOf(artWorkList.get(position).getId()); } public void orderMode() { isOrder = !isOrder; if (isOrder) { orderedList.clear(); orderedList.addAll(artWorkList); Collections.sort(orderedList); notifyDataSetChanged(); } else { notifyDataSetChanged(); } } @Override public View getView(int position, View convertView, ViewGroup parent) { final ArtWork artWork; if (isOrder) { artWork = orderedList.get(position); } else { artWork = artWorkList.get(position); } View row; switch (artWork.getType()) { case ArtWork.QUOTE: row = getLayoutInflater().inflate(R.layout.text_row, null); TextView quote = (TextView) row.findViewById(R.id.quote); TextView author = (TextView) row.findViewById(R.id.author); quote.setText(""" + artWork.getText() + """); author.setText(artWork.getAuthor()); break; case ArtWork.PAINTING: final SharedPreferences preferences = getSharedPreferences(getPackageName() , Context.MODE_PRIVATE); final HashSet<String> favs = (HashSet<String>) preferences .getStringSet(KEY_FAVS, new HashSet<String>()); row = getLayoutInflater().inflate(R.layout.painting_row, null); ImageView image = (ImageView) row.findViewById(R.id.painting); TextView painter = (TextView) row.findViewById(R.id.author); painter.setText(artWork.getTitle() + " by " + artWork.getAuthor()); Picasso.with(MainActivity.this).load(artWork.getContentUrl()).fit() .into(image); RatingBar rating = (RatingBar) row.findViewById(R.id.rate); rating.setRating(artWork.getRating()); rating.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() { @Override public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { preferences.edit().putFloat(PACKAGE + artWork.getId(), rating).apply(); artWork.setRating(rating); } }); CheckBox fav = (CheckBox) row.findViewById(R.id.fav); fav.setChecked(favs.contains(artWork.getId())); fav.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { final HashSet<String> favs = new HashSet<String>((HashSet<String>) preferences .getStringSet(KEY_FAVS, new HashSet<String>())); if (isChecked) { favs.add(artWork.getId()); } else { favs.remove(artWork.getId()); } preferences.edit().putStringSet(KEY_FAVS, favs).apply(); } }); break; case ArtWork.MOVIE: case ArtWork.OPERA: row = new ViewStub(MainActivity.this); break; default: row = getLayoutInflater().inflate(R.layout.text_row, null); } return row; } } }
  • 55.
    apply plugin: 'com.android.application' applyplugin: 'jacoco' apply plugin: 'sonar-runner' sonarRunner{ sonarProperties{ property "sonar.host.url", "http://localhost:9000" property "sonar.jdbc.url", "jdbc:mysql://localhost:3306/sonar?useUnicode=true&charac property "sonar.jdbc.driverClassName","com.mysql.jdbc.Driver" property "sonar.jdbc.username","root" property "sonar.jdbc.password","root" property "sonar.projectKey", "RIIS:CropCompare" property "sonar.projectVersion", "2.0" property "sonar.projectName","CropCompare" property "sonar.java.coveragePlugin", "jacoco" property "sonar.sources","srcmain" property "sonar.tests", "srctest" property "sonar.jacoco.reportPath", "buildjacocojacocoTest.exec" property "sonar.java.binaries", "build" property "sonar.dynamicAnalysis", "resuseReports" } }
  • 56.
    LESSONS LEARNEDLESSONS LEARNED Lookat different architectures MVP, MVVM w/data binding What worked Take baby steps, Metrics should evolve…. Not so much Don’t be driven by metrics Remember…. You don’t need anyone’s permission to start
  • 57.
  • 58.