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
Tool
Purpose
Runs On
Logcat
View runtime logs
Device / emulator
Debugger
Step through code, inspect variables
Device / emulator
JUnit
Unit test ViewModels, Repositories
JVM (local machine)
Espresso
Automated UI interaction tests
Device / 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
Level
Method
When to Use
Verbose
Log.v()
Detailed tracing (remove in production)
Debug
Log.d()
Development debugging info
Info
Log.i()
General informational messages
Warning
Log.w()
Potential issues, recoverable errors
Error
Log.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
Reproduce the bug consistently
Check Logcat for exceptions and error messages
Add strategic Log.d() statements or breakpoints
Inspect variable values in the debugger
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
Feature
Debug Build
Release Build
Debugging
Enabled
Disabled
Logcat logs
Visible
Stripped / minimized
Code obfuscation
Off
ProGuard / R8 enabled
App signing
Debug keystore
Release 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
Click the gutter (left margin) next to a line number to set a breakpoint
Run the app in Debug mode (Shift+F9 or Debug button)
Trigger the code path — execution pauses at the breakpoint
Inspect variables in the Variables panel
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
Action
Shortcut (Windows/Linux)
Debug app
Shift + F9
Resume program
F9
Step over
F8
Step into
F7
Evaluate expression
Alt + F8
View breakpoints
Ctrl + 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.
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.