How To Improve Test Coverage For Your Android App Using Mockito And Espresso
How To Improve Test Coverage For Your Android App Using Mockito And Espresso
Vivek Maskara2018-07-25T14:00:04+02:002018-07-25T17:27:43+00:00
In app development, a variety of use cases and interactions come up as one iterates the code. The app might need to fetch data from a server, interact with the device’s sensors, access local storage or render complex user interfaces.
The important thing to consider while writing tests is the units of responsibility that emerge as you design the new feature. The unit test should cover all possible interactions with the unit, including standard interactions and exceptional scenarios.
In this article, we will cover the fundamentals of testing and frameworks such as Mockito and Espresso, which developers can use to write unit tests. I will also briefly discuss how to write testable code. I’ll also explain how to get started with local and instrumented tests in Android.
Recommended reading: How To Set Up An Automated Testing System Using Android Phones (A Case Study)
Fundamentals Of Testing
A typical unit test contains three phases.
- First, the unit test initializes a small piece of an application it wants to test.
- Then, it applies some stimulus to the system under test, usually by calling a method on it.
- Finally, it observes the resulting behavior.
If the observed behavior is consistent with the expectations, the unit test passes; otherwise, it fails, indicating that there is a problem somewhere in the system under test. These three unit test phases are also known as arrange, act and assert, or simply AAA. The app should ideally include three categories of tests: small, medium and large.
- Small tests comprise unit tests that mock every major component and run quickly in isolation.
- Medium tests are integration tests that integrate several components and run on emulators or real devices.
- Large tests are integration and UI tests that run by completing a UI workflow and ensure that the key end-user tasks work as expected.
Note: An instrumentation test is a type of integration test. These are tests that run on an Android device or emulator. These tests have access to instrumentation information, such as the context of the app under test. Use this approach to run unit tests that have Android dependencies that mock objects cannot easily satisfy.
Nope, we can’t do any magic tricks, but we have articles, books and webinars featuring techniques we all can use to improve our work. Smashing Members get a seasoned selection of magic front-end tricks — e.g. live designing sessions and perf audits, too. Just sayin’!
Writing small tests allows you to address failures quickly, but it’s difficult to gain confidence that a passing test will allow your app to work. It’s important to have tests from all categories in the app, although the proportion of each category can vary from app to app. A good unit test should be easy to write, readable, reliable and fast.
Here’s a brief introduction to Mockito and Espresso, which make testing Android apps easier.
Mockito
There are various mocking frameworks, but the most popular of them all is Mockito:
Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.
Its fluent API separates pre-test preparation from post-test validation. Should the test fail, Mockito makes it clear to see where our expectations differ from reality! The library has everything you need to write complete tests.
Espresso
Espresso helps you write concise, beautiful and reliable Android UI tests.
The code snippet below shows an example of an Espresso test. We will take up the same example later in this tutorial when we talk in detail about instrumentation tests.
@Test
public void setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Vivek Maskara"));
onView(withId(R.id.set_user_name)).perform(click());
onView(withText("Hello Vivek Maskara!")).check(matches(isDisplayed()));
}
Espresso tests state expectations, interactions and assertions clearly, without the distraction of boilerplate content, custom infrastructure or messy implementation details getting in the way. Whenever your test invokes onView()
, Espresso waits to perform the corresponding UI action or assertion until the synchronization conditions are met, meaning:
- the message queue is empty,
- no instances of
AsyncTask
are currently executing a task, - the idling resources are idle.
These checks ensure that the test results are reliable.
Writing Testable Code
Unit testing Android apps is difficult and sometimes impossible. A good design, and only a good design, can make unit testing easier. Here are some of the concepts that are important for writing testable code.
Avoid Mixing Object Graph Construction With Application Logic
In a test, you want to instantiate the class under test and apply some stimulus to the class and assert that the expected behavior was observed. Make sure that the class under test doesn’t instantiate other objects and that those objects do not instantiate more objects and so on. In order to have a testable code base, your application should have two kinds of classes:
- The factories, which are full of the “new” operators and which are responsible for building the object graph of your application;
- The application logic classes, which are devoid of the “new” operator and which are responsible for doing the work.
Constructors Should Not Do Any Work
The most common operation you will do in tests is the instantiation of object graphs. So, make it easy on yourself, and make the constructors do no work other than assigning all of the dependencies into the fields. Doing work in the constructor not only will affect the direct tests of the class, but will also affect related tests that try to instantiate your class indirectly.
Avoid Static Methods Wherever Possible
The key to testing is the presence of places where you can divert the normal execution flow. Seams are needed so that you can isolate the unit of test. If you build an application with nothing but static methods, you will have a procedural application. How much a static method will hurt from a testing point of view depends on where it is in your application call graph. A leaf method such as Math.abs()
is not a problem because the execution call graph ends there. But if you pick a method in a core of your application logic, then everything behind the method will become hard to test, because there is no way to insert test doubles
Avoid Mixing Of Concerns
A class should be responsible for dealing with just one entity. Inside a class, a method should be responsible for doing just one thing. For example, BusinessService
should be responsible just for talking to a Business
and not BusinessReceipts
. Moreover, a method in BusinessService
could be getBusinessProfile
, but a method such as createAndGetBusinessProfile
would not be ideal for testing. SOLID design principles must be followed for good design:
- S: single-responsibility principle;
- O: open-closed principle;
- L: Liskov substitution principle;
- I: interface segregation principle;
- D: dependency inversion principle.
In the next few sections, we will be using examples from a really simple application that I built for this tutorial. The app has an EditText
that takes a user name as input and displays the name in a TextView
upon the click of a button. Feel free to take the complete source code for the project from GitHub. Here’s a screenshot of the app:
Writing Local Unit Tests
Unit tests can be run locally on your development machine without a device or an emulator. This testing approach is efficient because it avoids the overhead of having to load the target app and unit test code onto a physical device or emulator every time your test is run. In addition to Mockito, you will also need to configure the testing dependencies for your project to use the standard APIs provided by the JUnit 4 framework.
Setting Up The Development Environment
Start by adding a dependency on JUnit4 in your project. The dependency is of the type testImplementation
, which means that the dependencies are only required to compile the test source of the project.
testImplementation 'junit:junit:4.12'
We will also need the Mockito library to make interaction with Android dependencies easier.
testImplementation "org.mockito:mockito-core:$MOCKITO_VERSION"
Make sure to sync the project after adding the dependency. Android Studio should have created the folder structure for unit tests by default. If not, make sure the following directory structure exists:
<Project Dir>/app/src/test/java/com/maskaravivek/testingExamples
Creating Your First Unit Test
Suppose you want to test the displayUserName
function in the UserService
. For the sake of simplicity, the function simply formats the input and returns it back. In a real-world application, it could make a network call to fetch the user profile and return the user’s name.
@Singleton
class UserService @Inject
constructor(private var context: Context) {
fun displayUserName(name: String): String {
val userNameFormat = context.getString(R.string.display_user_name)
return String.format(Locale.ENGLISH, userNameFormat, name)
}
}
We will start by creating a UserServiceTest
class in our test directory. The UserService
class uses Context
, which needs to be mocked for the purpose of testing. Mockito provides a @Mock
notation for mocking objects, which can be used as follows:
@Mock internal var context: Context? = null
Similarly, you’ll need to mock all dependencies required to construct the instance of the UserService
class. Before your test, you’ll need to initialize these mocks and inject them into the UserService
class.
@InjectMock
creates an instance of the class and injects the mocks that are marked with the annotations@Mock
into it.MockitoAnnotations.initMocks(this);
initializes those fields annotated with Mockito annotations.
Here’s how it can be done:
class UserServiceTest {
@Mock internal var context: Context? = null
@InjectMocks internal var userService: UserService? = null
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
}
Now you are done setting up your test class. Let’s add a test to this class that verifies the functionality of the displayUserName
function. Here’s what the test looks like:
@Test
fun displayUserName() {
doReturn("Hello %s!").`when`(context)!!.getString(any(Int::class.java))
val displayUserName = userService!!.displayUserName("Test")
assertEquals(displayUserName, "Hello Test!")
}
The test uses a doReturn().when()
statement to provide a response when a context.getString()
is invoked. For any input integer, it will return the same result, "Hello %s!"
. We could have been more specific by making it return this response only for a particular string resource ID, but for the sake of simplicity, we are returning the same response to any input.
Finally, here’s what the test class looks like:
class UserServiceTest {
@Mock internal var context: Context? = null
@InjectMocks internal var userService: UserService? = null
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
@Test
fun displayUserName() {
doReturn("Hello %s!").`when`(context)!!.getString(any(Int::class.java))
val displayUserName = userService!!.displayUserName("Test")
assertEquals(displayUserName, "Hello Test!")
}
}
Running Your Unit Tests
In order to run the unit tests, you need to make sure that Gradle is synchronized. In order to run a test, click on the green play icon in the IDE.
making sure that Gradle is synchronized
When the unit tests are run, successfully or otherwise, you should see this in the “Run” menu at the bottom of the screen:
You are done with your first unit test!
Writing Instrumentation Tests
Instrumentation tests are most suited for checking values of UI components when an activity is run. For instance, in the example above, we want to make sure that the TextView
shows the correct user name after the Button
is clicked. They run on physical devices and emulators and can take advantage of the Android framework APIs and supporting APIs, such as the Android Testing Support Library.
We’ll use Espresso to take actions on the main thread, such as button clicks and text changes.
Setting Up The Development Environment
Add a dependency on Espresso:
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
Instrumentation tests are created in an androidTest
folder.
<Project Dir>/app/src/androidTest/java/com/maskaravivek/testingExamples
If you want to test a simple activity, create your test class in the same package as your activity.
Creating Your First Instrumentation Test
Let’s start by creating a simple activity that takes a name as input and, on the click of a button, displays the user name. The code for the activity above is quite simple:
class MainActivity : AppCompatActivity() {
var button: Button? = null
var userNameField: EditText? = null
var displayUserName: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_main)
initViews()
}
private fun initViews() {
button = this.findViewById(R.id.set_user_name)
userNameField = this.findViewById(R.id.name_field)
displayUserName = this.findViewById(R.id.display_user_name)
this.button!!.setOnClickListener({
displayUserName!!.text = "Hello ${userNameField!!.text}!"
})
}
}
To create a test for the MainActivity
, we will start by creating a MainActivityTest
class under the androidTest
directory. Add the AndroidJUnit4
annotation to the class to indicate that the tests in this class will use the default Android test runner class.
@RunWith(AndroidJUnit4::class)
class MainActivityTest {}
Next, add an ActivityTestRule
to the class. This rule provides functional testing of a single activity. For the duration of the test, you will be able to manipulate your activity directly using the reference obtained from getActivity()
.
@Rule @JvmField var activityActivityTestRule = ActivityTestRule(MainActivity::class.java)
Now that you are done setting up the test class, let’s add a test that verifies that the user name is displayed by clicking the “Set User Name” button.
@Test
fun setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Vivek Maskara"))
onView(withId(R.id.set_user_name)).perform(click())
onView(withText("Hello Vivek Maskara!")).check(matches(isDisplayed()))
}
The test above is quite simple to follow. It first simulates some text being typed in the EditText
, performs the click action on the button, and then checks whether the correct text is displayed in the TextView
.
The final test class looks like this:
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule @JvmField var activityActivityTestRule = ActivityTestRule(MainActivity::class.java)
@Test
fun setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Vivek Maskara"))
onView(withId(R.id.set_user_name)).perform(click())
onView(withText("Hello Vivek Maskara!")).check(matches(isDisplayed()))
}
}
Running Your Instrumentation Tests
Just like for unit tests, click on the green play button in the IDE to run the test.
Upon a click of the play button, the test version of the app will be installed on the emulator or device, and the test will run automatically on it.
Intrumentation Testing Using Dagger, Mockito, And Espresso
Espresso is one of the most popular UI testing frameworks, with good documentation and community support. Mockito ensures that objects perform the actions that are expected of them. Mockito also works well with dependency-injection libraries such as Dagger. Mocking the dependencies allows us to test a scenario in isolation.
Until now, our MainActivity
hasn’t used any dependency injection, and, as a result, we were able to write our UI test very easily. To make things a bit more interesting, let’s inject UserService
in the MainActivity
and use it to get the text to be displayed.
class MainActivity : AppCompatActivity() {
var button: Button? = null
var userNameField: EditText? = null
var displayUserName: TextView? = null
@Inject lateinit var userService: UserService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_main)
initViews()
}
private fun initViews() {
button = this.findViewById(R.id.set_user_name)
userNameField = this.findViewById(R.id.name_field)
displayUserName = this.findViewById(R.id.display_user_name)
this.button!!.setOnClickListener({
displayUserName!!.text = userService.displayUserName(userNameField!!.text.toString())
})
}
}
With Dagger in the picture, we will have to set up a few things before we write instrumentation tests.
Imagine that the displayUserName
function internally uses some API to fetch the details of the user. There should not be a situation in which a test does not pass due to a server fault. To avoid such a situation, we can use the dependency-injection framework Dagger and, for networking, Retrofit.
Setting Up Dagger In The Application
We will quickly set up the basic modules and components required for Dagger. If you are not
familiar with Dagger, check out Google’s documentation on it. We will start adding dependencies for using Dagger in the build.gradle
file.
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
Create a component in the Application
class, and add the necessary modules that will be used in our project. We need to inject dependencies in the MainActivity
of our app. We will add a @Module
for injecting in the activity.
@Module
abstract class ActivityBuilder {
@ContributesAndroidInjector
internal abstract fun bindMainActivity(): MainActivity
}
The AppModule
class will provide the various dependencies required by the application. For our example, it will just provide an instance of Context
and UserService
.
@Module
open class AppModule(val application: Application) {
@Provides
@Singleton
internal open fun provideContext(): Context {
return application
}
@Provides
@Singleton
internal open fun provideUserService(context: Context): UserService {
return UserService(context)
}
}
The AppComponent
class lets you build the object graph for the application.
@Singleton
@Component(modules = [(AndroidSupportInjectionModule::class), (AppModule::class), (ActivityBuilder::class)])
interface AppComponent {
@Component.Builder
interface Builder {
fun appModule(appModule: AppModule): Builder
fun build(): AppComponent
}
fun inject(application: ExamplesApplication)
}
Create a method that returns the already built component, and then inject this component into onCreate()
.
open class ExamplesApplication : Application(), HasActivityInjector {
@Inject lateinit var dispatchingActivityInjector: DispatchingAndroidInjector<Activity>
override fun onCreate() {
super.onCreate()
initAppComponent().inject(this)
}
open fun initAppComponent(): AppComponent {
return DaggerAppComponent
.builder()
.appModule(AppModule(this))
.build()
}
override fun activityInjector(): DispatchingAndroidInjector<Activity>? {
return dispatchingActivityInjector
}
}
Setting Up Dagger In The Test Application
In order to mock responses from the server, we need to create a new Application
class that extends the class above.
class TestExamplesApplication : ExamplesApplication() {
override fun initAppComponent(): AppComponent {
return DaggerAppComponent.builder()
.appModule(MockApplicationModule(this))
.build()
}
@Module
private inner class MockApplicationModule internal constructor(application: Application) : AppModule(application) {
override fun provideUserService(context: Context): UserService {
val mock = Mockito.mock(UserService::class.java)
`when`(mock!!.displayUserName("Test")).thenReturn("Hello Test!")
return mock
}
}
}
As you can see in the example above, we’ve used Mockito to mock UserService
and assume the results. We still need a new runner that will point to the new application class with the overwritten data.
class MockTestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
super.onCreate(arguments)
}
@Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class)
override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
return super.newApplication(cl, TestExamplesApplication::class.java.name, context)
}
}
Next, you need to update the build.gradle
file to use the MockTestRunner
.
android {
...
defaultConfig {
...
testInstrumentationRunner ".MockTestRunner"
}
}
Running The Test
All tests with the new TestExamplesApplication
and MockTestRunner
should be added at androidTest
package. This implementation makes the tests fully independent from the server and gives us the ability to manipulate responses.
With the setup above in place, our test class won’t change at all. When the test is run, the app will use TestExamplesApplication
instead of ExamplesApplication
, and, thus, a mocked instance of UserService
will be used.
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule @JvmField var activityActivityTestRule = ActivityTestRule(MainActivity::class.java)
@Test
fun setUserName() {
onView(withId(R.id.name_field)).perform(typeText("Test"))
onView(withId(R.id.set_user_name)).perform(click())
onView(withText("Hello Test!")).check(matches(isDisplayed()))
}
}
The test will run successfully when you click on the green play button in the IDE.
That’s it! You have successfully set up Dagger and run tests using Espresso and Mockito.
Conclusion
We’ve highlighted that the most important aspect of improving code coverage is to write testable code. Frameworks such as Espresso and Mockito provide easy-to-use APIs that make writing tests for various scenarios easier. Tests should be run in isolation, and mocking the dependencies gives us an opportunity to ensure that objects perform the actions that are expected of them.
A variety of Android testing tools are available, and, as the ecosystem matures, the process of setting up a testable environment and writing tests will become easier.
Writing unit tests requires some discipline, concentration and extra effort. By creating and running unit tests against your code, you can easily verify that the logic of individual units is correct. Running unit tests after every build helps you to quickly catch and fix software regressions introduced by code changes to your app. Google’s testing blog discusses the advantages of unit testing.
The complete source code for the examples used in this article is available on GitHub. Feel free to take a look at it.
source https://www.smashingmagazine.com/2018/07/improving-test-coverage-android-app-mockito-espresso/
source https://derekpackard.com/how-to-improve-test-coverage-for-your-android-app-using-mockito-and-espresso-4/
No comments:
Post a Comment