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?
- Low time consumption
- Local JVM
- Mocking dependencies
- Spock & Groovy
- 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
- Robolectric
- Android developers
- RoboSpock
-
"Data matures like wine, applications like fish" James Governor, source
- "Rozum i Serce" - Orange commercial
Follow up
Home/blog: erbetowski.pl
Twitter: erbetowski
Facebook: wojtekerbetowski
GitHub:
wojtekerbetowski
Awesome
By Wojtek Erbetowski
Awesome
- 2,052