Skip to content

Commit b93a153

Browse files
committed
milestone commit towards 1.6.0
1 parent debe0cc commit b93a153

15 files changed

Lines changed: 1304 additions & 1 deletion

File tree

src/app/build.gradle.kts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
id("com.android.application")
33
id("org.jetbrains.kotlin.android")
44
id("org.jetbrains.kotlin.plugin.parcelize")
5+
id("jacoco")
56
}
67

78
android {
@@ -22,6 +23,10 @@ android {
2223
}
2324

2425
buildTypes {
26+
debug {
27+
enableAndroidTestCoverage = true
28+
enableUnitTestCoverage = true
29+
}
2530
release {
2631
isMinifyEnabled = false
2732
proguardFiles(
@@ -103,10 +108,44 @@ dependencies {
103108
testImplementation("junit:junit:4.13.2")
104109
testImplementation("io.mockk:mockk:1.13.10")
105110
testImplementation("androidx.arch.core:core-testing:2.2.0")
111+
testImplementation("com.google.truth:truth:1.1.3")
112+
testImplementation("app.cash.turbine:turbine:1.0.0")
113+
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
114+
testImplementation("org.robolectric:robolectric:4.11.1")
106115
androidTestImplementation("androidx.test.ext:junit:1.2.1")
107116
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
108117
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
109118
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
110119
debugImplementation("androidx.compose.ui:ui-tooling")
111120
debugImplementation("androidx.compose.ui:ui-test-manifest")
121+
}
122+
123+
// JaCoCo coverage configuration
124+
tasks.register<JacocoReport>("jacocoTestReport") {
125+
dependsOn("testDebugUnitTest", "createDebugCoverageReport")
126+
127+
reports {
128+
xml.required.set(true)
129+
html.required.set(true)
130+
}
131+
132+
val fileFilter = listOf(
133+
"**/R.class",
134+
"**/R\$*.class",
135+
"**/BuildConfig.*",
136+
"**/Manifest*.*",
137+
"**/*Test*.*",
138+
"android/**/*.*"
139+
)
140+
141+
val debugTree = fileTree("${buildDir}/intermediates/classes/debug") {
142+
exclude(fileFilter)
143+
}
144+
val mainSrc = "${project.projectDir}/src/main/java"
145+
146+
sourceDirectories.setFrom(files(mainSrc))
147+
classDirectories.setFrom(files(debugTree))
148+
executionData.setFrom(fileTree(buildDir) {
149+
include("**/*.exec", "**/*.ec")
150+
})
112151
}

src/app/src/androidTest/java/com/melodee/autoplayer/ui/HomeScreenInstrumentedTest.kt

Lines changed: 376 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.melodee.autoplayer.network
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import org.junit.Test
5+
6+
/**
7+
* Basic tests to verify test infrastructure works
8+
*/
9+
class BasicTest {
10+
11+
@Test
12+
fun `basic truth assertion works`() {
13+
val result: Int = 2 + 2
14+
assertThat(result).isEqualTo(4)
15+
}
16+
17+
18+
@Test
19+
fun `string operations work`() {
20+
val testString = "test-operation"
21+
assertThat(testString).contains("operation")
22+
assertThat(testString.length).isGreaterThan(0)
23+
}
24+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.melodee.autoplayer.network
2+
3+
import kotlinx.coroutines.flow.first
4+
import kotlinx.coroutines.flow.flowOf
5+
import kotlinx.coroutines.test.runTest
6+
import com.google.common.truth.Truth.assertThat
7+
import org.junit.Test
8+
import java.io.IOException
9+
import java.net.UnknownHostException
10+
11+
/**
12+
* Simple network tests that validate core functionality
13+
*/
14+
class SimpleNetworkTest {
15+
16+
@Test
17+
fun `simple network test passes`() = runTest {
18+
val testFlow = flowOf("test")
19+
val result = testFlow.first()
20+
assertThat(result).isEqualTo("test")
21+
}
22+
23+
@Test
24+
fun `network operations can be tested`() = runTest {
25+
val data = listOf("item1", "item2", "item3")
26+
val processedData = data.map { it.uppercase() }
27+
28+
assertThat(processedData).containsExactly("ITEM1", "ITEM2", "ITEM3")
29+
assertThat(processedData).hasSize(3)
30+
}
31+
32+
@Test
33+
fun `result handling works correctly`() = runTest {
34+
val successValue = "success"
35+
val result = Result.success(successValue)
36+
37+
assertThat(result.isSuccess).isTrue()
38+
assertThat(result.getOrNull()).isEqualTo(successValue)
39+
}
40+
41+
@Test
42+
fun `exception handling works correctly`() = runTest {
43+
val errorMessage = "Network timeout"
44+
val exception = RuntimeException(errorMessage)
45+
val result = Result.failure<String>(exception)
46+
47+
assertThat(result.isFailure).isTrue()
48+
assertThat(result.exceptionOrNull()?.message).isEqualTo(errorMessage)
49+
}
50+
51+
@Test
52+
fun `flow operations work correctly`() = runTest {
53+
val numbers = flowOf(1, 2, 3, 4, 5)
54+
val firstItem = numbers.first()
55+
56+
assertThat(firstItem).isEqualTo(1)
57+
}
58+
59+
@Test
60+
fun `exception types are handled correctly`() {
61+
val unknownHost = UnknownHostException("Host not found")
62+
val ioException = IOException("IO error")
63+
64+
assertThat(unknownHost).isInstanceOf(IOException::class.java)
65+
assertThat(ioException.message).isEqualTo("IO error")
66+
}
67+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.melodee.autoplayer.presentation
2+
3+
import app.cash.turbine.test
4+
import com.google.common.truth.Truth.assertThat
5+
import kotlinx.coroutines.ExperimentalCoroutinesApi
6+
import kotlinx.coroutines.flow.MutableStateFlow
7+
import kotlinx.coroutines.flow.StateFlow
8+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
9+
import kotlinx.coroutines.test.runTest
10+
import org.junit.Ignore
11+
import org.junit.Test
12+
13+
/**
14+
* Contract tests that lock PlayerViewModel external behavior under slow networks.
15+
* Implement the Adapter below and delegate to your real ViewModel.
16+
*/
17+
@OptIn(ExperimentalCoroutinesApi::class)
18+
class PlayerViewModelStateTest {
19+
20+
sealed class UiState {
21+
object Idle : UiState()
22+
object Loading : UiState()
23+
data class Error(val message: String) : UiState()
24+
data class Playing(val trackId: String) : UiState()
25+
}
26+
27+
interface PlayerViewModelAdapter {
28+
val state: StateFlow<UiState>
29+
suspend fun play(trackId: String)
30+
fun cancel() // should cancel ongoing work
31+
}
32+
33+
/** TODO: Wire to your real ViewModel. */
34+
private val vm: PlayerViewModelAdapter = object : PlayerViewModelAdapter {
35+
private val _state = MutableStateFlow<UiState>(UiState.Idle)
36+
override val state = _state
37+
override suspend fun play(trackId: String) { _state.value = UiState.Playing(trackId) }
38+
override fun cancel() { _state.value = UiState.Idle }
39+
}
40+
41+
@Test @Ignore("Wire vm to real ViewModel then enable")
42+
fun slowNetwork_showsLoading_thenPlaying_orError() = runTest(UnconfinedTestDispatcher()) {
43+
vm.state.test {
44+
assertThat(awaitItem()).isInstanceOf(UiState.Idle::class.java)
45+
// trigger a play action on a slow network (simulate in repository with delay)
46+
// then assert states: Loading -> Playing or Loading -> Error
47+
}
48+
}
49+
50+
@Test @Ignore("Wire vm to real ViewModel then enable")
51+
fun cancel_whileLoading_transitionsToIdle() = runTest(UnconfinedTestDispatcher()) {
52+
vm.cancel()
53+
assertThat(vm.state.value).isInstanceOf(UiState.Idle::class.java)
54+
}
55+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.melodee.autoplayer.repository
2+
3+
import app.cash.turbine.test
4+
import com.google.common.truth.Truth.assertThat
5+
import kotlinx.coroutines.ExperimentalCoroutinesApi
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.MutableStateFlow
8+
import kotlinx.coroutines.flow.first
9+
import kotlinx.coroutines.flow.flow
10+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
11+
import kotlinx.coroutines.test.runTest
12+
import org.junit.Ignore
13+
import org.junit.Test
14+
15+
/**
16+
* Contract tests that define expected behavior for your PlaylistRepository.
17+
* Implement the Adapter inside this test to delegate to your real repository.
18+
*/
19+
@OptIn(ExperimentalCoroutinesApi::class)
20+
class PlaylistRepositoryContractTest {
21+
22+
// Minimal domain model used by the contract.
23+
data class Playlist(val id: String, val name: String)
24+
25+
interface PlaylistRepository {
26+
fun getPlaylistsPaged(pageSize: Int): Flow<List<Playlist>>
27+
suspend fun refresh(): Unit
28+
}
29+
30+
/** TODO: Wire this to your real repository (delegate). */
31+
private val sut: PlaylistRepository = object : PlaylistRepository {
32+
private val data = MutableStateFlow<List<Playlist>>(emptyList())
33+
override fun getPlaylistsPaged(pageSize: Int): Flow<List<Playlist>> = data
34+
override suspend fun refresh() { /* TODO delegate */ }
35+
}
36+
37+
@Test @Ignore("Wire sut to real repository then enable")
38+
fun paging_respectsPageSize_andEmitsIncreasingPages() = runTest(UnconfinedTestDispatcher()) {
39+
val first = sut.getPlaylistsPaged(pageSize = 20).first()
40+
assertThat(first.size).isAtMost(20)
41+
}
42+
43+
@Test @Ignore("Wire sut to real repository then enable")
44+
fun refresh_inflightRequestsAreDeduped() = runTest {
45+
// Recommend: coalesce duplicate requests by key in repository layer.
46+
// This test should assert that parallel refresh calls do not produce duplicated network calls.
47+
assertThat(true).isTrue() // Replace with interaction assertions once wired.
48+
}
49+
}

src/benchmark/build.gradle.kts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
plugins {
2+
id("com.android.test")
3+
id("org.jetbrains.kotlin.android")
4+
}
5+
6+
android {
7+
namespace = "com.melodee.autoplayer.benchmark"
8+
compileSdk = 35
9+
10+
defaultConfig {
11+
minSdk = 23
12+
targetSdk = 35
13+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
14+
15+
testInstrumentationRunnerArguments += mapOf(
16+
"androidx.benchmark.suppressErrors" to "EMULATOR,DEBUGGABLE"
17+
)
18+
}
19+
20+
buildTypes {
21+
create("release") {
22+
isMinifyEnabled = false // Disable minification for benchmarks
23+
isDebuggable = false
24+
}
25+
create("benchmark") {
26+
initWith(getByName("release"))
27+
isMinifyEnabled = false
28+
isDebuggable = false
29+
signingConfig = signingConfigs.getByName("debug")
30+
matchingFallbacks += listOf("release")
31+
}
32+
}
33+
34+
compileOptions {
35+
sourceCompatibility = JavaVersion.VERSION_17
36+
targetCompatibility = JavaVersion.VERSION_17
37+
}
38+
kotlinOptions { jvmTarget = "17" }
39+
40+
targetProjectPath = ":app"
41+
experimentalProperties["android.experimental.self-instrumenting"] = true
42+
}
43+
44+
dependencies {
45+
implementation("androidx.test.ext:junit:1.2.1")
46+
implementation("androidx.test.espresso:espresso-core:3.6.1")
47+
implementation("androidx.benchmark:benchmark-macro-junit4:1.2.4")
48+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.melodee.autoplayer
2+
3+
import androidx.benchmark.macro.CompilationMode
4+
import androidx.benchmark.macro.MacrobenchmarkRule
5+
import androidx.benchmark.macro.StartupMode
6+
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
7+
import androidx.test.ext.junit.runners.AndroidJUnit4
8+
import androidx.test.filters.LargeTest
9+
import org.junit.Rule
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
13+
/**
14+
* Replace "com.melodee.autoplayer" and ".MainActivity" if different in your app.
15+
*/
16+
@RunWith(AndroidJUnit4::class)
17+
@LargeTest
18+
class StartupBenchmark {
19+
@get:Rule
20+
val benchmarkRule = MacrobenchmarkRule()
21+
22+
@Test
23+
fun coldStartup() = benchmarkRule.measureRepeated(
24+
packageName = "com.melodee.autoplayer",
25+
metrics = listOf(androidx.benchmark.macro.StartupTimingMetric()),
26+
compilationMode = CompilationMode.Partial(),
27+
iterations = 5,
28+
startupMode = StartupMode.COLD
29+
) {
30+
pressHome()
31+
startActivityAndWait()
32+
}
33+
}

0 commit comments

Comments
 (0)