package com.accounting.util; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 减重计划计算测试。 * *
这块是「设置目标 → 每周该减多少」的核心算术,也是最容易出现 * 边界问题的地方:同日、超期、目标比起始还重、除零。所以边界用例 * 写得比正常用例还多。
*/ class WeightPlanCalculatorTest { @Test @DisplayName("三个月 65→58kg:总减 7kg 分到 13 周,每周 0.54kg") void weeklyLossForThreeMonths() { LocalDate start = LocalDate.of(2026, 1, 1); LocalDate target = LocalDate.of(2026, 4, 1); // 90 天 int weeks = WeightPlanCalculator.totalWeeks(start, target); assertEquals(13, weeks); BigDecimal weekly = WeightPlanCalculator.weeklyLoss( new BigDecimal("65"), new BigDecimal("58"), weeks); assertEquals(new BigDecimal("0.54"), weekly); } @Test @DisplayName("总天数除不尽 7 时向上取整 —— 少算一周会让每周目标偏轻") void roundsUpPartialWeek() { // 8 天 = 1 周 + 1 天,应该算 2 周而不是 1 周 assertEquals(2, WeightPlanCalculator.totalWeeks( LocalDate.of(2026, 1, 1), LocalDate.of(2026, 1, 9))); // 6 天算 1 周 assertEquals(1, WeightPlanCalculator.totalWeeks( LocalDate.of(2026, 1, 1), LocalDate.of(2026, 1, 7))); } @Test @DisplayName("同日或倒填日期都算 1 周,不除零") void sameDayFallsBackToOneWeek() { LocalDate day = LocalDate.of(2026, 1, 1); assertEquals(1, WeightPlanCalculator.totalWeeks(day, day)); // 目标日期早于开始日期也不该崩 assertEquals(1, WeightPlanCalculator.totalWeeks(day, day.minusDays(5))); // 1 周减 7kg 是个荒谬的值,但必须是个数、不能是异常 assertEquals(new BigDecimal("7.00"), WeightPlanCalculator.weeklyLoss( new BigDecimal("65"), new BigDecimal("58"), 1)); } @Test @DisplayName("目标比起始还重时每周应减为 0,不能是负数") void gainingWeightGivesZeroNotNegative() { // 负数会让进度条宽度算反、也会让「每周应减」显示成 -0.5kg assertEquals(BigDecimal.ZERO, WeightPlanCalculator.weeklyLoss( new BigDecimal("58"), new BigDecimal("65"), 13)); assertEquals(BigDecimal.ZERO, WeightPlanCalculator.weeklyLoss( new BigDecimal("60"), new BigDecimal("60"), 13)); } @Test @DisplayName("理想体重曲线:第 0 周是起始体重,最后一周落在目标上") void idealCurveEndsAtTarget() { BigDecimal start = new BigDecimal("65"); BigDecimal target = new BigDecimal("58"); BigDecimal weekly = new BigDecimal("0.54"); assertEquals(new BigDecimal("65.0"), WeightPlanCalculator.idealWeightAt(start, target, weekly, 0)); // 第 13 周:65 - 0.54*13 = 57.98,低于目标 58 → 应该钉在 58 assertEquals(new BigDecimal("58"), WeightPlanCalculator.idealWeightAt(start, target, weekly, 13)); } @Test @DisplayName("理想体重不会跌破目标 —— 否则参照线会穿到目标下方") void idealWeightNeverGoesBelowTarget() { BigDecimal target = new BigDecimal("58"); // 刻意给一个过大的每周减重 BigDecimal ideal = WeightPlanCalculator.idealWeightAt( new BigDecimal("65"), target, new BigDecimal("2.0"), 10); assertEquals(target, ideal); } @Test @DisplayName("时间进度:未开始 0、进行中在 0~1、超期封顶 1.0") void timeProgressIsClamped() { LocalDate start = LocalDate.of(2026, 1, 1); LocalDate target = LocalDate.of(2026, 1, 11); // 10 天 // 还没开始 assertEquals(0.0, WeightPlanCalculator.timeProgress(start, target, LocalDate.of(2025, 12, 20))); // 正中间 assertEquals(0.5, WeightPlanCalculator.timeProgress(start, target, LocalDate.of(2026, 1, 6)), 0.001); // 已超期:封顶 1.0,不是 1.5 —— 进度条宽度不能溢出 assertEquals(1.0, WeightPlanCalculator.timeProgress(start, target, LocalDate.of(2026, 3, 1))); } @Test @DisplayName("每周超过 1kg 会被标记成不健康") void flagsUnhealthyPace() { // 65→58 只用 3 周 = 每周 2.33kg BigDecimal weekly = WeightPlanCalculator.weeklyLoss( new BigDecimal("65"), new BigDecimal("58"), 3); assertTrue(weekly.compareTo(WeightPlanCalculator.HEALTHY_WEEKLY_LOSS_KG) > 0); // 正常节奏不该被标 BigDecimal healthy = WeightPlanCalculator.weeklyLoss( new BigDecimal("65"), new BigDecimal("58"), 13); assertFalse(healthy.compareTo(WeightPlanCalculator.HEALTHY_WEEKLY_LOSS_KG) > 0); } }