Instrumentation Testing

Instrumentation Testing

Instrumentation tests require a device (either an emulator or real device) to run test cases. This distinguishes them from local unit tests, as the tests execute on an actual Android runtime environment.

Diagram — Instrumentation Test vs. Local Unit Test

flowchart LR
    A["Test Code"] --> B{"Needs Android\nruntime/UI?"}
    B -->|No| C["Local Unit Test\nsrc/test · JVM only"]
    B -->|Yes| D["Instrumentation Test\nsrc/androidTest · device/emulator"]

Types of Instrumentation Tests

TypeDescriptionExample Classes Used
UI TestsInteract with UI elements such as clicking views and typing into input fields. Requires special libraries.Espresso, UIAutomator
Non-UI TestsTests that do not interact with the UI but test Android components like AssetManager, Contacts, etc.AssetManager, Contact class

Diagram — Instrumentation Test Types

flowchart TD
    A["Instrumentation Tests\nrun on device/emulator"] --> B["UI Tests\nEspresso, UIAutomator\nclicks, typing, view assertions"]
    A --> C["Non-UI Tests\nAssetManager, Contacts\nno UI interaction"]


Accessing Assets and Managing Data

Asset Folder Usage

  • Android apps can contain an assets folder where static files (e.g., JSON files) are stored.
  • To read files from the assets folder, you need to use the Context of the app, which provides access to the assets through context.assets.

Example: CourtManager Class

  • A CourtManager class is created to handle all operations related to "courts" (data objects).
  • Data is stored as JSON in the assets folder.
  • A data class represents the court with the following properties:
  • text (String): The court text
  • author (String): The author of the court

data class Court(
    val text: String,
    val author: String
)

Reading and Parsing JSON from Assets

  • Method populateCodeFromAssets takes the file name and a Context to read the JSON file.
  • Uses assets.open(filename) to open the file.
  • Reads bytes and uses Gson library to parse the JSON into an array or list of Court objects.
  • Note: Gson converts JSON data into Kotlin/Java objects automatically.

Diagram — populateCodeFromAssets() Flow

flowchart LR
    A["context.assets.open(filename)"] --> B["Read bytes\nfrom InputStream"]
    B --> C["Gson.fromJson()"]
    C --> D["List<Court>"]

Example

class CourtManager {
    private var courts: List<Court> = emptyList()
    private var currentIndex = 0

    fun populateCodeFromAssets(context: Context, fileName: String) {
        val jsonString = context.assets.open(fileName)
            .bufferedReader()
            .use { it.readText() }

        val type = object : TypeToken<List<Court>>() {}.type
        courts = Gson().fromJson(jsonString, type)
    }
}


CourtManager Functionalities

FunctionPurposeBehavior
getCurrentCode()Returns the current court object based on the current index.Returns the court at the current index.
getNextCode()Returns the next court object in the list.If the current index is the last element, returns the last court to prevent an array out-of-range exception.
getPreviousCode()Returns the previous court object in the list.If the current index is 0, returns the first court to prevent an index-out-of-range exception.


Boundary Conditions

  • When the current index reaches the last court, getNextCode() will return the last court repeatedly.
  • When the current index is zero, getPreviousCode() will keep returning the first court.

Diagram — Navigation Boundary Handling

flowchart LR
    subgraph Next["getNextCode()"]
        N1["index < lastIndex"] --> N2["index++\nreturn courts[index]"]
        N3["index == lastIndex"] --> N4["stay at lastIndex\nreturn courts[lastIndex]"]
    end
    subgraph Prev["getPreviousCode()"]
        P1["index > 0"] --> P2["index--\nreturn courts[index]"]
        P3["index == 0"] --> P4["stay at 0\nreturn courts[0]"]
    end

Example

fun getCurrentCode(): Court = courts[currentIndex]

fun getNextCode(): Court {
    if (currentIndex < courts.lastIndex) {
        currentIndex++
    }
    return courts[currentIndex]   // stays at last court instead of throwing
}

fun getPreviousCode(): Court {
    if (currentIndex > 0) {
        currentIndex--
    }
    return courts[currentIndex]   // stays at first court instead of throwing
}


Writing Instrumentation Tests for CourtManager

Key Testing Concepts

  • Arrange: Setup required objects or states (e.g., create CourtManager, provide context).
  • Act: Call the method or function under test.
  • Assert: Verify that the outcome matches expected behavior.

Diagram — Arrange-Act-Assert for Instrumentation Tests

flowchart LR
    A["Arrange\nCourtManager() +\nInstrumentationRegistry context"] --> B["Act\ncall populateCodeFromAssets()\nor navigation method"]
    B --> C["Assert\ncheck result or\nexpected exception"]

Testing Scenarios

Test CaseDescriptionExpected Outcome
Passing an invalid or missing file nameCall populateCodeFromAssets without a valid file name.Should throw an exception (FileNotFoundException or JsonSyntaxException).
Passing a corrupted or incorrect JSON fileCall populateCodeFromAssets with a malformed JSON file.Should throw a JsonSyntaxException.
Valid file input testingCall populateCodeFromAssets with a valid JSON file containing courts.Should return a list/array of Court objects correctly parsed.
Testing navigation methods (getNextCode etc.)Test boundary conditions for next and previous court retrieval.Should handle index boundaries gracefully without exceptions.

Example Test Structure

@Test(expected = FileNotFoundException::class)
fun testPopulateCodeFromAssets_withInvalidFile_throwsException() {
    // Arrange
    val courtManager = CourtManager()
    val context = InstrumentationRegistry.getInstrumentation().targetContext

    // Act
    courtManager.populateCodeFromAssets(context, "invalidFile.json")

    // Assert is implicit via expected exception
}

Example — Covering the Remaining Scenarios

@RunWith(AndroidJUnit4::class)
class CourtManagerInstrumentedTest {

    private lateinit var courtManager: CourtManager
    private lateinit var context: Context

    @Before
    fun setUp() {
        courtManager = CourtManager()
        context = InstrumentationRegistry.getInstrumentation().targetContext
    }

    @Test(expected = JsonSyntaxException::class)
    fun testPopulateCodeFromAssets_withMalformedJson_throwsException() {
        courtManager.populateCodeFromAssets(context, "corrupted_courts.json")
    }

    @Test
    fun testPopulateCodeFromAssets_withValidFile_parsesCourts() {
        courtManager.populateCodeFromAssets(context, "courts.json")
        assertEquals("The bus fare kitna hai", courtManager.getCurrentCode().text)
    }

    @Test
    fun testGetNextCode_atLastIndex_returnsLastCourtRepeatedly() {
        courtManager.populateCodeFromAssets(context, "courts.json")
        repeat(10) { courtManager.getNextCode() }   // overshoot the list size
        assertEquals(courtManager.getNextCode(), courtManager.getNextCode())
    }
}


Test Implementation Details

  • Use Android test directory for instrumentation tests.
  • Use InstrumentationRegistry.getInstrumentation().targetContext to get the app context.
  • Generate test methods for each logical functionality:
  • File reading and exception handling.
  • Correct parsing of JSON files.
  • Navigation methods for current, next, and previous courts.
  • Use @Test(expected = ExceptionType::class) annotation to verify expected exceptions.


Exception Handling in Tests

  • Tests verify that the application throws exceptions when invalid input or files are provided.
  • Common exceptions:
  • FileNotFoundException: When file is missing.
  • JsonSyntaxException: When JSON is malformed.
  • This helps ensure robustness and predictability of the app's error handling.

Diagram — Exception Paths Covered by Tests

flowchart TD
    A["populateCodeFromAssets(context, fileName)"] --> B{"File exists?"}
    B -->|No| C["FileNotFoundException"]
    B -->|Yes| D{"Valid JSON?"}
    D -->|No| E["JsonSyntaxException"]
    D -->|Yes| F["List<Court> returned"]


Summary of Test Workflow

StepDescription
ArrangeInitialize CourtManager and obtain app Context.
ActCall the method to read JSON or retrieve courts using populateCodeFromAssets or navigation methods.
AssertCheck for expected output or exceptions to verify correctness.

Example JSON Structure for Courts

[
  {
    "text": "The bus fare kitna hai",
    "author": "Author Name"
  },
  {
    "text": "Another court text",
    "author": "Another Author"
  }
]


Key Takeaways

  • Instrumentation tests require an Android device or emulator because they interact with Android framework classes.
  • Non-UI instrumentation tests focus on backend functionality like file reading and data parsing.
  • The CourtManager class manages the logic of reading courts from JSON assets and navigating through them.
  • Proper exception handling and boundary condition checks are crucial and must be tested thoroughly.
  • Tests are structured around Arrange, Act, Assert to maintain clarity and organization.
  • Using Gson simplifies JSON-to-object conversions in asset reading.

The design of CourtManager and its tests ensures that the app can reliably load and navigate static data stored in JSON format, handling incorrect or missing data gracefully without crashing.