Robolectric: Unit Testing Android Apps Without an Emulator

Robolectric: Unit Testing Android Apps Without an Emulator

Robolectric runs Android tests on the JVM — no emulator, no device, no waiting for adb to connect. It implements Android's framework classes as JVM-compatible stubs, so your Activities, Fragments, ViewModels, and Repositories run in a regular JUnit test process.

A Robolectric test suite that takes 30 seconds to run would take 10 minutes on an emulator.

When to Use Robolectric

Robolectric fits between pure unit tests (no Android dependencies) and instrumented tests (full emulator or device).

Use Robolectric when:

  • Your code under test references Context, Resources, SharedPreferences, or other Android framework classes
  • You're testing an Activity or Fragment lifecycle
  • You need to verify UI state without the overhead of a full instrumented test
  • You want fast feedback in CI without provisioning an emulator

Don't use Robolectric for tests that depend on hardware (camera, Bluetooth, sensors), GPU rendering, or system dialogs.

Setup

In build.gradle.kts:

dependencies {
    testImplementation("org.robolectric:robolectric:4.12.1")
    testImplementation("androidx.test:core:1.5.0")
    testImplementation("androidx.test.ext:junit:1.1.5")
    testImplementation("androidx.fragment:fragment-testing:1.6.2")
}

android {
    testOptions {
        unitTests {
            isIncludeAndroidResources = true
        }
    }
}

isIncludeAndroidResources = true is required — without it, Robolectric can't resolve layouts, strings, or drawables.

Your First Robolectric Test

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class MainActivityTest {

    @Test
    fun clickingButtonUpdatesText() {
        val activity = Robolectric.buildActivity(MainActivity::class.java)
            .create()
            .resume()
            .get()

        activity.findViewById<Button>(R.id.submit_button).performClick()

        val result = activity.findViewById<TextView>(R.id.result_text)
        assertThat(result.text.toString()).isEqualTo("Submitted")
    }
}

Robolectric.buildActivity() creates the activity and drives its lifecycle. create().resume() is equivalent to the user seeing the screen.

Testing Fragments

@RunWith(RobolectricTestRunner::class)
class ProfileFragmentTest {

    @Test
    fun displaysUserName() {
        val fragmentScenario = launchFragmentInContainer<ProfileFragment>(
            fragmentArgs = bundleOf("userId" to "u123")
        )

        fragmentScenario.onFragment { fragment ->
            val nameView = fragment.requireView().findViewById<TextView>(R.id.user_name)
            assertThat(nameView.text.toString()).isEqualTo("Alice")
        }
    }
}

launchFragmentInContainer is from androidx.fragment:fragment-testing. It works with Robolectric out of the box.

Testing SharedPreferences

@RunWith(RobolectricTestRunner::class)
class SettingsRepositoryTest {

    private lateinit var repository: SettingsRepository
    private lateinit var prefs: SharedPreferences

    @Before
    fun setUp() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
        repository = SettingsRepository(prefs)
    }

    @Test
    fun savesThemePreference() {
        repository.setTheme(Theme.DARK)

        assertThat(prefs.getString("theme", null)).isEqualTo("dark")
    }

    @Test
    fun defaultThemeIsLight() {
        assertThat(repository.getTheme()).isEqualTo(Theme.LIGHT)
    }
}

Robolectric's SharedPreferences is backed by an in-memory store — no disk I/O, no state leaking between tests.

Testing ViewModels with LiveData

Robolectric makes it easy to test ViewModels that update LiveData in response to Android lifecycle events.

@RunWith(RobolectricTestRunner::class)
class UserViewModelTest {

    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    private lateinit var viewModel: UserViewModel
    private val repository = FakeUserRepository()

    @Before
    fun setUp() {
        viewModel = UserViewModel(repository)
    }

    @Test
    fun loadUserUpdatesUiState() {
        viewModel.loadUser("u123")

        val state = viewModel.uiState.value
        assertThat(state).isInstanceOf(UiState.Success::class.java)
        assertThat((state as UiState.Success).user.name).isEqualTo("Alice")
    }
}

InstantTaskExecutorRule makes LiveData synchronous — no observeForever or CountDownLatch needed.

Shadow Classes

Shadows are Robolectric's mechanism for replacing Android framework behavior with test-controlled stubs.

To check if an Intent was started:

@Test
fun navigatesToDetailActivity() {
    val activity = Robolectric.buildActivity(MainActivity::class.java)
        .create().resume().get()

    activity.openDetail(itemId = 42)

    val intent = Shadows.shadowOf(activity).nextStartedActivity
    assertThat(intent.component?.className).isEqualTo(DetailActivity::class.java.name)
    assertThat(intent.getIntExtra("item_id", -1)).isEqualTo(42)
}

Custom shadows let you replace system calls your code depends on:

@Implements(TelephonyManager::class)
class ShadowTelephonyManager {
    @Implementation
    fun getNetworkType(): Int = TelephonyManager.NETWORK_TYPE_LTE
}

@Config(shadows = [ShadowTelephonyManager::class])
class NetworkStatusTest { ... }

SDK Configuration

You can test against multiple Android SDK levels in one pass:

@Config(sdk = [28, 30, 33])
class MultiSdkTest { ... }

Or set a default in robolectric.properties in src/test/resources/:

sdk=33

Robolectric vs Instrumented Tests

Concern Robolectric Instrumented
Speed Fast (JVM, seconds) Slow (emulator, minutes)
Android fidelity Partial (stubs) Full (real framework)
Hardware access No Yes
CI setup Simple (no emulator) Complex
Rendering Simulated Real GPU

Run Robolectric tests for logic, layout binding, and lifecycle. Run instrumented tests for end-to-end UI flows, hardware features, and rendering-sensitive behavior.

Common Issues

java.lang.RuntimeException: Stub! — You're missing isIncludeAndroidResources = true in testOptions, or a dependency is not test-compatible.

LiveData never updates — Add InstantTaskExecutorRule or use getOrAwaitValue() extension.

Resources not found — Check that src/main/res is in the source set and isIncludeAndroidResources = true is set.

Slow test startup — Robolectric loads an SDK jar on first run; subsequent runs are faster. Use @Config(sdk = [33]) consistently to avoid reloading multiple SDK jars.

Summary

Robolectric belongs in every Android project. It covers the middle tier of the test pyramid — tests that need Android framework classes but don't need a real device. Your CI pipeline can run hundreds of Robolectric tests in the time it takes to boot one emulator. Combine it with pure unit tests at the bottom and Espresso or Kaspresso at the top for a complete, fast test suite.

Read more