Date post: | 16-Jan-2017 |
Category: |
Technology |
Upload: | tomas-kypta |
View: | 30 times |
Download: | 1 times |
Unit Testing Experience on Android
Android apps are difficult to test
Types of Android tests
Types of Android tests
Instrumentation tests
Local unit tests
Android test code• project sources
• ${module}/src/main/java
• instrumentation tests • ${module}/src/androidTest/java
• unit tests • ${module}/src/test/java
• full Gradle and Android Studio support
• the essential piece of both instrumentation and unit tests
• alone can be used only for pure Java
• doesn’t provide any mocks or Android APIs
Instrumentation Tests
Instrumentation Tests
• running on physical device or emulator
• gradle connectedAndroidTest
• ${module}/build/reports/androidTests/connected/index.html
Instrumentation Tests
Legacy instrumentation tests or
Testing Support Library
Legacy Instrumentation Tests• JUnit3 • Tests extend from TestCase
• AndroidTestCase • ActivityInstrumentationTestCase2 • ServiceTestCase • …
deprecated since API level 24
Testing Support Library• JUnit4 compatible
• AndroidJUnitRunnerandroid { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" }}
dependencies { androidTestCompile 'com.android.support.test:runner:0.5'}
Testing Support Library• JUnit test rules
• AndroidTestRule
• ServiceTestRule
• DisableOnAndroidDebug
• LogLogcatRule
• …
androidTestCompile 'com.android.support.test:rules:0.5'
• framework for functional UI tests
• part of Android Testing Support Library
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
@Testpublic void sayHello() { onView(withId(R.id.edit_text)) .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
onView(withText("Say hello!")) .perform(click());
String expectedText = "Hello, " + STRING_TO_BE_TYPED + "!"; onView(withId(R.id.textView)) .check(matches(withText(expectedText)));}
Problems
• testing on device is not isolated
• device state affects the result
• e.g. screen on/off might affect test result
onView(withId(R.id.my_view)) .check(matches(isDisplayed()));
Some annoyances
android.support.test.espresso.NoActivityResumedException: No activities in stage RESUMED. Did you forget to launch the activity. (test.getActivity() or similar)?
Instrumentation tests are
kindaSLOOOOOW
Unit Tests
Unit Tests
• run on JVM
• mockable android.jar
• gradle test
• ${module}/build/reports/tests/${variant}/index.html
…...
• Helps rarely • returns 0, false, null, …
Method ... not mocked.
android { testOptions { unitTests.returnDefaultValues = true } }
• mocking framework • easy to use • compatible with Android unit testing
testCompile 'org.mockito:mockito-core:2.2.11'
• can be used also in instrumentation tests • needs dexmaker
androidTestCompile 'org.mockito:mockito-core:2.2.11'androidTestCompile 'com.google.dexmaker:dexmaker:1.2'androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
@RunWith(JUnit4.class)public class ContextManagerTest {
@Mock Context mAppContext;
@Before public void setUp() { MockitoAnnotations.initMocks(this); }
@Test public void testWithContext() { … }}
@RunWith(MockitoJUnitRunner.class)public class ContextManagerTest {
@Mock Context mAppContext;
@Test public void testWithContext() { … }}
@RunWith(JUnit4.class)public class ContextManagerTest {
@Test public void testWithContext() { Context appContext = mock(Context.class); Mockito.when(appContext.getPackageName()) .thenReturn(“com.example.app”); … }}
• Mockito.spy()
• wrapping a real object
• Mockito.verify()
• verify that special condition are met
• e.g. method called, method called twice, …
Limitations
• final classes
• opt-in incubating support in Mockito 2
• anonymous classes
• primitive types
• static methods
• functional testing framework
• runs on JVM
• at first, might be difficult to use
• the ultimate mock of Android APIs
• provides mocks of system managers
• allows custom shadows
• possible to use for UI testing
• better to use for business logic
@RunWith(RobolectricTestRunner.class)public class MyTest { …}
• Robolectric
• RuntimeEnvironment
• Shadows
• ShadowApplication
• ShadowLooper
Potential problems
• difficult to search for solutions
• long history of bigger API changes
• many obsolete posts
• Can mock static methods
• Can be used together with Mockito
@RunWith(PowerMockRunner.class)
@PrepareForTest(Static.class);
PowerMockito.mockStatic(Static.class);
Mockito.when(Static.staticMethod())
.thenReturn(value);
PowerMockito.verifyStatic(Static.class);
• “matchers on steroids”
• offers more complex checks
assertThat(myClass, isInstanceOf(MainActivity.class));
assertThat(myManager.getValue(), isEqualTo(someValue));
assertThat(value, isIn(listOfValues));
assertThat(value, not(isIn(listOfValues)));
• cross-platform BDD framework
• human-like test definitions
testCompile ‘junit:junit:4.12'testCompile ‘info.cukes:cucumber-java:1.2.5'testCompile 'info.cukes:cucumber-junit:1.2.5'
• describe the desired behaviour
Feature: CoffeeMaker Scenario: a few coffees Given I previously had 3 coffees When I add one coffee Then I had 4 coffees
• create the mapping
public class CoffeeMakerDefs { CoffeeMaker mCoffeeMaker = new CoffeeMaker();
}
• create the mapping
public class CoffeeMakerDefs { CoffeeMaker mCoffeeMaker = new CoffeeMaker(); @Given("^I previously had (\\d+) coffees$") public void hadCoffeesPreviously(int coffees) { mCoffeeMaker.setCoffeeCount(coffees); }
}
• create the mapping
public class CoffeeMakerDefs { CoffeeMaker mCoffeeMaker = new CoffeeMaker(); @Given("^I previously had (\\d+) coffees$") public void hadCoffeesPreviously(int coffees) { mCoffeeMaker.setCoffeeCount(coffees); } @When("^I add one coffee$") public void addCoffee() { mCoffeeMaker.addCoffee(); }
}
• create the mapping
public class CoffeeMakerDefs { CoffeeMaker mCoffeeMaker = new CoffeeMaker(); @Given("^I previously had (\\d+) coffees$") public void hadCoffeesPreviously(int coffees) { mCoffeeMaker.setCoffeeCount(coffees); } @When("^I add one coffee$") public void addCoffee() { mCoffeeMaker.addCoffee(); } @Then("^I had (\\d+) coffees$") public void hadCoffees(int coffees) { Assert.assertEquals(coffees, mCoffeeMaker.getCoffeeCount()); }}
• place definition and mapping at the same paths! • ${module}/src/test/java/com/example/MyMapping.java
• ${module}/src/test/resources/com/example/MyDefinition.feature
@RunWith(Cucumber.class) public class RunCucumberTest {}
Code Coverage
Code Coverage
• instrumentation tests
• JaCoCo
• EMMA
• obsolete
• unit tests
• JaCoCo
Instrumentation Tests & Code Coverage• has to be explicitly enabled
• gradle createDebugCoverageReport
• ${module}/build/reports/coverage/debug/index.html
• ${module}/build/outputs/code-coverage/connected/${deviceName}-coverage.ec
• doesn’t work on some devices!!!
buildTypes { debug { testCoverageEnabled true }}
JaCoCo
JaCoCo
• enabled by default for unit tests
• gradle test
• generates binary report in build/jacoco
• ${module}/build/jacoco/testDebugUnitTest.exec
• it’s necessary to create a JacocoReport task to obtain a readable report
Good tests
Good tests
• run in any order
• run in isolation
• run consistently
• run fast
• are orthogonal
How to write testable apps?
Rules of thumb• prefer pure Java • abstract away from Android APIs • separate business logic and UI
• don’t write business logic into activities and fragments • MVP, MVVM is a way to go
• try avoid static and final • use dependency injection
Questions?