Skip to content

Commit 2e1f345

Browse files
committed
Add SpaceX launch tracking feature with multiplatform support
This commit introduces a new multiplatform SpaceX launch tracking app. It includes Compose-based Android and SwiftUI-based iOS UIs, shared Kotlin logic, SQLDelight for local storage, and Koin for dependency injection. The app tracks and displays launch details in a clean interface.
1 parent b7de9c3 commit 2e1f345

File tree

5 files changed

+209
-6
lines changed

5 files changed

+209
-6
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ plugins {
88
alias(libs.plugins.kotlinMultiplatform) apply false
99
alias(libs.plugins.kotlinxSerialization) apply false
1010
alias(libs.plugins.sqldelight) apply false
11+
id("org.jetbrains.kotlinx.kover") version "0.9.1"
1112
}

composeApp/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ kotlin {
1515
jvmTarget.set(JvmTarget.JVM_11)
1616
}
1717
}
18-
18+
1919
sourceSets {
2020
androidMain.dependencies {
2121
implementation(compose.preview)
@@ -72,4 +72,3 @@ android {
7272
debugImplementation(compose.uiTooling)
7373
}
7474
}
75-

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33
<uses-permission android:name="android.permission.INTERNET" />
44

5-
<uses-permission android:name="android.permission.INTERNET" />
6-
75
<application
86
android:allowBackup="true"
97
android:icon="@mipmap/ic_launcher"

shared/build.gradle.kts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.androidLibrary)
77
alias(libs.plugins.kotlinxSerialization)
88
alias(libs.plugins.sqldelight)
9+
id("org.jetbrains.kotlinx.kover")
910
}
1011

1112
kotlin {
@@ -15,7 +16,7 @@ kotlin {
1516
jvmTarget.set(JvmTarget.JVM_11)
1617
}
1718
}
18-
19+
1920
listOf(
2021
iosX64(),
2122
iosArm64(),
@@ -26,7 +27,7 @@ kotlin {
2627
isStatic = true
2728
}
2829
}
29-
30+
3031
sourceSets {
3132
commonMain.dependencies {
3233
implementation(libs.kotlinx.coroutines.core)
@@ -45,6 +46,14 @@ kotlin {
4546
implementation(libs.ktor.client.darwin)
4647
implementation(libs.native.driver)
4748
}
49+
val androidUnitTest by getting {
50+
dependencies {
51+
implementation(kotlin("test"))
52+
implementation(kotlin("test-junit"))
53+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
54+
implementation("io.mockk:mockk:1.13.8")
55+
}
56+
}
4857
}
4958
}
5059

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package com.jetbrains.spacetutorial
2+
3+
import app.cash.sqldelight.db.SqlDriver
4+
import com.jetbrains.spacetutorial.cache.Database
5+
import com.jetbrains.spacetutorial.cache.DatabaseDriverFactory
6+
import com.jetbrains.spacetutorial.entity.Links
7+
import com.jetbrains.spacetutorial.entity.Patch
8+
import com.jetbrains.spacetutorial.entity.RocketLaunch
9+
import com.jetbrains.spacetutorial.network.SpaceXApi
10+
import io.mockk.coEvery
11+
import io.mockk.every
12+
import io.mockk.mockk
13+
import kotlinx.coroutines.test.runTest
14+
import org.junit.Before
15+
import org.junit.Test
16+
import kotlin.test.assertEquals
17+
import kotlin.test.assertFailsWith
18+
19+
/**
20+
* Android-specific unit tests for the SpaceXSDK class.
21+
*/
22+
class SpaceXSDKTest {
23+
24+
// Mock dependencies
25+
private val mockApi = mockk<SpaceXApi>()
26+
private val mockDatabaseDriverFactory = mockk<DatabaseDriverFactory>()
27+
private val mockSqlDriver = mockk<SqlDriver>()
28+
29+
// Create the SDK instance with mock dependencies
30+
private lateinit var sdk: SpaceXSDK
31+
32+
// Sample data for testing
33+
private val sampleLaunches = listOf(
34+
RocketLaunch(
35+
flightNumber = 1,
36+
missionName = "Test Mission 1",
37+
launchDateUTC = "2023-01-01T12:00:00Z",
38+
details = "Test details 1",
39+
launchSuccess = true,
40+
links = Links(
41+
patch = Patch(
42+
small = "small_url_1",
43+
large = "large_url_1"
44+
),
45+
article = "article_url_1"
46+
)
47+
),
48+
RocketLaunch(
49+
flightNumber = 2,
50+
missionName = "Test Mission 2",
51+
launchDateUTC = "2023-02-01T12:00:00Z",
52+
details = "Test details 2",
53+
launchSuccess = false,
54+
links = Links(
55+
patch = Patch(
56+
small = "small_url_2",
57+
large = "large_url_2"
58+
),
59+
article = "article_url_2"
60+
)
61+
)
62+
)
63+
64+
private val sampleLaunches2 = listOf(
65+
RocketLaunch(
66+
flightNumber = 3,
67+
missionName = "Test Mission 3",
68+
launchDateUTC = "2023-01-01T12:00:00Z",
69+
details = "Test details 3",
70+
launchSuccess = true,
71+
links = Links(
72+
patch = Patch(
73+
small = "small_url_3",
74+
large = "large_url_3"
75+
),
76+
article = "article_url_3"
77+
)
78+
),
79+
)
80+
81+
// Cache for storing launches in tests
82+
private val launchesCache = mutableListOf<RocketLaunch>()
83+
84+
@Before
85+
fun setup() {
86+
// Clear the cache before each test
87+
launchesCache.clear()
88+
89+
// Setup the mock database driver factory
90+
every { mockDatabaseDriverFactory.createDriver() } returns mockSqlDriver
91+
92+
// Create a new SDK instance for each test with mocked dependencies
93+
sdk = SpaceXSDK(mockDatabaseDriverFactory, mockApi)
94+
}
95+
96+
/**
97+
* Test that getLaunches returns cached data when available and forceReload is false.
98+
*/
99+
@Test
100+
fun testGetLaunchesFromCache() = runTest {
101+
// Setup: Mock the database to return cached launches
102+
val databaseField = SpaceXSDK::class.java.getDeclaredField("database")
103+
databaseField.isAccessible = true
104+
val mockDatabase = mockk<Database>()
105+
databaseField.set(sdk, mockDatabase)
106+
107+
// Mock the getAllLaunches method of the Database class
108+
every { mockDatabase.getAllLaunches() } returns sampleLaunches
109+
110+
// Act: Call getLaunches with forceReload = false
111+
val result = sdk.getLaunches(forceReload = false)
112+
113+
// Assert: The result should be the cached launches
114+
assertEquals(sampleLaunches, result)
115+
}
116+
117+
/**
118+
* Test that getLaunches fetches data from the API when cache is empty.
119+
*/
120+
@Test
121+
fun testGetLaunchesFromApiWhenCacheEmpty() = runTest {
122+
// Setup: Mock the database to return empty list and the API to return sample launches
123+
val databaseField = SpaceXSDK::class.java.getDeclaredField("database")
124+
databaseField.isAccessible = true
125+
val mockDatabase = mockk<Database>()
126+
databaseField.set(sdk, mockDatabase)
127+
128+
// Mock the getAllLaunches method of the Database class to return an empty list
129+
every { mockDatabase.getAllLaunches() } returns emptyList()
130+
131+
// Mock the clearAndCreateLaunches method of the Database class
132+
every { mockDatabase.clearAndCreateLaunches(any()) } returns Unit
133+
134+
// Mock the API to return sample launches
135+
coEvery { mockApi.getAllLaunches() } returns sampleLaunches
136+
137+
// Act: Call getLaunches with forceReload = false (but cache is empty)
138+
val result = sdk.getLaunches(forceReload = false)
139+
140+
// Assert: The result should be the launches from the API
141+
assertEquals(sampleLaunches, result)
142+
}
143+
144+
/**
145+
* Test that getLaunches fetches data from the API when forceReload is true.
146+
*/
147+
@Test
148+
fun testGetLaunchesWithForceReload() = runTest {
149+
// Setup: Mock the database and API
150+
val databaseField = SpaceXSDK::class.java.getDeclaredField("database")
151+
databaseField.isAccessible = true
152+
val mockDatabase = mockk<Database>()
153+
databaseField.set(sdk, mockDatabase)
154+
155+
// Mock the getAllLaunches method of the Database class to return cached launches
156+
every { mockDatabase.getAllLaunches() } returns sampleLaunches
157+
158+
// Mock the clearAndCreateLaunches method of the Database class
159+
every { mockDatabase.clearAndCreateLaunches(any()) } returns Unit
160+
161+
// Mock the API to return sample launches
162+
coEvery { mockApi.getAllLaunches() } returns sampleLaunches2
163+
164+
// Act: Call getLaunches with forceReload = true
165+
val result = sdk.getLaunches(forceReload = true)
166+
167+
// Assert: The result should be the launches from the API
168+
assertEquals(sampleLaunches2, result)
169+
}
170+
171+
/**
172+
* Test that getLaunches throws an exception when the API call fails.
173+
*/
174+
@Test
175+
fun testGetLaunchesException() = runTest {
176+
// Setup: Mock the database and API
177+
val databaseField = SpaceXSDK::class.java.getDeclaredField("database")
178+
databaseField.isAccessible = true
179+
val mockDatabase = mockk<Database>()
180+
databaseField.set(sdk, mockDatabase)
181+
182+
// Mock the getAllLaunches method of the Database class to return an empty list
183+
every { mockDatabase.getAllLaunches() } returns emptyList()
184+
185+
// Mock the API to throw an exception
186+
coEvery { mockApi.getAllLaunches() } throws Exception("API error")
187+
188+
// Act & Assert: Call getLaunches should throw an exception
189+
val exception = assertFailsWith<Exception> {
190+
sdk.getLaunches(forceReload = true)
191+
}
192+
193+
// Verify the exception message
194+
assertEquals("API error", exception.message)
195+
}
196+
}

0 commit comments

Comments
 (0)