Skip to content

Commit ef1087c

Browse files
authored
Merge pull request #313 from shining-cat/unit-tests-enhancement
Unit tests enhancement
2 parents f9bd86b + 2c1ec25 commit ef1087c

48 files changed

Lines changed: 4863 additions & 1285 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.clinerules

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -190,21 +190,27 @@ dependencies {
190190

191191
### Naming Conventions for _WIP-plans/
192192

193+
**ALL documents MUST be prefixed with last edit date in YYYYMMDD_ format for chronological sorting.**
194+
193195
**Active work (ongoing) - Use present tense:**
194-
- `WIP-[feature-name]-[date].md` - Main tracking document for ongoing work
195-
- `[FEATURE]_MODULE_REFACTORING_PLAN.md` - Planning documents (uppercase feature name)
196-
- `[FEATURE]_MODULE_LOGIC_ANALYSIS.md` - Analysis documents
196+
- `YYYYMMDD_WIP-[feature-name].md` - Main tracking document for ongoing work
197+
- `YYYYMMDD_[FEATURE]_MODULE_REFACTORING_PLAN.md` - Planning documents (uppercase feature name)
198+
- `YYYYMMDD_[FEATURE]_MODULE_LOGIC_ANALYSIS.md` - Analysis documents
197199

198200
**Completed work (merged) - Use past tense suffix:**
199-
- `WIP-[feature-name]-merged-[date].md` - Summary after branch merge
200-
- `[FEATURE]_MODULE_REFACTORING_SUMMARY.md` - Final summary of completed work
201-
- `[FEATURE]_MODULE_EXTRACTION_SUMMARY.md` - Summary of extraction/major refactoring
201+
- `YYYYMMDD_WIP-[feature-name]-merged.md` - Summary after branch merge
202+
- `YYYYMMDD_[FEATURE]_MODULE_REFACTORING_SUMMARY.md` - Final summary of completed work
203+
- `YYYYMMDD_[FEATURE]_MODULE_EXTRACTION_SUMMARY.md` - Summary of extraction/major refactoring
202204

203205
**Examples:**
204-
- Active: `WIP-viewmodel-refactoring-2026-01-02.md`
205-
- Merged: `WIP-models-extraction-merged-2026-01-03.md`
206-
- Active: `HOME_MODULE_REFACTORING_PLAN.md`
207-
- Complete: `HOME_MODULE_REFACTORING_SUMMARY.md`
206+
- Active: `20260102_WIP-viewmodel-refactoring.md`
207+
- Merged: `20260103_WIP-models-extraction-merged.md`
208+
- Active: `20260102_HOME_MODULE_REFACTORING_PLAN.md`
209+
- Complete: `20260103_HOME_MODULE_REFACTORING_SUMMARY.md`
210+
211+
**When updating existing documents:**
212+
- Rename the file with the new date prefix reflecting the update date
213+
- This keeps files sorted by recency automatically
208214

209215
### Mandatory Workflow Steps
210216

@@ -218,7 +224,7 @@ dependencies {
218224
Execute this workflow:
219225

220226
a) **Create Merge Summary Document:**
221-
- Filename: `WIP-[feature-name]-merged-[YYYY-MM-DD].md`
227+
- Filename: `YYYYMMDD_WIP-[feature-name]-merged.md`
222228
- Include:
223229
- What was completed in this branch
224230
- What was NOT completed (remaining work)
@@ -253,16 +259,16 @@ d) **Confirm Cleanup with User:**
253259
```
254260
Task: Home Module Refactoring
255261
├─ Active Phase:
256-
│ ├─ HOME_MODULE_LOGIC_ANALYSIS.md (created during analysis)
257-
│ ├─ HOME_MODULE_REFACTORING_PLAN.md (created for planning)
258-
│ └─ WIP-viewmodel-refactoring-2026-01-02.md (main tracker)
262+
│ ├─ 20260102_HOME_MODULE_LOGIC_ANALYSIS.md (created during analysis)
263+
│ ├─ 20260102_HOME_MODULE_REFACTORING_PLAN.md (created for planning)
264+
│ └─ 20260102_WIP-viewmodel-refactoring.md (main tracker)
259265
260266
├─ Merge Phase:
261-
│ ├─ WIP-home-refactor-merged-2026-01-02.md (created)
262-
│ ├─ HOME_MODULE_REFACTORING_SUMMARY.md (created, permanent)
263-
│ ├─ DELETE: HOME_MODULE_LOGIC_ANALYSIS.md (work complete)
264-
│ ├─ DELETE: HOME_MODULE_REFACTORING_PLAN.md (work complete)
265-
│ └─ KEEP: WIP-viewmodel-refactoring-2026-01-02.md (tracks ALL modules)
267+
│ ├─ 20260102_WIP-home-refactor-merged.md (created)
268+
│ ├─ 20260103_HOME_MODULE_REFACTORING_SUMMARY.md (created, permanent)
269+
│ ├─ DELETE: 20260102_HOME_MODULE_LOGIC_ANALYSIS.md (work complete)
270+
│ ├─ DELETE: 20260102_HOME_MODULE_REFACTORING_PLAN.md (work complete)
271+
│ └─ KEEP: 20260102_WIP-viewmodel-refactoring.md (tracks ALL modules)
266272
```
267273

268274
### Philosophy
@@ -291,6 +297,54 @@ Task: Home Module Refactoring
291297

292298
**Philosophy:** Proper imports improve readability and maintainability. Let the IDE manage imports, not inline qualifications.
293299

300+
### Idiomatic Kotlin
301+
302+
**Always use idiomatic Kotlin syntax:**
303+
- Leverage Kotlin language features over Java-style code
304+
- Use extension functions, data classes, sealed classes, and other Kotlin idioms
305+
- Prefer Kotlin standard library functions (let, apply, run, also, with) when appropriate
306+
- Use property delegation, lambda expressions, and destructuring where it improves clarity
307+
- Follow Kotlin naming conventions and style guidelines
308+
309+
**Avoid static methods and utility classes:**
310+
- ❌ WRONG: Object classes with static utility methods
311+
- ❌ WRONG: Companion objects used as static utility holders
312+
- ✅ RIGHT: Extension functions for utility behavior
313+
- ✅ RIGHT: Dependency injection for shared logic
314+
- ✅ RIGHT: Top-level functions when appropriate
315+
316+
**Philosophy:** Write Kotlin code that leverages the language's strengths. Avoid Java patterns that Kotlin provides better alternatives for. Make code testable and maintainable by favoring injection over static utilities.
317+
318+
## Code Accuracy - Verify Don't Guess
319+
320+
**NEVER guess parameter names, enum values, function names, or other code identifiers:**
321+
- ❌ WRONG: Assume a parameter is named `userId` because it seems conventional
322+
- ❌ WRONG: Guess an enum value is `ACTIVE` without checking the actual definition
323+
- ❌ WRONG: Use `getUserById()` without verifying the actual method name
324+
- ✅ RIGHT: Use `read_file` to check the actual class/interface definition
325+
- ✅ RIGHT: Use `search_files` to find how the code is used elsewhere
326+
- ✅ RIGHT: Use `list_code_definition_names` to see available methods/properties
327+
328+
**Before writing code that references existing types, methods, or values:**
329+
1. **Locate the source** - Find where the type/method/enum is defined
330+
2. **Read the definition** - Verify exact names, parameter types, and signatures
331+
3. **Check usage examples** - See how it's used elsewhere in the codebase
332+
4. **Use exact names** - Match capitalization, spelling, and parameter order precisely
333+
334+
**Common mistakes to avoid:**
335+
- Guessing constructor parameter names without checking the data class definition
336+
- Assuming enum values follow a pattern without verifying all cases
337+
- Using camelCase when the project uses snake_case (or vice versa)
338+
- Inverting boolean parameter names (e.g., `isEnabled` vs `isDisabled`)
339+
- Getting parameter order wrong in function calls
340+
341+
**Tools for verification:**
342+
- `read_file` - Read class definitions, interfaces, enums directly
343+
- `search_files` - Find usage patterns across the codebase
344+
- `list_code_definition_names` - Survey available methods/classes in a module
345+
346+
**Philosophy:** Build errors from typos and wrong names are preventable. Always verify actual code definitions before referencing them. A 30-second file check prevents a 5-minute build failure.
347+
294348
## File Change Handling
295349

296350
**When files have been modified since your last edit:**

build-logic/convention/src/main/kotlin/fr/shiningcat/simplehiit/plugins/TestingConventionPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class TestingConventionPlugin : Plugin<Project> {
2828
// Android-specific test dependencies
2929
pluginManager.withPlugin("com.android.base") {
3030
dependencies {
31-
add("testImplementation", libs.findLibrary("test.runner").get())
31+
add("testImplementation", libs.findLibrary("androidx.test.runner").get())
3232
}
3333

3434
// Configure Android test options

build.gradle.kts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,21 @@ fun kotlinx.kover.gradle.plugin.dsl.KoverReportFiltersConfig.applyCommonExclusio
7373
annotatedBy("javax.annotation.processing.Generated")
7474
annotatedBy("javax.annotation.Generated")
7575

76+
// Exclude classes/functions annotated with custom coverage exclusion
77+
annotatedBy("fr.shiningcat.simplehiit.commonutils.annotations.ExcludeFromCoverage")
78+
7679
// Exclude Room generated classes
7780
classes("*.*_Impl")
7881

82+
// Exclude Room database layer - framework code and data classes
83+
// DAOs: Abstract methods with Room annotations (no business logic)
84+
// Database: Room database declaration (framework boilerplate)
85+
// Entities: Data classes with no logic
86+
packages("*.data.local.database.dao")
87+
packages("*.data.local.database.entities")
88+
classes("*SimpleHiitDatabase")
89+
// NOTE: Migrations are NOT excluded - see docs/KOVER_CODE_COVERAGE.md
90+
7991
// Exclude BuildConfig and R classes
8092
classes("*.BuildConfig", "*.R", "*.R$*")
8193

@@ -89,6 +101,12 @@ fun kotlinx.kover.gradle.plugin.dsl.KoverReportFiltersConfig.applyCommonExclusio
89101
packages("*.models")
90102
classes("*DTO")
91103

104+
// Exclude Android UI layers (mobile and TV)
105+
// Pure Compose UI components without business logic
106+
// Tested via screenshot tests and interaction tests only
107+
packages("*.android.mobile.ui")
108+
packages("*.android.tv.ui")
109+
92110
// Exclude Compose-generated classes
93111
classes("*PreviewParameterProvider")
94112
classes("*ComposableSingletons*")

commonUtils/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ kover {
1616
// Exclude by annotation (Dagger generates with @DaggerGenerated)
1717
annotatedBy("dagger.internal.DaggerGenerated")
1818

19+
// Exclude custom coverage exclusion annotation
20+
annotatedBy("fr.shiningcat.simplehiit.commonutils.annotations.ExcludeFromCoverage")
21+
1922
// Also try pattern like BuildConfig (which works)
2023
classes("*._Factory")
2124
classes("*._MembersInjector")

commonUtils/src/main/java/fr/shiningcat/simplehiit/commonutils/AndroidVersionProviderImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
package fr.shiningcat.simplehiit.commonutils
66

77
import android.os.Build
8+
import fr.shiningcat.simplehiit.commonutils.annotations.ExcludeFromCoverage
89

910
/**
1011
* Production implementation of AndroidVersionProvider.
1112
* Returns the actual Android SDK version from the system.
13+
*
14+
* Excluded from coverage: Trivial wrapper with no business logic to test.
1215
*/
16+
@ExcludeFromCoverage
1317
class AndroidVersionProviderImpl : AndroidVersionProvider {
1418
override fun getSdkVersion(): Int = Build.VERSION.SDK_INT
1519
}

commonUtils/src/main/java/fr/shiningcat/simplehiit/commonutils/TimeProviderImpl.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@
44
*/
55
package fr.shiningcat.simplehiit.commonutils
66

7-
// We can't mock System classes, so this wrapper won't be tested
8-
// there is no logic though, and it's really only a wrapper that allows testing other usecases of System.currentTimeMillis
9-
// see: https://github.com/mockk/mockk/issues/98
7+
import fr.shiningcat.simplehiit.commonutils.annotations.ExcludeFromCoverage
8+
9+
/**
10+
* Production implementation of TimeProvider.
11+
* Returns the current system time in milliseconds.
12+
*
13+
* Excluded from coverage: Trivial wrapper around System.currentTimeMillis() that exists
14+
* solely to enable testing of code that depends on current time.
15+
* Cannot be meaningfully tested as System classes cannot be mocked.
16+
*
17+
* See: https://github.com/mockk/mockk/issues/98
18+
*/
19+
@ExcludeFromCoverage
1020
class TimeProviderImpl : TimeProvider {
1121
override fun getCurrentTimeMillis(): Long = System.currentTimeMillis()
1222
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024-2026 shining-cat
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
package fr.shiningcat.simplehiit.commonutils.annotations
6+
7+
/**
8+
* Marks classes or functions to be excluded from code coverage reports.
9+
*
10+
* Apply to:
11+
* - Trivial wrapper classes with no business logic (e.g., system API wrappers)
12+
* - Code that cannot be meaningfully tested (e.g., System.currentTimeMillis())
13+
* - Generated or framework-delegate code
14+
*
15+
* This annotation is used by Kover to filter out code that doesn't provide
16+
* meaningful coverage metrics from reports.
17+
*/
18+
@Retention(AnnotationRetention.RUNTIME)
19+
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.FILE)
20+
annotation class ExcludeFromCoverage

data/build.gradle.kts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ plugins {
1010
alias(libs.plugins.kover)
1111
}
1212

13+
// Export Room schemas for migration testing
14+
ksp {
15+
arg("room.schemaLocation", "$projectDir/schemas")
16+
}
17+
18+
android {
19+
sourceSets {
20+
// Include Room schema directory in androidTest assets
21+
getByName("androidTest") {
22+
assets.srcDirs("$projectDir/schemas")
23+
}
24+
}
25+
}
26+
1327
dependencies {
1428
implementation(projects.models)
1529
implementation(projects.domain.common)
@@ -21,4 +35,8 @@ dependencies {
2135
implementation(libs.androidx.room.runtime)
2236
implementation(libs.androidx.room.coroutines)
2337
ksp(libs.androidx.room.compiler)
38+
39+
// Room migration instrumented testing
40+
androidTestImplementation(libs.androidx.room.testing)
41+
androidTestImplementation(libs.androidx.test.ext.junit)
2442
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 1,
5+
"identityHash": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
6+
"entities": [
7+
{
8+
"tableName": "simple_hiit_users",
9+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `selected` INTEGER NOT NULL)",
10+
"fields": [
11+
{
12+
"fieldPath": "userId",
13+
"columnName": "userId",
14+
"affinity": "INTEGER",
15+
"notNull": true
16+
},
17+
{
18+
"fieldPath": "name",
19+
"columnName": "name",
20+
"affinity": "TEXT",
21+
"notNull": true
22+
},
23+
{
24+
"fieldPath": "selected",
25+
"columnName": "selected",
26+
"affinity": "INTEGER",
27+
"notNull": true
28+
}
29+
],
30+
"primaryKey": {
31+
"autoGenerate": true,
32+
"columnNames": [
33+
"userId"
34+
]
35+
},
36+
"indices": [
37+
{
38+
"name": "index_simple_hiit_users_userId",
39+
"unique": false,
40+
"columnNames": [
41+
"userId"
42+
],
43+
"orders": [],
44+
"createSql": "CREATE INDEX IF NOT EXISTS `index_simple_hiit_users_userId` ON `${TABLE_NAME}` (`userId`)"
45+
}
46+
]
47+
},
48+
{
49+
"tableName": "simple_hiit_sessions",
50+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `duration` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `simple_hiit_users`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
51+
"fields": [
52+
{
53+
"fieldPath": "sessionId",
54+
"columnName": "session_id",
55+
"affinity": "INTEGER",
56+
"notNull": true
57+
},
58+
{
59+
"fieldPath": "userId",
60+
"columnName": "user_id",
61+
"affinity": "INTEGER",
62+
"notNull": true
63+
},
64+
{
65+
"fieldPath": "timeStamp",
66+
"columnName": "timestamp",
67+
"affinity": "INTEGER",
68+
"notNull": true
69+
},
70+
{
71+
"fieldPath": "durationMs",
72+
"columnName": "duration",
73+
"affinity": "INTEGER",
74+
"notNull": true
75+
}
76+
],
77+
"primaryKey": {
78+
"autoGenerate": true,
79+
"columnNames": [
80+
"session_id"
81+
]
82+
},
83+
"indices": [
84+
{
85+
"name": "index_simple_hiit_sessions_user_id",
86+
"unique": false,
87+
"columnNames": [
88+
"user_id"
89+
],
90+
"orders": [],
91+
"createSql": "CREATE INDEX IF NOT EXISTS `index_simple_hiit_sessions_user_id` ON `${TABLE_NAME}` (`user_id`)"
92+
}
93+
],
94+
"foreignKeys": [
95+
{
96+
"table": "simple_hiit_users",
97+
"onDelete": "CASCADE",
98+
"onUpdate": "NO ACTION",
99+
"columns": [
100+
"user_id"
101+
],
102+
"referencedColumns": [
103+
"userId"
104+
]
105+
}
106+
]
107+
}
108+
],
109+
"setupQueries": [
110+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
111+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6')"
112+
]
113+
}
114+
}

0 commit comments

Comments
 (0)