Chapter 9 - Testing and Validating the GetArtUseCase (Data layer)
In this chapter, we will test how data is fetched, stored, and mapped between the data and domain layers.
We will test these components for the GetArtUseCase
ArtEntity - Room database table
ArtDao - database access functions
Mappers - convert between Entity/DTO ↔ Domain model. We are going to test both the functions Art.toEntity() and ArtEntity.toDomainModel()
ArtRepositoryImpl class functions.
STEP 1 - Testing the Mapper (Data - Domain Bridge)
We want to ensure that our data model ArtEntity correctly map to domain model Art and vice versa. This test validates that data integrity is preserved when crossing architectural boundaries.
If mapping is wrong:
The UI will show wrong text or image.
Domain logic may break silently.
Even if database is correct, we will get wrong behaviour.
Hence mappers are small but critical to ensure clean flow between layers.
package com.learning.artsnapapp.data.mappers
import com.learning.artsnapapp.data.local.entities.ArtEntity
import com.learning.artsnapapp.data.mapper.toDomainModel
import com.learning.artsnapapp.data.mapper.toEntity
import com.learning.artsnapapp.domain.model.Art
import com.google.common.truth.Truth.assertThat
import org.junit.Test
/**
* ## ArtMapperTest
*
* ### Purpose
* This test verifies that the mapping functions between
* - Data layer (`ArtEntity`)
* - Domain layer (`Art`)
* are consistent and lossless.
*
* ### Importance
* Mapper tests are crucial in Clean Architecture because:
* - They ensure the **Domain layer** receives clean, correctly-shaped data.
* - They validate that no field is lost or wrongly mapped between layers.
* - They isolate mapping logic for easier debugging.
*
* ### Type
* Unit Test (pure Kotlin, no Android dependencies)
*/
class ArtMapperTest {
@Test
fun `entity to domain art mapping is correct`(){
//step 1: given a fake ArtEntity
val entity = ArtEntity(name = "Shiny srinkle", artistName = "Demarc Ratra",year = "1989", imageUrl = "url1.jpg",id = 1)
//step 2: map the entity to domain
val domainArt = entity.toDomainModel()
//step 3: then both should hold identical values
assertThat(domainArt.year).isEqualTo(entity.year)
assertThat(domainArt.artistName).isEqualTo(entity.artistName)
assertThat(domainArt.name).isEqualTo(entity.name)
assertThat(domainArt.imageUrl).isEqualTo(entity.imageUrl)
assertThat(domainArt.id).isEqualTo(entity.id)
}
@Test
fun `domain to entity art mapping is correct`(){
//step 1: given a fake domain Art object
val domain = Art(
id = 3,
name = "Ahuja Mishi",
artistName = "Shan Parshi",
year = "1989",
imageUrl = "image.png"
)
//step 2: map the domain to Entity
val entity = domain.toEntity()
//step 3: then all values must match
assertThat(entity.id).isEqualTo(domain.id)
assertThat(entity.name).isEqualTo(domain.name)
assertThat(entity.imageUrl).isEqualTo(domain.imageUrl)
assertThat(entity.artistName).isEqualTo(domain.artistName)
assertThat(entity.year).isEqualTo(domain.year)
}
}
| Concept | Explanation |
| Purpose | Ensure no data mismatch between Entity and Domain |
| Type of test | Pure Kotlin unit test (no Android dependencies) |
| Why important | Keeps layers clean and predictable |
| Expected outcome | All assertions pass, showing correct mapping |
STEP 2 — DAO Testing with Room (in-memory database)
We now test if the DAO (Data Access Object) properly interacts with Room —
that is, can we insert, retrieve, and delete artworks in a local database correctly?
We use an in-memory Room database, so the tests run fast, leave no files, and can be repeated endlessly.
DAO is the actual link to persistent storage.
If DAO misbehaves:
The repository might emit wrong results.
Deletions or insertions could silently fail.
By testing the DAO directly, we ensure Room is correctly set up before the repository starts using it.
It is important for u to introduce the Database creation here that holds the ArtEntity table. Without this DB there is no DAO interaction possible.
package com.learning.artsnapapp.data.local.db
import androidx.room.Database
import androidx.room.RoomDatabase
import com.learning.artsnapapp.data.local.dao.ArtDao
import com.learning.artsnapapp.data.local.entities.ArtEntity
/**
* The ArtDatabase class represents the Room database for the app.
*
* - It defines which entities (tables) belong to this database.
* - It exposes abstract DAO functions, like `artDao()`, which Room
* auto-generates implementations for at compile time.
*
* This class is abstract because Room will generate the underlying
* implementation (containing SQL and caching logic) behind the scenes.
*
* `version` is used for migration tracking.
* `exportSchema` is disabled here to keep things simple during development.
*/
@Database(
entities = [ArtEntity::class],
version = 1,
exportSchema = false
)
abstract class ArtDatabase : RoomDatabase() {
abstract fun artDao(): ArtDao
}
To test ArtDao means means to verify whether :
insertArt() correctly stores entities.
getAllArts() returns what we inserted.
deleteArt() removes the correct record.
package com.learning.artsnapapp.data.local
import com.learning.artsnapapp.data.local.dao.ArtDao
import com.learning.artsnapapp.data.local.db.ArtDatabase
import com.learning.artsnapapp.data.local.entities.ArtEntity
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import com.google.common.truth.Truth.assertThat
/**
* DAO Layer Testing :
*
* - We use `Room.inMemoryDatabaseBuilder()` so the DB is recreated fresh for every test.
* - These are Android instrumentation tests (require AndroidJUnit4).
* - The goal is to validate pure data operations (CRUD) — not business logic.
* - Using `assertThat()` ensures the DAO behaves as expected before integrating it with Repository.
* - No Hilt, Retrofit, or UseCases involved — this isolates the DAO behavior.
*/
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class ArtDaoTest {
private lateinit var database: ArtDatabase
private lateinit var dao: ArtDao
/**
* This method runs before every test.
* It sets up an in-memory database (temporary, does not persist data).
* Room provides this utility to isolate tests without touching real storage.
*/
@Before
fun setup(){
database = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(),ArtDatabase::class.java)
.allowMainThreadQueries().build() // Allow main-thread access only for test simplicity (never in production)
dao = database.artDao() //retrieve DAO instance from our in-memory DB
}
/*
* This method runs after each test
* It closes the in-memory database to release resources
* */
@After
fun teardown(){
database.close()
}
/*
* Test 1: Insert operation
*
* Goal: Verify that inserting an ArtEntity successfully saves it in the database.
* Steps:
* - create a dummy entity
* - insert it via DAO
* - retrieve all records
* - assert that the inserted entity exists in the result
*
* runBlocking needed because Room DAO uses suspend functions
* */
@Test
fun insert_savesEntitySuccessfully() = runBlocking {
val artEntity = ArtEntity(name="Rang de basanti", year = "2001", artistName = "Raj Kunder", imageUrl = "url1")
dao.insertArt(artEntity) // Insert into in-memory DB
//collect the first emitted list from the DAO's flow
val allArts = dao.getAllArts().first()
//verify inserted record is present
assertThat(allArts).contains(artEntity)
}
/*
* Test 2: Delete operation
*
* Goal: Verify that deleting an ArtEntity removes it from the database.
Steps:
* - Insert a dummy entity first.
* - Delete it using DAO.
* - Retrieve all records.
* - Assert that the deleted entity no longer exists.
*
* runBlocking needed because Room DAO uses suspend functions
* */
@Test
fun delete_savesEntitySuccessfully() = runBlocking {
val artEntity = ArtEntity(name="Rang de basanti", year = "2001", artistName = "Raj Kunder", imageUrl = "url1")
dao.insertArt(artEntity) // Insert into in-memory DB
dao.deleteArt(artEntity)
val allArts = dao.getAllArts().first()
// Verify record was removed
assertThat(allArts).doesNotContain(artEntity)
}
}
The in-line Javadoc comments above each function underlines the idea behind the testing logic.
STEP 3 - Testing ArtRepositoryImpl class
We want to test and confirm that the repository correctly:
Converts data between layers (Entity ↔ Art).
Delegates to the DAO as expected.
Emits the right data (Flow).
package com.learning.artsnapapp.data.repository
import com.learning.artsnapapp.data.local.dao.ArtDao
import com.learning.artsnapapp.data.local.entities.ArtEntity
import com.learning.artsnapapp.data.repo.ArtRepositoryImpl
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
/**
* Tests the ArtRepositoryImpl class to verify that
* - Data from DAO is correctly mapped to domain model.
* - The Flow emitted matches the expected data.
* - The repository interacts correctly with DAO and API layers.
*/
class ArtRepositoryImplTest {
private lateinit var fakeDao: FakeArtDao
private lateinit var repository: ArtRepositoryImpl
@Before
fun setup(){
fakeDao = FakeArtDao()
repository = ArtRepositoryImpl(fakeDao)
}
@Test
fun `getArt returns mapped domain objects`() = runTest {
//arrange
val testArtEntity =
ArtEntity(name = "jalwa fariq", artistName = "rehman malik", year = "1989", imageUrl = "url2")
fakeDao.insertArt(testArtEntity)
val result = repository.getArt().first()
//assert
assertThat(result.size).isEqualTo(1)
assertThat(result[0].year).isEqualTo("1989")
}
}
class FakeArtDao : ArtDao {
private val data = mutableListOf<ArtEntity>()
override fun getAllArts(): Flow<List<ArtEntity>> = flowOf(data)
override suspend fun insertArt(artEntity: ArtEntity) {
data.add(artEntity)
}
override suspend fun deleteArt(artEntity: ArtEntity) {
data.remove(artEntity)
}
}
Using FakeArtDao
We build a FakeArtDao that implements the DAO contract; it holds data in a MutableStateFlow so getAllArts() behaves like Room’s reactive stream — immediately emitting the current list and emitting updates when inserts or deletes occur. This keeps tests fast, deterministic, and realistic.
A Fake is a lightweight, in-memory implementation of the real ArtDao interface.
FakeArtDao mimics the behaviour of the Room DAO but uses plain Kotlin collections and Flows instead of a real database.
It's designed for fast, deterministic tests of higher layers (repository, use cases, ViewModel) without using Room or an emulator.
Fake has real behaviour (stores and returns data) so it's ideal for validating interactions and data flow.
It must Implement the ArtDao interface exactly (same function signatures).
Expose a Flow<List<ArtEntity>> for getAllArts() that behaves like Room: emits the current list and emits new lists when data changes.
Provide working insertArt() and deleteArt() semantics which mutate internal state.
Be coroutine-friendly (suspend functions behave like the real DAO).