📜 ⬆️ ⬇️

Immersion in Robolectric

In the Android world, development is increasingly using unit testing. Checking the correct operation of individual modules of the application helps to identify and eliminate errors in the code at an early stage. Coupled with assembly automation, component and integration tests, unit tests allow you to make a quality product, regardless of the size of your development team.


Under the cut, I’ll tell you about the internal device framework for unit-testing Android applications - Robolectric.




Why test Android-specific code?


First we will try to answer the question - why test the code in the field of integration with the Android framework?



This is only part of the scenarios in which code testing in the field of integration with Android becomes an urgent task.


Problems testing code using Android


When trying to solve this problem head on you may encounter the following problems:


RuntimeException with the cause - method not mocked when trying to run a test code of a framework calling some method. And if you use the following option in Gradle -


 testOptions { unitTests.returnDefaultValues = true } 

then, RuntimeException will not be thrown. This behavior can lead to heavily detectable errors in the tests.


Another problem of testing are final classes and a great many static methods of the framework, which further complicates testing the code that uses it.


Solutions


For all the above problems, there are certain solutions:



Robolectric


There is another solution to the problem of Unit-testing Android applications - Robolectrics. Robolectric is a framework developed by PivotalLabs in 2010. He is in an intermediate position between the “bare” JUnit tests and instrumented tests run on the device, simulating the real Android environment. The framework is a compiled android.jar with a bundle of utilities for running tests and simplifying testing. It supports resource loading, a primitive view blowing implementation, provides local SQLite ( sqlite4java ), easily customizable and extensible.


Use android.util.Log


Suppose we are developing a library for third-party developers and want to make sure that our library prints some important information in Logcat.


Implement the following interface - Logger , with one method for displaying messages of the “Info” level.


 interface Logger { fun info(tag: String, message: String, throwable: Throwable? = null) } 

Let's write the AndroidLogger implementation - which will use android.util.Log .


 class AndroidLogger: Logger { override fun info(tag: String, message: String, throwable: Throwable?) { Log.i(tag, message, throwable) } } 

We are testing android.util.Log


Let's write a Junit test using Robolectric and make sure that the info method of our AndroidLogger implementation actually prints the messages in Logcat with the info level.


 @RunWith(RobolectricTestRunner::class) @Config(constants = BuildConfig::class, sdk = intArrayOf(23)) class RobolectricAndroidLoggerTest { private val logger: Logger = AndroidLogger() @Test fun `info - should log to logcat with info level`() { val throwable = Throwable() logger.info("Tag", "Message", throwable) val logInfo: LogInfo = ShadowLog.getLogs().last() assertThat(logInfo.type, Is(Log.INFO)) assertThat(logInfo.tag, Is("Tag")) assertThat(logInfo.msg, Is("Message")) assertThat(logInfo.throwable, Is(throwable)) } } 

Annotation @RunWith we indicate that we will run the test using RobolectricTestRunner . In the parameters for the @Config annotation, we pass the BuildConfig class and specify the version of the Android SDK that Robolectric will simulate.


In the test, we call the info method on the AndroidLogger object. With the help of the ShadowLog class, ShadowLog retrieve the last message recorded in the log and make assert by its contents.


Internal organization


The internal structure of the Robolectric can be divided into 3 parts: Shadow classes, RobolectricTestRunner and InstrumentingClassLoader .


Shadow classes


The creators of Robolectric introduce a new type of “test twins” (test double) - Shadow. According to the official website, the Shadows are “... not quite Proxies, not quite Fakes, not quite Mocks or Stubs."


A shadow object exists parallel to a real object and can intercept calls to methods and constructors, thereby changing the behavior of the real object.


Shadow connection with Robolectric


The @Implements indicates the class for which a particular Shadow class is intended.


 @Implements(className = ContextImpl.class) public class ShadowContextImpl { ... } 

In the annotation of the @Config test, you can specify Shadow-classes that are not included in the standard Robolectric package.


 @Config(..., shadows = {CustomShadow.class}, ...) public class CustomTest { ... } 

Redefinition of methods


The overridden method in the Shadow class is annotated with @Implementation, it is important to preserve the signature of the original method.


 @Implementation public Object getSystemService(String name) { ... } 

When overriding the native method, the native code word is omitted.


 private static native long nativeReadLong(long nativePtr); 

 @Implementation public static long nativeReadLong(long nativePtr) { return ... } 

Constructor overrides


To override the constructor in the Shadow class, the __constructor__ constructor __constructor__ method is implemented with the same arguments.


 public Canvas(@NonNull Bitmap bitmap) { ... } 

 public void __constructor__(Bitmap bitmap) { this.targetBitmap = bitmap; } 

Call this object


To get a reference to a real object in the Shadow class, it is enough to declare a field with the type of “shaded” object marked with the @RealObject annotation:


 @RealObject private Context realObject; 

Robolectric provides the ability to call a real implementation of the method, bypassing the Shadow implementation, using Shadow.directlyOn .


 Shadow.directlyOn(realObject, "android.app.ContextImpl", "getDatabasesDir"); 

Own Shadow


Writing your own Shadow-class is not a big problem, even for a third-party library that is not included in the standard distribution with Android.


Let's write a class that gets a user's token using GoogleAuthUtil .


 class GoogleAuthInteractor { fun getToken(context: Context, account: Account): String { return GoogleAuthUtil.getToken(context, account, null) } } 

Implement the Shadow class for GoogleAuthUtil that allows you to override the token for a specific Account :


 @Implements(GoogleAuthUtil::class) object ShadowGoogleAuthUtil { private val tokens = ArrayMap<Account, String>() @Implementation @JvmStatic fun getToken(context: Context, account: Account, scope: String?): String { return tokens[account].orEmpty() } fun setToken(account: Account, token: String?) { tokens.put(account, token) } } 

Let's write a test for GoogleAuthInteractor using Robolectric. In the configuration for the test, we indicate that we want to use ShadowGoogleAuthUtil and ShadowGoogleAuthUtil classes from the com.google.android.gms.auth package.


 @RunWith(RobolectricTestRunner::class) @Config(shadows = arrayOf(ShadowGoogleAuthUtil::class), instrumentedPackages = arrayOf("com.google.android.gms.auth")) class GoogleAuthInteractorTest { private val context = RuntimeEnvironment.application private val interactor = GoogleAuthInteractor() @Test fun `provide token - provides token for correct account`() { val account = Account("name", "type") ShadowGoogleAuthUtil.setToken(account, "token") val token = interactor.getToken(context, account) assertThat(token, Is("token")) } } 

RobolectricTestRunner


From the Shadow classes, let's move on to RobolectricTestRunner - this is the first part of the Robolectric that your tests communicate with. Runner is responsible for dynamic dependency loading (Shadow-classes and android.jar for the specified SDK version) during the execution of tests.


Robolectric is configured by the @Config annotation, with which you can change the parameters of the simulated environment for the test class and for each test separately. The configuration for running tests will be collected sequentially across the entire hierarchy of the test class from parent to heir, and finally to the method being tested itself. Configuration allows you to configure:



InstrumentingClassLoader


Before running the tests, the RobolectricTestRunner replaces the system ClassLoader with the InstrumentingClassLoader .


InstrumentingClassLoader provides the connection of real objects with Shadow-classes, the substitution of certain classes for fake classes and the proxying of calls to certain methods to Shadow-classes directly.


Robolectric does not tool classes from the java.* Package, so method calls that are missing in an ordinary JVM, but added to the Android SDK, are proxied directly to Shadow at the call site.


In the framework, there are two options for instructing loadable classes. The original implementation generates a bytecode that uses the internal interface of the ClassHandler and implements its ShadowWrangler class, which essentially wraps each method call through a Shadow class into a separate Runnable similar object and calls it. In April 2015, a second version of the bytecode modification was added to the project, using the JVM instruction invokeDynamic .


During the instrumentation, Robolectric adds a ShadowedObject interface to each class loaded with one single method, $$robo$getData() , in which the real object returns its Shadow.


 public interface ShadowedObject { Object $$robo$getData(); } 

For each constructor, the InstrumentingClassLoader creates a private $$robo$$__constructor__ method with preserving its signature and instructions (except for calling super ).


 public Size(int width, int height) { super(width, height); ... } 

 private void $$robo$$__constructor__(int width, int height) { mWidth = width; mHeight = height; } 

In turn, the body of the original constructor will consist of:



Constructor modified using invokeDynamic instructions:


 public Size(int width, int height) { this.$$robo$init(); InvokeDynamicSupport.bootstrap($$robo$$__constructor__(int int), this, width, height); } 

Constructor modified using ClassHandler:


 public Size(int width, int height) { this.$$robo$init(); ClassHandler.Plan plan = RobolectricInternals.methodInvoked("android/util/Size/__constructor__(II)V", false, Size.class); if (plan != null) { try { plan.run(this, $$robo$getData(), new Object[]{new Integer(width), new Integer(height)}); return; } catch (Throwable throwable) { throw RobolectricInternals.cleanStackTrace(throwable); } } try { this.$$robo$$__constructor__(width, height); } catch (Throwable throwable) { throw RobolectricInternals.cleanStackTrace(throwable); } } 

To instrument the methods, Robolectric uses a similar mechanism, the present method code is allocated to a private method with the $$robo$$ prefix and the method call is delegated to the Shadow object.


Method modified using invokeDynamic :


 public int getWidth() { return (int)InvokeDynamicSupport.bootstrap($$robo$$getWidth(),this); } 

For native methods, Robolectric omits the corresponding modifier and returns the default value if this method is not overridden in the Shadow class.


Performance


Robolectric is far from the most productive framework. Running an empty test on the RobolectricTestRunner takes about 2 seconds. Compared to “clean” JUnit tests, 2 seconds is a significant delay.


Profiling tests on Robolectric shows that the framework spends most of the time instructing loadable classes.
Below are the results of Robolectric profiling and the PowerMock + Mockito bundles for the android.util.Log test described above.


Robolectric ~ 2400 ms.:


Methodms
java.lang.ClassLoader.loadClass(String)913
org.robolectric.internal.bytecode.InstrumentingClassLoader.
getInstrumentedBytes(ClassNode, boolean)
767
org.objectweb.asm.tree.ClassNode.accept(ClassVisitor)407
org.objectweb.asm.tree.MethodNode.accept(ClassVisitor)367
org.robolectric.internal.bytecode.InstrumentingClassLoader
$ClassInstrumentor.instrument()
298
org.objectweb.asm.ClassReader.accept(ClassVisitor, Attribute[], int)277
org.robolectric.shadows.ShadowResources.getSystem()268

PowerMock + Mockito ~ 200 ms:


Methodms
org.powermock.api.extension.proxyframework.ProxyFrameworkImpl.isProxy(Class)304
org.powermock.api.mockito.repackaged.cglib.core.KeyFactory$Generator
.generateClass(ClassVisitor)
131
sun.launcher.LauncherHelper.checkAndLoadMain(boolean, int, String)103
javassist.bytecode.MethodInfo.rebuildStackMap(ClassPool)85
java.lang.Class.getResource(String)84
org.mockito.internal.MockitoCore.<init>()67

Experience of use


Currently, there are more than 3000 Unit tests in our project, about half of which use Robolectric.


Faced with the performance issues of the framework, it was decided to use the Robolectric only to test a limited set of cases:



For all other cases, we wrap Android dependencies in easily tested wrappers, or use unmock-plugin for Gradle.


Video with my report on the same topic at the MBLTdev 16 conference



')

Source: https://habr.com/ru/post/320898/


All Articles