☕ Advanced Espresso Concepts 04 : Custom Rules

EspressoLab.Ai
4 min readJun 25, 2024

--

Creating custom rules in UI testing for Android can significantly enhance your test suite by allowing more flexible and reusable test conditions. This guide will show you how to create custom rules using Kotlin, providing realistic examples to illustrate the process.

✨ Introduction

Custom rules in UI testing enable you to encapsulate reusable logic for setting up and tearing down test conditions. These rules can be particularly useful for handling common test setup procedures, such as database initialization or dependency injection using Koin and Hilt.

🔧 Setting Up Custom Rules

To create custom rules in Android UI testing, you can use JUnit rules, which are part of the JUnit framework. Here’s a basic outline of how to set up and use custom rules:

1. Define Your Custom Rule Class

Start by creating a class that implements the TestRule interface. This class should encapsulate the logic for your custom rule.

import org.junit.rules.TestWatcher
import org.junit.runner.Description

class CustomRule : TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
setup()
}

override fun finished(description: Description) {
super.finished(description)
teardown()
}

private fun setup() {
// Custom setup logic
println("Setting up custom rule...")
}

private fun teardown() {
// Custom teardown logic
println("Tearing down custom rule...")
}
}

2. Use Your Custom Rule in Tests

Once you have defined your custom rule, you can use it in your test classes by adding a @Rule annotation.

import org.junit.Rule
import org.junit.Test

class MyUITest {
@get:Rule
val customRule = CustomRule()

@Test
fun testExample() {
// Your test code here
println("Running the test...")
}
}

📦 Realistic Example: Database Initialization Rule

Let’s create a more practical example where a custom rule initializes a database before each test and cleans it up after each test.

1. Define the Database Rule

Create a custom rule class for database setup and teardown.

import org.junit.rules.TestWatcher
import org.junit.runner.Description

class DatabaseRule : TestWatcher() {
lateinit var database: MyDatabase

override fun starting(description: Description) {
super.starting(description)
// Initialize the database before the test
database = MyDatabase.create()
}

override fun finished(description: Description) {
super.finished(description)
// Clean up the database after the test
database.close()
}
}

2. Use the Database Rule in Tests

Integrate the custom database rule into your test class.

import org.junit.Rule
import org.junit.Test

class MyDatabaseTest {
@get:Rule
val databaseRule = DatabaseRule()

@Test
fun testDatabaseInsertion() {
val db = databaseRule.database
// Perform database operations
db.insertData("Test Data")
val result = db.queryData()
assert(result.contains("Test Data"))
}
}

🚀 Using Dependency Injection with Koin and Hilt

Custom rules can also be used to handle dependency injection setup, such as with Koin or Hilt.

Koin Custom Rule

  1. Define the Koin Rule Create a custom rule class for initializing Koin modules before each test.
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.core.module.Module

class KoinRule(private val modules: List<Module>) : TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
startKoin { modules(modules) }
}

override fun finished(description: Description) {
super.finished(description)
stopKoin()
}
}

2. Use the Koin Rule in Tests Integrate the custom Koin rule into your test class.

import org.junit.Rule
import org.junit.Test
import org.koin.dsl.module

class MyKoinTest {
private val testModule = module {
// Define your test dependencies here
}

@get:Rule(order = 1)
val koinRule = KoinRule(listOf(testModule))

@Test
fun testKoinDependency() {
// Your test code using Koin dependencies
}
}

Hilt Custom Rule

  1. Define the Hilt Rule Create a custom rule class for initializing Hilt before each test.
import dagger.hilt.android.testing.HiltAndroidRule
import org.junit.rules.TestWatcher
import org.junit.runner.Description

class HiltRule(val testInstance: Any) : TestWatcher() {
private val hiltRule = HiltAndroidRule(testInstance)

override fun starting(description: Description) {
super.starting(description)
hiltRule.inject()
}
}

2. Use the Hilt Rule in Tests Integrate the custom Hilt rule into your test class.

import org.junit.Rule
import org.junit.Test
import dagger.hilt.android.testing.HiltAndroidTest

@HiltAndroidTest
class MyHiltTest {
@get:Rule(order = 2)
val hiltRule = HiltRule(this)

@Test
fun testHiltDependency() {
// Your test code using Hilt dependencies
}
}

✨ Animation Disabler Rule

Here is an example of an AnimationDisablerRule to disable animations during tests:

import android.app.UiAutomation
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.rules.TestWatcher
import org.junit.runner.Description

class AnimationDisablerRule : TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
changeAnimationScale(0.0)
}

override fun finished(description: Description) {
super.finished(description)
changeAnimationScale(1.0)
}

private fun changeAnimationScale(scale: Double) {
with(InstrumentationRegistry.getInstrumentation().uiAutomation) {
executeShellCommand("settings put global window_animation_scale $scale")
executeShellCommand("settings put global transition_animation_scale $scale")
executeShellCommand("settings put global animator_duration_scale $scale")
}
}
}

🔄 Understanding Rule Order

The @get:Rule annotation defines the order in which rules are applied. The order of rule application can be controlled by specifying the order. This is important because the setup and teardown of rules will be performed in this order.

For example:

open class ComponentsTest {
@get:Rule(order = 1)
val koinRule = KoinRule(listOf(testModule))

@get:Rule(order = 2)
val hiltRule = HiltRule(this)

@get:Rule(order = 3)
val mockServerRule = MockServerRule()

@get:Rule(order = 4)
val idlingRule = IdlingRule()

@get:Rule(order = 5)
val screenshotRule = CustomTestRules.createScreenshotRule(isEnabled = false)

@get:Rule(order = 6)
val animationDisablerRule = AnimationDisablerRule()
}

In this example, the rules will be applied in the specified order, which ensures that the dependency injection setup happens before other rules like mock server or idling resources.

Next Article Teaser

Stay tuned for our next article, where we’ll explore creating custom matchers in Android UI testing. Custom matchers allow you to define more precise and flexible conditions for finding and interacting with UI elements during testing. We’ll guide you through the process of creating and using custom matchers, including practical examples for views like RecyclerView and Toolbar.

Follow Us for More Insights 🌟

Stay updated with the latest tips and best practices for QA engineers in Android UI testing by following us on Medium. For more resources and tools to enhance your testing skills, visit our website EspressoLab.ai.

Follow us on Medium: EspressoLab Medium

Thank you for reading! 🚀

--

--

EspressoLab.Ai

Empowering Software Engineers with top-notch digital products and online courses.