完成自动化测试套件实施(W1-W11)

W1-W3: 基线修复与测试基础设施搭建
- 修复 Jenkins JDK 21 兼容性,统一 E2E 目录,修复 storageState 冲突
- 搭建后端测试基类 BaseContractTest + Testcontainers PostgreSQL
- 创建 TestDataFactory 链式构造,完善 Vitest 基座与 Playwright fixtures
- 建立 docker-compose.test.yml 与测试数据隔离方案

W4-W5: 单元测试补齐(阶段 2)
- 补齐 gym-member/gym-groupCourse/gym-checkIn/gym-payment 核心模块单元测试
- 补齐 gym-coach/manage-sys 模块单元测试
- 前端 utils/composables/stores 单元测试,37 文件 502 项测试
- JaCoCo 覆盖率门禁从 30% 调整至 55%,21 模块全部通过

W6-W7: 集成与契约测试(阶段 3)
- Repository 集成测试:会员/团课/签到/支付关键表,Testcontainers 100% 通过
- Handler 集成测试:WebTestClient 覆盖正向/异常/权限路径
- 网关集成测试:JWT/RBAC/签名/限流/重试
- Flyway 迁移测试:验证迁移脚本可重复执行
- OpenAPI 契约测试:覆盖 ≥80% P0 接口,202 项契约测试 0 失败
- 跨模块契约测试:会员-支付-团课数据一致性

W8-W9: E2E 与用户旅程测试(阶段 4)
- 管理员 Web 核心流程 E2E:用户/角色/菜单/字典/配置
- 小程序会员端核心页面 E2E:购卡/预约/签到
- 5 条 P0 用户旅程全链路自动化,60 条 journey 测试 0 失败

W10: 变异测试与质量门禁(阶段 5)
- 后端 PIT 配置:pitest-maven 1.19.1 + JUnit 5,覆盖率阈值 55%/变异阈值 45%
- P0 模块基线:manage-sys 48%,gym-member 30%,gym-payment 36%
- 前端 StrykerJS 配置:utils/stores 变异测试,dateFormat.ts 70.83%
- Jenkins 质量门禁:JaCoCo/PIT/E2E 统一检查,不达标阻断构建

W11: 持续运行与改进(阶段 6)
- 测试指标收集脚本 scripts/collect-test-metrics.py + HTML 看板生成器
- Flaky Test 治理 SOP:检测→隔离→根因分析→修复→验证闭环
- 测试资产定期评审流程:月度/季度/事件驱动三级机制
- 快速参考指南 docs/testing/quick-reference.md
- 累计 10 份测试文档,7 个里程碑全部达成
This commit was merged in pull request #54.
This commit is contained in:
2026-08-02 08:28:37 +08:00
parent dc68581c5e
commit 015cb0dc78
119 changed files with 21873 additions and 553 deletions
@@ -0,0 +1,550 @@
package cn.novalon.gym.manage.checkin.service.impl;
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
import cn.novalon.gym.manage.checkIn.repository.SignInRecordRepository;
import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl;
import cn.novalon.gym.manage.checkIn.vo.QRCodeVo;
import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CheckServiceImplTest {
@Mock
private QRCodeConfig qrCodeConfig;
@Mock
private RedisUtil redisUtil;
@Mock
private MemberCardRecordRepository memberCardRecordRepository;
@Mock
private MemberCardRepository memberCardRepository;
@Mock
private SignInRecordRepository signInRecordRepository;
@Mock
private IGroupCourseBookingService groupCourseBookingService;
@Mock
private IGroupCourseBookingRepository groupCourseBookingRepository;
@InjectMocks
private CheckServiceImpl checkService;
private static final Long MEMBER_ID = 10001L;
private static final String QR_CONTENT = "test-qr-content-abc123";
// ==================== getQRCode ====================
@Test
@DisplayName("获取二维码 - 成功")
void getQRCode_shouldReturnQRCodeVo() {
when(qrCodeConfig.getWidth()).thenReturn(300);
when(qrCodeConfig.getHeight()).thenReturn(300);
when(redisUtil.setWithExpire(anyString(), any(Map.class), anyLong()))
.thenReturn(Mono.just(true));
Mono<QRCodeVo> result = checkService.getQRCode(MEMBER_ID);
StepVerifier.create(result)
.expectNextMatches(vo -> {
assertThat(vo.getQrContent()).isNotNull();
assertThat(vo.getQrCodeBase64()).isNotNull();
assertThat(vo.getWidth()).isEqualTo(300);
assertThat(vo.getHeight()).isEqualTo(300);
assertThat(vo.isUsed()).isFalse();
assertThat(vo.getCreateTime()).isEqualTo(LocalDate.now());
return true;
})
.verifyComplete();
}
// ==================== checkIn - 正向场景 ====================
@Test
@DisplayName("签到成功 - 正常流程(无预约、无会员卡记录ID)")
void checkIn_shouldSucceed() {
// 未签到
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
// Redis 中有有效二维码数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false);
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
// 无预约
when(groupCourseBookingService.getBookingsByMemberId(MEMBER_ID)).thenReturn(Flux.empty());
// 保存签到记录
when(signInRecordRepository.save(any(SignInRecord.class)))
.thenReturn(Mono.just(createMockSignInRecord()));
// 更新缓存
when(redisUtil.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
// 清除缓存
when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
StepVerifier.create(result)
.expectNextMatches(response -> response.contains("签到成功"))
.verifyComplete();
verify(signInRecordRepository).save(any(SignInRecord.class));
verify(redisUtil).set(anyString(), any(Map.class));
verify(redisUtil, times(2)).deleteByPattern(anyString());
}
@Test
@DisplayName("签到成功 - 有有效团课预约")
void checkIn_shouldSucceedWithValidBooking() {
// 未签到
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
// Redis 中有有效二维码数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false);
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
// 有有效预约
GroupCourseBooking booking = createValidBooking();
when(groupCourseBookingService.getBookingsByMemberId(MEMBER_ID)).thenReturn(Flux.just(booking));
when(groupCourseBookingRepository.updateStatus(booking.getId(), "2")).thenReturn(Mono.just(1));
// 保存签到记录
when(signInRecordRepository.save(any(SignInRecord.class)))
.thenReturn(Mono.just(createMockSignInRecord()));
// 更新缓存
when(redisUtil.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
// 清除缓存
when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
StepVerifier.create(result)
.expectNextMatches(response -> response.contains("签到成功"))
.verifyComplete();
verify(groupCourseBookingRepository).updateStatus(booking.getId(), "2");
}
@Test
@DisplayName("签到成功 - Redis 中缓存数据为 String 格式(兼容旧格式)")
void checkIn_shouldSucceedWithStringCachedData() {
// 未签到
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
// Redis 中缓存数据为 JSON 字符串格式
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
String jsonData = "{\"qrContent\":\"" + QR_CONTENT + "\",\"isUsed\":false,\"memberId\":" + MEMBER_ID + "}";
when(redisUtil.get(eq(key))).thenReturn(Mono.just(jsonData));
// 无预约
when(groupCourseBookingService.getBookingsByMemberId(MEMBER_ID)).thenReturn(Flux.empty());
// 保存签到记录
when(signInRecordRepository.save(any(SignInRecord.class)))
.thenReturn(Mono.just(createMockSignInRecord()));
// 更新缓存
when(redisUtil.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
// 清除缓存
when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
StepVerifier.create(result)
.expectNextMatches(response -> response.contains("签到成功"))
.verifyComplete();
}
// ==================== checkIn - 异常场景 ====================
@Test
@DisplayName("签到失败 - 当天已签到(数据库查重)")
void checkIn_shouldFailWhenAlreadySignedIn() {
SignInRecord existingRecord = SignInRecord.builder()
.id(1L)
.memberId(MEMBER_ID)
.signInTime(LocalDateTime.now())
.signInType("QR_CODE")
.signInStatus("SUCCESS")
.build();
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(existingRecord));
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
StepVerifier.create(result)
.expectErrorMatches(e -> e instanceof RuntimeException
&& e.getMessage().contains("请勿重复签到"))
.verify();
verify(redisUtil, never()).get(anyString());
}
@Test
@DisplayName("签到失败 - 二维码已使用(isUsed=true")
void checkIn_shouldFailWhenQRCodeUsed() {
// 未签到
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
// Redis 中二维码已被使用
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(true);
qrData.put("checkInTime", "2026-07-31 10:00:00");
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
StepVerifier.create(result)
.expectErrorMatches(e -> e instanceof RuntimeException
&& e.getMessage().contains("请勿重复签到"))
.verify();
}
@Test
@DisplayName("签到失败 - 二维码内容不匹配")
void checkIn_shouldFailWhenQRCodeInvalid() {
// 未签到
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
// Redis 中二维码内容不匹配
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false);
qrData.put("qrContent", "different-qr-content");
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
StepVerifier.create(result)
.expectErrorMatches(e -> e instanceof RuntimeException
&& e.getMessage().contains("二维码无效"))
.verify();
}
@Test
@DisplayName("签到 - Redis 中无二维码数据时直接完成(无副作用)")
void checkIn_shouldCompleteWhenQRCodeNotFound() {
// 未签到
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
// Redis 中无数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, "not-exist");
// 当 Redis 中无数据时,flatMap 不会执行,Mono 直接 complete
StepVerifier.create(result)
.verifyComplete();
}
@Test
@DisplayName("签到失败 - 二维码数据格式错误")
void checkIn_shouldFailWhenQRCodeDataFormatError() {
// 未签到
when(signInRecordRepository.findByMemberIdAndDate(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
// Redis 返回非 Map 非 String 的数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.just(12345));
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
StepVerifier.create(result)
.expectErrorMatches(e -> e instanceof RuntimeException
&& e.getMessage().contains("二维码数据格式错误"))
.verify();
}
// ==================== getSignInRecords ====================
@Test
@DisplayName("查询签到记录列表 - 成功")
void getSignInRecords_shouldReturnRecords() {
SignInRecord record = createMockSignInRecord();
when(signInRecordRepository.findByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Flux.just(record));
Flux<SignInRecordVO> result = checkService.getSignInRecords(
MEMBER_ID, LocalDate.now().minusDays(30), LocalDate.now());
StepVerifier.create(result)
.expectNextMatches(vo -> {
assertThat(vo.getMemberId()).isEqualTo(MEMBER_ID);
assertThat(vo.getSignInType()).isEqualTo("QR_CODE");
assertThat(vo.getSignInStatus()).isEqualTo("SUCCESS");
return true;
})
.verifyComplete();
}
@Test
@DisplayName("查询签到记录列表 - 无记录")
void getSignInRecords_shouldReturnEmptyWhenNoRecords() {
when(signInRecordRepository.findByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Flux.empty());
Flux<SignInRecordVO> result = checkService.getSignInRecords(
MEMBER_ID, LocalDate.now().minusDays(30), LocalDate.now());
StepVerifier.create(result)
.verifyComplete();
}
// ==================== getSignInRecordById ====================
@Test
@DisplayName("查询单条签到记录 - 存在")
void getSignInRecordById_shouldReturnRecord() {
SignInRecord record = createMockSignInRecord();
when(signInRecordRepository.findById(1L)).thenReturn(Mono.just(record));
Mono<SignInRecordVO> result = checkService.getSignInRecordById(1L);
StepVerifier.create(result)
.expectNextMatches(vo -> vo.getId() == 1L && vo.getMemberId().equals(MEMBER_ID))
.verifyComplete();
}
@Test
@DisplayName("查询单条签到记录 - 不存在")
void getSignInRecordById_shouldReturnEmptyWhenNotFound() {
when(signInRecordRepository.findById(999L)).thenReturn(Mono.empty());
Mono<SignInRecordVO> result = checkService.getSignInRecordById(999L);
StepVerifier.create(result)
.verifyComplete();
}
// ==================== getSignInStats ====================
@Test
@DisplayName("获取签到统计 - 成功")
void getSignInStats_shouldReturnStats() {
when(signInRecordRepository.countByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(10L));
when(signInRecordRepository.countSuccessByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(8L));
when(signInRecordRepository.getFirstSignInTime(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(LocalDateTime.now().minusDays(29)));
when(signInRecordRepository.getLastSignInTime(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(LocalDateTime.now()));
Mono<SignInStatsVO> result = checkService.getSignInStats(
MEMBER_ID, LocalDate.now().minusDays(30), LocalDate.now());
StepVerifier.create(result)
.expectNextMatches(stats -> {
assertThat(stats.getTotalCount()).isEqualTo(10L);
assertThat(stats.getSuccessCount()).isEqualTo(8L);
assertThat(stats.getSuccessRate()).isEqualTo(80.0);
assertThat(stats.getStartDate()).isNotNull();
assertThat(stats.getEndDate()).isNotNull();
assertThat(stats.getFirstSignInTime()).isNotNull();
assertThat(stats.getLastSignInTime()).isNotNull();
return true;
})
.verifyComplete();
}
@Test
@DisplayName("获取签到统计 - 无记录时 Mono.zip 中任一源为空则整体为空")
void getSignInStats_shouldCompleteWhenNoRecords() {
when(signInRecordRepository.countByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(0L));
when(signInRecordRepository.countSuccessByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(0L));
when(signInRecordRepository.getFirstSignInTime(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
when(signInRecordRepository.getLastSignInTime(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
Mono<SignInStatsVO> result = checkService.getSignInStats(
MEMBER_ID, LocalDate.now().minusDays(30), LocalDate.now());
// Mono.zip 中任意源为 empty 时,整体结果也为 empty
StepVerifier.create(result)
.verifyComplete();
}
// ==================== getDailySignInStats ====================
@Test
@DisplayName("获取每日签到统计 - 成功")
void getDailySignInStats_shouldReturnStats() {
when(signInRecordRepository.countByTimeRange(any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(50L));
when(signInRecordRepository.countSuccessByTimeRange(any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(45L));
when(signInRecordRepository.countDistinctMembersByTimeRange(any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(30L));
Mono<SignInStatsVO> result = checkService.getDailySignInStats(LocalDate.now());
StepVerifier.create(result)
.expectNextMatches(stats -> {
assertThat(stats.getTotalCount()).isEqualTo(50L);
assertThat(stats.getSuccessCount()).isEqualTo(45L);
assertThat(stats.getUniqueMemberCount()).isEqualTo(30L);
assertThat(stats.getSuccessRate()).isEqualTo(90.0);
assertThat(stats.getStartDate()).isEqualTo(LocalDate.now());
assertThat(stats.getEndDate()).isEqualTo(LocalDate.now());
return true;
})
.verifyComplete();
}
@Test
@DisplayName("获取每日签到统计 - 无记录时成功率应为0")
void getDailySignInStats_shouldReturnZeroRateWhenNoRecords() {
when(signInRecordRepository.countByTimeRange(any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(0L));
when(signInRecordRepository.countSuccessByTimeRange(any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(0L));
when(signInRecordRepository.countDistinctMembersByTimeRange(any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.just(0L));
Mono<SignInStatsVO> result = checkService.getDailySignInStats(LocalDate.now());
StepVerifier.create(result)
.expectNextMatches(stats -> {
assertThat(stats.getTotalCount()).isZero();
assertThat(stats.getSuccessCount()).isZero();
assertThat(stats.getUniqueMemberCount()).isZero();
assertThat(stats.getSuccessRate()).isZero();
return true;
})
.verifyComplete();
}
// ==================== exportSignInRecords ====================
@Test
@DisplayName("导出签到记录 - 生成CSV格式")
void exportSignInRecords_shouldReturnCsvBytes() {
SignInRecord record = createMockSignInRecord();
when(signInRecordRepository.findByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Flux.just(record));
Mono<byte[]> result = checkService.exportSignInRecords(
MEMBER_ID, LocalDate.now().minusDays(7), LocalDate.now());
StepVerifier.create(result)
.expectNextMatches(bytes -> {
String csv = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
assertThat(csv).contains("签到记录ID,会员ID,会员卡ID,签到时间,签到方式,签到状态,失败原因");
assertThat(csv).contains("扫码签到");
assertThat(csv).contains("成功");
return true;
})
.verifyComplete();
}
@Test
@DisplayName("导出签到记录 - 无记录时只返回表头")
void exportSignInRecords_shouldReturnHeaderOnlyWhenNoRecords() {
when(signInRecordRepository.findByMemberIdAndTimeRange(
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Flux.empty());
Mono<byte[]> result = checkService.exportSignInRecords(
MEMBER_ID, LocalDate.now().minusDays(7), LocalDate.now());
StepVerifier.create(result)
.expectNextMatches(bytes -> {
String csv = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
assertThat(csv).isEqualTo("签到记录ID,会员ID,会员卡ID,签到时间,签到方式,签到状态,失败原因");
return true;
})
.verifyComplete();
}
// ==================== helper methods ====================
private Map<String, Object> buildQrData(boolean isUsed) {
Map<String, Object> map = new HashMap<>();
map.put("qrContent", QR_CONTENT);
map.put("isUsed", isUsed);
map.put("memberId", MEMBER_ID);
return map;
}
private SignInRecord createMockSignInRecord() {
return SignInRecord.builder()
.id(1L)
.memberId(MEMBER_ID)
.memberCardId(1L)
.signInTime(LocalDateTime.now())
.signInType("QR_CODE")
.signInStatus("SUCCESS")
.source("MINI_PROGRAM")
.isDelete(false)
.build();
}
private GroupCourseBooking createValidBooking() {
GroupCourseBooking booking = new GroupCourseBooking();
booking.setId(100L);
booking.setMemberId(MEMBER_ID);
booking.setCourseId(200L);
booking.setStatus("0");
booking.setCourseStartTime(LocalDateTime.now().plusMinutes(5));
booking.setCourseEndTime(LocalDateTime.now().plusMinutes(65));
return booking;
}
}