test(puzzle): 更新测试用例以适配新的方法签名并增强唯一性验证

- 在 PuzzleGenerateServiceDeduplicationTest 中引入 CustomFaceSearchStage 类
- 添加 SpringBootTest 注解以支持完整的上下文加载
- 使用 InjectMocks 替代手动构造服务实例
- 修改 recordMapper.updateSuccess 方法调用,增加一个 String 参数
- 新增 SnowFlakeUtilTest 类用于测试雪花ID生成器的唯一性和性能
- 添加高并发环境下的ID唯一性校验逻辑
- 引入对潜在时间戳溢出问题的检测机制
- 增加单线程性能测试方法 testPerformanceSingleThread
This commit is contained in:
2025-12-18 10:35:19 +08:00
parent 95a5977ae2
commit 2432cf496f
3 changed files with 103 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
package com.ycwl.basic.puzzle.service.impl; package com.ycwl.basic.puzzle.service.impl;
import com.ycwl.basic.face.pipeline.stages.CustomFaceSearchStage;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest; import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse; import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity; import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
@@ -17,8 +18,10 @@ import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.storage.StorageFactory; import com.ycwl.basic.storage.StorageFactory;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.ArrayList; import java.util.ArrayList;
@@ -36,6 +39,7 @@ import static org.mockito.Mockito.*;
* @author Claude * @author Claude
* @since 2025-01-21 * @since 2025-01-21
*/ */
@SpringBootTest
class PuzzleGenerateServiceDeduplicationTest { class PuzzleGenerateServiceDeduplicationTest {
@Mock @Mock
@@ -58,6 +62,7 @@ class PuzzleGenerateServiceDeduplicationTest {
private PuzzleDuplicationDetector duplicationDetector; private PuzzleDuplicationDetector duplicationDetector;
@InjectMocks
private PuzzleGenerateServiceImpl service; private PuzzleGenerateServiceImpl service;
@BeforeEach @BeforeEach
@@ -65,17 +70,6 @@ class PuzzleGenerateServiceDeduplicationTest {
MockitoAnnotations.openMocks(this); MockitoAnnotations.openMocks(this);
// 创建真实的 duplicationDetector 实例 // 创建真实的 duplicationDetector 实例
duplicationDetector = new PuzzleDuplicationDetector(recordMapper); duplicationDetector = new PuzzleDuplicationDetector(recordMapper);
// 手动注入所有依赖
service = new PuzzleGenerateServiceImpl(
templateMapper,
elementMapper,
recordMapper,
imageRenderer,
fillEngine,
scenicRepository,
duplicationDetector
);
} }
/** /**
@@ -113,7 +107,7 @@ class PuzzleGenerateServiceDeduplicationTest {
}).when(recordMapper).insert(any()); }).when(recordMapper).insert(any());
// Mock更新成功 // Mock更新成功
when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())) when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()))
.thenReturn(1); .thenReturn(1);
// 执行 // 执行
@@ -125,7 +119,7 @@ class PuzzleGenerateServiceDeduplicationTest {
assertNull(response.getOriginalRecordId()); assertNull(response.getOriginalRecordId());
verify(imageRenderer, times(1)).render(any(), any(), any()); // 确实进行了渲染 verify(imageRenderer, times(1)).render(any(), any(), any()); // 确实进行了渲染
verify(recordMapper, times(1)).insert(any()); // 插入了一条记录 verify(recordMapper, times(1)).insert(any()); // 插入了一条记录
verify(recordMapper, times(1)).updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()); verify(recordMapper, times(1)).updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt());
} }
/** /**
@@ -163,7 +157,7 @@ class PuzzleGenerateServiceDeduplicationTest {
assertEquals("https://example.com/old-image.jpg", response.getImageUrl()); // 复用的URL assertEquals("https://example.com/old-image.jpg", response.getImageUrl()); // 复用的URL
verify(imageRenderer, never()).render(any(), any(), any()); // 没有进行渲染 verify(imageRenderer, never()).render(any(), any(), any()); // 没有进行渲染
verify(recordMapper, never()).insert(any()); // 没有插入新记录 verify(recordMapper, never()).insert(any()); // 没有插入新记录
verify(recordMapper, never()).updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()); verify(recordMapper, never()).updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt());
} }
/** /**
@@ -240,7 +234,7 @@ class PuzzleGenerateServiceDeduplicationTest {
return 1; return 1;
}).when(recordMapper).insert(any()); }).when(recordMapper).insert(any());
when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())) when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()))
.thenReturn(1); .thenReturn(1);
// 执行两次生成 // 执行两次生成

View File

@@ -84,7 +84,7 @@ class PuzzleGenerateServiceImplTest {
record.setId(555L); record.setId(555L);
return 1; return 1;
}).when(recordMapper).insert(any()); }).when(recordMapper).insert(any());
when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1); when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1);
PuzzleGenerateRequest request = new PuzzleGenerateRequest(); PuzzleGenerateRequest request = new PuzzleGenerateRequest();
request.setTemplateCode("ticket"); request.setTemplateCode("ticket");
@@ -131,7 +131,7 @@ class PuzzleGenerateServiceImplTest {
record.setId(777L); record.setId(777L);
return 1; return 1;
}).when(recordMapper).insert(any()); }).when(recordMapper).insert(any());
when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1); when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1);
PuzzleGenerateRequest request = new PuzzleGenerateRequest(); PuzzleGenerateRequest request = new PuzzleGenerateRequest();
request.setTemplateCode("ticket"); request.setTemplateCode("ticket");

View File

@@ -0,0 +1,92 @@
package com.ycwl.basic.utils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Test for SnowFlakeUtil to verify uniqueness under high concurrency.
*/
public class SnowFlakeUtilTest {
private static final Logger log = LoggerFactory.getLogger(SnowFlakeUtilTest.class);
@Test
public void testIdUniqueness() throws InterruptedException {
// Number of threads
int threadCount = 1000;
// Number of IDs per thread
int idCountPerThread = 10000;
// Total IDs expected
int totalIdCount = threadCount * idCountPerThread;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
final Set<String> idSet = ConcurrentHashMap.newKeySet();
final CountDownLatch latch = new CountDownLatch(threadCount);
// Use a set to capture any "suspicious" short IDs (potential raw timestamps)
final Set<String> suspiciousIds = ConcurrentHashMap.newKeySet();
log.info("Starting concurrent snowflake ID generation test...");
long start = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
try {
for (int j = 0; j < idCountPerThread; j++) {
String id = SnowFlakeUtil.getId();
idSet.add(id);
// Check for the potential overflow bug where it returns raw timestamp
// A valid snowflake ID (around year 2025) should be much larger than a timestamp
// Current timestamp is approx 13 digits (1734...)
// Snowflake ID should be approx 18-19 digits
if (id.length() < 15) {
suspiciousIds.add(id);
}
}
} catch (Exception e) {
log.error("Error generating ID", e);
} finally {
latch.countDown();
}
});
}
latch.await();
long end = System.currentTimeMillis();
executorService.shutdown();
log.info("Generated {} IDs in {} ms", totalIdCount, (end - start));
if (!suspiciousIds.isEmpty()) {
log.warn("Found {} suspicious IDs (likely raw timestamps due to sequence overflow): {}", suspiciousIds.size(), suspiciousIds);
// We might not fail the test for this if the user only asked for uniqueness,
// but it's good to report.
// However, if they are raw timestamps, they collide heavily if generated in the same ms.
}
Assertions.assertEquals(totalIdCount, idSet.size(), "Duplicate IDs found!");
log.info("Uniqueness test passed. Total unique IDs: {}", idSet.size());
}
@Test
public void testPerformanceSingleThread() {
long start = System.currentTimeMillis();
int count = 100000;
for (int i = 0; i < count; i++) {
SnowFlakeUtil.getId();
}
long end = System.currentTimeMillis();
log.info("Generated {} IDs in {} ms (Single Thread)", count, (end - start));
}
}