From 3b3ec8ea7d58571241f843666528668c04e498fc Mon Sep 17 00:00:00 2001 From: yovinchen Date: Tue, 1 Jul 2025 17:18:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FlashSaleSystemApplicationTests.java | 13 + .../service/FlashSaleServiceTest.java | 280 +++++++++++++++++ .../service/RedisServiceTest.java | 287 ++++++++++++++++++ 3 files changed, 580 insertions(+) create mode 100644 src/test/java/com/org/flashsalesystem/FlashSaleSystemApplicationTests.java create mode 100644 src/test/java/com/org/flashsalesystem/service/FlashSaleServiceTest.java create mode 100644 src/test/java/com/org/flashsalesystem/service/RedisServiceTest.java diff --git a/src/test/java/com/org/flashsalesystem/FlashSaleSystemApplicationTests.java b/src/test/java/com/org/flashsalesystem/FlashSaleSystemApplicationTests.java new file mode 100644 index 0000000..698316a --- /dev/null +++ b/src/test/java/com/org/flashsalesystem/FlashSaleSystemApplicationTests.java @@ -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() { + } + +} diff --git a/src/test/java/com/org/flashsalesystem/service/FlashSaleServiceTest.java b/src/test/java/com/org/flashsalesystem/service/FlashSaleServiceTest.java new file mode 100644 index 0000000..aec9a95 --- /dev/null +++ b/src/test/java/com/org/flashsalesystem/service/FlashSaleServiceTest.java @@ -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); + } +} diff --git a/src/test/java/com/org/flashsalesystem/service/RedisServiceTest.java b/src/test/java/com/org/flashsalesystem/service/RedisServiceTest.java new file mode 100644 index 0000000..3b2786a --- /dev/null +++ b/src/test/java/com/org/flashsalesystem/service/RedisServiceTest.java @@ -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 hash = new HashMap<>(); + hash.put("field2", "value2"); + hash.put("field3", "value3"); + redisService.hMSet(key, hash); + + Map 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 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 range = redisService.zRange(key, 0, -1); + assertEquals(3, range.size()); + + // 测试按分数倒序获取 + Set 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() { + // 这里可以添加清理逻辑,删除所有测试键 + // 由于是测试环境,可以使用通配符删除 + } +}