android-sdd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integration-testing
|
|
3
|
+
description: >
|
|
4
|
+
Integration testing for Android — testing multiple components together.
|
|
5
|
+
Load this skill when testing Repository with a real Room database,
|
|
6
|
+
testing ViewModel with real UseCases, verifying data flow across layers,
|
|
7
|
+
or using Hilt for test dependency injection.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Integration Testing
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Integration tests verify that multiple components work correctly together. Common targets: Repository + Room (real DB), ViewModel + UseCase + fake Repository, or full data layer with in-memory database. These run on a device or emulator (androidTest) or on JVM with Robolectric.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Integration tests sit **between unit and UI tests** — test real wiring, fake I/O
|
|
20
|
+
- Use **in-memory Room databases** — fast, no disk I/O, auto-cleaned
|
|
21
|
+
- Use **Hilt testing** — swap real implementations with fakes via `@TestInstallIn`
|
|
22
|
+
- Run on **JVM with Robolectric** where possible — faster than device
|
|
23
|
+
- Keep integration tests **focused** — one subsystem per test class
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Dependencies
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// build.gradle.kts
|
|
31
|
+
dependencies {
|
|
32
|
+
// Instrumented tests (androidTest)
|
|
33
|
+
androidTestImplementation(libs.hilt.android.testing)
|
|
34
|
+
kspAndroidTest(libs.hilt.compiler)
|
|
35
|
+
androidTestImplementation(libs.androidx.test.runner)
|
|
36
|
+
androidTestImplementation(libs.androidx.test.rules)
|
|
37
|
+
androidTestImplementation(libs.kotlinx.coroutines.test)
|
|
38
|
+
androidTestImplementation(libs.turbine)
|
|
39
|
+
|
|
40
|
+
// JVM tests with Robolectric (test)
|
|
41
|
+
testImplementation(libs.robolectric)
|
|
42
|
+
testImplementation(libs.androidx.test.core)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Repository + Room Integration Test
|
|
49
|
+
|
|
50
|
+
```kotlin
|
|
51
|
+
// ✅ Test Repository with real in-memory Room database
|
|
52
|
+
@RunWith(AndroidJUnit4::class)
|
|
53
|
+
class UserRepositoryIntegrationTest {
|
|
54
|
+
|
|
55
|
+
private lateinit var database: AppDatabase
|
|
56
|
+
private lateinit var dao: UserDao
|
|
57
|
+
private lateinit var api: UserApiService
|
|
58
|
+
private lateinit var repository: UserRepositoryImpl
|
|
59
|
+
|
|
60
|
+
@Before
|
|
61
|
+
fun setup() {
|
|
62
|
+
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
63
|
+
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
|
64
|
+
.allowMainThreadQueries() // ✅ allowed in tests only
|
|
65
|
+
.build()
|
|
66
|
+
dao = database.userDao()
|
|
67
|
+
api = mockk()
|
|
68
|
+
repository = UserRepositoryImpl(dao, api)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@After
|
|
72
|
+
fun teardown() {
|
|
73
|
+
database.close()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun `getUser caches API response in local database`() = runTest {
|
|
78
|
+
// Arrange
|
|
79
|
+
val dto = UserDto(id = "1", name = "Ali", email = "ali@test.com", role = "member")
|
|
80
|
+
coEvery { api.getUser("1") } returns dto
|
|
81
|
+
|
|
82
|
+
// Act — first call fetches from API
|
|
83
|
+
val result = repository.getUser("1")
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
assertThat(result.isSuccess).isTrue()
|
|
87
|
+
assertThat(result.getOrNull()?.name).isEqualTo("Ali")
|
|
88
|
+
|
|
89
|
+
// Verify cached — second call should not hit API
|
|
90
|
+
repository.getUser("1")
|
|
91
|
+
coVerify(exactly = 1) { api.getUser("1") }
|
|
92
|
+
|
|
93
|
+
// Verify in DB
|
|
94
|
+
val cached = dao.getById("1")
|
|
95
|
+
assertThat(cached).isNotNull()
|
|
96
|
+
assertThat(cached?.name).isEqualTo("Ali")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
fun `observeUsers emits updates when database changes`() = runTest {
|
|
101
|
+
// Arrange
|
|
102
|
+
repository.observeUsers().test {
|
|
103
|
+
assertThat(awaitItem()).isEmpty()
|
|
104
|
+
|
|
105
|
+
// Act — insert directly into DB
|
|
106
|
+
dao.insert(UserEntity(id = "1", name = "Ali", email = "ali@test.com", role = "member"))
|
|
107
|
+
|
|
108
|
+
// Assert — Flow emits new value
|
|
109
|
+
val users = awaitItem()
|
|
110
|
+
assertThat(users).hasSize(1)
|
|
111
|
+
assertThat(users[0].name).isEqualTo("Ali")
|
|
112
|
+
|
|
113
|
+
cancelAndIgnoreRemainingEvents()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Hilt Integration Test
|
|
122
|
+
|
|
123
|
+
```kotlin
|
|
124
|
+
// ✅ Replace real dependencies with fakes for specific tests
|
|
125
|
+
|
|
126
|
+
// 1. Define a fake module
|
|
127
|
+
@Module
|
|
128
|
+
@TestInstallIn(
|
|
129
|
+
components = [SingletonComponent::class],
|
|
130
|
+
replaces = [UserRepositoryModule::class] // replaces real binding
|
|
131
|
+
)
|
|
132
|
+
abstract class FakeUserRepositoryModule {
|
|
133
|
+
@Binds
|
|
134
|
+
@Singleton
|
|
135
|
+
abstract fun bindUserRepository(fake: FakeUserRepository): UserRepository
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 2. Inject fake into test
|
|
139
|
+
@HiltAndroidTest
|
|
140
|
+
class UserListViewModelIntegrationTest {
|
|
141
|
+
|
|
142
|
+
@get:Rule(order = 0)
|
|
143
|
+
val hiltRule = HiltAndroidRule(this)
|
|
144
|
+
|
|
145
|
+
@get:Rule(order = 1)
|
|
146
|
+
val mainDispatcherRule = MainDispatcherRule()
|
|
147
|
+
|
|
148
|
+
@Inject lateinit var fakeRepository: FakeUserRepository
|
|
149
|
+
@Inject lateinit var getUsersUseCase: GetUsersUseCase
|
|
150
|
+
|
|
151
|
+
private lateinit var viewModel: UserListViewModel
|
|
152
|
+
|
|
153
|
+
@Before
|
|
154
|
+
fun setup() {
|
|
155
|
+
hiltRule.inject()
|
|
156
|
+
viewModel = UserListViewModel(getUsersUseCase)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
fun `ViewModel reflects repository data changes`() = runTest {
|
|
161
|
+
viewModel.state.test {
|
|
162
|
+
assertThat(awaitItem()).isEqualTo(UserListUiState.Loading)
|
|
163
|
+
|
|
164
|
+
val users = listOf(User(id = "1", name = "Ali", email = Email("ali@test.com")))
|
|
165
|
+
fakeRepository.emit(users)
|
|
166
|
+
|
|
167
|
+
val success = awaitItem() as UserListUiState.Success
|
|
168
|
+
assertThat(success.users).isEqualTo(users)
|
|
169
|
+
|
|
170
|
+
cancelAndIgnoreRemainingEvents()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Robolectric (JVM-based Android Tests)
|
|
179
|
+
|
|
180
|
+
```kotlin
|
|
181
|
+
// ✅ Run Android-dependent tests on JVM — faster than emulator
|
|
182
|
+
@RunWith(RobolectricTestRunner::class)
|
|
183
|
+
@Config(sdk = [34])
|
|
184
|
+
class UserRepositoryRobolectricTest {
|
|
185
|
+
|
|
186
|
+
private lateinit var database: AppDatabase
|
|
187
|
+
private lateinit var repository: UserRepositoryImpl
|
|
188
|
+
|
|
189
|
+
@Before
|
|
190
|
+
fun setup() {
|
|
191
|
+
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
192
|
+
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
|
193
|
+
.allowMainThreadQueries()
|
|
194
|
+
.build()
|
|
195
|
+
repository = UserRepositoryImpl(database.userDao(), mockk())
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@After
|
|
199
|
+
fun teardown() { database.close() }
|
|
200
|
+
|
|
201
|
+
@Test
|
|
202
|
+
fun `delete user removes from database`() = runTest {
|
|
203
|
+
// Arrange
|
|
204
|
+
database.userDao().insert(
|
|
205
|
+
UserEntity(id = "1", name = "Ali", email = "ali@test.com", role = "member")
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// Act
|
|
209
|
+
repository.deleteUser("1")
|
|
210
|
+
|
|
211
|
+
// Assert
|
|
212
|
+
val result = database.userDao().getById("1")
|
|
213
|
+
assertThat(result).isNull()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## DAO Integration Test
|
|
221
|
+
|
|
222
|
+
```kotlin
|
|
223
|
+
// ✅ Test DAO queries with real Room in-memory database
|
|
224
|
+
@RunWith(AndroidJUnit4::class)
|
|
225
|
+
class UserDaoTest {
|
|
226
|
+
|
|
227
|
+
private lateinit var database: AppDatabase
|
|
228
|
+
private lateinit var dao: UserDao
|
|
229
|
+
|
|
230
|
+
@Before
|
|
231
|
+
fun setup() {
|
|
232
|
+
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
233
|
+
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
|
234
|
+
.allowMainThreadQueries()
|
|
235
|
+
.build()
|
|
236
|
+
dao = database.userDao()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@After
|
|
240
|
+
fun teardown() { database.close() }
|
|
241
|
+
|
|
242
|
+
@Test
|
|
243
|
+
fun `insert and retrieve user`() = runTest {
|
|
244
|
+
val entity = UserEntity(id = "1", name = "Ali", email = "ali@test.com", role = "member")
|
|
245
|
+
dao.insert(entity)
|
|
246
|
+
|
|
247
|
+
val retrieved = dao.getById("1")
|
|
248
|
+
assertThat(retrieved).isEqualTo(entity)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@Test
|
|
252
|
+
fun `observeAll emits empty list initially`() = runTest {
|
|
253
|
+
dao.observeAll().test {
|
|
254
|
+
assertThat(awaitItem()).isEmpty()
|
|
255
|
+
cancelAndIgnoreRemainingEvents()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Test
|
|
260
|
+
fun `delete removes entity`() = runTest {
|
|
261
|
+
val entity = UserEntity(id = "1", name = "Ali", email = "ali@test.com", role = "member")
|
|
262
|
+
dao.insert(entity)
|
|
263
|
+
dao.deleteById("1")
|
|
264
|
+
|
|
265
|
+
assertThat(dao.getById("1")).isNull()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Anti-Patterns
|
|
273
|
+
|
|
274
|
+
- Using real network in integration tests — mock the API layer; only test DB + logic integration
|
|
275
|
+
- Not closing the in-memory database in `@After` — causes resource leaks between tests
|
|
276
|
+
- `allowMainThreadQueries()` in production code — only acceptable in tests
|
|
277
|
+
- Testing too many layers at once — keep each integration test focused on one boundary
|
|
278
|
+
- Slow tests due to full Hilt component initialization — use `@UninstallModules` selectively
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Related Skills
|
|
283
|
+
- `unit-testing` — isolated unit tests for individual classes
|
|
284
|
+
- `ui-testing` — Compose UI tests on device/emulator
|
|
285
|
+
- `mocking` — MockK patterns used in integration tests
|
|
286
|
+
- `fake-data` — test data factories
|
|
287
|
+
- `hilt` — Hilt test setup with `@HiltAndroidTest`
|
|
288
|
+
- `room` — Room database under test
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mocking
|
|
3
|
+
description: >
|
|
4
|
+
Mocking with MockK for Android unit tests.
|
|
5
|
+
Load this skill when creating mocks, stubs, and spies with MockK,
|
|
6
|
+
stubbing suspend functions and Flows, verifying interactions,
|
|
7
|
+
using argument captors, or deciding between mocks and fakes.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Mocking
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
MockK is the idiomatic mocking library for Kotlin. It supports suspend functions, coroutines, extension functions, and object mocking — areas where Mockito struggles with Kotlin code. Use mocks to isolate the class under test from its dependencies and verify interactions.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- **Fakes over mocks** for repositories and data sources — fakes are more maintainable
|
|
20
|
+
- **Mocks** for verifying interactions — "was this method called with these arguments?"
|
|
21
|
+
- **Stubs** for providing return values — "when called, return this"
|
|
22
|
+
- **Never mock what you don't own** — mock your own interfaces, not third-party classes
|
|
23
|
+
- `coEvery` / `coVerify` for suspend functions — never use `every`/`verify` for coroutines
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Creating Mocks
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Create mock
|
|
31
|
+
val repository = mockk<UserRepository>()
|
|
32
|
+
|
|
33
|
+
// ✅ Relaxed mock — returns default values without explicit stubs
|
|
34
|
+
val repository = mockk<UserRepository>(relaxed = true)
|
|
35
|
+
|
|
36
|
+
// ✅ Relaxed Unit mock — only relaxes Unit-returning functions
|
|
37
|
+
val repository = mockk<UserRepository>(relaxUnitFun = true)
|
|
38
|
+
|
|
39
|
+
// ✅ Spy — wraps real object, overrides only stubbed methods
|
|
40
|
+
val realRepository = UserRepositoryImpl(dao, api)
|
|
41
|
+
val spy = spyk(realRepository)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Stubbing
|
|
47
|
+
|
|
48
|
+
```kotlin
|
|
49
|
+
// ✅ Stub regular function
|
|
50
|
+
every { repository.getCount() } returns 5
|
|
51
|
+
every { repository.getCount() } returns 5 andThen 10 // different on subsequent calls
|
|
52
|
+
every { repository.getCount() } throws RuntimeException("error")
|
|
53
|
+
|
|
54
|
+
// ✅ Stub suspend function
|
|
55
|
+
coEvery { repository.getUser("1") } returns Result.success(user)
|
|
56
|
+
coEvery { repository.getUser("1") } throws IOException("Network error")
|
|
57
|
+
coEvery { repository.getUser(any()) } returns Result.failure(Exception("Not found"))
|
|
58
|
+
|
|
59
|
+
// ✅ Stub Flow
|
|
60
|
+
every { repository.observeUsers() } returns flowOf(listOf(user))
|
|
61
|
+
every { repository.observeUsers() } returns flow {
|
|
62
|
+
emit(emptyList())
|
|
63
|
+
delay(100)
|
|
64
|
+
emit(listOf(user))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ✅ Stub with argument matching
|
|
68
|
+
coEvery { repository.getUser(match { it.startsWith("user_") }) } returns Result.success(user)
|
|
69
|
+
coEvery { repository.getUser(any()) } returns Result.failure(Exception())
|
|
70
|
+
|
|
71
|
+
// ✅ Stub with answer (access arguments)
|
|
72
|
+
coEvery { repository.getUser(any()) } answers {
|
|
73
|
+
val id = firstArg<String>()
|
|
74
|
+
Result.success(User(id = id, name = "User $id", email = Email("$id@test.com")))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ✅ Unit-returning suspend — use coJustRun
|
|
78
|
+
coJustRun { repository.deleteUser(any()) }
|
|
79
|
+
|
|
80
|
+
// ✅ Non-suspend Unit — use justRun
|
|
81
|
+
justRun { analytics.log(any()) }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Verification
|
|
87
|
+
|
|
88
|
+
```kotlin
|
|
89
|
+
// ✅ Verify call was made
|
|
90
|
+
verify { repository.getCount() }
|
|
91
|
+
coVerify { repository.getUser("1") }
|
|
92
|
+
|
|
93
|
+
// ✅ Verify call count
|
|
94
|
+
verify(exactly = 1) { repository.getCount() }
|
|
95
|
+
verify(exactly = 0) { repository.getCount() } // never called
|
|
96
|
+
verify(atLeast = 1) { repository.getCount() }
|
|
97
|
+
verify(atMost = 2) { repository.getCount() }
|
|
98
|
+
|
|
99
|
+
// ✅ Verify with argument matchers
|
|
100
|
+
coVerify { repository.getUser(eq("1")) }
|
|
101
|
+
coVerify { repository.getUser(any()) }
|
|
102
|
+
coVerify { repository.getUser(match { it.isNotEmpty() }) }
|
|
103
|
+
|
|
104
|
+
// ✅ Verify order
|
|
105
|
+
verifyOrder {
|
|
106
|
+
repository.getCount()
|
|
107
|
+
repository.getUser("1")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ✅ Verify sequence (exactly in order, no other calls)
|
|
111
|
+
verifySequence {
|
|
112
|
+
repository.getCount()
|
|
113
|
+
repository.getUser("1")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ✅ Verify no interactions
|
|
117
|
+
verify { repository wasNot Called }
|
|
118
|
+
confirmVerified(repository) // all interactions accounted for
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Argument Captors
|
|
124
|
+
|
|
125
|
+
```kotlin
|
|
126
|
+
// ✅ Capture arguments for complex assertions
|
|
127
|
+
val userSlot = slot<User>()
|
|
128
|
+
coEvery { repository.createUser(capture(userSlot)) } returns Result.success(user)
|
|
129
|
+
|
|
130
|
+
viewModel.onCreateUser("Ali", "ali@test.com")
|
|
131
|
+
|
|
132
|
+
assertThat(userSlot.captured.name).isEqualTo("Ali")
|
|
133
|
+
assertThat(userSlot.captured.email.value).isEqualTo("ali@test.com")
|
|
134
|
+
|
|
135
|
+
// ✅ Capture multiple calls
|
|
136
|
+
val capturedIds = mutableListOf<String>()
|
|
137
|
+
coEvery { repository.getUser(captureMany = capturedIds) } returns Result.success(user)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Mocking Objects and Companions
|
|
143
|
+
|
|
144
|
+
```kotlin
|
|
145
|
+
// ✅ Mock Kotlin object
|
|
146
|
+
mockkObject(DateUtils)
|
|
147
|
+
every { DateUtils.now() } returns Instant.parse("2024-01-01T00:00:00Z")
|
|
148
|
+
// ... test ...
|
|
149
|
+
unmockkObject(DateUtils)
|
|
150
|
+
|
|
151
|
+
// ✅ Mock companion object
|
|
152
|
+
mockkObject(User.Companion)
|
|
153
|
+
every { User.Companion.create(any()) } returns mockUser
|
|
154
|
+
|
|
155
|
+
// ✅ Mock top-level functions
|
|
156
|
+
mockkStatic("com.example.app.utils.ExtensionsKt")
|
|
157
|
+
every { anyString().toFormattedDate() } returns "Jan 1, 2024"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Clearing Mocks
|
|
163
|
+
|
|
164
|
+
```kotlin
|
|
165
|
+
// ✅ Clear recorded calls (keep stubs)
|
|
166
|
+
clearMocks(repository, answers = false)
|
|
167
|
+
|
|
168
|
+
// ✅ Clear stubs and calls
|
|
169
|
+
clearMocks(repository)
|
|
170
|
+
|
|
171
|
+
// ✅ Clear all mocks in a test class
|
|
172
|
+
@AfterEach
|
|
173
|
+
fun teardown() {
|
|
174
|
+
clearAllMocks()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ✅ Unmock objects
|
|
178
|
+
@AfterEach
|
|
179
|
+
fun teardown() {
|
|
180
|
+
unmockkAll()
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Mocks vs Fakes Decision Guide
|
|
187
|
+
|
|
188
|
+
| Scenario | Use |
|
|
189
|
+
|---|---|
|
|
190
|
+
| Repository with simple CRUD | Fake |
|
|
191
|
+
| Repository — verify specific API calls | Mock |
|
|
192
|
+
| External service (Analytics, Firebase) | Mock |
|
|
193
|
+
| Database layer in integration test | Real (in-memory Room) |
|
|
194
|
+
| Verifying a method was called N times | Mock |
|
|
195
|
+
| Controlling returned data in multiple tests | Fake |
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
// ✅ Fake — simpler, no verify needed
|
|
199
|
+
class FakeUserRepository : UserRepository {
|
|
200
|
+
private val _users = MutableStateFlow<List<User>>(emptyList())
|
|
201
|
+
var shouldFail = false
|
|
202
|
+
|
|
203
|
+
override fun observeUsers() = _users.asStateFlow()
|
|
204
|
+
override suspend fun getUser(id: String) =
|
|
205
|
+
if (shouldFail) Result.failure(Exception("error"))
|
|
206
|
+
else _users.value.find { it.id == id }
|
|
207
|
+
?.let { Result.success(it) }
|
|
208
|
+
?: Result.failure(Exception("not found"))
|
|
209
|
+
|
|
210
|
+
fun emit(users: List<User>) { _users.value = users }
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Anti-Patterns
|
|
217
|
+
|
|
218
|
+
- `every` for suspend functions — use `coEvery`; `every` will not work correctly
|
|
219
|
+
- `verify` for suspend functions — use `coVerify`
|
|
220
|
+
- Mocking data classes — just create them directly
|
|
221
|
+
- Over-verifying — only verify interactions that matter for the behavior being tested
|
|
222
|
+
- Not clearing mocks between tests — stale stubs cause test pollution
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Related Skills
|
|
227
|
+
- `unit-testing` — full unit test structure using MockK
|
|
228
|
+
- `fake-data` — building fake implementations and test data
|
|
229
|
+
- `integration-testing` — when to use fakes vs mocks
|