Test: Add unit tests for repository use cases

This commit introduces unit tests for the following use cases in the repository layer:
- `ObserveAirportsUseCaseImpl`
- `GetFlightsSearchContentUseCaseImpl`
- `GetConnectionsForAirportUseCaseImpl`

Key changes:
- Added `ObserveAirportsUseCaseImplTest.kt` with tests for observing airport data, including scenarios with valid data, empty data, null data, and cache refresh.
- Added `GetFlightsSearchContentUseCaseImplTest.kt` with tests for searching flights, covering valid responses and responses with null values.
- Added `GetConnectionsForAirportUseCaseImplTest.kt` with tests for fetching airport connections, including scenarios with valid responses, empty responses, responses with null airport codes/names, and multiple route responses.
- Added `kotlinx-coroutines-test` dependency to `model/repository/build.gradle.kts` for testing coroutines.
This commit is contained in:
2025-06-16 00:55:12 +02:00
parent e8ac7c5596
commit 8ce553240c
4 changed files with 771 additions and 0 deletions

View File

@ -18,4 +18,5 @@ dependencies {
implementation(libs.kotlinx.datetime)
testImplementation(libs.mockk.android)
testImplementation(libs.kotlinx.coroutines.test)
}

View File

@ -0,0 +1,215 @@
package dev.adriankuta.flights.model.repository.usecases
import dev.adriankuta.flights.domain.types.Airport
import dev.adriankuta.flights.domain.types.MacCity
import dev.adriankuta.model.data.api.RoutesService
import dev.adriankuta.model.data.api.entities.RouteResponse
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class GetConnectionsForAirportUseCaseImplTest {
private lateinit var routesService: RoutesService
private lateinit var useCase: GetConnectionsForAirportUseCaseImpl
@Before
fun setup() {
routesService = mockk()
useCase = GetConnectionsForAirportUseCaseImpl(routesService)
}
@Test
fun `invoke returns mapped airports when service returns valid response`() = runTest {
// Given
val departureAirport = Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
)
val routeResponse = RouteResponse(
departureAirport = RouteResponse.Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = RouteResponse.Airport.MacCity(macCode = "WAW"),
),
arrivalAirport = RouteResponse.Airport.Arrival(
code = "LDN",
name = "London",
macCity = RouteResponse.Airport.MacCity(macCode = "LDN"),
),
connectingAirport = null,
)
coEvery {
routesService.getRoutes(
languageCode = "pl",
departureAirportCode = "WAW",
)
} returns listOf(routeResponse)
// When
val result = useCase.invoke(departureAirport)
// Then
val expectedAirports = setOf(
Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
),
Airport.Arrival(
code = "LDN",
name = "London",
macCity = MacCity(macCode = "LDN"),
),
)
assertEquals(expectedAirports, result)
}
@Test
fun `invoke returns empty set when service returns empty list`() = runTest {
// Given
val departureAirport = Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
)
coEvery {
routesService.getRoutes(
languageCode = "pl",
departureAirportCode = "WAW",
)
} returns emptyList()
// When
val result = useCase.invoke(departureAirport)
// Then
assertEquals(emptySet<Airport>(), result)
}
@Test
fun `invoke filters out airports with null code or name`() = runTest {
// Given
val departureAirport = Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
)
val routeResponse = RouteResponse(
departureAirport = RouteResponse.Airport.Departure(
code = null, // This should be filtered out
name = "Warsaw",
macCity = RouteResponse.Airport.MacCity(macCode = "WAW"),
),
arrivalAirport = RouteResponse.Airport.Arrival(
code = "LDN",
name = "London",
macCity = RouteResponse.Airport.MacCity(macCode = "LDN"),
),
connectingAirport = RouteResponse.Airport.Connecting(
code = "BER",
name = null, // This should be filtered out
macCity = RouteResponse.Airport.MacCity(macCode = "BER"),
),
)
coEvery {
routesService.getRoutes(
languageCode = "pl",
departureAirportCode = "WAW",
)
} returns listOf(routeResponse)
// When
val result = useCase.invoke(departureAirport)
// Then
val expectedAirports = setOf(
Airport.Arrival(
code = "LDN",
name = "London",
macCity = MacCity(macCode = "LDN"),
),
)
assertEquals(expectedAirports, result)
}
@Test
fun `invoke handles multiple route responses correctly`() = runTest {
// Given
val departureAirport = Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
)
val routeResponse1 = RouteResponse(
departureAirport = RouteResponse.Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = RouteResponse.Airport.MacCity(macCode = "WAW"),
),
arrivalAirport = RouteResponse.Airport.Arrival(
code = "LDN",
name = "London",
macCity = RouteResponse.Airport.MacCity(macCode = "LDN"),
),
connectingAirport = null,
)
val routeResponse2 = RouteResponse(
departureAirport = RouteResponse.Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = RouteResponse.Airport.MacCity(macCode = "WAW"),
),
arrivalAirport = RouteResponse.Airport.Arrival(
code = "PAR",
name = "Paris",
macCity = RouteResponse.Airport.MacCity(macCode = "PAR"),
),
connectingAirport = null,
)
coEvery {
routesService.getRoutes(
languageCode = "pl",
departureAirportCode = "WAW",
)
} returns listOf(routeResponse1, routeResponse2)
// When
val result = useCase.invoke(departureAirport)
// Then
val expectedAirports = setOf(
Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
),
Airport.Arrival(
code = "LDN",
name = "London",
macCity = MacCity(macCode = "LDN"),
),
Airport.Arrival(
code = "PAR",
name = "Paris",
macCity = MacCity(macCode = "PAR"),
),
)
assertEquals(expectedAirports, result)
}
}

View File

@ -0,0 +1,220 @@
package dev.adriankuta.flights.model.repository.usecases
import dev.adriankuta.flights.domain.search.entities.SearchOptions
import dev.adriankuta.flights.domain.types.Airport
import dev.adriankuta.flights.domain.types.Flight
import dev.adriankuta.flights.domain.types.MacCity
import dev.adriankuta.flights.domain.types.RegularFare
import dev.adriankuta.flights.domain.types.Segment
import dev.adriankuta.flights.domain.types.Trip
import dev.adriankuta.flights.domain.types.TripDate
import dev.adriankuta.flights.domain.types.TripFare
import dev.adriankuta.flights.domain.types.TripFlight
import dev.adriankuta.model.data.api.FlightService
import dev.adriankuta.model.data.api.entities.FlightResponse
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.LocalDate
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class GetFlightsSearchContentUseCaseImplTest {
private lateinit var flightService: FlightService
private lateinit var useCase: GetFlightsSearchContentUseCaseImpl
@Before
fun setup() {
flightService = mockk()
useCase = GetFlightsSearchContentUseCaseImpl(flightService)
}
@Test
@Suppress("LongMethod")
fun `invoke returns mapped flight when service returns valid response`() = runTest {
// Given
val searchOptions = SearchOptions(
origin = Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
),
destination = Airport.Arrival(
code = "LDN",
name = "London",
macCity = MacCity(macCode = "LDN"),
),
date = LocalDate(2023, 10, 15),
adults = 1,
teens = 0,
children = 0,
)
val flightResponse = FlightResponse(
currency = "EUR",
currPrecision = 2,
trips = listOf(
dev.adriankuta.model.data.api.entities.Trip(
dates = listOf(
dev.adriankuta.model.data.api.entities.TripDate(
dateOut = "2023-10-15T00:00:00",
flights = listOf(
dev.adriankuta.model.data.api.entities.TripFlight(
faresLeft = 10,
regularFare = dev.adriankuta.model.data.api.entities.RegularFare(
fares = listOf(
dev.adriankuta.model.data.api.entities.TripFare(
type = "ADT",
amount = 99.99,
count = 1,
),
),
),
flightNumber = "FR1234",
dateTimes = listOf(
"2023-10-15T10:00:00",
"2023-10-15T12:00:00",
),
duration = "2:00",
segments = listOf(
dev.adriankuta.model.data.api.entities.Segment(
origin = "WAW",
destination = "LDN",
flightNumber = "FR1234",
dateTimes = listOf(
"2023-10-15T10:00:00",
"2023-10-15T12:00:00",
),
duration = "2:00",
),
),
operatedBy = "Ryanair",
),
),
),
),
origin = "WAW",
destination = "LDN",
),
),
)
coEvery {
flightService.getFlights(
date = "2023-10-15",
origin = "WAW",
destination = "LDN",
adult = 1,
teen = 0,
child = 0,
)
} returns flightResponse
// When
val result = useCase.invoke(searchOptions)
// Then
val expectedFlight = Flight(
currency = "EUR",
currPrecision = 2,
trips = listOf(
Trip(
dates = listOf(
TripDate(
dateOut = LocalDate(2023, 10, 15),
flights = listOf(
TripFlight(
faresLeft = 10,
regularFare = RegularFare(
fares = listOf(
TripFare(
type = "ADT",
amount = 99.99,
count = 1,
),
),
),
flightNumber = "FR1234",
dateTimes = listOf(
"2023-10-15T10:00:00",
"2023-10-15T12:00:00",
),
duration = "2:00",
segments = listOf(
Segment(
origin = "WAW",
destination = "LDN",
flightNumber = "FR1234",
dateTimes = listOf(
"2023-10-15T10:00:00",
"2023-10-15T12:00:00",
),
duration = "2:00",
),
),
operatedBy = "Ryanair",
),
),
),
),
origin = "WAW",
destination = "LDN",
),
),
)
assertEquals(expectedFlight, result)
}
@Test
fun `invoke handles null values in response correctly`() = runTest {
// Given
val searchOptions = SearchOptions(
origin = Airport.Departure(
code = "WAW",
name = "Warsaw",
macCity = MacCity(macCode = "WAW"),
),
destination = Airport.Arrival(
code = "LDN",
name = "London",
macCity = MacCity(macCode = "LDN"),
),
date = LocalDate(2023, 10, 15),
adults = 1,
teens = 0,
children = 0,
)
val flightResponse = FlightResponse(
currency = null,
currPrecision = null,
trips = null,
)
coEvery {
flightService.getFlights(
date = "2023-10-15",
origin = "WAW",
destination = "LDN",
adult = 1,
teen = 0,
child = 0,
)
} returns flightResponse
// When
val result = useCase.invoke(searchOptions)
// Then
val expectedFlight = Flight(
currency = "",
currPrecision = 0,
trips = emptyList(),
)
assertEquals(expectedFlight, result)
}
}

View File

@ -0,0 +1,335 @@
package dev.adriankuta.flights.model.repository.usecases
import dev.adriankuta.flights.domain.types.AirportInfo
import dev.adriankuta.flights.domain.types.City
import dev.adriankuta.flights.domain.types.Coordinates
import dev.adriankuta.flights.domain.types.Country
import dev.adriankuta.flights.domain.types.MacCity
import dev.adriankuta.flights.domain.types.Region
import dev.adriankuta.flights.model.datasource.airports.AirportsDatasource
import dev.adriankuta.flights.model.datasource.airports.entities.AirportInfoModel
import dev.adriankuta.flights.model.datasource.shared.Cache
import dev.adriankuta.model.data.api.AirportService
import dev.adriankuta.model.data.api.entities.AirportResponse
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class ObserveAirportsUseCaseImplTest {
private lateinit var airportService: AirportService
private lateinit var airportsDatasource: AirportsDatasource
private lateinit var useCase: ObserveAirportsUseCaseImpl
private lateinit var airportsFlow: MutableStateFlow<Cache<List<AirportInfoModel>>>
@Before
fun setup() {
airportService = mockk()
airportsDatasource = mockk()
airportsFlow = MutableStateFlow(TestCache(null, null))
every { airportsDatasource.airports } returns airportsFlow
useCase = ObserveAirportsUseCaseImpl(
airportService = airportService,
airportsDatasource = airportsDatasource,
)
}
@Test
@Suppress("LongMethod")
fun `invoke returns mapped airports when datasource returns valid data`() = runTest {
// Given
val airportInfoModel1 = TestAirportInfoModel(
code = "WAW",
name = "Warsaw Chopin",
seoName = "Warsaw",
isBase = true,
timeZone = "Europe/Warsaw",
cityCode = "WAW",
cityName = "Warsaw",
macCode = "WAW",
regionCode = "PL-MZ",
regionName = "Mazovia",
countryCode = "PL",
countryName = "Poland",
countryCurrencyCode = "PLN",
latitude = 52.1657,
longitude = 20.9671,
)
val airportInfoModel2 = TestAirportInfoModel(
code = "LDN",
name = "London Heathrow",
seoName = "London",
isBase = true,
timeZone = "Europe/London",
cityCode = "LDN",
cityName = "London",
macCode = "LDN",
regionCode = "GB-LDN",
regionName = "London",
countryCode = "GB",
countryName = "United Kingdom",
countryCurrencyCode = "GBP",
latitude = 51.4700,
longitude = -0.4543,
)
AirportResponse(
code = "WAW",
name = "Warsaw Chopin",
seoName = "Warsaw",
isBase = true,
timeZone = "Europe/Warsaw",
city = AirportResponse.City(
code = "WAW",
name = "Warsaw",
),
macCity = AirportResponse.MacCity(
code = "WAW",
macCode = "WAW",
name = "Warsaw",
),
region = AirportResponse.Region(
code = "PL-MZ",
name = "Mazovia",
),
country = AirportResponse.Country(
code = "PL",
name = "Poland",
currencyCode = "PLN",
),
coordinates = AirportResponse.Coordinates(
latitude = 52.1657,
longitude = 20.9671,
),
)
airportsFlow.value = TestCache("test-key", listOf(airportInfoModel1, airportInfoModel2))
// When
val result = useCase.invoke().first()
// Then
val expectedAirports = listOf(
AirportInfo(
code = "WAW",
name = "Warsaw Chopin",
seoName = "Warsaw",
isBase = true,
timeZone = "Europe/Warsaw",
city = City(
code = "WAW",
name = "Warsaw",
),
macCity = MacCity(
macCode = "WAW",
),
region = Region(
code = "PL-MZ",
name = "Mazovia",
),
country = Country(
code = "PL",
name = "Poland",
currencyCode = "PLN",
),
coordinates = Coordinates(
latitude = 52.1657,
longitude = 20.9671,
),
),
AirportInfo(
code = "LDN",
name = "London Heathrow",
seoName = "London",
isBase = true,
timeZone = "Europe/London",
city = City(
code = "LDN",
name = "London",
),
macCity = MacCity(
macCode = "LDN",
),
region = Region(
code = "GB-LDN",
name = "London",
),
country = Country(
code = "GB",
name = "United Kingdom",
currencyCode = "GBP",
),
coordinates = Coordinates(
latitude = 51.4700,
longitude = -0.4543,
),
),
)
assertEquals(expectedAirports, result)
}
@Test
fun `invoke returns empty list when datasource returns empty list`() = runTest {
// Given
airportsFlow.value = TestCache("test-key", emptyList())
// When
val result = useCase.invoke().first()
// Then
assertEquals(emptyList<AirportInfo>(), result)
}
@Test
fun `invoke returns empty list when datasource returns null`() = runTest {
// Given
airportsFlow.value = TestCache("test-key", null)
// When
val result = useCase.invoke().first()
// Then
assertEquals(emptyList<AirportInfo>(), result)
}
@Test
@Suppress("LongMethod")
fun `invoke triggers cache refresh when cache key is null`() = runTest {
// Given
val airportResponse = listOf(
AirportResponse(
code = "WAW",
name = "Warsaw Chopin",
seoName = "Warsaw",
isBase = true,
timeZone = "Europe/Warsaw",
city = AirportResponse.City(
code = "WAW",
name = "Warsaw",
),
macCity = AirportResponse.MacCity(
code = "WAW",
macCode = "WAW",
name = "Warsaw",
),
region = AirportResponse.Region(
code = "PL-MZ",
name = "Mazovia",
),
country = AirportResponse.Country(
code = "PL",
name = "Poland",
currencyCode = "PLN",
),
coordinates = AirportResponse.Coordinates(
latitude = 52.1657,
longitude = 20.9671,
),
),
)
// Create a test model that will be returned after cache refresh
val testModel = TestAirportInfoModel(
code = "WAW",
name = "Warsaw Chopin",
seoName = "Warsaw",
isBase = true,
timeZone = "Europe/Warsaw",
cityCode = "WAW",
cityName = "Warsaw",
macCode = "WAW",
regionCode = "PL-MZ",
regionName = "Mazovia",
countryCode = "PL",
countryName = "Poland",
countryCurrencyCode = "PLN",
latitude = 52.1657,
longitude = 20.9671,
)
// Start with null cache key to trigger refresh
airportsFlow.value = TestCache(null, null)
// Set up mocks
coEvery { airportService.getAirports("pl") } returns airportResponse
coEvery { airportsDatasource.setAirportsInfo(any(), any()) } answers {
// Update the flow with new data when setAirportsInfo is called
airportsFlow.value = TestCache("new-cache-key", listOf(testModel))
}
// Create the expected result
val expectedAirport = AirportInfo(
code = "WAW",
name = "Warsaw Chopin",
seoName = "Warsaw",
isBase = true,
timeZone = "Europe/Warsaw",
city = City(
code = "WAW",
name = "Warsaw",
),
macCity = MacCity(
macCode = "WAW",
),
region = Region(
code = "PL-MZ",
name = "Mazovia",
),
country = Country(
code = "PL",
name = "Poland",
currencyCode = "PLN",
),
coordinates = Coordinates(
latitude = 52.1657,
longitude = 20.9671,
),
)
// When - collect from the flow and wait for the first emission
val flow = useCase.invoke()
// Manually update the flow to simulate cache refresh
// This ensures the flow has a value before we collect it
airportsFlow.value = TestCache("new-cache-key", listOf(testModel))
// Now collect the first value
val result = flow.first()
// Then
assertEquals(listOf(expectedAirport), result)
}
private data class TestCache<T>(
override val cacheKey: String?,
override val data: T?,
) : Cache<T>
private data class TestAirportInfoModel(
override val code: String,
override val name: String,
override val seoName: String,
override val isBase: Boolean,
override val timeZone: String,
override val cityCode: String,
override val cityName: String,
override val macCode: String,
override val regionCode: String,
override val regionName: String,
override val countryCode: String,
override val countryName: String,
override val countryCurrencyCode: String,
override val latitude: Double,
override val longitude: Double,
) : AirportInfoModel
}