Table of Contents
- Write Unit Tests against Activities and other Android components
- Write Unit Tests without Espresso that touches Android code
- Inject your dependencies manually
Intro
Let’s be honest. Writing tests on Android is a horrible experience. Most of the time when I want to cover a single method under a test, it is more research and boilerplate than I would like to spend my precious time on. Reading Android documentation is pretty much no help either. It gives you a few bits of pointers which almost always leaves you with no clue on how to actually implement what they are documenting.
So how do most Android developers write tests? I think most avoid writing tests like the plague (or maybe Covid these days?). Luckily, my current company writes quite a lot of tests and has good code coverage. Every Pull Request requires that we add tests to whatever new features we are implementing. However, it took a significant amount of work to architect our code in such a way that we could actually write unit tests and Espresso tests thoroughly.
Here are three Android testing tricks that I have discovered over the years that will hopefully help you to add more tests to your code without all the headaches that come with Android. I have not seen these posted previously and thought they are all pretty interesting in working around how difficult it is to test on Android.
Write Unit Tests against Activities and other Android components
Are your activities a nightmare to create tests for? You have to potentially wire up all your dependencies and figure out the lifecycle of your activity with potentially lifecycles of your dependencies as well. What if you just want to write a test against a simple function in the activity? Most activities in code bases that I have worked on tend to increase in size and complexity on a regular basis. Since Google architects made the excellent decision of making Activities God classes that your entire code base needs to know or depend on, it tends to create nightmares when trying to test. I would rather spend my time actually implementing new features and writing small tests to verify those features are working as expected. Let’s see if we can simplify how we write a unit test against an activity.
// Needs to be 'open' to use Mockito with this class
// If you use mockk then 'open' is not required
open class Trick1Activity : AppCompatActivity() {
private val incompleteUser = User(null)
private val completedUser = User("Timothy Paetz")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_trick1)
if (hasCompletedProfile(incompleteUser)) {
toast("User has completed profile")
} else {
toast("User has not completed profile")
}
}
@VisibleForTesting
fun hasCompletedProfile(user: User): Boolean {
return user.name != null
}
private fun toast(message: String) {
Toast.makeText(
this,
message,
Toast.LENGTH_LONG
).show()
}
}
As you can see, this is a very simple Activity with hasCompletedProfile() method. We want to write a unit test against this method. You can actually write JUnit (not Espresso) tests against this Activity or any other Android Component without the need of Espresso.
import org.junit.Assert.*
import org.junit.Test
import org.mockito.Mockito.*
import org.mockito.kotlin.mock
class Trick1ActivityTest {
/**
* Mockito Example
*/
@Test
fun `verify no name user returns false for hasCompletedProfile`() {
// given
val trick1Activity: Trick1Activity = mock(
defaultAnswer = CALLS_REAL_METHODS
)
val noNameUser = User(name = null)
// when
val actual = trick1Activity.hasCompletedProfile(
noNameUser
)
// then
assertFalse(actual)
}
@Test
fun `verify named user returns true for hasCompletedProfile`() {
// given
val trick1Activity: Trick1Activity = mock(
defaultAnswer = CALLS_REAL_METHODS
)
val namedUser = User(name = "Timothy Paetz")
// when
val actual = trick1Activity.hasCompletedProfile(
namedUser
)
// then
assertTrue(actual)
}
/**
* Mockk example
*/
@Test
fun `verify no name user returns false for hasCompletedProfile using mockk`() {
// given
val trick1Activity = mockk<Trick1Activity>()
val noNameUser = User(name = null)
every { trick1Activity.hasCompletedProfile(noNameUser) } answers { callOriginal() }
// when
val actual = trick1Activity.hasCompletedProfile(
noNameUser
)
// then
assertFalse(actual)
}
@Test
fun `verify named user returns true for hasCompletedProfile using mockk`() {
// given
val trick1Activity = mockk<Trick1Activity>()
val namedUser = User(name = "Timothy Paetz")
every { trick1Activity.hasCompletedProfile(namedUser) } answers { callOriginal() }
// when
val actual = trick1Activity.hasCompletedProfile(
namedUser
)
// then
assertTrue(actual)
}
}
This code actually lives inside /test directory rather than /androidTest even though it is testing an Activity. As long as your real code that is being tested does not call any Android code, then you can put your tests inside /test directory and run junit tests against your classes rather than Espresso tests.
This code is creating a mock of your Activity under test and configures the mock to call the real methods.
val trick1Activity: Trick1Activity = mock(defaultAnswer = CALLS_REAL_METHODS)
This prevents you from having to wire up any dependencies or handling the activity lifecycle. Then you just write the test as you normally would a JUnit test. Once you have your mock, you can call any method on it that you want to test. In this example, we want to test if the user has completed their profile.
val noNameUser = User(name = null)
val actual = trick1Activity.hasCompletedProfile(noNameUser)
assertFalse(actual)
If you want to use mockk instead of Mockito, you have to tell mockk to call the original code for the method under test:
val trick1Activity = mockk<Trick1Activity>()
val namedUser = User(name = “Timothy Paetz”)// This makes the mockk call the original function
every { trick1Activity.hasCompletedProfile(namedUser) } answers { callOriginal() }
val actual = trick1Activity.hasCompletedProfile(namedUser)
assertTrue(actual)
Hopefully you understand how you can write a simple test against a much more complex activity using this technique. Rather than having to wire the Espresso tests all up, worrying about lifecycles and dependencies, you can just write a simple test like the above and test a method in the activity. This technique does require you to architect your code in such a way that does not call any Android code, though. What happens if your code does call Android code? Keep reading to find out!
Write Unit Tests without Espresso that touches Android code
Have you ever ran into Android’s lovely error message telling you an Android method is not mocked?
Method formatNumber in android.telephony.PhoneNumberUtils not mocked. See http://g.co/androidstudio/not-mocked for details.
java.lang.RuntimeException: Method formatNumber in android.telephony.PhoneNumberUtils not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.telephony.PhoneNumberUtils.formatNumber(PhoneNumberUtils.java)
Luckily, Google provides a broken link to tell you what to do if you run into this problem. The real link is supposed to point here (Google’s superb documentation strikes again!). If you are on the latest version of Android Studio, the link points to here and then you have to scroll down to the Error: “Method .. not mocked” section. The suggested solution is to update your build.gradle to return default values with this flag: unitTests.returnDefaultValues = true. If your project has this flag enabled, then more than likely your tests are not testing what you think they are and I recommend updating your code to use this trick instead. Your code is probably creating mocks of these methods and then you are testing against the mocked response instead of the actual response. The problem with that is Google can change the implementation details tomorrow and your real app will be broken but your tests will still be passing.
A much simpler solution is to do the inverse of what we did in Trick 1. We move the JUnit test into the androidTest directory. It will then run the tests against the actual, real implementation details. You can also provide different phone emulators to run these tests against multiple versions of Android to make sure it works across various versions.
// PhoneNumberUtils can't be tested from /test directory and throws an exception
import android.telephony.PhoneNumberUtils
class Trick2(
private val userEnteredPhoneNumber: String
) {
fun getFormattedNumber(): String {
return PhoneNumberUtils.formatNumber(
userEnteredPhoneNumber,
"US"
)
}
}
Here’s a simple class showing an example of calling Android code from a basic Kotlin class. Most developers will think that they can not write a unit test against this code because when a developer creates a new JUnit test inside the /test directory they will get the Not Mocked Error when they try to run the test.
class Trick2Test {
@Test
fun verifyPhoneFormatIsCorrect() {
// given
val expected = "(555) 555-1234"
val trick2 = Trick2("5555551234")
// when
val actual = trick2.getFormattedNumber()
// then
assertEquals(expected, actual)
}
}
The only difference between the broken test source code link and working test source code link is the location of the test. If you move your JUnit test into /androidTest then the test will run against the Android SDK and it will work. You will not have to mock out anything or worry that the implementation details will change in the next version of Android and break your code since you have a test running against the Android SDK. Pretty nice, right?
Inject your dependencies manually
I debated on including this trick, mainly because it requires the use of your imagination since the code is incomplete. This trick assumes you are injecting your dependencies with Hilt, Dagger, or some other Dependency Injection/Service Locator framework that has a lateinit var property. This code is pretty much the same as Trick 1 except your User dependency would be injected (commented out for brevity).
/**
* Not actually wiring up Dagger/Hilt for such a small project
* Commenting it out just to show the example
*/
// @AndroidEntryPoint
open class Trick3Activity : AppCompatActivity() {
/**
* Commented out to show the example
*/
// @Inject
lateinit var user: User
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_trick3)
user = User("Timothy Paetz") // Only added to get test to run. This would actually be injected
initUserName()
}
@VisibleForTesting
fun initUserName() {
// Same logic as Trick1Activity except user dependency is injected
if (hasCompletedProfile()) {
findViewById<TextView>(R.id.name).text = user.name
} else {
findViewById<TextView>(R.id.name).text = getString(R.string.incomplete_profile)
}
}
@VisibleForTesting
fun hasCompletedProfile(): Boolean {
return user.name != null
}
}
Ignore the line: user = User(“Timothy Paetz”) as that is just hard coded for the purposes of this demo and to get the Espresso test to run. This User object would actually be injected using whichever Dependency Injection framework you use. Now we want to run an Espresso test against initUserName() to verify the correct text is displayed based on the User object that was manually injected via setting the user field.
@RunWith(AndroidJUnit4::class)
class Trick3ActivityTest {
@Test
fun testInjectedNoNameUser() {
launchActivity<Trick3Activity>().use { scenario ->
scenario.onActivity { activity ->
/**
* This is basically what dagger does. It just sets your dependencies.
* You can do the same within your Espresso tests
*/
activity.user = User(null)
activity.initUserName()
}
onView(withId(R.id.name)).check(matches(withText(R.string.incomplete_profile)))
}
}
@Test
fun testInjectedNamedUserIsDisplayed() {
launchActivity<Trick3Activity>().use { scenario ->
scenario.onActivity { activity ->
/**
* This is basically what dagger does. It just sets your dependencies.
* You can do the same within your Espresso tests
*/
activity.user = User("Hello World")
activity.initUserName()
}
onView(withId(R.id.name)).check(matches(withText("Hello World")))
}
}
}
We can test both when a User has a null name and when the User has any name we manually set via activity.user = User(“Hello World”). Basically what this means is any dependency that is injected, you can manually set and test since it is a public field. Also, we can do almost the exact same code as Trick 1 but instead manually set the dependency and use a regular JUnit test instead of Espresso test.
class Trick3ActivityTest {
@Test
fun `verify no name user returns false for hasCompletedProfile`() {
// given
val trick3Activity: Trick3Activity = mock(
defaultAnswer = Mockito.CALLS_REAL_METHODS
)
val noNameUser = User(name = null)
trick3Activity.user = noNameUser // manually inject user here
// when
val actual = trick3Activity.hasCompletedProfile()
// then
assertFalse(actual)
}
@Test
fun `verify named user returns true for hasCompletedProfile`() {
// given
val trick3Activity: Trick3Activity = mock(
defaultAnswer = Mockito.CALLS_REAL_METHODS
)
val namedUser = User(name = "Timothy Paetz")
trick3Activity.user = namedUser // manually inject user here
// when
val actual = trick3Activity.hasCompletedProfile()
// then
assertTrue(actual)
}
}
So depending on the way your code is architected, you are able to run an Espresso test or a JUnit test against an activity and test UI logic or business logic with minimal boilerplate to run the tests.
Hopefully you learned something new about Android testing in this post. I find it most interesting that I haven’t seen these patterns discussed any where else. I don’t know if developers think this is common knowledge or if developers don’t know these options are possible. Reach out if you have any questions, comments, or concerns on LinkedIn, Twitter, Email or comment below.
Thanks
Tim
Leave a Reply