Testing & Debugging

Find bugs faster with Logcat, breakpoints, unit tests, and Espresso UI tests

Logcat Debug JUnit Espresso

Table of Contents

Testing Overview

Quality Android apps require both debugging (finding and fixing issues during development) and testing (automated checks that prevent regressions). Android Studio provides Logcat, the debugger, JUnit for unit tests, and Espresso for UI tests.

Development Testing Pyramid ───────────── ─────────────── Logcat + Debugger UI Tests (Espresso) — few, slow ↓ Integration Tests — some Fix bugs manually ↓ Unit Tests (JUnit) — many, fast
ToolPurposeRuns On
LogcatView runtime logsDevice / emulator
DebuggerStep through code, inspect variablesDevice / emulator
JUnitUnit test ViewModels, RepositoriesJVM (local machine)
EspressoAutomated UI interaction testsDevice / emulator

1Logcat

Logcat displays system and app log messages in real time. Use it to trace execution flow, inspect API responses, and diagnose crashes by reading stack traces.

Log levels

LevelMethodWhen to Use
VerboseLog.v()Detailed tracing (remove in production)
DebugLog.d()Development debugging info
InfoLog.i()General informational messages
WarningLog.w()Potential issues, recoverable errors
ErrorLog.e()Failures, exceptions with stack trace

Writing logs in Kotlin

Kotlin — Log usageprivate const val TAG = "TaskViewModel" class TaskViewModel : ViewModel() { fun loadTasks() { Log.d(TAG, "loadTasks() called") viewModelScope.launch { try { val tasks = repository.getTasks() Log.d(TAG, "Fetched ${tasks.size} tasks") _tasks.value = tasks } catch (e: Exception) { Log.e(TAG, "Failed to load tasks", e) _error.value = e.message } } } }

Logcat filters

Logcat — Useful filter expressions// Filter by tag tag:TaskViewModel // Filter by package package:com.example.myapp // Filter by log level level:ERROR // Combine filters tag:Retrofit level:DEBUG // Search for crash FATAL EXCEPTION

Timber — structured logging library

Kotlin — Timber setup// build.gradle.kts implementation("com.jakewharton.timber:timber:5.0.1") // Application class class MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } } } // Usage — no TAG needed Timber.d("User logged in: %s", userId) Timber.e(exception, "API call failed")

2Debugging Techniques

Effective debugging combines logging, Android Studio tools, and systematic investigation. Start by reproducing the bug, then narrow down the cause using logs, the Layout Inspector, and Network Inspector.

Common debugging workflow

  1. Reproduce the bug consistently
  2. Check Logcat for exceptions and error messages
  3. Add strategic Log.d() statements or breakpoints
  4. Inspect variable values in the debugger
  5. Fix, verify, and remove temporary debug code

Layout Inspector

Open View → Tool Windows → Layout Inspector while the app runs on a device. It shows the live view hierarchy, properties, and helps debug UI overlap, wrong dimensions, and missing views.

Network Inspector

Open View → Tool Windows → App Inspection → Network Inspector to monitor HTTP requests, response codes, timing, and payload sizes — essential for debugging API integration issues.

Debug build vs release

FeatureDebug BuildRelease Build
DebuggingEnabledDisabled
Logcat logsVisibleStripped / minimized
Code obfuscationOffProGuard / R8 enabled
App signingDebug keystoreRelease keystore

StrictMode — detect main thread violations

Kotlin — StrictMode in Applicationclass MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build() ) } } }

3Breakpoints

Breakpoints pause app execution at a specific line so you can inspect variables, evaluate expressions, and step through code line by line in Android Studio's debugger.

Setting and using breakpoints

  1. Click the gutter (left margin) next to a line number to set a breakpoint
  2. Run the app in Debug mode (Shift+F9 or Debug button)
  3. Trigger the code path — execution pauses at the breakpoint
  4. Inspect variables in the Variables panel
  5. Use Step Over (F8), Step Into (F7), Step Out (Shift+F8) to navigate

Conditional breakpoint

Kotlin — Code with conditional breakpointfun processTasks(tasks: List<Task>) { for (task in tasks) { // Set breakpoint here with condition: task.id == 42 val result = validateTask(task) saveTask(result) } }

Right-click a breakpoint → Edit Breakpoint → add a condition like task.id == 42 so it only pauses for that specific case.

Logpoint (breakpoint without pausing)

Right-click gutter → Add Logpoint. Logs a message when the line is hit without stopping execution — useful when you don't want to pause the app flow.

Evaluate Expression

Kotlin — Evaluate during debug pause// While paused at breakpoint, open Evaluate Expression (Alt+F8) tasks.size tasks.filter { it.completed }.map { it.title } repository.getTasks()

Debugger shortcuts

ActionShortcut (Windows/Linux)
Debug appShift + F9
Resume programF9
Step overF8
Step intoF7
Evaluate expressionAlt + F8
View breakpointsCtrl + Shift + F8

4Unit Testing

Unit tests verify individual classes in isolation on the JVM — no device needed. Test ViewModels, Repositories, and utility functions with JUnit, Mockito, and coroutine test libraries.

Gradle dependencies

Kotlin — app/build.gradle.ktsdependencies { testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-core:5.8.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") testImplementation("androidx.arch.core:core-testing:2.2.0") }

Simple JUnit test

Kotlin — ValidatorTest.kt (test/)class ValidatorTest { @Test fun `valid email returns true`() { assertTrue(Validator.isValidEmail("user@example.com")) } @Test fun `invalid email returns false`() { assertFalse(Validator.isValidEmail("not-an-email")) } @Test fun `empty password returns false`() { assertFalse(Validator.isValidPassword("")) } }

ViewModel test with Mockito

Kotlin — TaskViewModelTest.kt@OptIn(ExperimentalCoroutinesApi::class) class TaskViewModelTest { @get:Rule val instantExecutorRule = InstantTaskExecutorRule() private val testDispatcher = StandardTestDispatcher() private lateinit var repository: TaskRepository private lateinit var viewModel: TaskViewModel @Before fun setup() { Dispatchers.setMain(testDispatcher) repository = mock() viewModel = TaskViewModel(repository) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `loadTasks updates tasks LiveData on success`() = runTest { val fakeTasks = listOf(Task(1, "Test Task", false)) whenever(repository.getTasks()).thenReturn(Result.success(fakeTasks)) viewModel.loadTasks() testDispatcher.scheduler.advanceUntilIdle() assertEquals(fakeTasks, viewModel.tasks.value) } @Test fun `loadTasks sets error on failure`() = runTest { whenever(repository.getTasks()).thenReturn(Result.failure(Exception("Network error"))) viewModel.loadTasks() testDispatcher.scheduler.advanceUntilIdle() assertEquals("Network error", viewModel.error.value) } }

Test directory structure

app/src/ ├── main/java/ ← production code ├── test/java/ ← unit tests (JVM, fast) └── androidTest/java/ ← instrumented tests (device, Espresso)

5Espresso UI Testing

Espresso automates UI interactions — clicks, text input, scrolling — and verifies what appears on screen. UI tests run on a device or emulator and catch regressions in user flows.

Gradle dependencies

Kotlin — app/build.gradle.ktsdependencies { androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:runner:1.5.2") androidTestImplementation("androidx.test:rules:1.5.0") }

Basic Espresso test

Kotlin — MainActivityTest.kt (androidTest/)@RunWith(AndroidJUnit4::class) class MainActivityTest { @get:Rule val activityRule = ActivityScenarioRule(MainActivity::class.java) @Test fun loginButton_isDisplayed() { onView(withId(R.id.btnLogin)) .check(matches(isDisplayed())) } @Test fun enterNameAndSubmit_showsGreeting() { onView(withId(R.id.etName)) .perform(typeText("Nikhil"), closeSoftKeyboard()) onView(withId(R.id.btnSubmit)) .perform(click()) onView(withId(R.id.tvGreeting)) .check(matches(withText("Hello, Nikhil!"))) } }

Common Espresso matchers and actions

CategoryExamples
Find viewwithId(R.id.btn), withText("Login")
Actionsclick(), typeText("hello"), scrollTo()
AssertionsisDisplayed(), withText("..."), isEnabled()
RecyclerViewonView(withId(R.id.recyclerView)).perform(actionOnItemAtPosition(0, click()))

RecyclerView item test

Kotlin — Test RecyclerView click@Test fun clickFirstTask_opensDetailScreen() { onView(withId(R.id.recyclerView)) .perform( RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()) ) onView(withId(R.id.detailTitle)) .check(matches(isDisplayed())) }

6Error Handling

Robust error handling prevents crashes and gives users clear feedback. Combine try-catch blocks, Result types, global exception handlers, and user-friendly error UI to build resilient Android apps.

Result type pattern

Kotlin — Repository with Resultclass TaskRepository(private val api: ApiService) { suspend fun getTasks(): Result<List<Task>> { return try { val tasks = api.getTasks() Result.success(tasks) } catch (e: IOException) { Log.e(TAG, "Network error", e) Result.failure(NetworkException("Check your internet connection")) } catch (e: HttpException) { Log.e(TAG, "HTTP ${e.code()}", e) Result.failure(ApiException("Server error: ${e.code()}")) } catch (e: Exception) { Log.e(TAG, "Unexpected error", e) Result.failure(e) } } }

UiState for error display

Kotlin — Handle errors in UIsealed class UiState<out T> { object Loading : UiState<Nothing>() data class Success<T>(val data: T) : UiState<T>() data class Error(val message: String, val retry: (() -> Unit)? = null) : UiState<Nothing>() } // In Fragment/Activity viewModel.uiState.observe(viewLifecycleOwner) { state -> when (state) { is UiState.Loading -> showLoading() is UiState.Success -> showData(state.data) is UiState.Error -> { hideLoading() binding.errorLayout.isVisible = true binding.tvError.text = state.message binding.btnRetry.setOnClickListener { state.retry?.invoke() } } } }

Global uncaught exception handler

Kotlin — Crash handler in Applicationclass MyApplication : Application() { override fun onCreate() { super.onCreate() val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> Log.e("CRASH", "Uncaught exception in ${thread.name}", throwable) // Report to Firebase Crashlytics: FirebaseCrashlytics.getInstance().recordException(throwable) defaultHandler?.uncaughtException(thread, throwable) } } }

Crashlytics integration

Kotlin — Firebase Crashlytics// build.gradle.kts implementation("com.google.firebase:firebase-crashlytics-ktx") // Log non-fatal exceptions try { riskyOperation() } catch (e: Exception) { FirebaseCrashlytics.getInstance().recordException(e) showError("Operation failed") } // Add custom keys for debugging FirebaseCrashlytics.getInstance().setCustomKey("user_id", currentUserId)

Error handling checklist

LayerStrategy
Repositorytry-catch, return Result<T>
ViewModelMap errors to UiState.Error
UIShow message, offer Retry button
App-wideCrashlytics, uncaught exception handler
DevelopmentLogcat, StrictMode, debug logs

7Hands-On Exercises

1

Add Log.d() and Log.e() to a ViewModel. Filter output in Logcat by tag and log level.

2

Use Layout Inspector and Network Inspector to debug a UI layout issue and an API call.

3

Set a conditional breakpoint in a loop. Inspect variables and use Step Over to trace execution.

4

Write a JUnit unit test for a Validator utility class with at least three test cases.

5

Test a ViewModel with Mockito — mock the Repository and verify success and error states.

6

Write an Espresso test that types text, clicks a button, and verifies the result TextView.

7

Implement error handling with UiState.Error, a Retry button, and Logcat logging in the Repository.

Testing & Debugging MCQ Practice

10 Basic MCQs 10 Advanced MCQs

10 Basic Testing & Debugging MCQs

1

Logcat displays?

AAPK signature
BSystem and app log messages
CLayout XML only
DGradle cache
Explanation: Filter by tag, level, package for debugging.
2

Log.d tag used for?

ARelease signing
BError only
CDebug level logging
DDatabase
Explanation: Log.d("TAG", "message") — stripped in release if configured.
3

JUnit used for?

ABluetooth
BUI tests on device only
CAPK publishing
DUnit tests on JVM
Explanation: Fast tests for ViewModel, repository logic without device.
4

Espresso tests?

AUI instrumentation tests on device/emulator
BUnit tests only
CNetwork layer
DProGuard
Explanation: onView(withId(R.id.btn)).perform(click()).
5

Breakpoint in Android Studio?

ADeletes code
BPauses debugger at line
CBuilds release
DSigns APK
Explanation: Inspect variables and step through execution.
6

androidTest folder contains?

AManifest
BUnit tests only
CInstrumented tests
DAssets
Explanation: Tests run on Android device with full framework.
7

test folder contains?

AEmulator images
BInstrumented tests
CRelease APK
DLocal unit tests (JVM)
Explanation: src/test — run on development machine quickly.
8

adb logcat useful for?

AView logs from connected device via CLI
BInstall Gradle
CEdit layouts
DSign bundle
Explanation: adb logcat | grep MyTag for filtered output.
9

Try-catch in production code?

AReplace all tests
BHandle expected exceptions gracefully
CDebug only pattern
DBanned
Explanation: Don't swallow — log and show user-friendly errors.
10

Debug build type?

ANo logging
BOptimized release
Cdebuggable true with debug keystore
DPlay Store only
Explanation: Attach debugger, use Logcat, run debug variants.

10 Advanced Testing & Debugging MCQs

11

MockK/Mockito used for?

AUI layout
BMock dependencies in unit tests
CNetwork TLS
DCamera
Explanation: whenever(repo.getData()).thenReturn(fakeList)
12

InstantTaskExecutorRule?

AProGuard
BEspresso rule
CRuns LiveData/arch tasks synchronously in tests
DLint
Explanation: JUnit rule for LiveData immediate execution in unit tests.
13

Layout Inspector?

AGradle
BSQL inspector
CNetwork profiler only
DVisual UI hierarchy debugger in Studio
Explanation: Inspect view properties on running app.
14

LeakCanary detects?

AMemory leaks in debug builds
BNetwork errors
CLint issues
DAPK size
Explanation: Square library watches retained objects.
15

runTest in coroutine tests?

AMain thread block
BStructured coroutine test scope with virtual time
CEspresso
DADB
Explanation: kotlinx-coroutines-test runTest { viewModel.load() }
16

Firebase Test Lab?

ALint server
BLocal unit only
CCloud device farm for instrumented tests
DPlay upload
Explanation: Run tests on many real device configurations.
17

StrictMode?

ASigning
BRelease optimization
CProGuard
DDetect main thread disk/network violations in debug
Explanation: Enable in Application onCreate for debug builds.
18

IdlingResource in Espresso?

AWait for async work before next UI action
BSkip animations always
CMock network
DUnit test
Explanation: Register idling resource until background job completes.
19

Test doubles: fake vs mock?

ASame
BFake has working simplified impl; mock verifies interactions
CMock always better
DFake is deprecated
Explanation: Fake in-memory repository vs mock verify call count.
20

Baseline Profile?

ALint baseline
BScreenshot test
CAOT hints improving startup via Macrobenchmark
DProGuard map
Explanation: Generate profile for performance regression testing.
Click an option to select, then check answers.

Testing & Debugging Interview Q&A

15 topic-focused questions for interviews and revision.

1How to filter Logcat.easy
Answer: Package name filter, tag:MyApp, level Error, regex search in Android Studio Logcat.
2Write simple JUnit test.easy
Answer: @Test fun addition() { assertEquals(4, 2+2) } in src/test.
3Espresso click button test.medium
Answer: onView(withId(R.id.submit)).perform(click()); onView(withText("Success")).check(matches(isDisplayed()))
4Debug with breakpoint steps.easy
Answer: Set breakpoint, Debug run, Step Over/Into, inspect Variables panel.
5Unit test ViewModel.medium
Answer: Fake repository, InstantTaskExecutorRule or runTest, assert LiveData/StateFlow values.
6Difference unit vs instrumented test.easy
Answer: Unit: JVM fast isolated. Instrumented: needs device, full Android framework.
7Find crash stack trace.easy
Answer: Logcat red stack trace; Firebase Crashlytics in production.
8Mock repository in test.medium
Answer: mockk(); coEvery { repo.load() } returns listOf(...)
9Why tests in CI?medium
Answer: Catch regressions before release; automated on every PR.
10Test coroutines in ViewModel.hard
Answer: runTest, StandardTestDispatcher, advanceUntilIdle, Turbine for Flow.
11Layout Inspector use case.easy
Answer: Debug wrong padding, overlapping views, missing constraints.
12StrictMode example penalty.medium
Answer: detectDiskReads(), penaltyLog() on main thread file read in debug.
13Espresso RecyclerView test.hard
Answer: onView(withId(R.id.recycler)).perform(actionOnItemAtPosition(0, click()))
14ProGuard and debugging release.hard
Answer: Upload mapping.txt to Play Console; use retrace for obfuscated stack traces.
15Testing pyramid on Android.medium
Answer: Many unit tests, fewer integration, few UI Espresso — fast feedback loop.