Inside Robolectric: How Android UI Tests Work Without an Emulator

Written by nestsiarenka | Published 2026/04/10
Tech Story Tags: android | robolectric | kotlin | robolectric-testing | android-ui-testing | robolectric-android | jvm-android-tests | hackernoon-top-story

TLDRRobolectric has an ambiguous reputation in the Android community due to difficulties of compatibility with other libraries. Robolectric is a powerful tool for UI testing of Android code without an emulator. In this article I'll figure out what technology consists of. via the TL;DR App

Sometimes there come a time in your career when you want to take the next step in your development, but you may not be sure what that next step is or what opportunities it will open up for you. I like the quote:

Life can only be understood backwards, but it must be lived forwards.

Kierkegaard

On my way I decided to choose Robolectric and answer my questions about the structure of a library with an ambiguous reputation, but mysterious capabilities.

Robolectric is a powerful tool for UI testing of Android code without an emulator. When I used Robolectric in my work, I was surprised by how the library works. In addition to the ability to work with Android components in unit tests, Robolectric allows you to work with View and even get a Bitmap from it, which solves not only the problem of checking interaction with View, but also allows you to check the display of the screen. And although Robolectric has an ambiguous reputation in the Android community due to the difficulties of compatibility with other libraries, its remarkable capabilities sparked curiosity, prompting a deeper exploration of this tool.

Goal: What do I want from Robolectric at the start?

I want to understand how Robolectric works with Android components without an emulator.

Press enter or click to view image in full size

What’s wrong with Java

First, you need to understand whether the regular version of Java is enough.

Due to legal disputes with Oracle, Google had to abandon Java SE in favor of OpenJDK (a free implementation of the Java SE specification). Along with this, Android also uses a set of implementations of Java libraries designed to work in a mobile application. They are compiled on Libcore. But, we do not need additional capabilities for writing unit tests, otherwise we are better off using instrumented tests.

Press enter or click to view image in full size

Android Platform Architecture

Android architecture has a number of layers, but we need to determine whether they are needed for writing unit tests. I won’t go into too much detail here, as my goal is to simply look at the topic through the lens of unit tests that require minimal dependencies but allow you to get rid of the mobile device.

Press enter or click to view image in full size

The platform is based on Linux. Testing low-level elements would be difficult, and the system’s functionality would not be guaranteed, because even with an emulator there are sometimes discrepancies in behavior on real devices from different manufacturers.

Press enter or click to view image in full size

In tests, we won’t be able to easily run an entire platform to work with hardware functions, which means we won’t be able to access OpenGL (Native Library), Camera (Kernel) and other specific elements, and therefore, the interface for working with hardware (HAL) won’t be useful to us either.

ART is a runtime environment used on real Android devices to execute application bytecode. Its features include garbage collection (GC), Just-In-Time (JIT), and Ahead-Of-Time (AOT) compilation. Our code should run within the JVM, so we won’t need ART. If your tests are based on the specifics of working with Native Libraries or ART, you will have to use instrumented tests.

Press enter or click to view image in full size

But we will need the Android Java Framework to work with Android components in tests. In particular, Handler and Looper are located here.

Since the very beginning of Android, there has been Binder IPC for communication between Android components, and starting with Android 8, it has also been used for communication between the Android Framework and the HAL layer.

Binder IPC is an important component, but we can no longer copy this part of the system, so all interaction with it is closed with mocks.

The situation with View

There is no direct connection to the GPU within unit tests, and therefore no rendering. Robolectric treats UI components as regular Java classes, meaning complex rendering operations cannot be supported.

However, by enabling the NativeMode parameter in Robolectric, you can get a more realistic Bitmap from a View. NativeMode allows you to switch legacy Shadows to NativeShadows, which contain more real class code, such as Bitmap or BitmapFactory.

Press enter or click to view image in full size

android-all

The AOSP repository contains a set of jars for each version of Android, which is our Android Framework used for Robolectric. The jars are built as each new version of Android is released, allowing us to write unit tests using different versions of the Android Framework in our tests.

But wait, how do we use the compiled jar files in tests?

ClassLoader

With ClassLoader, the JVM may not know in advance about the functionality it will use. It allows loading classes that have been requested into memory, and the code can be loaded dynamically during program execution, as well as loaded, e.g. from the network.

val androidAllJarPath = "android-all.jar"
val savePath = context.getDir("cache", Context.MODE_PRIVATE)

val classLoader = DexClassLoader(
  androidAllJarPath,
  savePath.absolutePath,
  null,
  commonClassLoader,
)

val loadedClass = classLoader.loadClass("android.widget.TextView")
val instance = loadedClass.newInstance()
val method = loadedClass.getMethod("requestLayout")

method.invoke(instance)

ClassLoader in Robolectric

Before starting, Robolectric initializes the Sandbox, which in addition to high-level settings contains SandboxClassLoader. It makes it possible to load Android classes not only from android.jar (here only Stub), but also from android-all.jar.

By the way, among other things, Sandbox also creates an ExecutorService to simulate the main thread work.

Press enter or click to view image in full size

Bytecode

The compiled android‑all.jar files in MavenCentral are not enough. You need to make a number of changes to the current code before work, which is where ObjectWeb ASM helps. It can be used to efficiently manipulate bytecode, which is what happens inside the Robolectric library within ClassInstrumentor.

override fun visitMethod(
    access: Int,
    name: String?,
    descriptor: String?,
    signature: String?,
    exceptions: Array<out String>?
): MethodVisitor {
    val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
    return if (name == "doSomething") {
        object : MethodVisitor(ASM9, methodVisitor) {
            override fun visitCode() {
                super.visitCode()
                methodVisitor.visitFieldInsn(
                    GETSTATIC,
                    "java/lang/System",
                    "out",
                    "Ljava/io/PrintStream;"
                )
                methodVisitor.visitLdcInsn("Hello world!")
                methodVisitor.visitMethodInsn(
                    INVOKEVIRTUAL,
                    "java/io/PrintStream",
                    "println",
                    "(Ljava/lang/String;)V",
                    false
                )
            }
        }
    } else {
        methodVisitor
    }
}

In the code above, I gave an example of a part of the functionality, in which I doSomethingsupplemented the method of my class with a line at the beginningprintln("Hello world!")

Robolectric Architecture

We came to the conclusion that Robolectric uses a set of Jar files built for different releases in Android platform sources. The set of Jar files (android-all) is built in MavenCentral and can be downloaded from there. This allows the library to use as many features of Android itself as possible.

What manipulations does ObjectWeb ASM perform:

  1. Modifying all methods, constructors and static blocks to intercept execution and delegate it to other entities for shadowing mechanism
  2. Constructors for creating shadow objects are changed separately
  3. Due to the difference between libcore (a modified Java implementation) and the JDK, some methods are rewritten
  4. The final keyword is removed from classes and methods
  5. Transformation of native methods and support for special tools

Press enter or click to view image in full size

Shadowing

A number of classes in the Android SDK work by default without code modifications, because they are based on pure Java. Such classes can be safely run on the JVM (for example, Intent).

However, in some cases it is necessary to intercept and replace method calls using the Shadowing mechanism (Shadow classes). With Robolectric you can also create custom Shadow classes.

Custom Shadow

@Implements(ImageView::class)
open class CustomShadowImageView : ShadowView() {
    @RealObject
    lateinit var realImageView: ImageView

    var longClickPerformed: Boolean = false
        private set

    @Implementation
    protected override fun performLongClick(): Boolean {
        longClickPerformed = true
        return super.performLongClick()
    }
}

Invokedynamic

Invokedynamic is a bytecode instruction to dynamically bind a method call at runtime, not at compile time, so that we know at runtime whether to use the real class or its Shadow.

Press enter or click to view image in full size

What do we get as a result?

We can write tests using Android classes, including interacting with interfaces and drawing our screens. I would like to emphasize that you can get Bitmap from View and build screenshot testing solutions based on Robolectric.

Robolectric in action

val controller = Robolectric.buildActivity(ConfiguredActivity::class.java)
controller.setup()
controller.applyState(state)
val activity = controller.get()
val view = activity.findViewById<View>(android.R.id.content)
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)

With the code above you can get your Activity displayed in Bitmap.

private fun ActivityController<out FragmentActivity>.applyState(
    state: ForComponentState
): ActivityController<out FragmentActivity> {
    val newConfig = Configuration(this@applyState.get().resources.configuration)
    return this@applyState.configurationChange(newConfig.applyState(state))
}

private fun Configuration.applyState(state: ForComponentState): Configuration {
    val newConfig = this@applyState
    state.uiMode?.let { newConfig.uiMode = it.configurationInt }
    state.displaySize?.let { newConfig.densityDpi = it.configurationInt }
    state.fontScale?.let { newConfig.fontScale = it }
    state.locale?.let { newConfig.setLocale(it) }
    return newConfig
}

Permissions

Robolectric does not emulate the display of the system dialog of requested permissions, and all permission requests will be considered DENIED by default. To work with permissions in the test, you need to explicitly specify the issuance or deletion of permissions:

Shadows.shadowOf(activity).grantPermissions(Manifest.permission.CAMERA)
Shadows.shadowOf(activity).denyPermissions(Manifest.permission.CAMERA)

Network requests

Real network requests are possible in tests, but there are problems:

  1. Tests may slow down due to main thread blocking.
  2. The stability of tests may begin to depend on the stability of the test environment, which increases the risk of flaking.

When I tested networking with a library for working with images, I found that with a caching strategy, the same images are not loaded over the network, and sometimes they manage to be taken from the cache, which is why they can be drawn. But it is still quite unstable.

If you intercept network requests, but do it not on the main thread asynchronously (downloading images for display elements is often done this way), then flaks still probably remain.

Dependencies

Screens within tests may require dependencies. If parameters need to be passed by fragment, the value of arguments can be explicitly set. Activity can be opened immediately with an intent enriched with data in the bundle:

buildActivity(Class<T> activityClass, Intent intent)

Compose

Compose can be rendered with almost no additional work, but there may be problems: for example, InfiniteTransition may overload the main Robolectric thread, and the test will hang.

My team also encountered the OutOfMemoryErrors problem in screenshot tests based on Robolectric. The problem was that Robolectric creates a new instance of the Application class for each test, which Compose was not prepared for. Details in the issue.

Animations

Robolectric doesn’t allow you to fully test animations, but you can still test things like animation end states or animation callbacks.

WebView

Since Robolectric doesn’t emulate WebView engines, its testing is limited. You’ll be able to test URL navigation or JavaScript calls, but you won’t be able to render a page.

Robolectric and JUnit 5

The downside of Robolectric, in my opinion, is its incompatibility with JUnit 5. You can read more about this topic in another article.

Interesting facts about Robolectric

  • Allows you to work with Room and SQLite;
  • Supports parameterized tests;
  • Works with MotionEvent, Telephony, Media;
  • Testing of memory leaks is possible GcFinalization.awaitClear(weekReference);
  • Compatible with Mockito and Mockk mocking libraries;
  • Security testing is possible (Java Security Providers);
  • Supports working with SparseArray.

Summary

I hope you, like me, have gotten a glimpse of the Robolectric device. It turns out that we are dealing with a familiar, albeit rare, set of tools, assembled into an interesting cocktail of possibilities.


Written by nestsiarenka | I'm a technical lead on the Android project, enthusiast in learning technologies, free spirit writer.
Published by HackerNoon on 2026/04/10