13.7. アプリケーションのテスト
適切なアプリケーションにはテストが含まれます。timetable プロジェクトで制約とソルバーをテストします。
13.7.1. 学校の時間割の制約をテストする
timetable プロジェクトの各制約を個別にテストするには、単体テストで ConstraintVerifier
を使用します。これにより、各制約のコーナーケースが他のテストから分離されてテストされるため、適切なテストカバレッジで新しい制約を追加する際のメンテナンスが軽減されます。
このテストは、制約 TimeTableConstraintProvider::roomConflict
が、同じ部屋で 3 つのレッスンを与えられ、そのうちの 2 つのレッスンが同じタイムスロットを持つ場合、一致の重み 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); } }
制約の重みが ConstraintProvider
でハードコーディングされている場合でも、ConstraintVerifier
がテスト中に制約の重みを無視することに注意してください。これは、実稼動に入る前に制約の重みが定期的に変更されるためです。このように、制約の重みの微調整によって単体テストが中断されることはありません。
13.7.2. 学校の時間割ソルバーをテストする
以下の例では、Red Hat ビルドの Quarkus で Red Hat ビルドの OptaPlanner の時間割プロジェクトをテストします。このアプリケーションは、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); } }
このテストは、解決後にすべての授業がタイムスロットと部屋に割り当てられていることを確認します。また、実行可能解 (ハード制約の違反なし) も確認します。
テストプロパティーを
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. 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/*soft
通常、ソルバーは 200 ミリ秒未満で実行可能解を検索します。application.properties
が、実行可能なソリューション (0hard/*soft
) が見つかると同時に終了するように、テスト中のソルバーの終了を上書きします。こうすることで、ユニットテストが任意のハードウェアで実行される可能性があるため、ソルバーの時間をハードコード化するのを回避します。このアプローチを使用することで、動きが遅いシステムであっても、実行可能なソリューションを検索するのに十分な時間だけテストが実行されます。ただし、高速システムでも、厳密に必要とされる時間よりもミリ秒単位で長く実行されることはありません。