You've already forked FrameTour-BE
test(puzzle): 更新测试用例以适配新的方法签名并增强唯一性验证
- 在 PuzzleGenerateServiceDeduplicationTest 中引入 CustomFaceSearchStage 类 - 添加 SpringBootTest 注解以支持完整的上下文加载 - 使用 InjectMocks 替代手动构造服务实例 - 修改 recordMapper.updateSuccess 方法调用,增加一个 String 参数 - 新增 SnowFlakeUtilTest 类用于测试雪花ID生成器的唯一性和性能 - 添加高并发环境下的ID唯一性校验逻辑 - 引入对潜在时间戳溢出问题的检测机制 - 增加单线程性能测试方法 testPerformanceSingleThread
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
// 执行两次生成
|
// 执行两次生成
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
92
src/test/java/com/ycwl/basic/utils/SnowFlakeUtilTest.java
Normal file
92
src/test/java/com/ycwl/basic/utils/SnowFlakeUtilTest.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user