43
We're an independent design & development agency.

Infinum Android Talks #17 - Testing your Android applications by Ivan Kust

  • Upload
    infinum

  • View
    168

  • Download
    1

Embed Size (px)

Citation preview

We're an independent design & development agency.

Testing your Android application

IVAN KUŠT

ROBOLECTRIC

ROBOLECTRIC

• http://robolectric.org/

• unit test framework

• tests run in JVM on your machine

ROBOLECTRIC SETUP

• 1. add gradle dependencies

• 2. write test application class

• 3. write test class

• 4. run tests

1. ADD GRADLE DEPENDENCIES

• make sure “Unit tests” is selected as Test Artifact under

Build Variants

• add the robolectric dependency in your app build.gradle file:

testCompile 'junit:junit:4.12'testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.robolectric:robolectric:3.0'testCompile 'org.robolectric:shadows-support-v4:3.0'

2. WRITE TEST APPLICATION CLASS

• all unit test code goes into test flavour

• Test{applicationName} • must extend application class • runs instead of application class in tests • should inject test dependencies

3. WRITE TEST CLASS

• create new class in test flavor

• specify runner with @RunWith annotation

• add @Config annotation

• add test methods and annotate them with @Test

TEST CONFIGURATION

• constants property of @Config annotation is mandatory

• for all other options check:http://robolectric.org/configuring/

@RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) public class DeckardActivityTest { @Test public void testSomething() throws Exception { assertTrue(Robolectric.setupActivity(DeckardActivity.class) != null); }}

4. RUN TESTS

• from Android studio

• ./gradlew clean test

MOCK WEB SERVER

MOCK WEB SERVER

• network responses should be mocked in unit tests • speed • consistency

GRADLE DEPENDENCIES

• add the MockWebServer dependency in your app

build.gradle file:

• MockWebServer depends on okhttp library

• make sure that mockwebserver version matches okhttp

version

testCompile 'com.squareup.okhttp:mockwebserver:2.7.4'

USAGE

• 1. instantiate MockWebServer

• 2. call start() method

• 3. get local server url by calling url(“/“)

• 4. inject server local url into networking module

• 5. enqueue responses using enqueue() method

USAGE

• in both unit and instrumentation tests (steps 1 - 4): • prepare and inject MockWebServer in @Before method

(called before every @Test method) • stop MockWebServer in @After method (called after

every @Test method)

USAGE

• or in Robolectric unit tests (steps 1 - 4): • prepare and inject MockWebServer in:

beforeTest(Method) method in Test application • stop MockWebServer in:

afterTest(Method method) in Test application

ENQUEUEING RESPONSES

• enqueue(), MockResponse object

• https://github.com/square/okhttp/blob/master/

mockwebserver/src/main/java/com/squareup/okhttp/

mockwebserver/MockResponse.java

mockWebServer.enqueue( new MockResponse() .setResponseCode(200) .setBody("Response body"); );

LARGE RESPONSES?

• 1. store response body content to a file

• 2. put the file inside /test/resources/ directory

• 3. read the contents of the file in test:

public static String readFromFile(String filename) { InputStream is = ResourceUtils.class.getClassLoader().getResourceAsStream(filename); return convertStreamToString(is);}

CHECKING REQUESTS

• mockWebServer.getRequestCount()

• mockWebServer.takeRequest()

• mockWebServer.takeRequest(long, TimeUnit)

@Test public void nameOk() throws Exception { PokemonTestApp.getMockWebServer().enqueue( new MockResponse() .setResponseCode(200) .setBody(ResourceUtils.readFromFile("charizard.json")) ); String resourceUri = "api/v1/pokemon/6/"; Pokemon pokemon = new Pokemon(); pokemon.setResourceUri(resourceUri); Activity activity = buildActivity(pokemon); RecordedRequest request = takeLastRequest(); //Perform the assertions}

(A)SYNCHRONOUS EXECUTORS

• unit tests run in a single thread

• waiting for background threads complicates tests

• solution: Synchronous executors

(A)SYNCHRONOUS EXECUTORS

• setting executor on Retrofit:

• specify executor for networking and for callback

• for tests: both synchronous

return new RestAdapter.Builder() ... .setExecutors(new BackgroundExecutor(), new CallbackExecutor()) ... .build();

SYNCHRONOUS EXECUTOR?

• runs the queued runnable in the same thread

• simple as that:

public class SynchronousExecutor implements Executor { @Override public void execute(Runnable command) { command.run(); }}

SHADOW CLASSES

SHADOWING A CLASS

• 1. create a custom Robolectric runner

• 2. declare shadowed classes in createClassLoaderConfig()

method

• 3. implement shadow class

• 4. run tests with custom runner

public class CustomRobolectricGradleTestRunner extends RobolectricGradleTestRunner { public CustomRobolectricGradleTestRunner(Class<?> klass) throws InitializationError { super(klass); } @Override protected AndroidManifest getAppManifest(Config config) { AndroidManifest appManifest = super.getAppManifest(config);

// needs to be the java package, not applicationId appManifest.setPackageName("co.infinum.app"); return appManifest; } @Override public InstrumentationConfiguration createClassLoaderConfig() { InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder(); builder.addInstrumentedClass(ShadowOutline.class.getName()); builder.addInstrumentedClass(ShadowAlertDialogSupportV7.class.getName()); builder.addInstrumentedClass(ShadowSpinnerView.class.getName()); return builder.build(); }}

3. IMPLEMENT SHADOW CLASS

• create a new class with @Implements(OriginalClass.class)

annotation

• add @Implementation annotation to methods that “override”

behaviour from original class

• you can leave some methods unchanged

• if you wish to use an instance of OriginalClass in your

shadow class it must be annotated with @RealObject

ADD SHADOWS TO TEST

• in order to make your shadows available in test add them to

@Config in your test: @Config(shadows = {ShadowClass.class})

@Implements(Outline.class) public class ShadowOutline { @RealObject private Outline outline; public Path path; public Rect rect; public float radius; public float alpha; @Implementation public void setConvexPath(Path convexPath) { if (path == null) { path = new Path(); } path.set(convexPath); }}

ESPRESSO

SETUP

• 1. add gradle dependencies

• 2. specify runner

• 3. write test class

• 4. run

1. ADD DEPENDENCIES

• make sure “Android Instrumentation tests” is selected as Test

Artifact under Build Variants

• add the robolectric dependency in your app build.gradle file:

// EspressoandroidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'androidTestCompile 'com.android.support.test:runner:0.4.1'androidTestCompile 'com.android.support.test:rules:0.4.1'

2. SPECIFY RUNNER

• specify default instrumentation test runner in application

build.gradle:

android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } }

3. WRITE TEST CLASS

• create new class in test flavor

• annotate with @LargeTest

• add field of type ActivityTestRule and annotate with @Rule

- defines the activity that will run

• add test methods and annotate them with @Test

4. RUN

• from Android studio

• ./gradlew clean connectedAndroidTest

FEW MORE TIPS

ASSERTJ ANDROID

• https://github.com/square/assertj-android

• extension of AssertJ

• you can extend and set up your own assertions

//Check that name in details is displayed properly.

assertThat(activity.findViewById(R.id.name).getVisibility()) .isEqualTo(View.VISIBLE);assertThat(((TextView) activity.findViewById(R.id.name)) .getText()).isEqualTo("Charizard");

TEST REPORT

• will be generated in:/app/build/reports/tests/{flavour}/index.html

• if tests faill, gradle will print out the location of the report in

console as well

LOGS

• to show Log.d() outputs add folloving in your @Before

method:ShadowLog.stream = System.out;

USING MOCK WEB SERVER

• like in unit tests (steps 1 - 4):

• prepare and inject MockWebServer in @Before method

(called before every @Test method)

• stop MockWebServer in @After method (called after every

@Test method)

• note that in instrument tests there is no Test application that

you can use

RESOURCES

• http://robolectric.org/

• https://github.com/square/assertj-android

• http://www.vogella.com/tutorials/

AndroidTestingEspresso/article.html

• http://developer.android.com/training/testing/ui-

testing/espresso-testing.html

Thank you!

Visit www.infinum.co or find us on social networks:

infinum.co infinumco infinumco infinum