forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
7.2 KiB
169 lines
7.2 KiB
package com.mydreamjob.competition;
|
|
|
|
import com.mydreamjob.application.Application;
|
|
import com.mydreamjob.candidatescore.CandidateDimensionScore;
|
|
import com.mydreamjob.candidatescore.CandidateDimensionScoreRepository;
|
|
import com.mydreamjob.candidatescore.CandidateScoringCalculator;
|
|
import com.mydreamjob.candidatescore.CandidateOverviewStatusPolicy;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
import org.mockito.junit.jupiter.MockitoSettings;
|
|
import org.mockito.quality.Strictness;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.*;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
/**
|
|
* 共享排名计算器单测(V32/V34 复用核心)。
|
|
*
|
|
* <p>覆盖稳定排序(有分优先 → overallScore DESC → createdAt ASC → studentUserId ASC →
|
|
* 无分最后)、cohort、百分位、最新快照周期选择与 overviewStatus。综合分走真实
|
|
* CandidateScoringCalculator 语义(此处以 mock 模拟按 studentUserId 返回预设分)。</p>
|
|
*/
|
|
@ExtendWith(MockitoExtension.class)
|
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
class CompetitionRankingCalculatorTest {
|
|
|
|
private static final Long ME = 100L;
|
|
|
|
@Mock private CandidateDimensionScoreRepository scoreRepository;
|
|
@Mock private CandidateScoringCalculator scoringCalculator;
|
|
|
|
private CompetitionRankingCalculator calculator;
|
|
|
|
private Map<Long, BigDecimal> overallByStudent;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
calculator = new CompetitionRankingCalculator(scoreRepository, scoringCalculator);
|
|
overallByStudent = Map.of();
|
|
when(scoringCalculator.overallScoreFromSnapshots(anyList())).thenAnswer(inv -> {
|
|
List<?> snaps = inv.getArgument(0);
|
|
if (snaps == null || snaps.isEmpty()) return null;
|
|
Number sid = ((CandidateDimensionScore) snaps.get(0)).getStudentUserId();
|
|
return overallByStudent.get(sid.longValue());
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void rankCohort_sortsScoredFirstThenScoreDescThenCreatedAtThenUserId() {
|
|
Application a = app(501L, "2026-01-01T10:00:00");
|
|
Application b = app(502L, "2026-01-02T10:00:00");
|
|
Application c = app(503L, "2026-01-03T10:00:00"); // 无分
|
|
Application me = app(ME, "2026-01-05T10:00:00");
|
|
Map<Long, List<CandidateDimensionScore>> latest = Map.of(
|
|
a.getStudentUserId(), List.of(dim(a.getStudentUserId(), "2026-07")),
|
|
b.getStudentUserId(), List.of(dim(b.getStudentUserId(), "2026-07")),
|
|
ME, List.of(dim(ME, "2026-07")));
|
|
overallByStudent = Map.of(a.getStudentUserId(), new BigDecimal("90.00"),
|
|
b.getStudentUserId(), new BigDecimal("80.00"),
|
|
ME, new BigDecimal("85.00"));
|
|
|
|
CompetitionRankingCalculator.CohortRanking cr =
|
|
calculator.rankCohort(List.of(me, a, b, c), latest, ME);
|
|
|
|
assertEquals(4, cr.cohortSize);
|
|
assertEquals(2, cr.rank.intValue());
|
|
assertEquals(new BigDecimal("85.00"), cr.overallScore);
|
|
assertEquals(new BigDecimal("50.00"), cr.percentile); // lower=1, denom=2
|
|
// 排序:A(90) -> me(85) -> B(80) -> C(null)
|
|
assertEquals(501L, cr.rows.get(0).studentUserId.longValue());
|
|
assertEquals(ME.longValue(), cr.rows.get(1).studentUserId.longValue());
|
|
assertEquals(502L, cr.rows.get(2).studentUserId.longValue());
|
|
assertEquals(503L, cr.rows.get(3).studentUserId.longValue());
|
|
assertNull(cr.rows.get(3).overallScore);
|
|
assertEquals("OK", cr.status);
|
|
assertEquals("2026-07", cr.mySnapshotPeriod);
|
|
}
|
|
|
|
@Test
|
|
void rankCohort_tieBrokenByCreatedAtThenStudentUserId() {
|
|
Application x = app(900L, "2026-04-01T10:00:00");
|
|
Application me = app(ME, "2026-04-02T10:00:00");
|
|
Map<Long, List<CandidateDimensionScore>> latest = Map.of(
|
|
x.getStudentUserId(), List.of(dim(x.getStudentUserId(), "2026-07")),
|
|
ME, List.of(dim(ME, "2026-07")));
|
|
overallByStudent = Map.of(x.getStudentUserId(), new BigDecimal("60.00"),
|
|
ME, new BigDecimal("60.00"));
|
|
|
|
CompetitionRankingCalculator.CohortRanking cr =
|
|
calculator.rankCohort(List.of(x, me), latest, ME);
|
|
|
|
assertEquals(2, cr.rank.intValue()); // x first, me second
|
|
assertEquals(900L, cr.rows.get(0).studentUserId.longValue());
|
|
}
|
|
|
|
@Test
|
|
void rankCohort_currentNoScore_rankScorePercentileNull() {
|
|
Application a = app(501L, "2026-01-01T10:00:00");
|
|
Application me = app(ME, "2026-01-02T10:00:00");
|
|
Map<Long, List<CandidateDimensionScore>> latest = Map.of(
|
|
a.getStudentUserId(), List.of(dim(a.getStudentUserId(), "2026-07")));
|
|
overallByStudent = Map.of(a.getStudentUserId(), new BigDecimal("90.00"));
|
|
|
|
CompetitionRankingCalculator.CohortRanking cr =
|
|
calculator.rankCohort(List.of(a, me), latest, ME);
|
|
|
|
assertEquals(2, cr.cohortSize);
|
|
assertNull(cr.rank);
|
|
assertNull(cr.overallScore);
|
|
assertNull(cr.percentile);
|
|
assertNull(cr.mySnapshotPeriod);
|
|
assertEquals("INSUFFICIENT_DATA", cr.status);
|
|
}
|
|
|
|
@Test
|
|
void rankCohort_scoredLessThan2_percentileNull() {
|
|
Application a = app(501L, "2026-01-01T10:00:00");
|
|
Application me = app(ME, "2026-01-02T10:00:00");
|
|
Map<Long, List<CandidateDimensionScore>> latest = Map.of(ME, List.of(dim(ME, "2026-07")));
|
|
overallByStudent = Map.of(ME, new BigDecimal("85.00"));
|
|
|
|
CompetitionRankingCalculator.CohortRanking cr =
|
|
calculator.rankCohort(List.of(a, me), latest, ME);
|
|
|
|
assertNull(cr.percentile);
|
|
}
|
|
|
|
@Test
|
|
void loadLatestSnapshots_picksLatestPeriodPerStudent() {
|
|
when(scoreRepository.findUpToPeriod(anyCollection(), anyString()))
|
|
.thenReturn(List.of(
|
|
dim(ME, "2026-06"),
|
|
dim(ME, "2026-07"),
|
|
dim(501L, "2026-05")));
|
|
|
|
Map<Long, List<CandidateDimensionScore>> latest =
|
|
calculator.loadLatestSnapshots(List.of(ME, 501L), "2026-07");
|
|
|
|
assertEquals(1, latest.get(ME).size());
|
|
assertEquals("2026-07", latest.get(ME).get(0).getPeriod());
|
|
assertEquals("2026-05", latest.get(501L).get(0).getPeriod());
|
|
}
|
|
|
|
@Test
|
|
void overviewStatus_emptyIsInsufficientData() {
|
|
assertEquals("INSUFFICIENT_DATA", CandidateOverviewStatusPolicy.fromSnapshots(List.of()));
|
|
}
|
|
|
|
private Application app(Long sid, String created) {
|
|
return Application.builder().studentUserId(sid)
|
|
.status("SUBMITTED")
|
|
.createdAt(LocalDateTime.parse(created)).build();
|
|
}
|
|
|
|
private CandidateDimensionScore dim(Long sid, String period) {
|
|
return CandidateDimensionScore.builder().studentUserId(sid).dimension("EDUCATION").period(period)
|
|
.score(new BigDecimal("80")).status("OK")
|
|
.weight(new BigDecimal("12.50")).rulesVersion("candidate-scoring-v0.1").build();
|
|
}
|
|
}
|
|
|