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
ActivityorFragmentlifecycle - 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=33Robolectric 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.