Testing with Robolectric IVAN KUŠT, ANDROID TEAM LEADER
ROBOLECTRIC
• http://robolectric.org/
• unit test framework
• tests run in JVM on your machine
CODE EXAMPLE
• https://github.com/infinum/Dagger-2-Example
ROBOLECTRIC SETUP
1. Add gradle dependencies
2. write test Application class
3. write test class
4. run tests
1. ADD DEPENDENCIES
• make sure “Unit tests” is selected as Test Artifact under
Build Variants
• add the robolectric dependency in your app build.gradle file:
// RobolectrictestCompile 'junit:junit:4.12'testCompile 'org.hamcrest:hamcrest-library:1.3'testCompile 'org.apache.maven:maven-ant-tasks:2.1.3' // fixes issue on linux/mactestCompile('org.robolectric:robolectric:3.0')
2. WRITE TEST APPLICATION CLASS
• all unit test code goes into test flavour
• Test{applicationName} • must extend your 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
3.5 TEST CONFIGURATION
• constants property of @Config annotation is mandatory
• options (except constants) can be specified in a file instead: /test/resources/robolectric.properties
• for 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); } }
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
(A)SYNCHRONOUS EXECUTORS
• unit tests run in a single thread
• waiting for background threads complicates tests
• solution: Synchronous executors (for Retrofit)
SETUP
• setting executor on Retrofit:
• specify executor for networking and callbacks
• 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(); } }
MOCK WEB SEVER
• part of OkHttp library by Square
• network responses must be mocked in unit tests • speed • consistency
MOCK WEB SERVER DEPENDENCIES
• add the MockWebServer dependency in your app
build.gradle file:
• MockWebServer depends on OkHttp library • make sure that versions match
testCompile ‘com.squareup.okhttp:mockwebserver:2.4.0'
USING MOCKWEBSERVER
1. Instantiate MockWebServer
2. call start() method
3. get local server url by calling url(“/“)
4. inject local server url into networking module
5. enqueue responses using enqueue() method
INSTANTIATING MOCK WEB SERVER
• prepare and inject MockWebServer in @Before method
(called before every @Test method)
• stop MockWebServer in @After method (called after every
@Test method)
INSTANTIATING MOCK WEB SERVER
• another option: • implement TestLifecycleApplication in Test application
class
• prepare and inject MockWebServer inbeforeTest(Method) method in Test application class
• stop MockWebServer in:afterTest(Method) in Test application class
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”) );
WHAT ABOUT 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)
EXAMPLE
@Testpublic 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}
SHADOW CLASSES
• if your project structure prevents you from injecting mock
classes in unit tests
• any class can be swapped with a “shadow” class
SHADOWING A CLASS
1. Create a custom Robolectric runner
2. Declare shadowed classes in createClassLoaderConfig()
method
3. Implement shadow class
4. Add shadow classes to tests in @Config
5. Run tests with custom runner
CREATE A CUSTOM RUNNER
public class CustomRobolectricTestRunner extends RobolectricTestRunner { /** * Creates a runner to run {@code testClass}. Looks in your working directory for your * AndroidManifest.xml file * and res directory by default. Use the {@link Config} annotation to configure. * * @param testClass the test class to be run * @throws InitializationError if junit says so */ public CustomRobolectricTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } /** * Declare custom classes to be shadowed when shadow exist. */ public InstrumentationConfiguration createClassLoaderConfig() { InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder(); builder.addInstrumentedClass(GoogleCloudMessaging.class.getName()); return builder.build(); } }
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 and
accessed through reflection
ADD SHADOWS TO TEST
• in order to make your shadows available in test add them to
@Config in your test: @Config(shadows = {ShadowClass.class})
EXAMPLE SHADOW CLASS
@Implements(GoogleCloudMessaging.class) public class ShadowGoogleCloudMessageing { @Implementation public void close() { Log.d("GoogleCloudMessageing", "close()"); } @Implementation public void send(String to, String msgId, Bundle data) throws IOException { Log.d("GoogleCloudMessageing", "send()"); } @Deprecated @Implementation public synchronized String register(String... senderIds) throws IOException { Log.d("GoogleCloudMessageing", "register()"); return "1234567890"; } @Implementation public String getMessageType(Intent intent) { Log.d("GoogleCloudMessageing", "getMessageType()"); return "gcm"; } }
ASSERT J ANDROID
• https://github.com/square/assertj-android
• extension of AssertJ
• you can extend and set up your own assertions
EXAMPLE
//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");
SUMMARY
• use Robolectric for tests of a single Activity or Fragment
• use synchronous executors with Retrofit in tests
• use MockWebServer to easily mock out network responses
• use Shadows for classes that you can’t replace via
dependency injection
• AssertJ Android simplifies your assertions on Android stuff
CODE EXAMPLE
• https://github.com/infinum/Dagger-2-Example
Thank you!
Visit www.infinum.co or find us on social networks:
infinum.co infinumco infinumco infinum