Android Espresso/Mockito/Hilt Test Note

Homan Huang
5 min readSep 17, 2020

--

This is the collection of notes from Espresso, Mockito, and Dagger Hilt Test note. Of course, it’s the 2020.9.16 version. I will update this story as long as I have some new notes.

— === Menu === —

✂️1. Shortcut in Android Studio
🔌2. Gradle Dependencies
⚙ 3. Functions
⛓4. Variables and Actions
☕5. Espresso
🏵🏵🏵🏵 doNotExist()
🏵🏵🏵🏵 Check Visibility
☕5–1. Espresso-IdlingResource
🏃6. Custom Runner
🃏7. Mockito
💞8. Hilt with Mockito

✂️1. Shortcut in Android Studio

< == Menu

Keyboard Shortcut

In Activity: Ctrl+Shift+T

Create a new test or move to the activity test case.

In Test: Ctrl+Shift+T

Move back to activity.

Go to source: Ctrl+B or Ctrl+Click

It returns the location where contains the value or function.

Ctrl+E

Recent files

LiveTemplate

LiveTemplate: Ctrl+Alt+S and search for live template

Live template will ease your coding work to implant your own shortcuts.

Kotlin LiveTemplate: testfun

@Test
fun $FUN_NAME$() {
$END$
}

Logcat: apply to Kotlin class

private const val tag = "MYLOG $CLASS$"
fun lgd(s:String) = Log.d(tag, s)
fun lgi(s:String) = Log.i(tag, s)
fun lge(s:String) = Log.e(tag, s)

Toast:

// Toast: len: 0-short, 1-long
fun msg(context:Context, s:String, len:Int) =
if (len > 0) Toast.makeText(context, s, LENGTH_LONG).show()
else Toast.makeText(context, s, LENGTH_SHORT).show()

🔌2. Gradle Dependencies

< == Menu

Gradle: plugin{}

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
}

Gradle: android{}

android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}

useLibrary 'android.test.runner'
useLibrary 'android.test.base'
useLibrary 'android.test.mock'

packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/license.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
exclude("META-INF/*.kotlin_module")
}
}

Gradle: androidTestImplementation — Espresso

// Espresso dependencies
def espresso_version = "3.3.0"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version"
androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espresso_version"

Gradle: androidTestImplementation — Core

// Android test
def archcore_version = '2.1.0'
androidTestImplementation "androidx.arch.core:core-testing:$archcore_version"

// AndroidJUnitRunner and JUnit Rules
def test_version = '1.3.0'
androidTestImplementation "androidx.test:runner:$test_version"
androidTestImplementation "androidx.test:rules:$test_version"
androidTestImplementation "androidx.test:core-ktx:$test_version"
debugImplementation "androidx.test:core:$test_version"
// Assertions
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.ext:truth:1.3.0'
androidTestImplementation 'com.google.truth:truth:1.0'

Gradle: androidTestImplementation — Hamcrest

// Optional -- Hamcrest library
androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'

Gradle: androidTestImplementation —Mockito

// Mockito framework
androidTestImplementation "org.mockito:mockito-android:3.3.1"

⚙ 3. Functions

< == Menu

Header: link with runner

@RunWith(AndroidJUnit4ClassRunner::class) // runner
@LargeTest // test level
class YourActivityTest{

Rule and Scenario

@get:Rule
var yourRule: ActivityScenarioRule<YourActivity>
= ActivityScenarioRule(YourActivity::class.java)

lateinit var yourScenario: ActivityScenario<YourActivity>

@Before: Run before the test.

@Before
fun setup() { ... }

@After: Run after the test.

@After
fun tearDown() {
yourScenario.close()
}

Espresso Pause > viewWait() or Thread.sleep()

// delay -- time unit: ms
fun viewWait(delay: Long) {
onView(isRoot()).perform(waitFor(delay))
}

fun waitFor(delay: Long):
ViewAction = object : ViewAction {
override fun perform(
uiController: UiController?, view: View?
) { uiController?.loopMainThreadForAtLeast(delay) }

override fun getConstraints(): Matcher<View> = isRoot()

override fun getDescription():
String = "wait for " + delay + "milliseconds"
}

Save functions into Delay.kt of androidTest directory.

Perform before onView:

viewWait(3000)
onView(...).check(...)

⛓4. Variables and Actions

< == Menu

TargetContext: It stands for your test subbject.

val ctx = InstrumentationRegistry.getInstrumentation().targetContext

ApplicationContext: With TestApp

val app = targetContext.applicationContext as TestApp

Launch Activity:

lateinit var mainScenario: ActivityScenario<MainActivity>...
mainScenario = launchActivity<MainActivity>()

Create Intent

var intent = Intent(
ApplicationProvider.getApplicationContext(),
YourActivity::class.java)
intent.putExtra(KEY, "value")
yourScenario = ActivityScenario.launch<YourActivity>(intent)

ActivityResult

val intent = Intent()
intent.putExtra(KEY_NAME, "value")
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, intent)

☕5. Espresso

< == Menu

TextView.text compare with message

onView(withId(R.id.TextViewId))
.check(matches(withText(message)))

Button: click()

onView(withId(R.id.ButtonId)).perform(click())

Intents.intended: Check outgoing intent bundle

intended(IntentMatchers.hasExtra(KEY, "value"))

Intents.intended: Match destination activity

intended(IntentMatchers.hasComponent(
ComponentNameMatchers.hasClassName(
"PACKAGE.SecondActivity"))
)

Intents.intending: check return bundle inside Intents.init()…Intents.release()

Intents.init()
val result = ActivityResult(Activity.RESULT_OK, intent)
...
intending(hasExtra(KEY, "value")).respondWith(result)
...
Intents.release()

doNotExist(): Generated by Espresso Recorder

textView.check(doesNotExist())

isDisplayed(): Generated by Espresso Recorder

val button = onView(
allOf(
withId(R.id.titleFab), withText(s),
withParent(
allOf(
withId(R.id.swipeRefresh),
withParent(IsInstanceOf.instanceOf(android.widget.FrameLayout::class.java))
)
),
isDisplayed()
)
)
button.check(matches(isDisplayed()))

Check Visibility: The isDisplay() doesn’t work on visibility.

menuFab.check(matches(
withEffectiveVisibility(Visibility.GONE)))

☕5–1. Espresso-IdlingResource

< == Menu

IdelingResource: Wait for process(network call) to get the result.

Resource activity or fragment implements IdlingResource.

class DiaFrgIdlingResource(
private val manager: FragmentManager,
private val tag: String
): IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null

override fun getName(): String = "DiaFrgIdlingResource:$tag"

override fun isIdleNow(): Boolean {
val idle = (manager.findFragmentByTag(tag) == null)
if (idle) callback?.onTransitionToIdle()
return idle
}

override fun registerIdleTransitionCallback(
callback: IdlingResource.ResourceCallback?
) { this.callback = callback }
}

Control the callback until the process is done.

Test: DiaglogFragment sample test.

@Test
fun done() {
mainScenario = launchActivity()
mainScenario.onActivity { activity->
val manager = activity.supportFragmentManager
idleResource = DiaFrgIdlingResource(
manager, MyDialogFragment.TAG)
IdlingRegistry.getInstance().register(idleResource) //ON
}
onView(withId(R.id.text))
.check(matches(withText(R.string.done)))
IdlingRegistry.getInstance().unregister(idleResource) //OFF
}

🏃6. Custom Runner

< == Menu

Create Test Application

class TestApp: CustomApplication() {
...
}

Create Custom Runner

class CustomTestRunner: AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(
cl,
"PACKAGE.TestApp",
context)
}
}

Update Gradle

android {
...


defaultConfig {
...


//testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
testInstrumentationRunner 'PACKAGE.CustomTestRunner'

}

🃏7. Mockito

< == Menu

Gradle:

// Mockito framework
androidTestImplementation "org.mockito:mockito-android:3.3.1"

when…thenReturn

Mockito.`when`( condition|fun ).thenReturn( fake result )

Fix for final Class: open

open class MineClock {
open fun getHour(): Int = DateTime().hourOfDay
}

💞8. Hilt with Mockito

< == Menu

Gradle:

classpath:

classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'

plugin:

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}

dependencies:

// Hilt
def hilt_version = '2.28-alpha'
def hilt_lifecycle_version = '1.0.0-alpha02'
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_lifecycle_version"
kapt "androidx.hilt:hilt-compiler:$hilt_lifecycle_version"

// For instrumented tests.
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
// ...with Kotlin.
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

TestRunner: With HiltTestApplication

class TestAppRunner: AndroidJUnitRunner() {
override fun newApplication(...): Application {
return super.newApplication(
cl,
HiltTestApplication::class.java.name,
context)
}
}

Gradle:

testInstrumentationRunner "PACKAGE.TestAppRunner"

Uninstall original and install mocked module

Original sample class before mocked.

open class MineClock {
open fun getHour(): Int = DateTime().hourOfDay
}

Original Activity:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject lateinit var clock: MineClock

override fun onCreate(savedInstanceState: Bundle?) {
...

Original Module Sample:

@Module @InstallIn(ApplicationComponent::class)
class ClockModule {
@Provides fun provideClock(): MineClock = MineClock()
}

ActivityTest:

  • Uninstall Original Module
  • Insert HiltAndroidRule
  • Install Mocked Module
  • Inject Mocked object
@UninstallModules(ClockModule::class)
@HiltAndroidTest
class MainActivityTest {
//@get:Rule var mainRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java)

@get:Rule
var hiltRule = HiltAndroidRule(this)

lateinit var mainScenario: ActivityScenario<MainActivity>

@Inject
lateinit var clock: MineClock


@Module @InstallIn(ApplicationComponent::class)
class TestClockModule { // test module
@Provides
fun provideClock(): MineClock =
Mockito.mock(MineClock::class.java)
}

Before:

@Before
fun setup() {
hiltRule.inject()
}

TestCase Sample:

@Test
fun nightTest() {
mainScenario = launchActivity()

Mockito.`when`(clock.getHour()).thenReturn(4)

viewWait(2000)
onView(withId(...)).check(matches(withText(...)))
}

😁Have Fun!

--

--

Homan Huang
Homan Huang

Written by Homan Huang

Computer Science BS from SFSU. I studied and worked on Android system since 2017. If you are interesting in my past works, please go to my LinkedIn.

No responses yet