笔记 / Timefold Solver / 约束与评分
01 / 03 / 08

ConstraintVerifier 约束单元测试

用 ConstraintVerifier 对单个约束或整个 ConstraintProvider 做单元测试,验证 penalize/reward 值和约束匹配数量

状态
持续整理
来源
Obsidian
创建
2026/07/21
公开整理
2026/07/31

30 秒速览#

ConstraintVerifier 是 Timefold 提供的约束单元测试工具。它让你在不启动 Solver 的情况下,直接验证某个约束对一组给定数据产生的惩罚/奖励值是否正确。

核心流程:verifyThat(约束方法) → given(测试数据) → penalizesBy(期望值)

为什么需要#

  • 约束逻辑错了,Solver 在优化错误的分数。
  • 解质量问题的根因往往是”约束没表达业务规则”,而不是算法不够好。
  • 约束测试比 Solver 集成测试快得多,适合高频运行。

基本结构#

private ConstraintVerifier<MyConstraintProvider, MySolution> constraintVerifier
    = ConstraintVerifier.build(
        new MyConstraintProvider(),
        MySolution.class,
        MyEntity.class,
        MyFact.class);
java

参数:ConstraintProvider 实例 → 规划解类 → 所有涉及的实体和事实类。

测试单个约束#

@Test
void roomConflict() {
    Room roomA = new Room("A");
    Lesson lesson1 = new Lesson(1, "Math", "Monday 08:00", roomA);
    Lesson lesson2 = new Lesson(2, "Physics", "Monday 08:00", roomA);

    constraintVerifier.verifyThat(MyConstraintProvider::roomConflict)
            .given(roomA, lesson1, lesson2)
            .penalizesBy(1);   // HardSoftScore.ONE_HARD 被匹配 1 次
}
java

penalizesBy(n) 的值 = 单次匹配权重 × 匹配次数。

动态权重的断言#

constraintVerifier.verifyThat(MyConstraintProvider::minimizeChangeover)
        .given(task1, task2)
        .penalizesBy(30);   // 换型时间 30 分钟 → 扣 30 soft
java

rewardsWith() 断言奖励#

constraintVerifier.verifyThat(MyConstraintProvider::preferredRoom)
        .given(roomB, lesson)
        .rewardsWith(1);
java

测试全部约束#

不传约束方法,用 scores() 断言总分数:

@Test
void givenFactsMultipleConstraints() {
    constraintVerifier.verifyThat()
            .given(roomA, lesson1, lesson2)
            .scores(HardSoftScore.ofSoft(20));  // 期望 0hard/-20soft
}
java

需要确保 given() 提供了约束涉及的所有实体和事实,否则会漏匹配或报错。

givenSolution 与影子变量#

可以直接传整个 PlanningSolution,但影子变量默认不更新:

// 影子变量不会自动设置
constraintVerifier.verifyThat(MyConstraintProvider::capacity)
        .givenSolution(solution)
        .penalizesBy(20);

// 显式要求更新所有影子变量
constraintVerifier.verifyThat(MyConstraintProvider::capacity)
        .givenSolution(solution)
        .settingAllShadowVariables()
        .penalizesBy(20);
java

注意:ConstraintVerifier 不会触发自定义变量监听器(custom variable listener),只会更新内置的影子变量。如果约束依赖手动变量监听器,需由测试代码自行赋值。

Spring Boot 集成#

在 Spring Boot 项目中直接注入:

@SpringBootTest
public class MyConstraintProviderTest {
    @Autowired
    ConstraintVerifier<MyConstraintProvider, MySolution> constraintProvider;
}
java

常用断言模式#

场景断言
无违反.penalizesBy(0).rewardsWith(0)
匹配 N 次.penalizesBy(N)
动态惩罚值.penalizesBy(expectedValue)
总分数验证.scores(HardSoftScore.ofHard(1))

排程约束测试示例#

约束可见性#

约束方法必须对测试类可见。在 ConstraintProvider 实现中,约束方法通常是 public,这自然满足要求。如果约束被提取为私有方法辅助,需要改成包可见或公开。

关联笔记#