轻松聘AI开发
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.
 
 
 

141 lines
6.8 KiB

package com.mydreamjob.common.r01c;
import com.mydreamjob.candidatescore.CandidateDimensionScore;
import com.mydreamjob.candidatescore.CandidateOverviewStatusPolicy;
import com.mydreamjob.candidatescore.CandidateScoringCalculator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* R01-C 特征测试:overviewStatus 聚合规则(唯一实现 {@code CandidateOverviewStatusPolicy})。
*
* <p>覆盖四种状态、空集合、null、以及"可计算但分数缺失"的边界;
* 并锁定两个入口(计算结果 / 快照)在等价输入下的产出关系。</p>
*/
class OverviewStatusPolicyCharacterizationTest {
private static final int EXPECTED = 8;
private static CandidateScoringCalculator.DimensionResult result(String status, String score) {
CandidateScoringCalculator.DimensionResult r =
new CandidateScoringCalculator.DimensionResult("EDUCATION");
r.status = status;
r.score = score == null ? null : new BigDecimal(score);
return r;
}
private static List<CandidateScoringCalculator.DimensionResult> results(String status, String score, int n) {
List<CandidateScoringCalculator.DimensionResult> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(result(status, score));
return list;
}
private static CandidateDimensionScore snapshot(String status) {
CandidateDimensionScore s = new CandidateDimensionScore();
s.setStatus(status);
return s;
}
// ── 计算结果入口 ──────────────────────────────────────────
@Test
@DisplayName("特征:8/8 可计算 → OK;7/8 → PARTIAL;0/8 且全 PENDING → PENDING;否则 INSUFFICIENT_DATA")
void computedResultsFourStates() {
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(results("OK", "80", 8), EXPECTED))
.isEqualTo("OK");
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(results("ESTIMATED", "80", 8), EXPECTED))
.as("ESTIMATED 与 OK 同为可计算")
.isEqualTo("OK");
List<CandidateScoringCalculator.DimensionResult> partial = results("OK", "80", 7);
partial.add(result("PENDING", null));
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(partial, EXPECTED)).isEqualTo("PARTIAL");
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(results("PENDING", null, 8), EXPECTED))
.isEqualTo("PENDING");
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(results("INSUFFICIENT_DATA", null, 8), EXPECTED))
.isEqualTo("INSUFFICIENT_DATA");
}
@Test
@DisplayName("特征:OK 但分数缺失 → 不可计算(status 与 score 都必须满足)")
void okStatusWithoutScoreIsNotComputable() {
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(results("OK", null, 8), EXPECTED))
.isEqualTo("INSUFFICIENT_DATA");
}
@Test
@DisplayName("特征:空列表 → PENDING(计算结果入口的 empty 语义)")
void emptyComputedResultsIsPending() {
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(List.of(), EXPECTED)).isEqualTo("PENDING");
}
@Test
@DisplayName("特征:null 结果列表 → NullPointerException(既有契约)")
void nullComputedResultsThrows() {
assertThatThrownBy(() -> CandidateOverviewStatusPolicy.fromComputedResults(null, EXPECTED))
.isInstanceOf(NullPointerException.class);
}
@Test
@DisplayName("特征:expectedDimensions 大于实际可计算数时不得判 OK")
void expectedDimensionsGatesOk() {
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(results("OK", "80", 8), 9))
.isEqualTo("PARTIAL");
}
@Test
@DisplayName("特征:PENDING 与 INSUFFICIENT_DATA 混合 → INSUFFICIENT_DATA(非 PENDING)")
void mixedPendingAndInsufficientIsInsufficient() {
List<CandidateScoringCalculator.DimensionResult> mixed = new ArrayList<>();
mixed.add(result("PENDING", null));
mixed.add(result("INSUFFICIENT_DATA", null));
assertThat(CandidateOverviewStatusPolicy.fromComputedResults(mixed, EXPECTED))
.isEqualTo("INSUFFICIENT_DATA");
}
// ── 快照入口 ──────────────────────────────────────────────
@Test
@DisplayName("特征:快照入口 —— OK/ESTIMATED 全可计算 → OK;空/null → INSUFFICIENT_DATA")
void snapshotEntryStates() {
assertThat(CandidateOverviewStatusPolicy.fromSnapshots(
List.of(snapshot("OK"), snapshot("ESTIMATED")))).isEqualTo("OK");
assertThat(CandidateOverviewStatusPolicy.fromSnapshots(
List.of(snapshot("OK"), snapshot("PENDING")))).isEqualTo("PARTIAL");
assertThat(CandidateOverviewStatusPolicy.fromSnapshots(
List.of(snapshot("PENDING"), snapshot("PENDING")))).isEqualTo("PENDING");
assertThat(CandidateOverviewStatusPolicy.fromSnapshots(
List.of(snapshot("INSUFFICIENT_DATA"), snapshot("PENDING")))).isEqualTo("INSUFFICIENT_DATA");
assertThat(CandidateOverviewStatusPolicy.fromSnapshots(List.of())).isEqualTo("INSUFFICIENT_DATA");
assertThat(CandidateOverviewStatusPolicy.fromSnapshots(null)).isEqualTo("INSUFFICIENT_DATA");
}
// ── 唯一实现门禁 ──────────────────────────────────────────
@Test
@DisplayName("结构门禁:overviewStatus 规则只有唯一的产出点(\"PARTIAL\" 只在该策略类出现)")
void partialStatusHasSingleProducer() throws Exception {
java.nio.file.Path root = java.nio.file.Path.of("src/main/java/com/mydreamjob");
java.util.List<String> producers = new java.util.ArrayList<>();
try (java.util.stream.Stream<java.nio.file.Path> walk = java.nio.file.Files.walk(root)) {
for (java.nio.file.Path p : walk.filter(f -> f.toString().endsWith(".java")).toList()) {
String src = java.nio.file.Files.readString(p);
if (src.contains("\"PARTIAL\"")) {
producers.add(p.getFileName().toString());
}
}
}
assertThat(producers)
.as("PARTIAL 只能由 CandidateOverviewStatusPolicy 产出")
.containsExactly("CandidateOverviewStatusPolicy.java");
}
}