아래는 **4장. 테스트 유틸리티와 도구**에 대한 글입니다. 이 장은 스프링 부트 테스트를 더 효율적이고 가독성 있게 작성할 수 있도록 돕는 유틸리티와 도구를 소개하며, 실습 예제를 포함해 실무에서의 활용성을 강조했습니다.

---

## 4장. 테스트 유틸리티와 도구

테스트 코드는 단순히 동작을 검증하는 것을 넘어, 가독성 있고 유지보수하기 쉬워야 합니다. 스프링 부트에서는 이를 지원하는 다양한 유틸리티와 도구가 제공됩니다. 이 장에서는 AssertJ와 Hamcrest로 검증을 강화하고, 스프링 REST Docs로 API 문서를 생성하며, 테스트 코드 리팩토링 팁을 다룹니다.

### 4.1 AssertJ로 깔끔한 검증 작성

AssertJ는 JUnit의 기본 `assert` 메서드보다 더 유연하고 가독성 높은 검증을 제공하는 라이브러리입니다. `spring-boot-starter-test`에 기본 포함되어 있어 별도 설정 없이 바로 사용할 수 있습니다. AssertJ의 장점은 메서드 체이닝으로 자연스러운 문장을 만들고, 풍부한 검증 옵션을 제공한다는 점입니다.

예를 들어, 기본 JUnit 검증과 AssertJ를 비교해 봅시다:

```java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.assertj.core.api.Assertions.assertThat;

class AssertJExampleTest {

    @Test
    void basicJUnitAssertion() {
        String result = "Hello, World!";
        assertEquals("Hello, World!", result); // JUnit 기본 방식
    }

    @Test
    void assertJAssertion() {
        String result = "Hello, World!";
        assertThat(result)
                .isNotNull()
                .startsWith("Hello")
                .contains("World")
                .hasLength(13); // AssertJ 방식
    }
}
```

AssertJ는 리스트, 맵, 객체 등 복잡한 데이터 구조도 쉽게 검증할 수 있습니다:

```java
@Test
void assertJListTest() {
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    assertThat(names)
            .hasSize(3)
            .containsExactly("Alice", "Bob", "Charlie")
            .doesNotContain("David");
}
```

AssertJ를 사용하면 실패 메시지도 더 자세히 제공되어 디버깅이 쉬워집니다.

### 4.2 Hamcrest를 활용한 유연한 매칭

Hamcrest는 테스트에서 조건을 더 유연하고 읽기 쉽게 표현할 수 있는 매처(matcher)를 제공합니다. AssertJ와 비슷한 역할을 하지만, 문법적으로 다르며 특정 상황에서 더 직관적일 수 있습니다. `spring-boot-starter-test`에도 포함되어 있습니다.

Hamcrest의 기본 사용법은 다음과 같습니다:

```java
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

class HamcrestExampleTest {

    @Test
    void hamcrestBasicTest() {
        String result = "Hello, World!";
        assertThat(result, is("Hello, World!")); // 기본 매칭
        assertThat(result, startsWith("Hello"));
        assertThat(result, containsString("World"));
    }

    @Test
    void hamcrestCollectionTest() {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
        assertThat(numbers, hasSize(4));
        assertThat(numbers, hasItem(greaterThan(2)));
        assertThat(numbers, everyItem(lessThan(5)));
    }
}
```

Hamcrest는 특히 컬렉션이나 복잡한 조건을 테스트할 때 유용하며, AssertJ와 함께 사용해도 무방합니다. 상황에 따라 두 도구를 혼합해 가독성을 높일 수 있습니다.

### 4.3 스프링 REST Docs로 API 문서화

스프링 REST Docs는 테스트를 기반으로 REST API 문서를 자동 생성하는 도구입니다. 기존 Swagger와 달리 테스트가 실패하면 문서도 갱신되지 않으므로, 항상 정확한 문서를 유지할 수 있습니다. 설정은 약간 복잡하지만, 품질 높은 문서를 위해 투자할 가치가 있습니다.

의존성을 추가합니다:

```xml
<dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <scope>test</scope>
</dependency>
```

테스트 예제:

```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
class UserControllerRestDocsTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void getUserName_documentation() throws Exception {
        mockMvc.perform(get("/users/1/name"))
                .andExpect(status().isOk())
                .andDo(document("user-get-name",
                        responseFields(
                                fieldWithPath("name").description("The name of the user")
                        )));
    }
}
```

위 코드는 `target/snippets` 폴더에 문서 조각을 생성하며, 이를 Asciidoctor로 통합해 HTML 문서로 변환할 수 있습니다. 예를 들어, `pom.xml`에 빌드 플러그인을 추가해 문서를 생성합니다:

```xml
<plugin>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId>
    <version>2.2.1</version>
    <executions>
        <execution>
            <id>generate-docs</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>process-asciidoc</goal>
            </goals>
            <configuration>
                <sourceDirectory>src/docs/asciidoc</sourceDirectory>
                <attributes>
                    <snippets>${project.build.directory}/snippets</snippets>
                </attributes>
            </configuration>
        </execution>
    </executions>
</plugin>
```

### 4.4 테스트 코드 리팩토링 팁

좋은 테스트 코드는 명확하고 중복이 적어야 합니다. 다음은 리팩토링 팁입니다:

- **공통 설정 분리**: `@BeforeEach`나 별도의 유틸리티 클래스를 활용해 반복 코드를 줄입니다.
  ```java
  @BeforeEach
  void setUp() {
      userRepository.save(new User(1L, "Alice"));
  }
  ```
- **테스트 메서드 이름 명확히**: `getUserName_withExistingUser_returnsName`처럼 의도를 드러내게 작성합니다.
- **작은 테스트 유지**: 한 테스트가 너무 많은 것을 검증하지 않도록 단일 책임 원칙을 따릅니다.
- **커스텀 Matcher/Assertion 사용**: AssertJ나 Hamcrest로 자주 사용하는 검증 로직을 재사용 가능하게 만듭니다.

예를 들어, 커스텀 AssertJ assertion:
```java
public class UserAssertions {
    public static AbstractStringAssert<?> assertThatUserName(String actual) {
        return assertThat(actual).isNotNull().isNotEmpty();
    }
}
```

사용:
```java
@Test
void testUserName() {
    String name = userService.getUserName(1L);
    UserAssertions.assertThatUserName(name).isEqualTo("Alice");
}
```

---

### 마무리

AssertJ와 Hamcrest는 테스트 검증을 풍부하게 하고, 스프링 REST Docs는 API 문서화를 혁신하며, 리팩토링은 테스트 코드의 품질을 높입니다. 이러한 도구와 기법을 활용하면 테스트가 단순한 검증을 넘어 팀과 프로젝트에 실질적인 가치를 더할 수 있습니다. 다음 장에서는 테스트 데이터 관리 전략을 다루며, 통합 테스트를 더 깊이 탐구해 보겠습니다!

---

이 글은 도구별 특징과 실습 예제를 중심으로 작성되었으며, 실무에서의 활용성을 높였습니다. 추가 요청이 있다면 말씀해 주세요!