Android Espresso/Mockito/Hilt Test Note
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
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
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
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
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
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
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
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
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
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(...)))
}