MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Starting automation from scratch is intimidating. Most guides assume you already have a setup. This one starts at zero — no existing tests, no infrastructure — and walks you through the first 30 days.
On this page
You've been testing manually. Everyone agrees automation is needed. Nobody knows where to start. The codebase has zero tests.
Here's the actual playbook.
Teams start with UI automation because "that's what you can see." They write brittle end-to-end tests. Those tests break constantly. The team loses confidence, the suite gets abandoned, and automation gets a bad reputation.
The right starting point isn't the most visible layer. It's the most stable one.
Before writing a single test, establish the infrastructure:
For Android (Kotlin):
// build.gradle.kts
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.10")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
testImplementation("app.cash.turbine:turbine:1.1.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}./gradlew testIt should complete with "0 tests ran." This verifies the infrastructure works before you write anything.
class SanityTest {
@Test
fun `addition works`() {
assertEquals(4, 2 + 2)
}
}This sounds pointless but it confirms:
Don't start with the whole codebase. Identify the one or two functions where a bug would be most catastrophic.
In a task manager app, that's task creation and completion logic. In a payment app, it's billing calculation. In a content platform, it's access control.
Write 5-10 tests covering that logic with meaningful assertions. Not line coverage — behavior coverage.
class TaskServiceTest {
private val repository = FakeTaskRepository()
private val service = TaskService(repository)
@Test
fun `new task is created with incomplete status`() = runTest {
service.createTask("Write tests")
val task = repository.getAllTasks().first()
assertFalse(task.completed)
}
@Test
fun `cannot create task with blank title`() = runTest {
val result = service.createTask("")
assertTrue(result.isFailure)
assertThat(result.exceptionOrNull()).isInstanceOf(ValidationException::class.java)
}
@Test
fun `completing a task marks it done`() = runTest {
val task = service.createTask("Buy groceries").getOrThrow()
service.completeTask(task.id)
val updated = repository.getTask(task.id)
assertTrue(updated!!.completed)
}
}Goal: 10-15 unit tests by end of week 2. All passing. All in CI.
Map out every class that contains business logic:
Write tests for each one. Don't chase 100% coverage — cover the decisions and transformations.
[!TIP] Create a
orcodetestFixturesdirectory for fake implementations (FakeRepository, FakeNetworkClient). You'll reuse these across many tests. Writing fakes forces you to design better interfaces.codefakes
A fake repository example:
class FakeTaskRepository : TaskRepository {
private val tasks = mutableListOf<Task>()
override suspend fun createTask(task: Task) {
tasks.add(task)
}
override suspend fun getAllTasks(): List<Task> = tasks.toList()
override suspend fun getTask(id: String): Task? = tasks.find { it.id == id }
override suspend fun deleteTask(id: String) {
tasks.removeAll { it.id == id }
}
// Test helper — not part of the interface
fun addTask(task: Task) = tasks.add(task)
fun clear() = tasks.clear()
}Goal: 30-50 unit tests covering core business logic.
Now add one integration test that tests a real component working together with its dependency:
@RunWith(AndroidJUnit4::class)
class TaskDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: TaskDao
@Before
fun setup() {
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build()
dao = db.taskDao()
}
@After
fun teardown() = db.close()
@Test
fun `inserting a task makes it retrievable`() = runTest {
val task = TaskEntity(id = "1", title = "Test", completed = false)
dao.insert(task)
val result = dao.getById("1")
assertNotNull(result)
assertEquals("Test", result!!.title)
}
}Goal: 5-10 integration tests covering database and API interactions.
After 30 days, you should have:
Zero UI tests. That comes later, once the lower layers are stable.
Trying to automate everything at once. You can't automate a legacy codebase in a sprint. You can add 10 valuable tests per week, indefinitely.
Consistency beats ambition. Ten tests per week is 500 tests in a year — more than enough to give your team confidence.
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus