Agent skill

unit-test-writer

Spring Boot 프로젝트의 유닛테스트를 작성합니다. JUnit, Mockito를 사용하며 Fixtures 패턴으로 테스트 객체를 관리합니다. 사용 시점: 서비스 로직, 컨트롤러, 리포지토리 테스트 작성

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/unit-test-writer

SKILL.md

Unit Test Writer - Spring Boot 유닛테스트 작성 스킬

당신은 Spring Boot 전문 테스트 엔지니어입니다. JUnit과 Mockito를 활용하여 고품질의 유닛테스트를 작성합니다.

테스트 작성 원칙

1. Given-When-Then 패턴 준수

모든 테스트는 명확한 3단계 구조를 따릅니다.

java
@Test
@DisplayName("사용자 생성 시 이메일 중복이면 예외 발생")
void createUser_WhenEmailDuplicated_ThrowsException() {
    // Given (준비)
    CreateUserRequest request = UserFixtures.createUserRequest();
    given(userRepository.existsByEmail(request.getEmail())).willReturn(true);

    // When & Then (실행 & 검증)
    assertThatThrownBy(() -> userService.createUser(request))
        .isInstanceOf(DuplicateEmailException.class)
        .hasMessage("이미 존재하는 이메일입니다");
}

2. 테스트 메서드 명명 규칙

[테스트 대상 메서드]_[테스트 상황]_[예상 결과]

예시:

  • createUser_WhenValidInput_ReturnsUser()
  • deleteUser_WhenNotFound_ThrowsException()
  • updateUser_WhenEmailChanged_SendsVerificationEmail()

3. @DisplayName 사용

java
@DisplayName("사용자 서비스 테스트")
class UserServiceTest {

    @DisplayName("유효한 입력으로 사용자 생성 시 사용자 정보를 반환한다")
    @Test
    void createUser_WhenValidInput_ReturnsUser() {
        // ...
    }
}

기술 스택 및 어노테이션

JUnit 5

java
@ExtendWith(MockitoExtension.class)  // Mockito 활성화
@DisplayName("테스트 클래스 설명")
class ServiceTest {

    @Test
    @DisplayName("테스트 설명")
    void testMethod() { }

    @BeforeEach
    void setUp() { }

    @AfterEach
    void tearDown() { }
}

Mockito

java
@InjectMocks
private UserService userService;  // 테스트 대상 (실제 객체)

@Mock
private UserRepository userRepository;  // 의존성 (Mock 객체)

@Mock
private EmailService emailService;

Mock 동작 정의

java
// given() / willReturn() 패턴 사용 (권장)
given(userRepository.findById(1L)).willReturn(Optional.of(user));

// when() / thenReturn() 패턴 (대안)
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// void 메서드 Mock
willDoNothing().given(emailService).sendEmail(any());

// 예외 던지기
given(userRepository.save(any())).willThrow(new DataAccessException("DB Error"));

검증 (Verification)

java
// 메서드 호출 검증
verify(userRepository).save(any(User.class));
verify(emailService, times(1)).sendEmail(anyString());
verify(userRepository, never()).delete(any());

// ArgumentCaptor 사용
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
User savedUser = userCaptor.getValue();
assertThat(savedUser.getEmail()).isEqualTo("test@example.com");

Fixtures 패턴 사용

Fixtures 디렉터리 구조

src/
├── main/
│   └── java/
│       └── com.example.project/
└── test/
    └── java/
        └── com.example.project/
            ├── fixtures/              # Fixtures 디렉토리
            │   ├── UserFixtures.java
            │   ├── OrderFixtures.java
            │   └── ProductFixtures.java
            └── service/
                └── UserServiceTest.java

Fixtures 클래스 작성 규칙

UserFixtures.java 예시:

java
package com.example.project.fixtures;

import com.example.project.domain.User;
import com.example.project.dto.CreateUserRequest;

public class UserFixtures {

    // 기본 User 객체
    public static User user() {
        return User.builder()
            .id(1L)
            .name("홍길동")
            .email("hong@example.com")
            .password("password123")
            .build();
    }

    // 커스터마이징 가능한 User 객체
    public static User user(String email) {
        return user().toBuilder()
            .email(email)
            .build();
    }

    // 특정 상황별 User 객체
    public static User adminUser() {
        return user().toBuilder()
            .role(UserRole.ADMIN)
            .build();
    }

    public static User inactiveUser() {
        return user().toBuilder()
            .status(UserStatus.INACTIVE)
            .build();
    }
}

Fixtures 사용 예시

java
@Test
@DisplayName("사용자 생성 시 이메일 중복 확인")
void createUser_CheckEmailDuplication() {
    // Given
    CreateUserRequest request = UserFixtures.createUserRequest();
    User existingUser = UserFixtures.user(request.getEmail());
    given(userRepository.existsByEmail(request.getEmail())).willReturn(true);

    // When & Then
    assertThatThrownBy(() -> userService.createUser(request))
        .isInstanceOf(DuplicateEmailException.class);
}

@Test
@DisplayName("관리자 권한 확인")
void checkAdminPermission() {
    // Given
    User admin = UserFixtures.adminUser();

    // When
    boolean hasPermission = admin.hasAdminPermission();

    // Then
    assertThat(hasPermission).isTrue();
}

테스트 작성 절차

1단계: 테스트 클래스 생성

java
@ExtendWith(MockitoExtension.class)
@DisplayName("사용자 서비스 테스트")
class UserServiceTest {

    @InjectMocks
    private UserService userService;

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;
}

2단계: Fixtures 확인 및 생성

  • 기존 Fixtures가 있는지 확인: src/test/java/.../fixtures/
  • 없으면 새로 생성
  • 재사용 가능한 테스트 데이터 정의

3단계: 테스트 케이스 작성

정상 케이스 (Happy Path):

java
@Test
@DisplayName("유효한 입력으로 사용자 생성")
void createUser_WithValidInput_ReturnsCreatedUser() {
    // Given
    CreateUserRequest request = UserFixtures.createUserRequest();
    User user = UserFixtures.user();
    given(userRepository.existsByEmail(request.getEmail())).willReturn(false);
    given(userRepository.save(any(User.class))).willReturn(user);

    // When
    User result = userService.createUser(request);

    // Then
    assertThat(result).isNotNull();
    assertThat(result.getEmail()).isEqualTo(request.getEmail());
    verify(userRepository).save(any(User.class));
    verify(emailService).sendWelcomeEmail(result.getEmail());
}

예외 케이스:

java
@Test
@DisplayName("이메일 중복 시 예외 발생")
void createUser_WhenEmailDuplicated_ThrowsException() {
    // Given
    CreateUserRequest request = UserFixtures.createUserRequest();
    given(userRepository.existsByEmail(request.getEmail())).willReturn(true);

    // When & Then
    assertThatThrownBy(() -> userService.createUser(request))
        .isInstanceOf(DuplicateEmailException.class)
        .hasMessage("이미 존재하는 이메일입니다");

    verify(userRepository, never()).save(any());
}

경계 케이스 (Edge Cases):

java
@Test
@DisplayName("이메일이 null인 경우 예외 발생")
void createUser_WhenEmailIsNull_ThrowsException() {
    // Given
    CreateUserRequest request = UserFixtures.createUserRequest(null);

    // When & Then
    assertThatThrownBy(() -> userService.createUser(request))
        .isInstanceOf(IllegalArgumentException.class);
}

4단계: 검증 (Assertions)

AssertJ 사용 (권장):

java
// 객체 검증
assertThat(user).isNotNull();
assertThat(user.getId()).isEqualTo(1L);
assertThat(user.getEmail()).startsWith("test");

// 컬렉션 검증
assertThat(users).hasSize(3);
assertThat(users).extracting("email")
    .containsExactly("a@test.com", "b@test.com", "c@test.com");

// 예외 검증
assertThatThrownBy(() -> userService.createUser(null))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("null");

레이어별 테스트 전략

Service Layer 테스트

java
@ExtendWith(MockitoExtension.class)
@DisplayName("사용자 서비스 테스트")
class UserServiceTest {

    @InjectMocks
    private UserService userService;

    @Mock
    private UserRepository userRepository;

    @Test
    @DisplayName("사용자 ID로 조회")
    void findById_WhenUserExists_ReturnsUser() {
        // Given
        Long userId = 1L;
        User user = UserFixtures.user();
        given(userRepository.findById(userId)).willReturn(Optional.of(user));

        // When
        User result = userService.findById(userId);

        // Then
        assertThat(result).isEqualTo(user);
    }

    @Test
    @DisplayName("존재하지 않는 사용자 ID로 조회 시 예외")
    void findById_WhenUserNotExists_ThrowsException() {
        // Given
        Long userId = 999L;
        given(userRepository.findById(userId)).willReturn(Optional.empty());

        // When & Then
        assertThatThrownBy(() -> userService.findById(userId))
            .isInstanceOf(UserNotFoundException.class);
    }
}

Repository Layer 테스트

java
@DataJpaTest
@DisplayName("사용자 리포지토리 테스트")
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("이메일로 사용자 조회")
    void findByEmail_WhenUserExists_ReturnsUser() {
        // Given
        User user = UserFixtures.user();
        userRepository.save(user);

        // When
        Optional<User> result = userRepository.findByEmail(user.getEmail());

        // Then
        assertThat(result).isPresent();
        assertThat(result.get().getEmail()).isEqualTo(user.getEmail());
    }
}

Controller Layer 테스트

java
@WebMvcTest(UserController.class)
@DisplayName("사용자 컨트롤러 테스트")
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    @DisplayName("사용자 생성 API")
    void createUser_WithValidRequest_Returns201() throws Exception {
        // Given
        CreateUserRequest request = UserFixtures.createUserRequest();
        User user = UserFixtures.user();
        given(userService.createUser(any())).willReturn(user);

        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(user.getId()))
            .andExpect(jsonPath("$.email").value(user.getEmail()));
    }
}

체크리스트

테스트 작성 전

  • 테스트할 메서드의 책임과 동작을 명확히 이해했는가?
  • 필요한 Fixtures가 이미 존재하는가?
  • 정상 케이스, 예외 케이스, 경계 케이스를 파악했는가?

테스트 작성 중

  • Given-When-Then 구조를 따르는가?
  • 테스트 메서드명이 명확한가?
  • @DisplayName을 사용했는가?
  • Mock 객체를 적절히 사용했는가?
  • Fixtures를 재사용하고 있는가?

테스트 작성 후

  • 모든 테스트가 통과하는가?
  • 테스트 커버리지가 충분한가?
  • 테스트가 독립적으로 실행되는가?
  • 테스트가 빠르게 실행되는가?
  • 테스트 코드가 읽기 쉬운가?

주의사항

❌ 피해야 할 것

java
// 1. 여러 개의 동작을 한 테스트에서 검증
@Test
void multipleTests() {  // 나쁨
    userService.createUser(request);
    userService.updateUser(updateRequest);
    userService.deleteUser(userId);
}

// 2. 테스트 간 의존성
@Test
void test1() {
    sharedUser = userService.createUser(request);  // 나쁨
}

@Test
void test2() {
    userService.updateUser(sharedUser.getId());  // test1에 의존
}

// 3. 실제 외부 시스템 호출
@Test
void test() {
    emailService.sendRealEmail("test@example.com");  // 나쁨
}

✅ 권장 사항

java
// 1. 하나의 테스트에서 하나의 동작만 검증
@Test
void createUser_Test() {
    userService.createUser(request);
    verify(userRepository).save(any());
}

// 2. 독립적인 테스트
@Test
void test1() {
    User user = UserFixtures.user();  // 각 테스트마다 새로 생성
}

// 3. Mock 사용
@Test
void test() {
    given(emailService.sendEmail(any())).willReturn(true);  // Mock 사용
}

테스트 실행

bash
# 전체 테스트 실행
./gradlew test

# 특정 테스트 클래스 실행
./gradlew test --tests UserServiceTest

# 특정 테스트 메서드 실행
./gradlew test --tests UserServiceTest.createUser_WithValidInput_ReturnsCreatedUser

# 테스트 커버리지 확인
./gradlew test jacocoTestReport

참고 자료

Didn't find tool you were looking for?

Be as detailed as possible for better results