Android App Development

Chapter 11
Testing, Debugging and Performance

Certificate in Web and Android Development
Android App Development   •   Certificate

Testing, Debugging and Performance

Introduction

An Android application is not complete when it merely builds or appears to work on a developer’s device. It must also behave correctly across screen sizes, Android versions, network conditions, device states, and lifecycle transitions. Testing provides evidence that requirements are satisfied; debugging identifies and removes causes of defects; performance engineering ensures that the application remains responsive, efficient, and reliable.

Android quality assurance is especially important because applications operate within a resource-constrained and event-driven environment. Activities and fragments may be destroyed and recreated, processes may be terminated when memory is low, network calls may fail, and users may rotate the device or move the application to the background. A professional developer therefore treats quality as a continuous process rather than a final inspection step.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish unit, integration, UI, instrumentation, and exploratory tests;
  • design effective tests using isolation, determinism, and meaningful assertions;
  • create basic Kotlin unit and UI tests;
  • use a systematic process to reproduce, diagnose, and correct defects;
  • identify common Android lifecycle, threading, memory, and configuration errors;
  • interpret profiling information about CPU, memory, battery, and responsiveness;
  • apply quality gates and risk-based testing practices to an Android project.

1. The Android Quality Model

Quality has several dimensions. Functional correctness asks whether the application produces the required result. Usability asks whether users can understand and operate it. Reliability concerns consistent operation over time. Performance concerns response time, smoothness, startup, and resource consumption. Security and privacy concern protection of users and their data. Maintainability concerns whether the application can be changed safely.

These dimensions can conflict. Aggressive background synchronization may improve data freshness but consume battery. Extensive logging may help diagnosis but expose personal data or reduce performance. A useful quality decision therefore considers the user, the device, the data, and the operational context.

A defect is an error in a product; a failure is observable incorrect behaviour; and a fault is the underlying cause. For example, an incorrect null-handling statement is a fault, a crash is a failure, and the report that the application crashes when the user opens a saved order describes the defect from the user’s perspective.

A practical defect record should include:

  1. a concise title;
  2. device model, Android version, and application version;
  3. preconditions and test data;
  4. exact reproduction steps;
  5. expected and actual results;
  6. logs, screenshots, or traces;
  7. severity, frequency, and business impact.

Severity describes the effect of a defect, whereas priority describes how soon it should be addressed. A reproducible payment crash is high severity and high priority. A minor alignment problem may be low severity, although it could receive higher priority before a public release if it affects accessibility or branding.

2. A Testing Strategy

Testing levels should be combined rather than treated as alternatives.

Test typeMain subjectTypical speedExample
Unit testOne class or function in isolationVery fastVerify a discount calculation
Integration testInteraction between componentsModerateRepository with a database
Instrumentation testCode running on an Android device or emulatorSlowerDatabase migration
UI testUser-visible interactionSlowerEnter text and press Save
Exploratory testHuman investigation of uncertain behaviourVariableTry unusual navigation paths

The test pyramid recommends many fast unit tests, fewer integration tests, and a smaller number of broad UI tests. The principle is economic as well as technical: fast tests provide rapid feedback, while UI tests are valuable but more sensitive to timing, device configuration, and layout changes.

A good test has four properties:

  • Isolation: it controls or replaces external dependencies.
  • Determinism: the same inputs normally produce the same result.
  • Clarity: the purpose and failure are easy to understand.
  • Value: it tests behaviour that matters to users or the system.

The arrange–act–assert structure makes these properties visible:

@Test
fun appliesStudentDiscount() {
    // Arrange
    val calculator = PriceCalculator()

    // Act
    val result = calculator.finalPrice(100.0, student = true)

    // Assert
    assertEquals(90.0, result, 0.001)
}

The tolerance in this example is appropriate because the result is a floating-point value. For currency, however, integer minor units such as cents or a decimal type are generally safer than binary floating-point arithmetic.

Tests should normally verify observable behaviour rather than private implementation details. A test that asserts that a particular private method was called may fail after a harmless refactoring. A test that verifies that the displayed total is correct is more stable and meaningful.

3. Unit Testing in Kotlin

Local unit tests are commonly placed in src/test. They run on the development machine and should avoid Android framework dependencies unless those dependencies are explicitly supported by the test environment. A unit test may use fakes, stubs, or mocks to control dependencies.

Consider a view-model that obtains a user from a repository. The repository can be represented by an interface:

interface UserRepository {
    suspend fun findUser(id: String): User?
}

data class User(val id: String, val name: String)

A fake repository makes the test predictable:

class FakeUserRepository(private val user: User?) : UserRepository {
    override suspend fun findUser(id: String): User? = user
}

The test can then check the success and missing-user paths without a network connection. Coroutine-based code should use a test dispatcher and controlled virtual time where appropriate. Avoid using arbitrary delays such as Thread.sleep; they make tests slow and flaky.

Important unit-test cases include:

  • normal valid input;
  • empty, null, boundary, and malformed input;
  • failure from each relevant dependency;
  • repeated calls and idempotency where applicable;
  • state transitions, such as loading to success or error;
  • permission-denied and offline conditions.

A test suite also needs useful assertions. A test that merely verifies that no exception occurred provides weak evidence. Assertions should express the contract: returned value, state, error type, emitted event, or interaction with a dependency.

Test doubles

A stub supplies predetermined data. A fake is a lightweight working implementation, such as an in-memory repository. A mock records interactions and can verify that a call occurred. A spy wraps a real implementation while recording selected behaviour. Excessive mocking can make tests describe the implementation rather than the requirement, so fakes are often preferable for simple data access.

Boundary testing

Many defects occur at boundaries. If an input must contain between 1 and 50 characters, test lengths 0, 1, 50, and 51, as well as representative values. Boundary tests are not a substitute for broader testing, but they efficiently target likely faults.

4. UI and Instrumentation Testing

UI tests run against an Android environment and verify interaction among views, state, navigation, and the platform. Depending on the toolkit, tests may use Espresso, UI Automator, or Compose testing APIs. A UI test should interact through semantics visible to a user, such as text, content descriptions, roles, or test tags, rather than relying unnecessarily on fragile view positions.

A typical UI test performs the following sequence:

  1. establish a known initial state;
  2. locate a user-visible control;
  3. perform an action;
  4. wait for the application’s legitimate idle or synchronization condition;
  5. assert the resulting visible state.

For example, a Compose test might conceptually follow this pattern:

@get:Rule
val rule = createComposeRule()

@Test
fun saveDisplaysConfirmation() {
    rule.setContent { OrderScreen() }
    rule.onNodeWithText("Save").performClick()
    rule.onNodeWithText("Order saved").assertIsDisplayed()
}

The exact APIs depend on project configuration, but the principle is stable: test user-observable behaviour. UI tests should not depend on network services that may be unavailable. Use a controlled fake server or repository, and reset application data between tests when state could leak from one test to another.

UI tests are vulnerable to flakiness. Common causes include arbitrary sleeps, animations, race conditions, shared test data, network dependence, and incomplete synchronization. The remedy is not simply to retry the test. Retries can hide real defects. Instead, identify the asynchronous operation and synchronize with its completion.

Accessibility should be included in UI quality assurance. Check meaningful content descriptions, sufficient touch targets, logical focus order, text scaling, contrast, and screen-reader interpretation. Accessibility testing is both an ethical responsibility and, in many contexts, a legal or organizational requirement.

5. Debugging as a Systematic Process

Debugging is hypothesis-driven investigation, not random editing. A reliable process is:

  1. Observe: capture the exact failure and context.
  2. Reproduce: find the smallest reliable sequence that triggers it.
  3. Localize: determine whether the cause is UI, state, data, concurrency, configuration, or platform interaction.
  4. Form a hypothesis: state what should explain the evidence.
  5. Instrument: add appropriate logs, breakpoints, assertions, or traces.
  6. Change one relevant factor: avoid unrelated edits.
  7. Verify: reproduce the original case and run regression tests.
  8. Document: record the cause, fix, and prevention measure.

The debugger supports breakpoints, stepping, variable inspection, call stacks, and conditional stops. Logging is useful for events that are difficult to reproduce, but logs should be structured and should not contain passwords, tokens, health information, or unnecessary personal data. Release builds should apply suitable logging policies.

A stack trace should be read from the exception type and message through the causal chain and application frames. The first application frame is often informative, but it is not automatically the root cause. For example, a null value may be created by an earlier parsing or lifecycle error.

Common debugging mistakes include changing several parts of the program at once, suppressing exceptions, treating a crash report as proof of the root cause, and fixing only the visible symptom. A regression test should be added whenever practical so that the defect does not return.

6. Lifecycle and Concurrency Defects

Android components have lifecycles controlled partly by the operating system. An activity can be recreated after rotation or process death. A fragment’s view can be destroyed while the fragment object remains. Code must therefore distinguish the lifetime of the component from the lifetime of its view.

Common lifecycle errors include:

  • retaining an activity or view in a long-lived object, causing a memory leak;
  • starting work in onCreate without cancelling or scoping it appropriately;
  • storing important state only in fields rather than a saved-state mechanism;
  • observing data with a lifecycle that is too long or too short;
  • updating a destroyed view from an asynchronous callback;
  • assuming that onDestroy is always called before process termination.

Use lifecycle-aware scopes, such as a view-model scope for work that should survive configuration changes and a view lifecycle scope for work tied to a particular view. Persist essential state in an appropriate repository or saved-state mechanism rather than relying solely on in-memory fields.

Concurrency defects include race conditions, deadlocks, and incorrect thread access. The user interface must remain responsive, so expensive I/O, database work, and computation should not run on the main thread. At the same time, UI updates normally must be delivered on the main thread. Structured concurrency helps by making ownership and cancellation explicit.

A practical test should rotate the device during loading, background and foreground the application, deny permissions, disable the network, and repeatedly open and close the screen. These transitions reveal defects that a single successful path does not expose.

7. Measuring Performance

Performance should be measured rather than inferred. The Android Studio Profiler and related tooling can help inspect CPU activity, memory allocation, network traffic, energy-related behaviour, and frame timing. Measurements should be made using representative devices, realistic data, and repeatable scenarios.

Responsiveness and frame rendering

A user perceives an application as responsive when input is acknowledged promptly and scrolling remains smooth. Long work on the main thread can cause dropped frames, visible pauses, or an Application Not Responding condition. Rendering performance should be examined with system traces and frame timing rather than judged only by subjective feel.

Useful questions include:

  • Is a slow operation caused by layout, drawing, garbage collection, database access, or synchronization?
  • Does the problem occur only on lower-end hardware?
  • Does performance degrade as the list grows?
  • Is work repeated unnecessarily after recomposition, recreation, or scrolling?

Optimisation should follow measurement. Common improvements include pagination, efficient lists, image resizing and caching, avoiding repeated database queries, reducing unnecessary allocations, and moving expensive work off the main thread. Do not trade correctness or maintainability for an unmeasured optimisation.

Memory

Memory problems include leaks, excessive allocation, retained large objects, and inefficient caches. A leak occurs when an object remains reachable even though it is no longer needed. Typical causes include a singleton retaining an activity, a callback retaining a view, an observer registered with an inappropriate lifecycle, and a static collection accumulating data.

The memory profiler can reveal allocation patterns and heap contents. A useful investigation compares memory before and after repeatedly entering and leaving a screen. If retained memory grows after garbage collection, examine object references and lifecycle ownership. Leak detection tools can provide additional evidence, but their reports still require interpretation.

Caches need explicit size and eviction policies. A cache without limits can convert a performance feature into a memory failure. Large images should be decoded near their display size rather than loaded at original camera resolution.

Battery and network

Battery consumption is affected by CPU work, sensors, wake locks, location, network requests, and repeated background scheduling. Batch non-urgent work, use appropriate constraints, stop work when it is no longer needed, and avoid polling when event-driven mechanisms are available. Network requests should use timeouts, cancellation, caching, and backoff where appropriate.

A useful operational measure is energy per user task, but energy varies by device and environment; therefore it should be compared under a defined scenario rather than treated as a universal constant. Performance claims should state the device, operating-system version, data volume, network condition, and measurement method.

8. Quality Assurance in the Development Process

Quality assurance extends beyond writing tests. A small project can establish a practical quality gate:

  • code compiles with warnings reviewed;
  • unit tests pass;
  • critical UI and integration tests pass;
  • static analysis and lint checks pass;
  • no known critical security or privacy issue remains;
  • accessibility and configuration-change checks are completed;
  • release build is tested on representative hardware;
  • crash monitoring and rollback procedures are available.

Continuous integration can execute these checks for every change. Version control enables review, traceability, and safe rollback. Code review should examine correctness, lifecycle ownership, error handling, test quality, data protection, and performance risks—not merely formatting.

Risk-based testing allocates effort according to likelihood and impact. Authentication, payment, personal data, offline recovery, and destructive operations deserve more extensive testing than a low-impact cosmetic preference. Requirements should be traceable to tests, but traceability does not mean that every line of code requires a separate test.

9. Applied Case: The Reappearing Order Screen

The following is an illustrative case.

Background

A small delivery application allows users to enter an order and submit it. Internal testing reports that, after rotating the device while submission is in progress, the order is sometimes submitted twice. On other occasions the screen displays an error even though the server accepted the order.

Problem analysis

The original activity starts a network request directly and stores the submitted flag in an activity field. Rotation destroys and recreates the activity. The new instance starts another request because the field has returned to its default value. In addition, the callback from the first request updates the old activity, while the second request may produce a different result.

The faults are lifecycle ownership, lack of idempotency, and an ambiguous UI state. The problem is not solved merely by disabling the button for a few seconds.

Approach

The request is moved to a view-model whose state survives configuration changes. The repository assigns an idempotency key to each logical submission; repeated transmissions with that key cannot create multiple orders. The UI observes explicit states such as Idle, Submitting, Success, and Failure. Cancellation and retry rules are documented. A fake repository is used in unit tests.

Tests include:

  • successful submission;
  • server rejection;
  • loss of network connectivity;
  • rotation during submission;
  • repeated taps;
  • recreation after a successful response;
  • retry using the same logical order.

A UI test verifies that the button becomes unavailable during submission and that exactly one confirmation appears. An integration test verifies that the repository preserves the idempotency key.

Outcome

The application now retains submission state across rotation, ignores duplicate user actions, and presents a consistent result. The server-side idempotency rule protects against retries caused by timeouts or process-level events. The team also adds a lifecycle-transition test to its release checklist.

Learning points

The case demonstrates that testing must include state transitions and failure conditions, not only the successful path. It also shows that lifecycle correctness may require cooperation between the client and the server. A local UI fix cannot guarantee business correctness when network operations can be repeated.

10. Challenges, Limitations, and Contemporary Practice

No test suite proves that an application has no defects. Tests sample behaviour, and simulated environments cannot reproduce every device, vendor modification, connectivity condition, or user action. A passing test can also encode an incorrect requirement. Human exploratory testing, production monitoring, and feedback remain important.

Device fragmentation makes a representative test matrix necessary. Select devices and Android versions based on the intended audience and risk. Cloud device testing can broaden coverage, but it may not reproduce local peripherals, unusual network conditions, or all accessibility contexts.

AI-assisted coding and test generation can suggest test cases, explain stack traces, or identify repetitive patterns. However, generated tests may be shallow, reproduce implementation details, contain insecure examples, or accept an incorrect expected result. Developers remain responsible for requirements, data privacy, code review, and verification. Do not upload confidential source code, credentials, or personal data to an unapproved service.

Contemporary Android practice increasingly emphasizes declarative UI, lifecycle-aware asynchronous programming, automated pipelines, baseline performance measurement, accessibility, privacy by design, and observability. These trends change tools, but not the central discipline: define expected behaviour, measure evidence, investigate causes, and prevent recurrence.

Chapter Summary

Testing, debugging, and performance engineering are connected activities in Android development. Unit tests provide fast evidence about isolated logic; integration and instrumentation tests examine component cooperation; UI tests validate user-visible behaviour. Effective tests are isolated, deterministic, clear, and valuable.

Debugging should proceed from observation and reproduction to hypothesis, instrumentation, correction, and regression testing. Lifecycle-aware scopes, explicit state models, cancellation, and idempotent operations prevent many Android-specific defects. Profiling should measure CPU, memory, battery, network, and responsiveness under stated conditions. Optimisation should follow evidence, not assumption.

Quality assurance also includes accessibility, privacy, code review, continuous integration, representative devices, exploratory testing, and release monitoring. No single tool or test level is sufficient; reliable applications emerge from layered evidence and disciplined engineering practice.

Key Terms

Unit test: A test of a small piece of logic in isolation.

UI test: A test of user-visible interaction and resulting interface state.

Instrumentation test: A test executed in an Android environment, often on a device or emulator.

Integration test: A test of cooperation between two or more components.

Test double: A replacement for a real dependency used during testing.

Stub: A test double that supplies predetermined responses.

Fake: A lightweight working implementation used instead of a production dependency.

Mock: A test double that records or verifies interactions.

Flaky test: A test whose result changes without a relevant code or input change.

Regression test: A test that guards against the return of a previously corrected defect.

Lifecycle: The sequence of states through which an Android component passes.

Race condition: Incorrect behaviour caused by timing or ordering of concurrent operations.

Memory leak: Unwanted retention of an object that should no longer be reachable.

Profiling: Measuring runtime behaviour and resource use.

Responsiveness: The speed and smoothness with which an application reacts to input.

Idempotency: A property in which repeating an operation has the same intended effect as performing it once.

Quality gate: A required condition that must be satisfied before progression or release.

Observability: The ability to understand system behaviour from logs, metrics, traces, and reports.

Review Questions

Short Answer Questions

  1. What is the difference between a unit test and a UI test?
  2. Why are arbitrary delays discouraged in asynchronous UI tests?
  3. Define a memory leak in an Android application.
  4. Name two common causes of lifecycle-related defects.
  5. Why should performance optimisation follow measurement?

Long Answer Questions

  1. Explain the arrange–act–assert structure and show how it improves test clarity.
  2. Describe a systematic debugging process from failure reproduction to regression testing.
  3. Compare stubs, fakes, mocks, and spies, including an appropriate use for each.
  4. Explain how configuration changes and process death can affect Android application state.
  5. Discuss how continuous integration, code review, accessibility checks, and release testing contribute to quality assurance.

Analytical / Application Questions

  1. An application crashes only after the user rotates the device during a database save. Design a testing and debugging plan that identifies the likely lifecycle and concurrency causes.
  2. A scrolling screen becomes slow as it displays more images. Identify measurements you would collect, possible causes, and evidence-based improvements.
  3. A payment request occasionally creates two charges after a timeout. Analyse the client and server responsibilities and propose a testing strategy using idempotency and failure simulation.

Multiple Choice Questions with Answers

Question 1

Which test type is normally fastest and most suitable for isolated business logic?

A. Unit test

B. UI test

C. Exploratory test

D. End-to-end device test

Correct Answer: A. Unit test

Question 2

What is the main purpose of a fake repository in a unit test?

A. To provide a controlled lightweight implementation

B. To increase network traffic realism

C. To test the production database directly

D. To replace all assertions

Correct Answer: A. To provide a controlled lightweight implementation

Question 3

Which practice most directly reduces UI-test flakiness?

A. Adding arbitrary long sleeps

B. Synchronizing with the actual asynchronous operation

C. Ignoring failed tests on retry

D. Sharing mutable state between tests

Correct Answer: B. Synchronizing with the actual asynchronous operation

Question 4

What is a likely consequence of retaining an Activity in a long-lived singleton?

A. A memory leak

B. Automatic accessibility improvement

C. Faster database indexing

D. Guaranteed process persistence

Correct Answer: A. A memory leak

Question 5

Why should expensive database or network work normally avoid the main thread?

A. It can make the interface unresponsive

B. It prevents all background execution

C. It disables lifecycle events

D. It guarantees duplicate requests

Correct Answer: A. It can make the interface unresponsive

Question 6

Which item is most appropriate for a regression test?

A. A previously corrected crash scenario

B. An unrecorded personal preference

C. A random unused code path

D. A formatting rule already enforced by the compiler

Correct Answer: A. A previously corrected crash scenario

Question 7

What does idempotency help prevent in a payment or order operation?

A. Duplicate effects when a request is repeated

B. All network latency

C. Every memory allocation

D. All user-interface rendering

Correct Answer: A. Duplicate effects when a request is repeated

Question 8

Which approach is most appropriate when investigating a suspected memory leak?

A. Compare retained memory after repeated screen entry and exit

B. Add more global references

C. Disable garbage collection permanently

D. Measure only application startup time

Correct Answer: A. Compare retained memory after repeated screen entry and exit

Question 9

What should a quality gate primarily provide?

A. A required, reviewable condition for progression or release

B. A replacement for all exploratory testing

C. A way to hide known failures

D. A guarantee that no defects exist

Correct Answer: A. A required, reviewable condition for progression or release

Question 10

Which statement about AI-generated tests is most accurate?

A. They should be reviewed because they may be shallow or encode incorrect expectations

B. They remove the need for requirements analysis

C. They are always safe for confidential source code

D. They prove correctness without execution

Correct Answer: A. They should be reviewed because they may be shallow or encode incorrect expectations

Android App Development
Chapter 11 of 15