Awesome

unit testing Android applications

by Wojtek Erbetowski

The road


Unit tests - understanding the needs

Android test framework

Getting faster, lighter, better, harder

Raising the bar

Kick off

Author


Leader and organizer

Warsaw Java User Group, MobileWarsaw, Warsjawa, Mobile Central Europe, Git Kata, NameCollision


Programmer
Groovy, Scala, Python, Java



Team


Tech Lead @ Polidea

The context



Server side development

Large and aged applications

TDD passionate and practitioner

Unit Testing Applications




  • Check if the code works!
  • Fast feedback loop
  • Point the exact broken piece
  • Stable and trusted
  • Readable, short, expressive

The need





Fast and stable

How fast?



How fast needed?



Android test framework


Project structure

MyProject/
  AndroidManifest.xml
    res/
      ... (resources for main application)
    src/
      ... (source code for main application) ...
    tests/
      AndroidManifest.xml
      res/
        ... (resources for tests)
      src/
        ... (source code for tests)

Running the tests



  • dex
  • package
  • install
  • run app

Problemo?



  • Continuous Integration
  • Parallelization
  • Time consuming
  • DalvikVM only (Java, no AOP)

Sample


public class ScanExampleTest
        extends ActivityInstrumentationTestCase2<MyScanActivity> {

    private Solo solo;

    public ScanExampleTest() {
        super("org.my.scanExample", MyScanActivity.class);
    }

    @Override
    public void setUp() throws Exception {
        solo = new Solo(getInstrumentation(), getActivity());
    }

    public void testManualEntry() throws Exception {
        solo.assertCurrentActivity("expecting example app", MyScanActivity.class);

        boolean ableToScan = CardIOActivity.canReadCardWithCamera(solo.getCurrentActivity());

        solo.clickOnButton(0);

        if (ableToScan) {
        // some devices don't support use of the camera.

            solo.assertCurrentActivity("Expected CardIOActivity (scan)", "CardIOActivity");
            solo.clickOnButton("Keyboard...");
        }

        solo.assertCurrentActivity("Expected DataEntryActivity", "DataEntryActivity");

        solo.enterText(0, "4111111111111111");
        solo.enterText(1, "12/22");

        solo.clickOnButton("Done");

        solo.assertCurrentActivity("Expected completion", MyScanActivity.class);

        assertEquals("Card Number not found", true, solo.searchText("Card Number:"));
        assertEquals("Expiry not found", true, solo.searchText("Expiration Date: 12/2022"));

    }
}Based on https://github.com/card-io/android-scan-demo

A better way



Why not JVM?


android.jar

|
\/

Stub!

Rewrite android.jar?

Classloaders FTW!

Meet Robolectric



Sample test


 // Test class for MyActivity
@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {

  @Test
  public void clickingButton_shouldChangeResultsViewText() throws Exception {
    Activity activity = Robolectric.buildActivity(MyActivity.class).create().get();

    Button pressMeButton = (Button) activity.findViewById(R.id.press_me_button);
    TextView results = (TextView) activity.findViewById(R.id.results_text_view);

    pressMeButton.performClick();
    String resultsText = results.getText().toString();
    assertThat(resultsText, equalTo("Testing Android Rocks!"));
  }
} 

Shadows


@Implements(AlphaAnimation.class)
public class ShadowAlphaAnimation extends ShadowAnimation {
  private float fromAlpha;
  private float toAlpha;

  public void __constructor__(float fromAlpha, float toAlpha) {
    this.fromAlpha = fromAlpha;
    this.toAlpha = toAlpha;
  }

  public float getFromAlpha() {
    return fromAlpha;
  }

  public float getToAlpha() {
    return toAlpha;
  }
}

Looking closer




Not bad.

Awesome


  • mocking static methods
  • fast execution
  • running on JVM

Raising the bar

JUnit

@Test
void schouldAggregateSevesUser() {
    // given
    User user = new User();

    // when
    aggregate.store(user);

    // then
    assertEquals(
        user,
        aggregate.findOnly()
    );
}

Spock

def 'aggregate should save user'() {
    given:
      def user = new User()

    when:
      aggregate.store user

    then:
      aggregate.findOnly() == user
}

Test parameters

JUnit

// ...

// then,
assertEquals(sum(3, 5), 8);
assertEquals(sum(1, 5), 6);
assertEquals(sum(4, 5), 9);
assertEquals(sum(5, 3), 8);
    

Spock

// ...

expect:
    sum(a, b) == c

where:
    a | b || c  
    3 | 5 || 8
    1 | 5 || 6
    4 | 5 || 9
    5 | 3 || 8
  

Mocks

JUnit

// given
User userMock = mock(User.class);
when(userMock.getEmail())
    .thenReturn("me@email.com")
    .thenReturn(null);

// ...

// then
verify(userMock, times(2)).getEmail()

Spock

given:
  def userMock = Mock(User)
  userMock.getEmail() >> ['me@email.com', null]

// ...

then:
  2 * userMock.getEmail()

Exception type

JUnit

@Test(expect=RuntimeException.class)
public void myTest() {

  thisThrowsSomething();
}

Spock

when:
thisThrowsSomething()

then:
thrown(RuntimeException)

Exception details

JUnit

@Test
public void myTest() {

  try {
    thisThrowsSomething();
    fail();

  } catch(RuntimeException e) {
    assertContains(
      e.getMessage(), 'No such user')
  }
}

Spock

when:
  thisThrowsSomething()

then:
  def e = thrown(RuntimeException)
  e.message =~ 'No such user'

Groovyness

then:
    userStorage.getAllUsers().find{it.id == id}?.name == "Szymon"
Condition not satisfied:

userRepository.findAll().find{it.name == 'Szymon'}?.age == 10
|              |         |                          |   |
|              |         null                       |   false
|              [A$User(Piotr, 12)]                  null
A$UserRepository@22d3d11f
<Click to see difference>

Hello RoboSpock


Robolectric

classloader, shadows, DB support 

    Spock

    runner, extension points, everything else


    Setup


    .
    ├── app
    │   ├── build.gradle
    │   ├── debug.keystore
    │   ├── local.properties
    │   └── src
    │       ├── instrumentTest
    │       ├── main
    │       └── release
    ├── robospock
    │   ├── build.gradle
    │   └── src
    │       └── test
    └── settings.gradle
    

    Setup

    buildscript {
      repositories {
        mavenCentral()
      }
      
      dependencies {
        classpath 'com.android.tools.build:gradle:0.5.+'
        classpath 'com.novoda:gradle-android-test-plugin:0.9.1-SNAPSHOT'
      }
    }
    
    apply plugin: 'groovy'
    apply plugin: 'android-test'
    
    repositories {
      mavenCentral()
    }
    
    dependencies {
      testCompile 'pl.polidea:RoboSpock:0.4'
      testCompile rootProject
    }

    Setup

    buildscript {
      repositories {
        mavenCentral()
      }
      dependencies {
        classpath 'com.android.tools.build:gradle:0.5.+'
      }
    }
    apply plugin: 'android'
    
    dependencies {
      compile 'com.android.support:support-v4:13.0.0'
      debugCompile 'com.android.support:support-v13:13.0.0'
    
      compile 'com.google.android.gms:play-services:3.1.36'
    }
    
    android {
      compileSdkVersion 15
      buildToolsVersion "17.0"
    
      testBuildType "debug"
    
      signingConfigs {
        myConfig {
          storeFile file("debug.keystore")
          storePassword "android"
          keyAlias "androiddebugkey"
          keyPassword "android"
        }
      }
    
      defaultConfig {
        versionCode 12
        versionName "2.0"
        minSdkVersion 16
        targetSdkVersion 16
      }
    
      buildTypes {
        debug {
          packageNameSuffix ".debug"
          signingConfig signingConfigs.myConfig
        }
      }
    }
    

    Are we awesome yet?


    1. Low time consumption
    2. Local JVM
    3. Mocking dependencies
    4. Spock & Groovy
    5. Simple setup

    Examples

    ORMLite


    def "should throw SQL Constraint exception on existing primary key"() {
      given:
        def dao = databaseHelper.getDao(DatabaseObject)
    
      and: 'stored object'
        def dbObject = new DatabaseObject("test", 4, 1)
        dao.create(dbObject)
    
      when: 'duplication'
        dao.create(dbObject)
    
      then:
        def exception = thrown(RuntimeException)
        exception.message =~ "SQLITE_CONSTRAINT"
        exception.cause.class == SQLException
    }

    RoboGuice

    class TaskActivityTestGroovy extends RoboSpecification {
    
      @Inject WebInterface webInterface
    
      def setup() {
        inject {
          install(TestTaskExecutorModule)
          bind(WebInterface).toInstance(Mock(WebInterface))
        }
      }
    
      def "should load text from async task"() {
        given:
          def taskActivity = new TaskActivity()      webInterface.getMainPageText() >> "WebText"
    
        when:
          taskActivity.onCreate(null)
    
        then:
          taskActivity.asyncText.text == "WebText"
      }
    }

    Sources


    Follow up


    Home/blog: erbetowski.pl
    Twitter: erbetowski
    Facebook: wojtekerbetowski
    GitHub:  wojtekerbetowski

    Awesome

    By Wojtek Erbetowski

    Awesome

    • 2,052