13.7. 测试应用
良好的应用程序包括测试覆盖。测试时间表项目中的限制和 solver。
13.7.1. 测试 school 计时的限制 复制链接链接已复制到粘贴板!
要以隔离方式测试每个 timetable 项目约束,请在单元测试中使用 ConstraintVerifier。这会测试每个约束与其他测试隔离的情况,这会在添加具有适当测试覆盖的新约束时降低维护。
此测试会验证约束 TimeTableConstraintProvider::roomConflict,当同一间有 3 个经验时,两个经验都有相同的 timeslot,被 penalizes 为 1。因此,如果约束权重为 10hard,它将分数降低为 -10hard。
流程
创建 src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java 类:
package org.acme.optaplanner.solver;
import java.time.DayOfWeek;
import java.time.LocalTime;
import javax.inject.Inject;
import io.quarkus.test.junit.QuarkusTest;
import org.acme.optaplanner.domain.Lesson;
import org.acme.optaplanner.domain.Room;
import org.acme.optaplanner.domain.TimeTable;
import org.acme.optaplanner.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;
@QuarkusTest
class TimeTableConstraintProviderTest {
private static final Room ROOM = new Room("Room1");
private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON);
private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON);
@Inject
ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier;
@Test
void roomConflict() {
Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1");
Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2");
Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3");
firstLesson.setRoom(ROOM);
firstLesson.setTimeslot(TIMESLOT1);
conflictingLesson.setRoom(ROOM);
conflictingLesson.setTimeslot(TIMESLOT1);
nonConflictingLesson.setRoom(ROOM);
nonConflictingLesson.setTimeslot(TIMESLOT2);
constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
.given(firstLesson, conflictingLesson, nonConflictingLesson)
.penalizesBy(1);
}
}
package org.acme.optaplanner.solver;
import java.time.DayOfWeek;
import java.time.LocalTime;
import javax.inject.Inject;
import io.quarkus.test.junit.QuarkusTest;
import org.acme.optaplanner.domain.Lesson;
import org.acme.optaplanner.domain.Room;
import org.acme.optaplanner.domain.TimeTable;
import org.acme.optaplanner.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;
@QuarkusTest
class TimeTableConstraintProviderTest {
private static final Room ROOM = new Room("Room1");
private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON);
private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON);
@Inject
ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier;
@Test
void roomConflict() {
Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1");
Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2");
Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3");
firstLesson.setRoom(ROOM);
firstLesson.setTimeslot(TIMESLOT1);
conflictingLesson.setRoom(ROOM);
conflictingLesson.setTimeslot(TIMESLOT1);
nonConflictingLesson.setRoom(ROOM);
nonConflictingLesson.setTimeslot(TIMESLOT2);
constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
.given(firstLesson, conflictingLesson, nonConflictingLesson)
.penalizesBy(1);
}
}
请注意,ConstraintVerifier 在测试过程中如何忽略约束权重,即使这些约束权重在 ConstraintProvider 中被硬编码。这是因为在进入生产环境前,会定期更改约束权重。这样,约束的权重调整不会破坏单元测试。
13.7.2. 测试 school timetable solver 复制链接链接已复制到粘贴板!
这个示例测试红帽在 Red Hat build of Quarkus 上构建的 OptaPlanner National Timetable 项目。它使用 JUnit 测试来生成测试数据集,并将其发送到 TimeTableController 来解决。
流程
使用以下内容创建
src/test/java/com/example/rest/TimeTableResourceTest.java类:package com.exmaple.optaplanner.rest; import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import io.quarkus.test.junit.QuarkusTest; import com.exmaple.optaplanner.domain.Room; import com.exmaple.optaplanner.domain.Timeslot; import com.exmaple.optaplanner.domain.Lesson; import com.exmaple.optaplanner.domain.TimeTable; import com.exmaple.optaplanner.rest.TimeTableResource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest public class TimeTableResourceTest { @Inject TimeTableResource timeTableResource; @Test @Timeout(600_000) public void solve() { TimeTable problem = generateProblem(); TimeTable solution = timeTableResource.solve(problem); assertFalse(solution.getLessonList().isEmpty()); for (Lesson lesson : solution.getLessonList()) { assertNotNull(lesson.getTimeslot()); assertNotNull(lesson.getRoom()); } assertTrue(solution.getScore().isFeasible()); } private TimeTable generateProblem() { List<Timeslot> timeslotList = new ArrayList<>(); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade")); lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade")); lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade")); lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } }package com.exmaple.optaplanner.rest; import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import io.quarkus.test.junit.QuarkusTest; import com.exmaple.optaplanner.domain.Room; import com.exmaple.optaplanner.domain.Timeslot; import com.exmaple.optaplanner.domain.Lesson; import com.exmaple.optaplanner.domain.TimeTable; import com.exmaple.optaplanner.rest.TimeTableResource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest public class TimeTableResourceTest { @Inject TimeTableResource timeTableResource; @Test @Timeout(600_000) public void solve() { TimeTable problem = generateProblem(); TimeTable solution = timeTableResource.solve(problem); assertFalse(solution.getLessonList().isEmpty()); for (Lesson lesson : solution.getLessonList()) { assertNotNull(lesson.getTimeslot()); assertNotNull(lesson.getRoom()); } assertTrue(solution.getScore().isFeasible()); } private TimeTable generateProblem() { List<Timeslot> timeslotList = new ArrayList<>(); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade")); lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade")); lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade")); lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } }Copy to Clipboard Copied! Toggle word wrap Toggle overflow 此测试会验证在解决后,所有课程都会被分配给一个时间插槽和房间。它还会验证它是否找到了可行的解决方案(没有硬约束问题)。
将 test 属性添加到
src/main/resources/application.properties文件中:The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation. It's recommended to run for at least 5 minutes ("5m") otherwise. Effectively disable this termination in favor of the best-score-limit# The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation. # It's recommended to run for at least 5 minutes ("5m") otherwise. quarkus.optaplanner.solver.termination.spent-limit=5s # Effectively disable this termination in favor of the best-score-limit %test.quarkus.optaplanner.solver.termination.spent-limit=1h %test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*softCopy to Clipboard Copied! Toggle word wrap Toggle overflow
通常,解决者在 200 毫秒内找到可行的解决方案。注意 application.properties 文件如何在测试过程中覆盖 solver 终止,以便在找到可行的解决方案 (0hard86_64) soft) 后马上终止。这可避免对解决者进行硬编码,因为单元测试可能会在任意硬件上运行。这种方法可确保测试运行足够长,以查找可行的解决方案,即使在较慢的系统中。但它不会严格运行比严格运行的时间更长,即使在快速系统上也是如此。