Android TDD

Godfrey Nolan

Founder RIIS LLC

Agenda

  • Unit Testing intro
  • Up and Running
  • Unit testing 101
  • Tools of the trade
  • Mocking
  • TDD
  • Espresso
  • Adding TDD to existing projects

FOLLOW ALONG

http://github.com/godfreynolan/AnDevConSF

https://slides.com/godfreynolan/android-tdd

Unit Testing intro

  • Hello World
  • Benefits
  • Android Testing Pyramid
  • Activity Testing

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 intro

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

UP And Running

  • Gradle version
  • build.gradle changes
  • Android Studio 2.2
  • Sample app
dependencies {
    // Unit testing dependencies.
    testCompile 'junit:junit:4.12'
}
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

UNIT TESTING 101

  • Command line
  • Setup and Teardown
  • Grouping
  • Parameters
  • Assertions
  • Code Coverage
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);
    }
}

ToOLS OF THE TRADE

  • Hamcrest
  • Mockito
  • Robolectic
  • Jenkins

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);
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.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!"));
    }
}
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);
    }
}
@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_array);
    }

    @Test
    public void listLoaded() throws Exception {
        assertThat("should be a dozen star signs", zodiacSigns.length, equalTo(listView.getCount()));
    }
}

TROUBLE SHOOTING

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

Mocking

  • Shared Prefs
  • Database
  • System Properties
  • Web Services

Mocking 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_VOLUME);

        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 DownloadUrlTest {

    public DownloadUrl tDownloadUrl = Mockito.mock(DownloadUrl.class);
    public String htmlString = "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/WebPage\" lang=\"en\"><head>";

    @Before
    public void setUp() {
        try {
            Mockito.when(tDownloadUrl.loadFromNetwork("http://www.google.com")).thenReturn(htmlString);
        } catch (IOException e) {
            // network error
        }
    }

    @Test
    public void downloadUrlTest_ReturnsTrue() {
        try {
            assertThat(tDownloadUrl.loadFromNetwork("http://www.google.com"),containsString("doctype"));
        } catch (IOException e) {
            //
        }
    }
}

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

SAMPLE 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));
    }
@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));
}
<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>
    public static final Zodiac[] signs = {
            new Zodiac("Aries","Enterprising, Incisive, Spontaneous, Daring, Active, Courageous and Energetic, the Aries are the proverbial infants, guileless and optimistic to the fault.", "Ram", "April"),
            new Zodiac("Taurus","Known for being reliable, practical, ambitious and sensual, the people born under the Zodiac Sign Taurus have an eye for beauty.", "Bull", "May"),
            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 believe that they are.", "Virgin", "September"),
            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 takes to achieve their goals.", "Scorpion", "November"),
            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") 
    };
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());
    }
}
@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 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("You’ll continue playing dress-up this week, despite being almost 30, " +
        "and feeling kind of silly every time you put on that professional-looking suit.", +        
            dailyTextView.getText().toString());
}

LESSONS LEARNED

  • What worked
    • No longer need emulator 
  • Not so much
    • Android Activities don't work well with TDD
    • Robolectric is your friend

Espresso

  • 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")));
 
    }
 
}

DEMO

EXISTING PROJECTS

  • Way more common
  • Essential Steps
  • Lessons Learned

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)
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&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance"
        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","src\\main"
        property "sonar.tests", "src\\test"
        property "sonar.jacoco.reportPath", "build\\jacoco\\jacocoTest.exec"
        property "sonar.java.binaries", "build"
        property "sonar.dynamicAnalysis", "resuseReports"
    }
}

LESSONS LEARNED

  • Look at different architectures
    • MVP, MVVM w/data binding, Clean
  • 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

 

RESOURCES

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://github.com/godfreynolan/andevconsf

CONTACT INFO

godfrey@riis.com

@godfreynolan

http://slides.com/godfreynolan

Android TDD

By godfreynolan

Android TDD

  • 931