[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EKE34R4ACPEAFYDTGENR9S":3,"skill-same:01M6EKE34R4ACPEAFYDTGENR9S":194},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":34,"publishedAt":46,"linkOnly":47,"markdownContent":48,"markdownBody":49,"frontMatter":50,"refName":51,"commitSha":52,"skillDirectory":53,"forksCount":54,"repositoryHtmlUrl":55,"repositoryHomepageUrl":56,"ownerHtmlUrl":57,"ownerBlogUrl":6,"ownerDescription":58,"updatedAt":59,"install":60,"resources":73},"01M6EKE34R4ACPEAFYDTGENR9S","allra-test-writing",null,"Allra 백엔드 테스트 작성 표준. Use when writing test code, choosing test helpers, generating test data with Fixture Monkey, or verifying test coverage.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fallra-fintech\u002Fallra-test-writing\u002FSKILL.md",[18,23,26,31],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",{"categoryNo":32,"categoryName":33,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000036","内容创作者",[35,36,37,38,39,40,41,42,43,44,45],"writing","test","allra test writing","code","data","coverage","use","test coverage","Generating Test Data","test data","fixture","2026-11-10 10:12:30",false,"---\nname: allra-test-writing\ndescription: Allra 백엔드 테스트 작성 표준. Use when writing test code, choosing test helpers, generating test data with Fixture Monkey, or verifying test coverage.\n---\n\n# Allra Test Writing Standards\n\nAllra 백엔드 팀의 테스트 작성 표준을 정의합니다. 테스트 헬퍼 선택, Fixture Monkey 데이터 생성, Given-When-Then 패턴, AssertJ 검증을 포함합니다.\n\n## 프로젝트 기본 정보\n\n이 가이드는 다음 환경을 기준으로 작성되었습니다:\n\n- **Java**: 17 이상\n- **Spring Boot**: 3.2 이상\n- **Testing Framework**: JUnit 5\n- **Assertion Library**: AssertJ\n- **Mocking**: Mockito\n- **Test Data**: Fixture Monkey (선택 사항)\n- **Container**: Testcontainers (선택 사항)\n\n**참고**: 프로젝트별로 사용하는 라이브러리나 버전이 다를 수 있습니다. 프로젝트에 맞게 조정하여 사용하세요.\n\n## 테스트 헬퍼 선택 가이드\n\n**주의**: 아래 테스트 헬퍼는 Allra 표준 템플릿에서 제공됩니다. 프로젝트에 이러한 헬퍼가 없는 경우, Spring Boot 기본 테스트 어노테이션(`@SpringBootTest`, `@DataJpaTest`, `@WebMvcTest` 등)을 직접 사용하되, 이 가이드의 테스트 패턴과 원칙은 동일하게 적용합니다.\n\n| 헬퍼 | 태그 | 용도 | 무게 | 언제? |\n|------|------|------|------|-------|\n| **IntegrationTest** | Integration | 여러 서비스 통합 | ? 무거움 | 전체 워크플로우 |\n| **RdbTest** | RDB | Repository, QueryDSL | ? 중간 | 쿼리 검증 |\n| **ControllerTest** | Controller | API 엔드포인트 | ? 가벼움 | REST API 검증 |\n| **RedisTest** | Redis | Redis 캐싱 | ? 가벼움 | 캐시 검증 |\n| **MockingUnitTest** | MockingUnit | Service 단위 | ? 매우 가벼움 | 비즈니스 로직 |\n| **PojoUnitTest** | PojoUnit | 도메인 로직 | ? 매우 가벼움 | 순수 자바 |\n\n### 선택 플로우\n\n```\nAPI 엔드포인트? → ControllerTest\n여러 서비스 통합? → IntegrationTest\nRepository\u002FQueryDSL? → RdbTest\nRedis 캐싱? → RedisTest\nService 로직 (Mock)? → MockingUnitTest\n도메인 로직 (POJO)? → PojoUnitTest\n```\n\n---\n\n## ? Mock vs Integration 선택 기준 (중요!)\n\n**원칙**: 기본은 MockingUnitTest, 꼭 필요할 때만 IntegrationTest\n\n**목표**: IntegrationTest 비율 5% 이하 유지\n\n### 의사결정 플로우차트\n\n```\n┌─────────────────────────────────┐\n│ 무엇을 테스트하려고 하는가?    │\n└────────────┬────────────────────┘\n             │\n    ┌────────▼────────┐\n    │ 도메인 로직만?  │ ──Yes──> PojoUnitTest\n    └────────┬────────┘\n             │ No\n    ┌────────▼─────────────────────┐\n    │ Repository\u002FQueryDSL 쿼리?   │ ──Yes──> RdbTest\n    └────────┬─────────────────────┘\n             │ No\n    ┌────────▼─────────────────────┐\n    │ API 엔드포인트 응답\u002F검증?   │ ──Yes──> ControllerTest\n    └────────┬─────────────────────┘\n             │ No\n    ┌────────▼─────────────────────────────┐\n    │ Service 비즈니스 로직 검증?         │\n    └────────┬─────────────────────────────┘\n             │\n    ┌────────▼──────────────────────────────────────────┐\n    │ 다음 중 하나라도 해당하는가?                      │\n    │                                                   │\n    │ 1. ? 금전 처리 (입금\u002F출금\u002F이체\u002F환불)            │\n    │ 2. ? 트랜잭션 롤백이 중요한 워크플로우           │\n    │ 3. ? 여러 테이블 데이터 정합성 검증             │\n    │ 4. ? 실제 DB 제약조건 검증 필수                 │\n    │ 5. ? 복잡한 상태 전이 (3단계 이상)              │\n    │ 6. ? 이벤트 발행\u002F리스너 통합 검증               │\n    │ 7. ? 3개 이상 서비스 필수 협력                  │\n    └────┬──────────────────────────────────────┬────────┘\n         │ Yes                                  │ No\n         │                                      │\n    ┌────▼────────────┐              ┌─────────▼──────────┐\n    │ IntegrationTest │              │ MockingUnitTest    │\n    │ (최소화)        │              │ (기본 선택)       │\n    └─────────────────┘              └────────────────────┘\n```\n\n### IntegrationTest가 필요한 구체적인 케이스\n\n#### ✅ 1. 금전 처리 (입금\u002F출금\u002F이체\u002F환불)\n\n**이유**: 돈이 관련된 로직은 실제 DB 트랜잭션 동작 검증 필수\n\n```java\n\u002F\u002F 예시: 펀딩 신청 (FsData → FsPayment → PointUsage → UserAccount 연계)\n@DisplayName(\"펀딩 신청 시 금액 차감 및 결제 생성\")\nclass ApplyServiceIntegrationTest extends IntegrationTest {\n\n    @Test\n    @Transactional\n    void apply_DecreasesAmount_Success() {\n        \u002F\u002F given: 사용자 잔액 100만원\n        User user = createUserWithBalance(1_000_000);\n\n        \u002F\u002F when: 50만원 펀딩 신청\n        applyService.apply(new ApplyRequest(user.getId(), 500_000));\n\n        \u002F\u002F then: 실제 DB에서 잔액 50만원 확인\n        User updated = userRepository.findById(user.getId()).get();\n        assertThat(updated.getBalance()).isEqualTo(500_000);\n\n        \u002F\u002F then: FsPayment 생성 확인\n        FsPayment payment = fsPaymentRepository.findByUserId(user.getId()).get();\n        assertThat(payment.getAmount()).isEqualTo(500_000);\n    }\n}\n```\n\n#### ✅ 2. 트랜잭션 롤백이 중요한 워크플로우\n\n**이유**: 실패 시 모든 작업이 원자적으로 롤백되어야 함\n\n```java\n\u002F\u002F 예시: 결제 실패 시 전체 롤백\n@Test\n@DisplayName(\"결제 실패 시 신청 데이터도 롤백\")\nvoid apply_PaymentFails_RollbackAll() {\n    \u002F\u002F given\n    User user = createUser();\n    mockPaymentGateway_ToFail(); \u002F\u002F 외부 결제는 Mock으로\n\n    \u002F\u002F when & then\n    assertThatThrownBy(() -> applyService.apply(request))\n        .isInstanceOf(PaymentException.class);\n\n    \u002F\u002F then: DB에 어떤 데이터도 저장되지 않음\n    assertThat(fsDataRepository.findAll()).isEmpty();\n    assertThat(fsPaymentRepository.findAll()).isEmpty();\n}\n```\n\n**참고**: 외부 연동(결제 게이트웨이, 외부 API)은 `@MockBean`으로 처리\n\n#### ✅ 3. 여러 테이블 데이터 정합성 검증\n\n**이유**: 관련된 모든 테이블의 상태가 일관되게 유지되는지 확인\n\n```java\n\u002F\u002F 예시: 계약 생성 시 UserAccount, Contract, FsData 모두 생성\n@Test\n@DisplayName(\"신규 계약 시 관련 테이블 모두 생성\")\nvoid createContract_CreatesAllRelatedData() {\n    \u002F\u002F when\n    contractService.createContract(userId, contractType);\n\n    \u002F\u002F then: 3개 테이블 모두 데이터 존재\n    assertThat(userAccountRepository.findByUserId(userId)).isPresent();\n    assertThat(contractRepository.findByUserId(userId)).isPresent();\n    assertThat(fsDataRepository.findByUserId(userId)).isPresent();\n}\n```\n\n#### ✅ 4. 실제 DB 제약조건 검증\n\n**이유**: Unique, FK, Check 제약조건은 실제 DB에서만 확인 가능\n\n```java\n\u002F\u002F 예시: 중복 계좌 등록 방지\n@Test\n@DisplayName(\"동일 계좌번호 중복 등록 시 예외\")\nvoid registerAccount_Duplicate_ThrowsException() {\n    \u002F\u002F given\n    userAccountRepository.save(new UserAccount(userId, \"123-456-789\"));\n\n    \u002F\u002F when & then: Unique 제약조건 위반\n    assertThatThrownBy(() ->\n        userAccountRepository.save(new UserAccount(userId, \"123-456-789\"))\n    ).isInstanceOf(DataIntegrityViolationException.class);\n}\n```\n\n#### ✅ 5. 복잡한 상태 전이 (3단계 이상)\n\n**이유**: 상태 변화 흐름을 실제 시나리오대로 검증\n\n```java\n\u002F\u002F 예시: 계약 상태 전이 (신청 → 심사 → 승인 → 완료)\n@Test\n@DisplayName(\"계약 워크플로우 전체 검증\")\nvoid contractWorkflow_FullCycle() {\n    \u002F\u002F given: 신청\n    Contract contract = contractService.create(userId);\n    assertThat(contract.getStatus()).isEqualTo(ContractStatus.PENDING);\n\n    \u002F\u002F when: 심사\n    contractService.review(contract.getId());\n    \u002F\u002F then\n    Contract reviewed = contractRepository.findById(contract.getId()).get();\n    assertThat(reviewed.getStatus()).isEqualTo(ContractStatus.REVIEWED);\n\n    \u002F\u002F when: 승인\n    contractService.approve(contract.getId());\n    \u002F\u002F then\n    Contract approved = contractRepository.findById(contract.getId()).get();\n    assertThat(approved.getStatus()).isEqualTo(ContractStatus.APPROVED);\n}\n```\n\n#### ✅ 6. 이벤트 발행\u002F리스너 통합 검증\n\n**이유**: 이벤트가 실제로 발행되고 리스너가 동작하는지 확인\n\n```java\n\u002F\u002F 예시: 계약 완료 이벤트 → 알림 발송\n@Test\n@DisplayName(\"계약 완료 시 알림 이벤트 발행\")\nvoid completeContract_PublishesEvent() {\n    \u002F\u002F given\n    Contract contract = createContract(userId);\n\n    \u002F\u002F when\n    contractService.complete(contract.getId());\n\n    \u002F\u002F then: 실제로 알림이 발송되었는가? (외부 알림은 @MockBean)\n    verify(notificationService).sendContractCompleteNotification(userId);\n}\n```\n\n#### ✅ 7. 3개 이상 서비스가 필수적으로 협력\n\n**이유**: 서비스 간 상호작용을 실제 환경에서 검증\n\n```java\n\u002F\u002F 예시: 주문 생성 → 재고 차감 → 결제 → 알림\n@Test\n@DisplayName(\"주문 생성 워크플로우\")\nvoid createOrder_FullWorkflow() {\n    \u002F\u002F given\n    Product product = createProductWithStock(100);\n\n    \u002F\u002F when\n    orderService.createOrder(userId, product.getId(), 10);\n\n    \u002F\u002F then: 재고 차감\n    Product updated = productRepository.findById(product.getId()).get();\n    assertThat(updated.getStock()).isEqualTo(90);\n\n    \u002F\u002F then: 결제 생성\n    Payment payment = paymentRepository.findByUserId(userId).get();\n    assertThat(payment.getStatus()).isEqualTo(PaymentStatus.COMPLETED);\n}\n```\n\n### MockingUnitTest로 충분한 케이스\n\n#### ✅ 대부분의 Service 로직\n\n- 단순 조회 (findById, findAll)\n- 데이터 변환\u002F계산\n- 검증 로직 (validation)\n- 단일 엔티티 CRUD\n- 비즈니스 규칙 검증\n\n```java\n\u002F\u002F 예시: 할인율 계산 로직 (Mock으로 충분)\n@ExtendWith(MockitoExtension.class)\nclass DiscountServiceTest {\n\n    @Mock\n    private UserRepository userRepository;\n\n    @InjectMocks\n    private DiscountService discountService;\n\n    @Test\n    @DisplayName(\"VIP 회원 10% 할인 계산\")\n    void calculateDiscount_VipUser_10Percent() {\n        \u002F\u002F given\n        User vipUser = User.builder().grade(\"VIP\").build();\n        when(userRepository.findById(1L)).thenReturn(Optional.of(vipUser));\n\n        \u002F\u002F when\n        BigDecimal discount = discountService.calculateDiscount(1L, new BigDecimal(\"10000\"));\n\n        \u002F\u002F then\n        assertThat(discount).isEqualByComparingTo(new BigDecimal(\"1000\"));\n    }\n}\n```\n\n### 외부 연동 처리 원칙\n\n**중요**: IntegrationTest에서도 외부 시스템은 `@MockBean`으로 처리\n\n```java\n@SpringBootTest\nclass PaymentServiceIntegrationTest extends IntegrationTest {\n\n    @Autowired\n    private PaymentService paymentService;\n\n    @MockBean \u002F\u002F 외부 결제 게이트웨이는 Mock\n    private ExternalPaymentGateway externalPaymentGateway;\n\n    @MockBean \u002F\u002F 외부 알림 서비스는 Mock\n    private ExternalNotificationService notificationService;\n\n    @Test\n    @DisplayName(\"결제 성공 시 내부 데이터 정합성 검증\")\n    void processPayment_Success() {\n        \u002F\u002F given: 외부 결제는 성공으로 Mock\n        when(externalPaymentGateway.charge(any()))\n            .thenReturn(new PaymentResult(\"SUCCESS\", \"tx-123\"));\n\n        \u002F\u002F when: 실제 내부 로직 검증\n        paymentService.processPayment(userId, amount);\n\n        \u002F\u002F then: 내부 DB 상태 확인\n        Payment payment = paymentRepository.findByUserId(userId).get();\n        assertThat(payment.getStatus()).isEqualTo(PaymentStatus.COMPLETED);\n        assertThat(payment.getExternalTxId()).isEqualTo(\"tx-123\");\n    }\n}\n```\n\n### 테스트 전략 요약\n\n| 테스트 유형 | 목표 비율 | 실행 속도 | 주요 사용처 |\n|------------|----------|----------|------------|\n| **PojoUnitTest** | 30% | ⚡️ 0.01초 | 도메인 로직, 유틸리티 |\n| **MockingUnitTest** | 50% | ⚡️ 0.1초 | Service 비즈니스 로직 |\n| **ControllerTest** | 10% | ? 0.5초 | API 검증 |\n| **RdbTest** | 5% | ? 1초 | 복잡한 쿼리 검증 |\n| **IntegrationTest** | 5% | ? 5초 | 금전\u002F트랜잭션\u002F워크플로우 |\n\n### 빠른 판단 체크리스트\n\n새로운 테스트를 작성할 때 다음을 확인하세요:\n\n```\n□ 돈이 관련되어 있나요? (입금\u002F출금\u002F결제)\n  → Yes: IntegrationTest\n\n□ 실패 시 데이터 롤백이 중요한가요?\n  → Yes: IntegrationTest\n\n□ 3개 이상 테이블의 정합성을 확인해야 하나요?\n  → Yes: IntegrationTest\n\n□ DB 제약조건(Unique\u002FFK)이 핵심인가요?\n  → Yes: IntegrationTest\n\n□ 복잡한 상태 전이(3단계+)를 검증하나요?\n  → Yes: IntegrationTest\n\n□ 이벤트 발행\u002F리스너를 검증하나요?\n  → Yes: IntegrationTest\n\n□ 3개 이상 서비스가 협력하나요?\n  → Yes: IntegrationTest\n\n모두 No → MockingUnitTest 사용\n```\n\n---\n\n## 테스트 헬퍼 구조\n\n### IntegrationTest - 통합 테스트\n\n```java\n@Tag(\"Integration\")\n@SpringBootTest\npublic abstract class IntegrationTest {\n    \u002F\u002F 전체 Spring Context, Testcontainers 활용\n}\n```\n\n**언제**: 여러 서비스 협력, 실제 DB\u002F외부 시스템 필요\n**주의**: 가장 무거움, 외부 API는 `@MockBean` 사용\n\n### RdbTest - Repository 테스트\n\n```java\n@Tag(\"RDB\")\n@DataJpaTest\npublic abstract class RdbTest {}\n```\n\n**언제**: Repository CRUD, QueryDSL 쿼리, N+1 문제 검증\n\n### ControllerTest - API 테스트\n\n```java\n@Tag(\"Controller\")\n@WebMvcTest(TargetController.class)\npublic abstract class ControllerTest {\n    @Autowired\n    protected MockMvc mockMvc;\n}\n```\n\n**언제**: API 엔드포인트, HTTP Status, 입력 검증\n**주의**: Service는 `@MockBean` 필수\n\n### RedisTest - Redis 테스트\n\n```java\n@Tag(\"Redis\")\n@DataRedisTest\npublic abstract class RedisTest {}\n```\n\n**언제**: Redis 캐싱, 세션 저장소 검증\n\n### MockingUnitTest - Service 단위 테스트\n\n```java\n@ExtendWith(MockitoExtension.class)\nclass UserServiceTest {\n    @Mock\n    private UserRepository userRepository;\n\n    @InjectMocks\n    private UserService userService;\n}\n```\n\n**언제**: Service 로직 단위 테스트, 빠른 테스트\n**주의**: Spring Context 없음, `@Autowired` 불가\n\n### PojoUnitTest - 도메인 로직 테스트\n\n```java\nclass UserTest {\n    @Test\n    void activate_Success() {\n        \u002F\u002F 순수 자바 로직 테스트\n    }\n}\n```\n\n**언제**: 도메인 엔티티, VO, 유틸리티 클래스\n\n---\n\n## Fixture Monkey - 테스트 데이터 생성\n\n### 의존성 설정\n\n```gradle\n\u002F\u002F Gradle\ntestImplementation 'com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.13'\n```\n\n```xml\n\u003C!-- Maven -->\n\u003Cdependency>\n    \u003CgroupId>com.navercorp.fixturemonkey\u003C\u002FgroupId>\n    \u003CartifactId>fixture-monkey-starter\u003C\u002FartifactId>\n    \u003Cversion>1.0.13\u003C\u002Fversion>\n    \u003Cscope>test\u003C\u002Fscope>\n\u003C\u002Fdependency>\n```\n\n### 사용법\n\n```java\nimport static {your.package}.fixture.FixtureFactory.FIXTURE_MONKEY;\n\n\u002F\u002F 단순 생성\nUser user = FIXTURE_MONKEY.giveMeOne(User.class);\n\n\u002F\u002F 특정 필드 지정\nUser user = FIXTURE_MONKEY.giveMeBuilder(User.class)\n    .set(\"email\", \"test@example.com\")\n    .set(\"active\", true)\n    .sample();\n\n\u002F\u002F 여러 개 생성\nList\u003CUser> users = FIXTURE_MONKEY.giveMe(User.class, 10);\n```\n\n---\n\n## Given-When-Then 패턴 (필수)\n\n**모든 테스트는 Given-When-Then 패턴 필수**\n\n```java\n@Test\n@DisplayName(\"사용자 생성 - 성공\")\nvoid createUser_Success() {\n    \u002F\u002F given - 테스트 준비\n    UserRequest request = new UserRequest(\"test@example.com\", \"password\");\n    User savedUser = FIXTURE_MONKEY.giveMeOne(User.class);\n    when(userRepository.save(any())).thenReturn(savedUser);\n\n    \u002F\u002F when - 실제 실행\n    UserResponse response = userService.createUser(request);\n\n    \u002F\u002F then - 검증\n    assertThat(response).isNotNull();\n    verify(userRepository, times(1)).save(any());\n}\n```\n\n---\n\n## AssertJ 검증 패턴\n\n```java\n\u002F\u002F 단일 값\nassertThat(response).isNotNull();\nassertThat(response.userId()).isEqualTo(1L);\n\n\u002F\u002F 컬렉션\nassertThat(users).hasSize(3);\nassertThat(users).extracting(User::getEmail)\n    .containsExactlyInAnyOrder(\"a@test.com\", \"b@test.com\");\n\n\u002F\u002F Boolean\nassertThat(user.isActive()).isTrue();\n\n\u002F\u002F 예외\nassertThatThrownBy(() -> userService.findById(999L))\n    .isInstanceOf(BusinessException.class)\n    .hasMessageContaining(\"USER_NOT_FOUND\");\n\n\u002F\u002F Optional\nassertThat(result).isPresent();\nassertThat(result.get().getName()).isEqualTo(\"홍길동\");\n```\n\n---\n\n## Mockito 패턴\n\n### Mock 설정\n\n```java\n\u002F\u002F 반환값\nwhen(userRepository.findById(1L)).thenReturn(Optional.of(user));\n\n\u002F\u002F void 메서드\ndoNothing().when(emailService).sendEmail(any());\n\n\u002F\u002F 예외 발생\nwhen(userRepository.findById(999L))\n    .thenThrow(new BusinessException(ErrorCode.USER_NOT_FOUND));\n```\n\n### Mock 호출 검증\n\n```java\n\u002F\u002F 호출 횟수\nverify(userRepository, times(1)).findById(1L);\nverify(userRepository, never()).delete(any());\n\n\u002F\u002F 인자 검증\nverify(userRepository).save(argThat(user ->\n    user.getEmail().equals(\"test@example.com\")\n));\n```\n\n---\n\n## 테스트 명명 규칙\n\n### 클래스\n\n```java\nclass ApplyServiceIntegrationTest extends IntegrationTest  \u002F\u002F Integration\nclass UserRepositoryTest extends RdbTest                   \u002F\u002F Repository\nclass UserControllerTest extends ControllerTest            \u002F\u002F Controller\nclass UserServiceTest                                      \u002F\u002F Service Unit\nclass UserTest                                             \u002F\u002F Domain\n```\n\n### 메서드\n\n```java\n\u002F\u002F 패턴: {메서드명}_{시나리오}_{예상결과}\n@Test\n@DisplayName(\"사용자 생성 - 성공\")\nvoid createUser_ValidRequest_Success()\n\n@Test\n@DisplayName(\"사용자 조회 - 사용자 없음\")\nvoid findById_UserNotFound_ThrowsException()\n```\n\n---\n\n## 테스트 예시\n\n### Controller 테스트\n\n```java\n@DisplayName(\"User -> UserController 테스트\")\n@WebMvcTest(UserController.class)\nclass UserControllerTest extends ControllerTest {\n\n    @MockBean\n    private UserService userService;\n\n    @Test\n    @DisplayName(\"사용자 조회 API - 성공\")\n    void getUser_Success() throws Exception {\n        \u002F\u002F given\n        Long userId = 1L;\n        UserResponse response = new UserResponse(userId, \"test@example.com\");\n        when(userService.findById(userId)).thenReturn(response);\n\n        \u002F\u002F when & then\n        mockMvc.perform(get(\"\u002Fapi\u002Fv1\u002Fusers\u002F{id}\", userId))\n            .andExpect(status().isOk())\n            .andExpect(jsonPath(\"$.userId\").value(userId));\n    }\n}\n```\n\n### Service 단위 테스트\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"User -> UserService 단위 테스트\")\nclass UserServiceTest {\n\n    @Mock\n    private UserRepository userRepository;\n\n    @InjectMocks\n    private UserService userService;\n\n    @Test\n    @DisplayName(\"사용자 조회 - 성공\")\n    void findById_Success() {\n        \u002F\u002F given\n        Long userId = 1L;\n        User user = FIXTURE_MONKEY.giveMeBuilder(User.class)\n            .set(\"id\", userId)\n            .sample();\n        when(userRepository.findById(userId)).thenReturn(Optional.of(user));\n\n        \u002F\u002F when\n        UserResponse response = userService.findById(userId);\n\n        \u002F\u002F then\n        assertThat(response).isNotNull();\n        assertThat(response.userId()).isEqualTo(userId);\n        verify(userRepository, times(1)).findById(userId);\n    }\n}\n```\n\n### Repository 테스트\n\n```java\n@DisplayName(\"User -> UserRepository 테스트\")\nclass UserRepositoryTest extends RdbTest {\n\n    @Autowired\n    private UserRepository userRepository;\n\n    @Test\n    @DisplayName(\"활성 사용자 조회 - 성공\")\n    void findActiveUsers_Success() {\n        \u002F\u002F given\n        User active = FIXTURE_MONKEY.giveMeBuilder(User.class)\n            .set(\"active\", true)\n            .sample();\n        userRepository.save(active);\n\n        \u002F\u002F when\n        List\u003CUserDto> result = userRepository.findActiveUsers();\n\n        \u002F\u002F then\n        assertThat(result).hasSize(1);\n        assertThat(result).extracting(UserDto::email)\n            .contains(active.getEmail());\n    }\n}\n```\n\n---\n\n## When to Use This Skill\n\n이 skill은 다음 상황에서 자동으로 적용됩니다:\n\n- 테스트 파일 생성 또는 수정\n- **테스트 헬퍼 선택 (IntegrationTest vs MockingUnitTest 판단)**\n- 테스트 데이터 생성 (Fixture Monkey 사용)\n- Given-When-Then 패턴 적용\n- AssertJ 검증 코드 작성\n- Mockito Mock 설정 및 검증\n\n**특히 중요**: 새로운 Service 테스트 작성 시 먼저 \"Mock vs Integration 선택 기준\"을 확인하세요!\n\n---\n\n## Checklist\n\n테스트 코드 작성 시 확인사항:\n\n**모든 테스트 공통**\n- [ ] Given-When-Then 패턴을 따르는가?\n- [ ] @DisplayName으로 테스트 의도가 명확한가?\n- [ ] AssertJ로 검증하는가?\n- [ ] 메서드명이 `메서드_시나리오_결과` 패턴인가?\n\n**테스트 헬퍼 선택 (가장 먼저 확인!)**\n- [ ] 금전 처리(입금\u002F출금\u002F결제) 또는 트랜잭션 롤백 검증이 필요한가? → IntegrationTest\n- [ ] 3개 이상 테이블 정합성 또는 DB 제약조건 검증이 필요한가? → IntegrationTest\n- [ ] 복잡한 상태 전이(3단계+) 또는 이벤트 발행\u002F리스너 검증이 필요한가? → IntegrationTest\n- [ ] 3개 이상 서비스가 협력하는가? → IntegrationTest\n- [ ] 위 조건 모두 해당 안됨 → MockingUnitTest 사용\n\n**IntegrationTest**\n- [ ] 위 선택 기준 중 하나 이상에 해당하는가?\n- [ ] 외부 API는 @MockBean으로 처리했는가?\n- [ ] 정말 IntegrationTest가 필요한지 다시 한번 검토했는가?\n\n**RdbTest**\n- [ ] Repository\u002FQueryDSL 테스트만 포함하는가?\n- [ ] N+1 문제를 검증했는가?\n\n**ControllerTest**\n- [ ] @WebMvcTest(TargetController.class)를 명시했는가?\n- [ ] Service는 @MockBean으로 처리했는가?\n- [ ] HTTP Status Code를 검증하는가?\n\n**MockingUnitTest**\n- [ ] @Mock으로 의존성, @InjectMocks로 테스트 대상을 주입했는가?\n- [ ] verify()로 Mock 호출을 검증했는가?\n\n**PojoUnitTest**\n- [ ] 도메인 로직만 테스트하는가?\n- [ ] 외부 의존성이 없는가?\n\n---\n\n## 테스트 실행 명령어\n\n### Gradle\n\n```bash\n.\u002Fgradlew test                                    # 전체 테스트\n.\u002Fgradlew test --tests * -Dtest.tags=Integration # 태그별 실행\n.\u002Fgradlew test --tests UserServiceTest            # 특정 클래스\n```\n\n### Maven\n\n```bash\n.\u002Fmvnw test                        # 전체 테스트\n.\u002Fmvnw test -Dgroups=Integration   # 태그별 실행\n.\u002Fmvnw test -Dtest=UserServiceTest # 특정 클래스\n```\n\n---\n\n## 테스트 품질 기준\n\n1. **커버리지**: 핵심 비즈니스 로직 70% 이상\n2. **격리성**: 각 테스트가 독립적으로 실행 가능\n3. **속도**: 단위 테스트 1초 이내, 통합 테스트 5초 이내\n4. **명확성**: 테스트 이름만으로 의도 파악 가능\n5. **신뢰성**: 같은 입력에 항상 같은 결과\n","# Allra Test Writing Standards\n\nAllra 백엔드 팀의 테스트 작성 표준을 정의합니다. 테스트 헬퍼 선택, Fixture Monkey 데이터 생성, Given-When-Then 패턴, AssertJ 검증을 포함합니다.\n\n## 프로젝트 기본 정보\n\n이 가이드는 다음 환경을 기준으로 작성되었습니다:\n\n- **Java**: 17 이상\n- **Spring Boot**: 3.2 이상\n- **Testing Framework**: JUnit 5\n- **Assertion Library**: AssertJ\n- **Mocking**: Mockito\n- **Test Data**: Fixture Monkey (선택 사항)\n- **Container**: Testcontainers (선택 사항)\n\n**참고**: 프로젝트별로 사용하는 라이브러리나 버전이 다를 수 있습니다. 프로젝트에 맞게 조정하여 사용하세요.\n\n## 테스트 헬퍼 선택 가이드\n\n**주의**: 아래 테스트 헬퍼는 Allra 표준 템플릿에서 제공됩니다. 프로젝트에 이러한 헬퍼가 없는 경우, Spring Boot 기본 테스트 어노테이션(`@SpringBootTest`, `@DataJpaTest`, `@WebMvcTest` 등)을 직접 사용하되, 이 가이드의 테스트 패턴과 원칙은 동일하게 적용합니다.\n\n| 헬퍼 | 태그 | 용도 | 무게 | 언제? |\n|------|------|------|------|-------|\n| **IntegrationTest** | Integration | 여러 서비스 통합 | ? 무거움 | 전체 워크플로우 |\n| **RdbTest** | RDB | Repository, QueryDSL | ? 중간 | 쿼리 검증 |\n| **ControllerTest** | Controller | API 엔드포인트 | ? 가벼움 | REST API 검증 |\n| **RedisTest** | Redis | Redis 캐싱 | ? 가벼움 | 캐시 검증 |\n| **MockingUnitTest** | MockingUnit | Service 단위 | ? 매우 가벼움 | 비즈니스 로직 |\n| **PojoUnitTest** | PojoUnit | 도메인 로직 | ? 매우 가벼움 | 순수 자바 |\n\n### 선택 플로우\n\n```\nAPI 엔드포인트? → ControllerTest\n여러 서비스 통합? → IntegrationTest\nRepository\u002FQueryDSL? → RdbTest\nRedis 캐싱? → RedisTest\nService 로직 (Mock)? → MockingUnitTest\n도메인 로직 (POJO)? → PojoUnitTest\n```\n\n---\n\n## ? Mock vs Integration 선택 기준 (중요!)\n\n**원칙**: 기본은 MockingUnitTest, 꼭 필요할 때만 IntegrationTest\n\n**목표**: IntegrationTest 비율 5% 이하 유지\n\n### 의사결정 플로우차트\n\n```\n┌─────────────────────────────────┐\n│ 무엇을 테스트하려고 하는가?    │\n└────────────┬────────────────────┘\n             │\n    ┌────────▼────────┐\n    │ 도메인 로직만?  │ ──Yes──> PojoUnitTest\n    └────────┬────────┘\n             │ No\n    ┌────────▼─────────────────────┐\n    │ Repository\u002FQueryDSL 쿼리?   │ ──Yes──> RdbTest\n    └────────┬─────────────────────┘\n             │ No\n    ┌────────▼─────────────────────┐\n    │ API 엔드포인트 응답\u002F검증?   │ ──Yes──> ControllerTest\n    └────────┬─────────────────────┘\n             │ No\n    ┌────────▼─────────────────────────────┐\n    │ Service 비즈니스 로직 검증?         │\n    └────────┬─────────────────────────────┘\n             │\n    ┌────────▼──────────────────────────────────────────┐\n    │ 다음 중 하나라도 해당하는가?                      │\n    │                                                   │\n    │ 1. ? 금전 처리 (입금\u002F출금\u002F이체\u002F환불)            │\n    │ 2. ? 트랜잭션 롤백이 중요한 워크플로우           │\n    │ 3. ? 여러 테이블 데이터 정합성 검증             │\n    │ 4. ? 실제 DB 제약조건 검증 필수                 │\n    │ 5. ? 복잡한 상태 전이 (3단계 이상)              │\n    │ 6. ? 이벤트 발행\u002F리스너 통합 검증               │\n    │ 7. ? 3개 이상 서비스 필수 협력                  │\n    └────┬──────────────────────────────────────┬────────┘\n         │ Yes                                  │ No\n         │                                      │\n    ┌────▼────────────┐              ┌─────────▼──────────┐\n    │ IntegrationTest │              │ MockingUnitTest    │\n    │ (최소화)        │              │ (기본 선택)       │\n    └─────────────────┘              └────────────────────┘\n```\n\n### IntegrationTest가 필요한 구체적인 케이스\n\n#### ✅ 1. 금전 처리 (입금\u002F출금\u002F이체\u002F환불)\n\n**이유**: 돈이 관련된 로직은 실제 DB 트랜잭션 동작 검증 필수\n\n```java\n\u002F\u002F 예시: 펀딩 신청 (FsData → FsPayment → PointUsage → UserAccount 연계)\n@DisplayName(\"펀딩 신청 시 금액 차감 및 결제 생성\")\nclass ApplyServiceIntegrationTest extends IntegrationTest {\n\n    @Test\n    @Transactional\n    void apply_DecreasesAmount_Success() {\n        \u002F\u002F given: 사용자 잔액 100만원\n        User user = createUserWithBalance(1_000_000);\n\n        \u002F\u002F when: 50만원 펀딩 신청\n        applyService.apply(new ApplyRequest(user.getId(), 500_000));\n\n        \u002F\u002F then: 실제 DB에서 잔액 50만원 확인\n        User updated = userRepository.findById(user.getId()).get();\n        assertThat(updated.getBalance()).isEqualTo(500_000);\n\n        \u002F\u002F then: FsPayment 생성 확인\n        FsPayment payment = fsPaymentRepository.findByUserId(user.getId()).get();\n        assertThat(payment.getAmount()).isEqualTo(500_000);\n    }\n}\n```\n\n#### ✅ 2. 트랜잭션 롤백이 중요한 워크플로우\n\n**이유**: 실패 시 모든 작업이 원자적으로 롤백되어야 함\n\n```java\n\u002F\u002F 예시: 결제 실패 시 전체 롤백\n@Test\n@DisplayName(\"결제 실패 시 신청 데이터도 롤백\")\nvoid apply_PaymentFails_RollbackAll() {\n    \u002F\u002F given\n    User user = createUser();\n    mockPaymentGateway_ToFail(); \u002F\u002F 외부 결제는 Mock으로\n\n    \u002F\u002F when & then\n    assertThatThrownBy(() -> applyService.apply(request))\n        .isInstanceOf(PaymentException.class);\n\n    \u002F\u002F then: DB에 어떤 데이터도 저장되지 않음\n    assertThat(fsDataRepository.findAll()).isEmpty();\n    assertThat(fsPaymentRepository.findAll()).isEmpty();\n}\n```\n\n**참고**: 외부 연동(결제 게이트웨이, 외부 API)은 `@MockBean`으로 처리\n\n#### ✅ 3. 여러 테이블 데이터 정합성 검증\n\n**이유**: 관련된 모든 테이블의 상태가 일관되게 유지되는지 확인\n\n```java\n\u002F\u002F 예시: 계약 생성 시 UserAccount, Contract, FsData 모두 생성\n@Test\n@DisplayName(\"신규 계약 시 관련 테이블 모두 생성\")\nvoid createContract_CreatesAllRelatedData() {\n    \u002F\u002F when\n    contractService.createContract(userId, contractType);\n\n    \u002F\u002F then: 3개 테이블 모두 데이터 존재\n    assertThat(userAccountRepository.findByUserId(userId)).isPresent();\n    assertThat(contractRepository.findByUserId(userId)).isPresent();\n    assertThat(fsDataRepository.findByUserId(userId)).isPresent();\n}\n```\n\n#### ✅ 4. 실제 DB 제약조건 검증\n\n**이유**: Unique, FK, Check 제약조건은 실제 DB에서만 확인 가능\n\n```java\n\u002F\u002F 예시: 중복 계좌 등록 방지\n@Test\n@DisplayName(\"동일 계좌번호 중복 등록 시 예외\")\nvoid registerAccount_Duplicate_ThrowsException() {\n    \u002F\u002F given\n    userAccountRepository.save(new UserAccount(userId, \"123-456-789\"));\n\n    \u002F\u002F when & then: Unique 제약조건 위반\n    assertThatThrownBy(() ->\n        userAccountRepository.save(new UserAccount(userId, \"123-456-789\"))\n    ).isInstanceOf(DataIntegrityViolationException.class);\n}\n```\n\n#### ✅ 5. 복잡한 상태 전이 (3단계 이상)\n\n**이유**: 상태 변화 흐름을 실제 시나리오대로 검증\n\n```java\n\u002F\u002F 예시: 계약 상태 전이 (신청 → 심사 → 승인 → 완료)\n@Test\n@DisplayName(\"계약 워크플로우 전체 검증\")\nvoid contractWorkflow_FullCycle() {\n    \u002F\u002F given: 신청\n    Contract contract = contractService.create(userId);\n    assertThat(contract.getStatus()).isEqualTo(ContractStatus.PENDING);\n\n    \u002F\u002F when: 심사\n    contractService.review(contract.getId());\n    \u002F\u002F then\n    Contract reviewed = contractRepository.findById(contract.getId()).get();\n    assertThat(reviewed.getStatus()).isEqualTo(ContractStatus.REVIEWED);\n\n    \u002F\u002F when: 승인\n    contractService.approve(contract.getId());\n    \u002F\u002F then\n    Contract approved = contractRepository.findById(contract.getId()).get();\n    assertThat(approved.getStatus()).isEqualTo(ContractStatus.APPROVED);\n}\n```\n\n#### ✅ 6. 이벤트 발행\u002F리스너 통합 검증\n\n**이유**: 이벤트가 실제로 발행되고 리스너가 동작하는지 확인\n\n```java\n\u002F\u002F 예시: 계약 완료 이벤트 → 알림 발송\n@Test\n@DisplayName(\"계약 완료 시 알림 이벤트 발행\")\nvoid completeContract_PublishesEvent() {\n    \u002F\u002F given\n    Contract contract = createContract(userId);\n\n    \u002F\u002F when\n    contractService.complete(contract.getId());\n\n    \u002F\u002F then: 실제로 알림이 발송되었는가? (외부 알림은 @MockBean)\n    verify(notificationService).sendContractCompleteNotification(userId);\n}\n```\n\n#### ✅ 7. 3개 이상 서비스가 필수적으로 협력\n\n**이유**: 서비스 간 상호작용을 실제 환경에서 검증\n\n```java\n\u002F\u002F 예시: 주문 생성 → 재고 차감 → 결제 → 알림\n@Test\n@DisplayName(\"주문 생성 워크플로우\")\nvoid createOrder_FullWorkflow() {\n    \u002F\u002F given\n    Product product = createProductWithStock(100);\n\n    \u002F\u002F when\n    orderService.createOrder(userId, product.getId(), 10);\n\n    \u002F\u002F then: 재고 차감\n    Product updated = productRepository.findById(product.getId()).get();\n    assertThat(updated.getStock()).isEqualTo(90);\n\n    \u002F\u002F then: 결제 생성\n    Payment payment = paymentRepository.findByUserId(userId).get();\n    assertThat(payment.getStatus()).isEqualTo(PaymentStatus.COMPLETED);\n}\n```\n\n### MockingUnitTest로 충분한 케이스\n\n#### ✅ 대부분의 Service 로직\n\n- 단순 조회 (findById, findAll)\n- 데이터 변환\u002F계산\n- 검증 로직 (validation)\n- 단일 엔티티 CRUD\n- 비즈니스 규칙 검증\n\n```java\n\u002F\u002F 예시: 할인율 계산 로직 (Mock으로 충분)\n@ExtendWith(MockitoExtension.class)\nclass DiscountServiceTest {\n\n    @Mock\n    private UserRepository userRepository;\n\n    @InjectMocks\n    private DiscountService discountService;\n\n    @Test\n    @DisplayName(\"VIP 회원 10% 할인 계산\")\n    void calculateDiscount_VipUser_10Percent() {\n        \u002F\u002F given\n        User vipUser = User.builder().grade(\"VIP\").build();\n        when(userRepository.findById(1L)).thenReturn(Optional.of(vipUser));\n\n        \u002F\u002F when\n        BigDecimal discount = discountService.calculateDiscount(1L, new BigDecimal(\"10000\"));\n\n        \u002F\u002F then\n        assertThat(discount).isEqualByComparingTo(new BigDecimal(\"1000\"));\n    }\n}\n```\n\n### 외부 연동 처리 원칙\n\n**중요**: IntegrationTest에서도 외부 시스템은 `@MockBean`으로 처리\n\n```java\n@SpringBootTest\nclass PaymentServiceIntegrationTest extends IntegrationTest {\n\n    @Autowired\n    private PaymentService paymentService;\n\n    @MockBean \u002F\u002F 외부 결제 게이트웨이는 Mock\n    private ExternalPaymentGateway externalPaymentGateway;\n\n    @MockBean \u002F\u002F 외부 알림 서비스는 Mock\n    private ExternalNotificationService notificationService;\n\n    @Test\n    @DisplayName(\"결제 성공 시 내부 데이터 정합성 검증\")\n    void processPayment_Success() {\n        \u002F\u002F given: 외부 결제는 성공으로 Mock\n        when(externalPaymentGateway.charge(any()))\n            .thenReturn(new PaymentResult(\"SUCCESS\", \"tx-123\"));\n\n        \u002F\u002F when: 실제 내부 로직 검증\n        paymentService.processPayment(userId, amount);\n\n        \u002F\u002F then: 내부 DB 상태 확인\n        Payment payment = paymentRepository.findByUserId(userId).get();\n        assertThat(payment.getStatus()).isEqualTo(PaymentStatus.COMPLETED);\n        assertThat(payment.getExternalTxId()).isEqualTo(\"tx-123\");\n    }\n}\n```\n\n### 테스트 전략 요약\n\n| 테스트 유형 | 목표 비율 | 실행 속도 | 주요 사용처 |\n|------------|----------|----------|------------|\n| **PojoUnitTest** | 30% | ⚡️ 0.01초 | 도메인 로직, 유틸리티 |\n| **MockingUnitTest** | 50% | ⚡️ 0.1초 | Service 비즈니스 로직 |\n| **ControllerTest** | 10% | ? 0.5초 | API 검증 |\n| **RdbTest** | 5% | ? 1초 | 복잡한 쿼리 검증 |\n| **IntegrationTest** | 5% | ? 5초 | 금전\u002F트랜잭션\u002F워크플로우 |\n\n### 빠른 판단 체크리스트\n\n새로운 테스트를 작성할 때 다음을 확인하세요:\n\n```\n□ 돈이 관련되어 있나요? (입금\u002F출금\u002F결제)\n  → Yes: IntegrationTest\n\n□ 실패 시 데이터 롤백이 중요한가요?\n  → Yes: IntegrationTest\n\n□ 3개 이상 테이블의 정합성을 확인해야 하나요?\n  → Yes: IntegrationTest\n\n□ DB 제약조건(Unique\u002FFK)이 핵심인가요?\n  → Yes: IntegrationTest\n\n□ 복잡한 상태 전이(3단계+)를 검증하나요?\n  → Yes: IntegrationTest\n\n□ 이벤트 발행\u002F리스너를 검증하나요?\n  → Yes: IntegrationTest\n\n□ 3개 이상 서비스가 협력하나요?\n  → Yes: IntegrationTest\n\n모두 No → MockingUnitTest 사용\n```\n\n---\n\n## 테스트 헬퍼 구조\n\n### IntegrationTest - 통합 테스트\n\n```java\n@Tag(\"Integration\")\n@SpringBootTest\npublic abstract class IntegrationTest {\n    \u002F\u002F 전체 Spring Context, Testcontainers 활용\n}\n```\n\n**언제**: 여러 서비스 협력, 실제 DB\u002F외부 시스템 필요\n**주의**: 가장 무거움, 외부 API는 `@MockBean` 사용\n\n### RdbTest - Repository 테스트\n\n```java\n@Tag(\"RDB\")\n@DataJpaTest\npublic abstract class RdbTest {}\n```\n\n**언제**: Repository CRUD, QueryDSL 쿼리, N+1 문제 검증\n\n### ControllerTest - API 테스트\n\n```java\n@Tag(\"Controller\")\n@WebMvcTest(TargetController.class)\npublic abstract class ControllerTest {\n    @Autowired\n    protected MockMvc mockMvc;\n}\n```\n\n**언제**: API 엔드포인트, HTTP Status, 입력 검증\n**주의**: Service는 `@MockBean` 필수\n\n### RedisTest - Redis 테스트\n\n```java\n@Tag(\"Redis\")\n@DataRedisTest\npublic abstract class RedisTest {}\n```\n\n**언제**: Redis 캐싱, 세션 저장소 검증\n\n### MockingUnitTest - Service 단위 테스트\n\n```java\n@ExtendWith(MockitoExtension.class)\nclass UserServiceTest {\n    @Mock\n    private UserRepository userRepository;\n\n    @InjectMocks\n    private UserService userService;\n}\n```\n\n**언제**: Service 로직 단위 테스트, 빠른 테스트\n**주의**: Spring Context 없음, `@Autowired` 불가\n\n### PojoUnitTest - 도메인 로직 테스트\n\n```java\nclass UserTest {\n    @Test\n    void activate_Success() {\n        \u002F\u002F 순수 자바 로직 테스트\n    }\n}\n```\n\n**언제**: 도메인 엔티티, VO, 유틸리티 클래스\n\n---\n\n## Fixture Monkey - 테스트 데이터 생성\n\n### 의존성 설정\n\n```gradle\n\u002F\u002F Gradle\ntestImplementation 'com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.13'\n```\n\n```xml\n\u003C!-- Maven -->\n\u003Cdependency>\n    \u003CgroupId>com.navercorp.fixturemonkey\u003C\u002FgroupId>\n    \u003CartifactId>fixture-monkey-starter\u003C\u002FartifactId>\n    \u003Cversion>1.0.13\u003C\u002Fversion>\n    \u003Cscope>test\u003C\u002Fscope>\n\u003C\u002Fdependency>\n```\n\n### 사용법\n\n```java\nimport static {your.package}.fixture.FixtureFactory.FIXTURE_MONKEY;\n\n\u002F\u002F 단순 생성\nUser user = FIXTURE_MONKEY.giveMeOne(User.class);\n\n\u002F\u002F 특정 필드 지정\nUser user = FIXTURE_MONKEY.giveMeBuilder(User.class)\n    .set(\"email\", \"test@example.com\")\n    .set(\"active\", true)\n    .sample();\n\n\u002F\u002F 여러 개 생성\nList\u003CUser> users = FIXTURE_MONKEY.giveMe(User.class, 10);\n```\n\n---\n\n## Given-When-Then 패턴 (필수)\n\n**모든 테스트는 Given-When-Then 패턴 필수**\n\n```java\n@Test\n@DisplayName(\"사용자 생성 - 성공\")\nvoid createUser_Success() {\n    \u002F\u002F given - 테스트 준비\n    UserRequest request = new UserRequest(\"test@example.com\", \"password\");\n    User savedUser = FIXTURE_MONKEY.giveMeOne(User.class);\n    when(userRepository.save(any())).thenReturn(savedUser);\n\n    \u002F\u002F when - 실제 실행\n    UserResponse response = userService.createUser(request);\n\n    \u002F\u002F then - 검증\n    assertThat(response).isNotNull();\n    verify(userRepository, times(1)).save(any());\n}\n```\n\n---\n\n## AssertJ 검증 패턴\n\n```java\n\u002F\u002F 단일 값\nassertThat(response).isNotNull();\nassertThat(response.userId()).isEqualTo(1L);\n\n\u002F\u002F 컬렉션\nassertThat(users).hasSize(3);\nassertThat(users).extracting(User::getEmail)\n    .containsExactlyInAnyOrder(\"a@test.com\", \"b@test.com\");\n\n\u002F\u002F Boolean\nassertThat(user.isActive()).isTrue();\n\n\u002F\u002F 예외\nassertThatThrownBy(() -> userService.findById(999L))\n    .isInstanceOf(BusinessException.class)\n    .hasMessageContaining(\"USER_NOT_FOUND\");\n\n\u002F\u002F Optional\nassertThat(result).isPresent();\nassertThat(result.get().getName()).isEqualTo(\"홍길동\");\n```\n\n---\n\n## Mockito 패턴\n\n### Mock 설정\n\n```java\n\u002F\u002F 반환값\nwhen(userRepository.findById(1L)).thenReturn(Optional.of(user));\n\n\u002F\u002F void 메서드\ndoNothing().when(emailService).sendEmail(any());\n\n\u002F\u002F 예외 발생\nwhen(userRepository.findById(999L))\n    .thenThrow(new BusinessException(ErrorCode.USER_NOT_FOUND));\n```\n\n### Mock 호출 검증\n\n```java\n\u002F\u002F 호출 횟수\nverify(userRepository, times(1)).findById(1L);\nverify(userRepository, never()).delete(any());\n\n\u002F\u002F 인자 검증\nverify(userRepository).save(argThat(user ->\n    user.getEmail().equals(\"test@example.com\")\n));\n```\n\n---\n\n## 테스트 명명 규칙\n\n### 클래스\n\n```java\nclass ApplyServiceIntegrationTest extends IntegrationTest  \u002F\u002F Integration\nclass UserRepositoryTest extends RdbTest                   \u002F\u002F Repository\nclass UserControllerTest extends ControllerTest            \u002F\u002F Controller\nclass UserServiceTest                                      \u002F\u002F Service Unit\nclass UserTest                                             \u002F\u002F Domain\n```\n\n### 메서드\n\n```java\n\u002F\u002F 패턴: {메서드명}_{시나리오}_{예상결과}\n@Test\n@DisplayName(\"사용자 생성 - 성공\")\nvoid createUser_ValidRequest_Success()\n\n@Test\n@DisplayName(\"사용자 조회 - 사용자 없음\")\nvoid findById_UserNotFound_ThrowsException()\n```\n\n---\n\n## 테스트 예시\n\n### Controller 테스트\n\n```java\n@DisplayName(\"User -> UserController 테스트\")\n@WebMvcTest(UserController.class)\nclass UserControllerTest extends ControllerTest {\n\n    @MockBean\n    private UserService userService;\n\n    @Test\n    @DisplayName(\"사용자 조회 API - 성공\")\n    void getUser_Success() throws Exception {\n        \u002F\u002F given\n        Long userId = 1L;\n        UserResponse response = new UserResponse(userId, \"test@example.com\");\n        when(userService.findById(userId)).thenReturn(response);\n\n        \u002F\u002F when & then\n        mockMvc.perform(get(\"\u002Fapi\u002Fv1\u002Fusers\u002F{id}\", userId))\n            .andExpect(status().isOk())\n            .andExpect(jsonPath(\"$.userId\").value(userId));\n    }\n}\n```\n\n### Service 단위 테스트\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"User -> UserService 단위 테스트\")\nclass UserServiceTest {\n\n    @Mock\n    private UserRepository userRepository;\n\n    @InjectMocks\n    private UserService userService;\n\n    @Test\n    @DisplayName(\"사용자 조회 - 성공\")\n    void findById_Success() {\n        \u002F\u002F given\n        Long userId = 1L;\n        User user = FIXTURE_MONKEY.giveMeBuilder(User.class)\n            .set(\"id\", userId)\n            .sample();\n        when(userRepository.findById(userId)).thenReturn(Optional.of(user));\n\n        \u002F\u002F when\n        UserResponse response = userService.findById(userId);\n\n        \u002F\u002F then\n        assertThat(response).isNotNull();\n        assertThat(response.userId()).isEqualTo(userId);\n        verify(userRepository, times(1)).findById(userId);\n    }\n}\n```\n\n### Repository 테스트\n\n```java\n@DisplayName(\"User -> UserRepository 테스트\")\nclass UserRepositoryTest extends RdbTest {\n\n    @Autowired\n    private UserRepository userRepository;\n\n    @Test\n    @DisplayName(\"활성 사용자 조회 - 성공\")\n    void findActiveUsers_Success() {\n        \u002F\u002F given\n        User active = FIXTURE_MONKEY.giveMeBuilder(User.class)\n            .set(\"active\", true)\n            .sample();\n        userRepository.save(active);\n\n        \u002F\u002F when\n        List\u003CUserDto> result = userRepository.findActiveUsers();\n\n        \u002F\u002F then\n        assertThat(result).hasSize(1);\n        assertThat(result).extracting(UserDto::email)\n            .contains(active.getEmail());\n    }\n}\n```\n\n---\n\n## When to Use This Skill\n\n이 skill은 다음 상황에서 자동으로 적용됩니다:\n\n- 테스트 파일 생성 또는 수정\n- **테스트 헬퍼 선택 (IntegrationTest vs MockingUnitTest 판단)**\n- 테스트 데이터 생성 (Fixture Monkey 사용)\n- Given-When-Then 패턴 적용\n- AssertJ 검증 코드 작성\n- Mockito Mock 설정 및 검증\n\n**특히 중요**: 새로운 Service 테스트 작성 시 먼저 \"Mock vs Integration 선택 기준\"을 확인하세요!\n\n---\n\n## Checklist\n\n테스트 코드 작성 시 확인사항:\n\n**모든 테스트 공통**\n- [ ] Given-When-Then 패턴을 따르는가?\n- [ ] @DisplayName으로 테스트 의도가 명확한가?\n- [ ] AssertJ로 검증하는가?\n- [ ] 메서드명이 `메서드_시나리오_결과` 패턴인가?\n\n**테스트 헬퍼 선택 (가장 먼저 확인!)**\n- [ ] 금전 처리(입금\u002F출금\u002F결제) 또는 트랜잭션 롤백 검증이 필요한가? → IntegrationTest\n- [ ] 3개 이상 테이블 정합성 또는 DB 제약조건 검증이 필요한가? → IntegrationTest\n- [ ] 복잡한 상태 전이(3단계+) 또는 이벤트 발행\u002F리스너 검증이 필요한가? → IntegrationTest\n- [ ] 3개 이상 서비스가 협력하는가? → IntegrationTest\n- [ ] 위 조건 모두 해당 안됨 → MockingUnitTest 사용\n\n**IntegrationTest**\n- [ ] 위 선택 기준 중 하나 이상에 해당하는가?\n- [ ] 외부 API는 @MockBean으로 처리했는가?\n- [ ] 정말 IntegrationTest가 필요한지 다시 한번 검토했는가?\n\n**RdbTest**\n- [ ] Repository\u002FQueryDSL 테스트만 포함하는가?\n- [ ] N+1 문제를 검증했는가?\n\n**ControllerTest**\n- [ ] @WebMvcTest(TargetController.class)를 명시했는가?\n- [ ] Service는 @MockBean으로 처리했는가?\n- [ ] HTTP Status Code를 검증하는가?\n\n**MockingUnitTest**\n- [ ] @Mock으로 의존성, @InjectMocks로 테스트 대상을 주입했는가?\n- [ ] verify()로 Mock 호출을 검증했는가?\n\n**PojoUnitTest**\n- [ ] 도메인 로직만 테스트하는가?\n- [ ] 외부 의존성이 없는가?\n\n---\n\n## 테스트 실행 명령어\n\n### Gradle\n\n```bash\n.\u002Fgradlew test                                    # 전체 테스트\n.\u002Fgradlew test --tests * -Dtest.tags=Integration # 태그별 실행\n.\u002Fgradlew test --tests UserServiceTest            # 특정 클래스\n```\n\n### Maven\n\n```bash\n.\u002Fmvnw test                        # 전체 테스트\n.\u002Fmvnw test -Dgroups=Integration   # 태그별 실행\n.\u002Fmvnw test -Dtest=UserServiceTest # 특정 클래스\n```\n\n---\n\n## 테스트 품질 기준\n\n1. **커버리지**: 핵심 비즈니스 로직 70% 이상\n2. **격리성**: 각 테스트가 독립적으로 실행 가능\n3. **속도**: 단위 테스트 1초 이내, 통합 테스트 5초 이내\n4. **명확성**: 테스트 이름만으로 의도 파악 가능\n5. **신뢰성**: 같은 입력에 항상 같은 결과\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Faiskillstore\u002Fmarketplace\u002Fallra-fintech\u002Fallra-test-writing",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":61,"prompt":71,"zipAvailable":72},[62,65,68],{"tool":63,"command":64},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill allra-test-writing",{"tool":66,"command":67},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill allra-test-writing",{"tool":69,"command":70},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill allra-test-writing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fallra-fintech\u002Fallra-test-writing\u002FSKILL.md",true,[74,80,84,90,94,98,102,106,110,114,118,122,126,130,134,138,142,146,150,154,158,162,166,170,174,178,182,186,190],{"path":75,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":78,"sourceHtmlUrl":79},"references\u002Fmodel-recommendations.md","REFERENCE","参考文档",2564,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Freferences\u002Fmodel-recommendations.md",{"path":81,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":82,"sourceHtmlUrl":83},"references\u002Fphase-separation-rules.md",1670,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Freferences\u002Fphase-separation-rules.md",{"path":85,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":88,"sourceHtmlUrl":89},"scripts\u002Fbash\u002Fcommon.sh","SCRIPT","脚本",25885,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fbash\u002Fcommon.sh",{"path":91,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":92,"sourceHtmlUrl":93},"scripts\u002Fbash\u002Fgenerate-dashboard-safe.sh",1885,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fbash\u002Fgenerate-dashboard-safe.sh",{"path":95,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":96,"sourceHtmlUrl":97},"scripts\u002Fbash\u002Fnext-step.sh",14665,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fbash\u002Fnext-step.sh",{"path":99,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":100,"sourceHtmlUrl":101},"scripts\u002Fbash\u002Ftestify-tdd.sh",24294,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fbash\u002Ftestify-tdd.sh",{"path":103,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":104,"sourceHtmlUrl":105},"scripts\u002Fbash\u002Fvalidate-premise.sh",6633,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fbash\u002Fvalidate-premise.sh",{"path":107,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":108,"sourceHtmlUrl":109},"scripts\u002Fdashboard\u002Fpublic\u002Findex.html",210668,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fpublic\u002Findex.html",{"path":111,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":112,"sourceHtmlUrl":113},"scripts\u002Fdashboard\u002Fsrc\u002Fanalyze.js",7204,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fanalyze.js",{"path":115,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":116,"sourceHtmlUrl":117},"scripts\u002Fdashboard\u002Fsrc\u002Fboard.js",3280,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fboard.js",{"path":119,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":120,"sourceHtmlUrl":121},"scripts\u002Fdashboard\u002Fsrc\u002Fbugs.js",5027,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fbugs.js",{"path":123,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":124,"sourceHtmlUrl":125},"scripts\u002Fdashboard\u002Fsrc\u002Fchecklist.js",2102,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fchecklist.js",{"path":127,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":128,"sourceHtmlUrl":129},"scripts\u002Fdashboard\u002Fsrc\u002Fgenerate-dashboard.js",10427,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fgenerate-dashboard.js",{"path":131,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":132,"sourceHtmlUrl":133},"scripts\u002Fdashboard\u002Fsrc\u002Fintegrity.js",1781,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fintegrity.js",{"path":135,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":136,"sourceHtmlUrl":137},"scripts\u002Fdashboard\u002Fsrc\u002Fparser.js",44591,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fparser.js",{"path":139,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":140,"sourceHtmlUrl":141},"scripts\u002Fdashboard\u002Fsrc\u002Fpipeline.js",6008,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fpipeline.js",{"path":143,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":144,"sourceHtmlUrl":145},"scripts\u002Fdashboard\u002Fsrc\u002Fplanview.js",9579,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fplanview.js",{"path":147,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":148,"sourceHtmlUrl":149},"scripts\u002Fdashboard\u002Fsrc\u002Fpublic\u002Findex.html",209985,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fpublic\u002Findex.html",{"path":151,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":152,"sourceHtmlUrl":153},"scripts\u002Fdashboard\u002Fsrc\u002Fstorymap.js",1463,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Fstorymap.js",{"path":155,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":156,"sourceHtmlUrl":157},"scripts\u002Fdashboard\u002Fsrc\u002Ftestify.js",6388,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Fsrc\u002Ftestify.js",{"path":159,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":160,"sourceHtmlUrl":161},"scripts\u002Fdashboard\u002Ftemplate.js",218109,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fdashboard\u002Ftemplate.js",{"path":163,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":164,"sourceHtmlUrl":165},"scripts\u002Fpowershell\u002Fcommon.ps1",14621,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fpowershell\u002Fcommon.ps1",{"path":167,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":168,"sourceHtmlUrl":169},"scripts\u002Fpowershell\u002Fgenerate-dashboard-safe.ps1",1830,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fpowershell\u002Fgenerate-dashboard-safe.ps1",{"path":171,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":172,"sourceHtmlUrl":173},"scripts\u002Fpowershell\u002Fnext-step.ps1",10993,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Fscripts\u002Fpowershell\u002Fnext-step.ps1",{"path":175,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":176,"sourceHtmlUrl":177},"skill-report.json",18021,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fallra-fintech\u002Fallra-test-writing\u002Fskill-report.json",{"path":179,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":180,"sourceHtmlUrl":181},"templates\u002Fconstitution-template.md",2360,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Ftemplates\u002Fconstitution-template.md",{"path":183,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":184,"sourceHtmlUrl":185},"templates\u002Fplan-template.md",3347,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Ftemplates\u002Fplan-template.md",{"path":187,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":188,"sourceHtmlUrl":189},"templates\u002Fspec-template.md",3950,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Ftemplates\u002Fspec-template.md",{"path":191,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":192,"sourceHtmlUrl":193},"templates\u002Ftasks-template.md",5386,"https:\u002F\u002Fgithub.com\u002Fhafidzlvm\u002Fopencode\u002Fblob\u002Fmain\u002Fskills\u002Fiikit-00-constitution\u002Ftemplates\u002Ftasks-template.md",[195,207,217,226,232,239],{"skillNo":196,"name":197,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":198,"categories":199,"keywords":203,"publishedAt":46},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[200],{"categoryNo":201,"categoryName":202,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[204,205,206],"model","model hierarchy","model hierarchy skill",{"skillNo":208,"name":209,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":210,"categories":211,"keywords":213,"publishedAt":46},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[212],{"categoryNo":201,"categoryName":202,"taxonomyType":21,"taxonomyTypeDesc":22},[214,215,216],"MCP","zoom","zoom mcp",{"skillNo":218,"name":219,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":220,"categories":221,"keywords":222,"publishedAt":46},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[223,224,225,215],"sdk","zoom apps sdk","apps",{"skillNo":227,"name":228,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":229,"categories":230,"keywords":231,"publishedAt":46},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[228],{"skillNo":233,"name":234,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":235,"categories":236,"keywords":238,"publishedAt":46},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[237],{"categoryNo":201,"categoryName":202,"taxonomyType":21,"taxonomyTypeDesc":22},[234],{"skillNo":240,"name":241,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":242,"categories":243,"keywords":244,"publishedAt":46},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[241]]