docs: Mock 数据源注入时钟,消除 flaky 断言(ledger R17)
- MockDataProvider 增加 now: () -> Long = System::currentTimeMillis - metricHistory / workouts 改用 now(),使「确定性」承诺成立 - 测试注入 FIXED_NOW,并补一条时间戳严格递增的断言
This commit is contained in:
@@ -1350,10 +1350,17 @@ git add -A && git commit -m "feat: 新增数值格式化工具(含单元测试
|
|||||||
- Consumes: `domain/model/*`、`domain/repository/*`
|
- Consumes: `domain/model/*`、`domain/repository/*`
|
||||||
- Produces:
|
- Produces:
|
||||||
- `interface AppContainer { healthDataRepository; ringDeviceRepository }`、`class DefaultAppContainer : AppContainer`、`RingHealthApplication.container`
|
- `interface AppContainer { healthDataRepository; ringDeviceRepository }`、`class DefaultAppContainer : AppContainer`、`RingHealthApplication.container`
|
||||||
- `class MockDataProvider(seed: Long = DEFAULT_SEED)`,方法:`todaySummary(): HealthSummary`、`metricHistory(metric: MetricType, range: DateRange): List<MetricPoint>`、`workouts(): List<WorkoutRecord>`、`deviceState(): RingDeviceState`、`defaultFunctionSettings(): List<DeviceFunctionSetting>`
|
- `class MockDataProvider(seed: Long = DEFAULT_SEED, now: () -> Long = System::currentTimeMillis)`,方法:`todaySummary(): HealthSummary`、`metricHistory(metric: MetricType, range: DateRange): List<MetricPoint>`、`workouts(): List<WorkoutRecord>`、`deviceState(): RingDeviceState`、`defaultFunctionSettings(): List<DeviceFunctionSetting>`
|
||||||
- `class MockHealthDataRepository : HealthDataRepository`(每次发射前 `delay(200)`)
|
- `class MockHealthDataRepository : HealthDataRepository`(每次发射前 `delay(200)`)
|
||||||
- `class MockRingDeviceRepository : RingDeviceRepository`(开关状态保存在内存 StateFlow)
|
- `class MockRingDeviceRepository : RingDeviceRepository`(开关状态保存在内存 StateFlow)
|
||||||
|
|
||||||
|
> **范围说明(见 ledger Ruling R17)**:`MockDataProvider` 增加一个**可注入的时间源** `now: () -> Long`
|
||||||
|
> (默认 `System::currentTimeMillis`),`metricHistory` / `workouts` 内一律用 `now()` 而非
|
||||||
|
> `System.currentTimeMillis()`。原因:本类的 KDoc 承诺「所有方法都是确定性的(同参数返回同结果)」,
|
||||||
|
> 但直接读挂钟时 `timestamp` 会随毫秒跳动 —— brief 原先的测试 `assertEquals(first, second)`
|
||||||
|
> 因此是一条**间歇失败的 flaky 断言**(两次调用跨毫秒即挂)。注入时间源后该承诺才成立,
|
||||||
|
> 测试也才能传固定时钟、真正验证「可重复」。这是本任务唯一的接口增补。
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败的测试**
|
- [ ] **Step 1: 写失败的测试**
|
||||||
|
|
||||||
`app/src/test/java/com/placeholder/ringhealth/data/mock/MockDataProviderTest.kt`:
|
`app/src/test/java/com/placeholder/ringhealth/data/mock/MockDataProviderTest.kt`:
|
||||||
@@ -1369,7 +1376,16 @@ import org.junit.Test
|
|||||||
|
|
||||||
class MockDataProviderTest {
|
class MockDataProviderTest {
|
||||||
|
|
||||||
private val provider = MockDataProvider()
|
/**
|
||||||
|
* 固定时钟。Mock 的「确定性」承诺依赖时间源固定,否则 `timestamp` 会随毫秒跳动
|
||||||
|
* (ledger Ruling R17)。
|
||||||
|
*/
|
||||||
|
private val provider = MockDataProvider(now = { FIXED_NOW })
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/** 固定的“当前时间”,让历史数据的时间戳可复现。 */
|
||||||
|
const val FIXED_NOW = 1_760_000_000_000L
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `今日概览返回固定演示数据`() {
|
fun `今日概览返回固定演示数据`() {
|
||||||
@@ -1395,6 +1411,8 @@ class MockDataProviderTest {
|
|||||||
val second = provider.metricHistory(MetricType.HEART_RATE, DateRange.WEEK)
|
val second = provider.metricHistory(MetricType.HEART_RATE, DateRange.WEEK)
|
||||||
assertEquals(first, second)
|
assertEquals(first, second)
|
||||||
assertTrue(first.all { it.value > 40.0 && it.value < 180.0 })
|
assertTrue(first.all { it.value > 40.0 && it.value < 180.0 })
|
||||||
|
// 时间戳须严格递增(“每点间隔一天”倒推,最早的在最前)
|
||||||
|
assertTrue(first.zipWithNext().all { (a, b) -> b.timestamp > a.timestamp })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1437,10 +1455,17 @@ import com.placeholder.ringhealth.domain.model.WorkoutRecord
|
|||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 演示数据生成器。所有方法都是确定性的(同参数返回同结果),
|
* 演示数据生成器。所有方法都是确定性的(同参数、同时刻返回同结果),
|
||||||
* 保证页面展示与截图稳定。M2 接入真机后本类整体弃用。
|
* 保证页面展示与截图稳定。M2 接入真机后本类整体弃用。
|
||||||
|
*
|
||||||
|
* @param seed 随机种子,决定历史数据的取值序列。
|
||||||
|
* @param now 时间源,默认取系统时钟;测试注入固定值后可得到完全可复现的时间戳
|
||||||
|
* (ledger Ruling R17)。
|
||||||
*/
|
*/
|
||||||
class MockDataProvider(private val seed: Long = DEFAULT_SEED) {
|
class MockDataProvider(
|
||||||
|
private val seed: Long = DEFAULT_SEED,
|
||||||
|
private val now: () -> Long = System::currentTimeMillis,
|
||||||
|
) {
|
||||||
|
|
||||||
/** 今日概览:固定演示值。血压为 null 以演示“未开启”占位态。 */
|
/** 今日概览:固定演示值。血压为 null 以演示“未开启”占位态。 */
|
||||||
fun todaySummary(): HealthSummary = HealthSummary(
|
fun todaySummary(): HealthSummary = HealthSummary(
|
||||||
@@ -1464,13 +1489,13 @@ class MockDataProvider(private val seed: Long = DEFAULT_SEED) {
|
|||||||
val count = if (range == DateRange.WEEK) 7 else 30
|
val count = if (range == DateRange.WEEK) 7 else 30
|
||||||
val random = Random(seed + metric.ordinal * 100L + range.ordinal)
|
val random = Random(seed + metric.ordinal * 100L + range.ordinal)
|
||||||
val (min, max) = valueRange(metric)
|
val (min, max) = valueRange(metric)
|
||||||
val now = System.currentTimeMillis()
|
val current = now()
|
||||||
val dayMillis = 24L * 60 * 60 * 1000
|
val dayMillis = 24L * 60 * 60 * 1000
|
||||||
return (0 until count).map { index ->
|
return (0 until count).map { index ->
|
||||||
val progress = index.toFloat() / (count - 1).coerceAtLeast(1)
|
val progress = index.toFloat() / (count - 1).coerceAtLeast(1)
|
||||||
val value = min + (max - min) * progress + random.nextDouble(-1.0, 1.0) * (max - min) * 0.15
|
val value = min + (max - min) * progress + random.nextDouble(-1.0, 1.0) * (max - min) * 0.15
|
||||||
MetricPoint(
|
MetricPoint(
|
||||||
timestamp = now - (count - 1 - index) * dayMillis,
|
timestamp = current - (count - 1 - index) * dayMillis,
|
||||||
value = value.coerceIn(min, max),
|
value = value.coerceIn(min, max),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1479,14 +1504,14 @@ class MockDataProvider(private val seed: Long = DEFAULT_SEED) {
|
|||||||
/** 运动记录:固定 5 条演示数据,倒序(最近的在最前)。 */
|
/** 运动记录:固定 5 条演示数据,倒序(最近的在最前)。 */
|
||||||
fun workouts(): List<WorkoutRecord> {
|
fun workouts(): List<WorkoutRecord> {
|
||||||
val random = Random(seed + 7)
|
val random = Random(seed + 7)
|
||||||
val now = System.currentTimeMillis()
|
val current = now()
|
||||||
val dayMillis = 24L * 60 * 60 * 1000
|
val dayMillis = 24L * 60 * 60 * 1000
|
||||||
val types = listOf("户外跑步", "健走", "骑行", "自由训练", "户外跑步")
|
val types = listOf("户外跑步", "健走", "骑行", "自由训练", "户外跑步")
|
||||||
return types.mapIndexed { index, type ->
|
return types.mapIndexed { index, type ->
|
||||||
WorkoutRecord(
|
WorkoutRecord(
|
||||||
id = "workout-$index",
|
id = "workout-$index",
|
||||||
type = type,
|
type = type,
|
||||||
startTime = now - (index + 1) * dayMillis,
|
startTime = current - (index + 1) * dayMillis,
|
||||||
durationMinutes = 25 + random.nextInt(0, 45),
|
durationMinutes = 25 + random.nextInt(0, 45),
|
||||||
distanceKm = 2.0 + random.nextDouble(0.0, 5.0),
|
distanceKm = 2.0 + random.nextDouble(0.0, 5.0),
|
||||||
calories = 120 + random.nextInt(0, 260),
|
calories = 120 + random.nextInt(0, 260),
|
||||||
|
|||||||
Reference in New Issue
Block a user