Face it. No matter how good of an Android Developer you are, if you don’t know at least a bit on how to write tests, your value drops far down below. When it comes to Test-Driven-Development in Android, there are 4 tools known to be pretty much crucial to testing: JUnit, Espresso, Mockito, and Robolectric.
The majority of your tests are going to be Unit Tests where you test your code at the lowest level. These are meant to be small tests that test separate “units” of your code, which is why it’s great to write plenty of them.
Unit Tests in Android have one underlying problem: they can’t access Android resources. Sure we can write Instrumented Tests, but these require a device or emulator and thus, they take longer to run. Ain’t nobody got time for that!
Robolectric solves this problem by letting you access Android resources within your unit tests and still have them run in the JVM. Robolectric lets you write tests such as this:
@RunWith(RobolectricTestRunner.class) public class MyActivityTest { @Test public void clickingButton_shouldChangeMessage() { MyActivity activity = Robolectric.setupActivity(MyActivity.class); activity.button.performClick(); assertThat(activity.message.getText()).isEqualTo("Robolectric Rocks!"); } }
Taken directly from the Robolectric site
This simple ability is what makes Robolectric so good, because it saves so much time, and saving time means saving money. While speed is definitely one advantage to Robolectric, it also lets you write more isolated tests from its neighbors so you have more control over tests, and can also rid the need for mocks which lets you write tests that closer resemble black box testing (though it can still be used in conjunction with mocking frameworks like Mockito).
Setting Up Robolectric
Add this to your app/build.gradle file.
android { testOptions { unitTests { includeAndroidResources = true } } } dependencies { testImplementation 'org.robolectric:robolectric:4.3' }
Next, annotate your test class with the Robolectric Test Runner
@RunWith(RobolectricTestRunner::class) class RobolectricTest
Writing the Test
I’ll write a simple activity that has a single button. When clicked, the button should launch an intent to another activity. Here’s what I have for my layout.
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/login_button" android:text="Login" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </FrameLayout>
And my activity
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) login_button.setOnClickListener { startActivity(Intent(this, LoginActivity::class.java)) } } }
(For this tutorial sake, the launched class can be literally anything)
All I’m going to write is a test that checks if the button, when clicked, launches the intent I’m expecting it to launch. Easy stuff with Robolectric.
@Test fun whenLoginClicked_thenStartLoginActivity() { val activity = Robolectric.setupActivity(MainActivity::class.java) activity.login_button.performClick() val expectedIntent = Intent(activity, LoginActivity::class.java) val actualIntent = shadowOf(RuntimeEnvironment.application).nextStartedActivity assertEquals(expectedIntent.component, actualIntent.component) }
Notice we start by calling Robolectric.setupActivity, which you might guess, gives us access to our activity. Robolectric also has the buildActivity method which gives us a bit more control of the activity’s lifecycle before we choose to get it.
val activity = Robolectric.buildActivity(MainActivity::class.java).create().start().resume().get()
And just like that, you can run Android components in the JVM. Robolectric is acquiring the Intent from within the activity and comparing to our expected Intent. So how does Robolectric do this exactly? Well the answer is…
Shadows
As defined in the Robolectric site, “Each shadow can modify or extend the behavior of a corresponding class in the Android OS”. We saw this when we see defined the shadow of the intent above. You can have shadows of pretty much most things within the Android framework
val shadowMainActivity = shadowOf(mainActivity) val shadowService = shadowOf(service) val shadowApplication = shadowOf(application)
More on shadows another day though…