I’ve been busy coding without integration tests until I realized that not writing the integration tests was the reason I was busy.
This is part 1 of the ongoing series of articles where we keep adding new screens with more complexity.
Today we are going to write a simple TODO app, so simple that it will just have a TODO list but with 100% integration test coverage. As we are going to follow the TDD principle, we will write failing tests first and then actual code!
We will use Jetpack compose for views with MVVM architecture. For the sake of simplicity, we will not use HILT but it can be added anytime and that should not affect the things we are going to learn in this post.
If you haven’t read our previous articles about jetpack compose with dagger/hilt, MVVM, Navcontroller, and MVVM state management in a simple way using jetpack compose, I suggest checking them out.
We will divide this post into small chunks to make sure we stay focused.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
Open android studio and create a new empty jetpack compose activity project. That will leave you with an app that has a “Hello android” greeting.
Let’s add a jetpack compose function to display an empty screen with “TODOs” title and replace the function in MainActivity
to use it.
@Composable
fun TodoList() {
}
MainActivity
onCreate
setContent {
TodoTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
TodoList()
}
}
}
Simple isn’t it? Now before we write code to display the title, let’s first write a test to verify that title is shown and make sure it fails.
Add TodoListTest
file in androidTest
directory. Make sure you don’t add it in test
directory as we are not writing unit tests.
The initial state of the test file should look like this.
class TodoListTest {
@Before
fun setUp() {
}
@After
fun tearDown() {}
}
Make sure you have the following dependencies added to your app module’s build.gradle
file.
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
To test the screen on an actual emulator, we will need to use createComposeRule()
as a rule that will launch the app with the compose view and will close the app once the test is done running.
Add this to the top of the class
@get:Rule
val composeTestRule = createComposeRule()
and then update setUp
function to set compose view as compose rule content
public override fun setUp() {
super.setUp()
composeTestRule.setContent { TodoList() }
composeTestRule.waitForIdle()
}
We are done with the basic setup, now it’s time to write the test.
Add the first test
@Test
fun testScreenTitleIsShown() {
composeTestRule.onNodeWithText("TODOs").assertIsDisplayed()
}
Well, that’s it! The test is pretty self-explanatory. We are trying to find a node (View) with the text “TODOs” and verifying that it’s actually displayed on the screen.
You can right-click on the test and select Run 'testScreenTitleIsShown
from the context menu, wait for the test to finish, and verify that it fails!
Now let’s update the code to make the test green!
@Composable
fun TodoList() {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "TODOs", fontSize = 22.sp,
modifier = Modifier.padding(vertical = 8.dp))
}
}
Now run tests again and verify that it passes. This is one iteration of the TDD cycle. Every iteration should add a failing test first and later actual code to make that test pass.
For simplicity, we will add a repository to provide 3 to-do items and use it here instead of fetching todos from API or DB.
Let’s start by adding Task
class.
data class Task(val title: String)
and a repository interface that ViewModel will use to fetch tasks
interface TaskRepository {
fun getTasks(): List<Task>
}
Let’s create an implementation that will provide 3 static tasks
class TaskRepositoryImpl : TaskRepository {
override fun getTasks(): List<Task> {
return listOf(
Task("Hello TDD"),
Task("This is fun"),
Task("I can not live without TDD")
)
}
}
and TodoViewModel
to manage the state for the view
class TodoViewModel(
private val taskRepository: TaskRepository
) : ViewModel() {
val tasks = MutableStateFlow(taskRepository.getTasks())
}
Now let’s update the view and activity to take ViewModel as an argument
val viewModel by viewModels<TodoViewModel>()
setContent {
TodoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TodoList(viewModel)
}
}
}
@Composable
fun TodoList(viewModel: TodoViewModel) {
val tasks by viewModel.tasks.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "TODOs", fontSize = 22.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
TaskView(tasks = tasks )
}
}
@Composable
fun TaskView(tasks: List<Task>) {
}
It’s time to write a failing test now, remember we need to write it before the actual code.
Add viewModel
variable and update setUp
function
private val viewModel = TodoViewModel(TaskRepositoryImpl())
@Before
fun setUp() {
composeTestRule.setContent { TodoList(viewModel) }
composeTestRule.waitForIdle()
}
and add the test
@Test
fun testTaskItemsAreShown() {
composeTestRule.onNodeWithText("Hello TDD").assertIsDisplayed()
composeTestRule.onNodeWithText("This is fun").assertIsDisplayed()
composeTestRule.onNodeWithText("I can not live without TDD").assertIsDisplayed()
}
If you run the test, it will fail as expected. Now let’s update the code to show the task list.
@Composable
fun TaskView(tasks: List<Task>) {
tasks.forEach {
Text(text = it.title, fontSize = 16.sp, modifier = Modifier.padding(16.dp))
}
}
That’s it. Run the tests again and verify that they all pass.
Hope you learned something useful!
Structured development with TDD and MVVM in Jetpack Compose ensures robust, scalable TODO apps. While opting out of HILT for simplicity, our approach prioritizes functionality and smooth integration, demonstrating adherence to best practices for future-proofed, maintainable projects.
Happy coding!