This commit is contained in:
2025-07-01 17:18:20 +08:00
parent 5916f076b7
commit 3b3ec8ea7d
3 changed files with 580 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.org.flashsalesystem;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class FlashSaleSystemApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,280 @@
package com.org.flashsalesystem.service;
import com.org.flashsalesystem.dto.FlashSaleDTO;
import com.org.flashsalesystem.entity.FlashSale;
import com.org.flashsalesystem.entity.Product;
import com.org.flashsalesystem.repository.FlashSaleRepository;
import com.org.flashsalesystem.repository.ProductRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* 秒杀服务测试类
* 测试秒杀核心功能和并发安全性
*/
@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class FlashSaleServiceTest {
@Autowired
private FlashSaleService flashSaleService;
@Autowired
private ProductRepository productRepository;
@Autowired
private FlashSaleRepository flashSaleRepository;
@Autowired
private RedisService redisService;
private Product testProduct;
private FlashSale testFlashSale;
@BeforeEach
void setUp() {
// 创建测试商品
testProduct = new Product();
testProduct.setName("测试商品");
testProduct.setDescription("测试商品描述");
testProduct.setPrice(new BigDecimal("100.00"));
testProduct.setStock(1000);
testProduct.setStatus(1);
testProduct = productRepository.save(testProduct);
// 创建测试秒杀活动
testFlashSale = new FlashSale();
testFlashSale.setProductId(testProduct.getId());
testFlashSale.setFlashPrice(new BigDecimal("50.00"));
testFlashSale.setFlashStock(100);
testFlashSale.setStartTime(LocalDateTime.now().minusMinutes(1));
testFlashSale.setEndTime(LocalDateTime.now().plusHours(1));
testFlashSale.setStatus(2); // 进行中
testFlashSale = flashSaleRepository.save(testFlashSale);
// 预热秒杀数据到Redis
flashSaleService.preloadFlashSale(testFlashSale.getId());
}
/**
* 测试创建秒杀活动
*/
@Test
void testCreateFlashSale() {
// 创建新商品
Product newProduct = new Product();
newProduct.setName("新测试商品");
newProduct.setPrice(new BigDecimal("200.00"));
newProduct.setStock(500);
newProduct.setStatus(1);
newProduct = productRepository.save(newProduct);
// 创建秒杀活动DTO
FlashSaleDTO.CreateDTO createDTO = new FlashSaleDTO.CreateDTO();
createDTO.setProductId(newProduct.getId());
createDTO.setFlashPrice(new BigDecimal("100.00"));
createDTO.setFlashStock(50);
createDTO.setStartTime(LocalDateTime.now().plusMinutes(10));
createDTO.setEndTime(LocalDateTime.now().plusHours(2));
// 创建秒杀活动
FlashSaleDTO result = flashSaleService.createFlashSale(createDTO);
assertNotNull(result);
assertEquals(newProduct.getId(), result.getProductId());
assertEquals(new BigDecimal("100.00"), result.getFlashPrice());
assertEquals(50, result.getFlashStock());
}
/**
* 测试单用户参与秒杀
*/
@Test
void testSingleUserParticipateFlashSale() {
Long userId = 1L;
FlashSaleDTO.ParticipateDTO participateDTO = new FlashSaleDTO.ParticipateDTO();
participateDTO.setFlashSaleId(testFlashSale.getId());
participateDTO.setQuantity(1);
FlashSaleDTO.ResultDTO result = flashSaleService.participateFlashSale(userId, participateDTO);
assertTrue(result.getSuccess());
assertNotNull(result.getOrderId());
assertEquals(testFlashSale.getId(), result.getFlashSaleId());
assertEquals(testProduct.getId(), result.getProductId());
assertEquals(1, result.getQuantity());
}
/**
* 测试重复参与秒杀
*/
@Test
void testDuplicateParticipation() {
Long userId = 2L;
FlashSaleDTO.ParticipateDTO participateDTO = new FlashSaleDTO.ParticipateDTO();
participateDTO.setFlashSaleId(testFlashSale.getId());
participateDTO.setQuantity(1);
// 第一次参与
FlashSaleDTO.ResultDTO result1 = flashSaleService.participateFlashSale(userId, participateDTO);
assertTrue(result1.getSuccess());
// 第二次参与(应该失败)
FlashSaleDTO.ResultDTO result2 = flashSaleService.participateFlashSale(userId, participateDTO);
assertFalse(result2.getSuccess());
assertTrue(result2.getMessage().contains("已经参与过"));
}
/**
* 测试库存不足情况
*/
@Test
void testInsufficientStock() {
// 将库存设置为0
String stockKey = "flashsale_stock:" + testFlashSale.getId();
redisService.set(stockKey, 0);
Long userId = 3L;
FlashSaleDTO.ParticipateDTO participateDTO = new FlashSaleDTO.ParticipateDTO();
participateDTO.setFlashSaleId(testFlashSale.getId());
participateDTO.setQuantity(1);
FlashSaleDTO.ResultDTO result = flashSaleService.participateFlashSale(userId, participateDTO);
assertFalse(result.getSuccess());
assertTrue(result.getMessage().contains("售罄"));
}
/**
* 测试并发秒杀安全性
*/
@Test
void testConcurrentFlashSale() throws InterruptedException {
int threadCount = 50;
int stockCount = 10;
// 设置较小的库存用于测试
String stockKey = "flashsale_stock:" + testFlashSale.getId();
redisService.set(stockKey, stockCount);
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
// 模拟多个用户同时参与秒杀
for (int i = 0; i < threadCount; i++) {
final Long userId = (long) (i + 100); // 避免与其他测试冲突
executor.submit(() -> {
try {
FlashSaleDTO.ParticipateDTO participateDTO = new FlashSaleDTO.ParticipateDTO();
participateDTO.setFlashSaleId(testFlashSale.getId());
participateDTO.setQuantity(1);
FlashSaleDTO.ResultDTO result = flashSaleService.participateFlashSale(userId, participateDTO);
if (result.getSuccess()) {
successCount.incrementAndGet();
} else {
failCount.incrementAndGet();
}
} catch (Exception e) {
failCount.incrementAndGet();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
// 验证结果
assertEquals(threadCount, successCount.get() + failCount.get());
assertEquals(stockCount, successCount.get()); // 成功数量应该等于库存数量
assertTrue(failCount.get() > 0); // 应该有失败的请求
// 验证Redis中的剩余库存
Integer remainingStock = flashSaleService.getFlashSaleStock(testFlashSale.getId());
assertEquals(0, remainingStock); // 库存应该为0
}
/**
* 测试获取秒杀活动详情
*/
@Test
void testGetFlashSaleDetail() {
FlashSaleDTO result = flashSaleService.getFlashSaleDTOById(testFlashSale.getId());
assertNotNull(result);
assertEquals(testFlashSale.getId(), result.getId());
assertEquals(testProduct.getId(), result.getProductId());
assertEquals(testProduct.getName(), result.getProductName());
assertEquals(testFlashSale.getFlashPrice(), result.getFlashPrice());
assertEquals(testProduct.getPrice(), result.getOriginalPrice());
assertTrue(result.getCanParticipate()); // 应该可以参与
}
/**
* 测试获取正在进行的秒杀活动
*/
@Test
void testGetActiveFlashSales() {
var activeFlashSales = flashSaleService.getActiveFlashSales();
assertNotNull(activeFlashSales);
assertTrue(activeFlashSales.size() > 0);
// 验证返回的活动包含我们的测试活动
boolean found = activeFlashSales.stream()
.anyMatch(fs -> fs.getId().equals(testFlashSale.getId()));
assertTrue(found);
}
/**
* 测试秒杀活动预热
*/
@Test
void testPreloadFlashSale() {
// 清除缓存
String stockKey = "flashsale_stock:" + testFlashSale.getId();
redisService.delete(stockKey);
// 预热
flashSaleService.preloadFlashSale(testFlashSale.getId());
// 验证缓存
Integer stock = flashSaleService.getFlashSaleStock(testFlashSale.getId());
assertEquals(testFlashSale.getFlashStock(), stock);
}
/**
* 测试获取秒杀库存
*/
@Test
void testGetFlashSaleStock() {
Integer stock = flashSaleService.getFlashSaleStock(testFlashSale.getId());
assertEquals(testFlashSale.getFlashStock(), stock);
// 测试不存在的秒杀活动
Integer nonExistentStock = flashSaleService.getFlashSaleStock(99999L);
assertEquals(0, nonExistentStock);
}
}

View File

@@ -0,0 +1,287 @@
package com.org.flashsalesystem.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
/**
* Redis服务测试类
* 测试Redis五种数据类型的操作
*/
@SpringBootTest
@ActiveProfiles("test")
public class RedisServiceTest {
private static final String TEST_KEY_PREFIX = "test:";
@Autowired
private RedisService redisService;
@BeforeEach
void setUp() {
// 清理测试数据
cleanupTestData();
}
/**
* 测试String类型操作
*/
@Test
void testStringOperations() {
String key = TEST_KEY_PREFIX + "string";
String value = "test_value";
// 测试设置和获取
redisService.set(key, value);
Object result = redisService.get(key);
assertEquals(value, result);
// 测试带过期时间的设置
redisService.set(key + "_expire", value, 1, TimeUnit.SECONDS);
assertTrue(redisService.exists(key + "_expire"));
// 测试递增
String counterKey = TEST_KEY_PREFIX + "counter";
Long count1 = redisService.incr(counterKey);
assertEquals(1L, count1);
Long count2 = redisService.incrBy(counterKey, 5);
assertEquals(6L, count2);
// 测试递减
Long count3 = redisService.decr(counterKey);
assertEquals(5L, count3);
// 测试SETNX
String nxKey = TEST_KEY_PREFIX + "nx";
Boolean result1 = redisService.setNx(nxKey, "value1");
assertTrue(result1);
Boolean result2 = redisService.setNx(nxKey, "value2");
assertFalse(result2);
}
/**
* 测试Hash类型操作
*/
@Test
void testHashOperations() {
String key = TEST_KEY_PREFIX + "hash";
// 测试单个字段操作
redisService.hSet(key, "field1", "value1");
Object value = redisService.hGet(key, "field1");
assertEquals("value1", value);
// 测试批量操作
Map<String, Object> hash = new HashMap<>();
hash.put("field2", "value2");
hash.put("field3", "value3");
redisService.hMSet(key, hash);
Map<Object, Object> allFields = redisService.hGetAll(key);
assertEquals(3, allFields.size());
assertEquals("value1", allFields.get("field1"));
assertEquals("value2", allFields.get("field2"));
assertEquals("value3", allFields.get("field3"));
// 测试字段存在性
assertTrue(redisService.hExists(key, "field1"));
assertFalse(redisService.hExists(key, "nonexistent"));
// 测试字段递增
redisService.hSet(key, "counter", "10");
Long newValue = redisService.hIncrBy(key, "counter", 5);
assertEquals(15L, newValue);
// 测试删除字段
Long deletedCount = redisService.hDel(key, "field1", "field2");
assertEquals(2L, deletedCount);
}
/**
* 测试List类型操作
*/
@Test
void testListOperations() {
String key = TEST_KEY_PREFIX + "list";
// 测试左侧推入
redisService.lPush(key, "item1", "item2", "item3");
Long length = redisService.lLen(key);
assertEquals(3L, length);
// 测试右侧推入
redisService.rPush(key, "item4", "item5");
length = redisService.lLen(key);
assertEquals(5L, length);
// 测试获取范围
var items = redisService.lRange(key, 0, -1);
assertEquals(5, items.size());
// 测试弹出
Object leftItem = redisService.lPop(key);
assertNotNull(leftItem);
Object rightItem = redisService.rPop(key);
assertNotNull(rightItem);
length = redisService.lLen(key);
assertEquals(3L, length);
}
/**
* 测试Set类型操作
*/
@Test
void testSetOperations() {
String key = TEST_KEY_PREFIX + "set";
// 测试添加元素
redisService.sAdd(key, "member1", "member2", "member3");
Long size = redisService.sCard(key);
assertEquals(3L, size);
// 测试成员存在性
assertTrue(redisService.sIsMember(key, "member1"));
assertFalse(redisService.sIsMember(key, "nonexistent"));
// 测试获取所有成员
Set<Object> members = redisService.sMembers(key);
assertEquals(3, members.size());
assertTrue(members.contains("member1"));
assertTrue(members.contains("member2"));
assertTrue(members.contains("member3"));
// 测试移除成员
Long removedCount = redisService.sRem(key, "member1", "member2");
assertEquals(2L, removedCount);
size = redisService.sCard(key);
assertEquals(1L, size);
}
/**
* 测试ZSet类型操作
*/
@Test
void testZSetOperations() {
String key = TEST_KEY_PREFIX + "zset";
// 测试添加元素
redisService.zAdd(key, "member1", 10.0);
redisService.zAdd(key, "member2", 20.0);
redisService.zAdd(key, "member3", 15.0);
Long size = redisService.zCard(key);
assertEquals(3L, size);
// 测试按分数排序获取
Set<Object> range = redisService.zRange(key, 0, -1);
assertEquals(3, range.size());
// 测试按分数倒序获取
Set<Object> revRange = redisService.zRevRange(key, 0, -1);
assertEquals(3, revRange.size());
// 测试分数递增
Double newScore = redisService.zIncrBy(key, "member1", 5.0);
assertEquals(15.0, newScore);
// 测试移除成员
Long removedCount = redisService.zRem(key, "member1");
assertEquals(1L, removedCount);
size = redisService.zCard(key);
assertEquals(2L, size);
}
/**
* 测试通用操作
*/
@Test
void testCommonOperations() {
String key = TEST_KEY_PREFIX + "common";
// 测试键存在性
assertFalse(redisService.exists(key));
redisService.set(key, "value");
assertTrue(redisService.exists(key));
// 测试设置过期时间
Boolean expireResult = redisService.expire(key, 10, TimeUnit.SECONDS);
assertTrue(expireResult);
Long ttl = redisService.getExpire(key);
assertTrue(ttl > 0 && ttl <= 10);
// 测试删除键
Boolean deleteResult = redisService.delete(key);
assertTrue(deleteResult);
assertFalse(redisService.exists(key));
}
/**
* 测试Lua脚本执行
*/
@Test
void testLuaScriptExecution() {
String stockKey = TEST_KEY_PREFIX + "stock";
// 初始化库存
redisService.set(stockKey, 100);
// 测试秒杀脚本
Long remainingStock = redisService.executeFlashSaleScript(stockKey, 10);
assertEquals(90L, remainingStock);
// 测试库存不足情况
redisService.set(stockKey, 5);
Long result = redisService.executeFlashSaleScript(stockKey, 10);
assertEquals(-2L, result); // 库存不足
}
/**
* 测试分布式锁脚本
*/
@Test
void testDistributedLockScript() {
String lockKey = TEST_KEY_PREFIX + "lock";
String lockValue = "test_value";
// 测试获取锁
String result1 = redisService.executeLockScript(lockKey, lockValue, 10);
assertEquals("OK", result1);
// 测试重复获取锁
String result2 = redisService.executeLockScript(lockKey, "other_value", 10);
assertEquals("FAIL", result2);
// 测试释放锁
Long unlockResult = redisService.executeUnlockScript(lockKey, lockValue);
assertEquals(1L, unlockResult);
// 测试释放不存在的锁
Long unlockResult2 = redisService.executeUnlockScript(lockKey, lockValue);
assertEquals(0L, unlockResult2);
}
/**
* 清理测试数据
*/
private void cleanupTestData() {
// 这里可以添加清理逻辑,删除所有测试键
// 由于是测试环境,可以使用通配符删除
}
}