코알못

[TDD] SpringBoot 를 통한 TDD 손쉬운 작성 - 03) 테스트 코드 품질 높이기! - 01 본문

JAVA

[TDD] SpringBoot 를 통한 TDD 손쉬운 작성 - 03) 테스트 코드 품질 높이기! - 01

코린이s 2026. 3. 2. 08:15


실무에서 테스트 코드를 작성하다 보면 반복 되는 코드가 많거나 가독성이 떨어지는 경우가 많다.
이런 테스트 코드의 품질을 높이기 위한 방법이 있다!
아래 방법을 통해 개선 작업을 진행해보자!
1) assertThat().satisfies()
satisfies 를 사용하면 아래와 같은 경우 사용 가능하다.

  • 해당 여러 검증 코드를 하나로 묶을 때
  • 자유롭게 검증 코드를 작성하기 위해

예시를 통해 알아보자!
우선 간단한 회원 가입 로직을 만들어보자!
간단한 API 를 만들기 위해 필요한 라이브러리는 아래와 같다.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    compileOnly("org.projectlombok:lombok")
    runtimeOnly("com.h2database:h2")
    annotationProcessor("org.projectlombok:lombok")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

간단한 유저 엔티티를 정의 하고

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Getter
@Entity
public class Users {
    @Id
    String id;
    String name;
    String password;
}

Repository 를 만들자

@Repository
public interface UserRespository extends JpaRepository<Users, Integer> {
}

그리고 '/users' 를 post 로 호출시 데이터가 저장 될 수 있도록 controller 에 정의 한다. (예시기에 contoller 에 로직을 작성한다.)

@RestController
public class TestController {

    @Autowired
    UserRespository userRespository;

    @PostMapping("/users")
    public ResponseEntity<Users> user(@RequestBody Users users) {

        Users response = userRespository.save(users);

        return ResponseEntity.ok().body(response);
    }
}

 
이제 검증 로직을 작성해보자.
여러 검증 코드를 하나로 묶기전에 아래와 같은 코드가 될 수 있다.

@Test
    void 비밀번호검증(
        @Autowired TestRestTemplate client
    ) {
        // Arrange
        Users users = Users.builder()
            .name("코린이")
            .id("test")
            .password("test")
            .build();

        // Act
        ResponseEntity<Users> response = client.postForEntity("/users", users, Users.class);

        // Assert
        assertThat(response.getStatusCode().value()).isEqualTo(200);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getName()).isEqualTo("코린이");

    }

해당 코드를 가독성을 위해 같은 코드 끼리 묶어 보자!

@Test
    void 비밀번호검증(
        @Autowired TestRestTemplate client
    ) {
        // Arrange
        Users users = Users.builder()
            .name("코린이")
            .id("test")
            .password("test")
            .build();

        // Act
        ResponseEntity<Users> response = client.postForEntity("/users", users, Users.class);

        // Assert
        assertThat(response.getStatusCode().value()).isEqualTo(200);
        assertThat(response).satisfies(res -> {
            assertThat(res.getBody()).isNotNull();
            assertThat(res.getBody().getName()).isEqualTo("코린이");
        });
    }

상태코드 검증은 따로 두고, body 검증은 하나로 묶어서 표현했다.
이를 통해 상태코드는 200 이여야 하고 ,, body 내용 관련 정책은 이게 맞아야 하는구나 한눈에 알 수 있다.
예시는 복잡하지 않으니 크게 느껴지지 않으나 많은 정책이 들어간다고 하면 관련 있는것끼리 묶었을때 가독성이 훨씬 좋아 질 것이다.
satisfies 는 내부에 자유롭게 코드를 작성할 수 있으므로, 아래와 같이 검증 관련 메소드를 따로 빼서 사용 가능하다.

@Test
    void 비밀번호검증(
        @Autowired TestRestTemplate client
    ) {
        // Arrange
        Users users = Users.builder()
            .name("코린이")
            .id("test")
            .password("test")
            .build();

        // Act
        ResponseEntity<Users> response = client.postForEntity("/users", users, Users.class);

        // Assert
        assertThat(response.getStatusCode().value()).isEqualTo(200);
        assertThat(response.getBody()).satisfies(this::assertName);
        assertThat(response.getBody()).satisfies(this::assertPassword);
    }

    private void assertName(Users users) {
        assertThat(users.getName()).isNotNull();
        assertThat(users.getName()).isEqualTo("코린이");
    }
    
    private void assertPassword(Users users) {
        assertThat(users.getPassword()).isNotNull();
        assertThat(users.getPassword()).matches( p -> p.length() >= 4);
    }

보기 좋게 칼럼 별 검증 로직은 따로 메소드로 빼서 표현하게 된다면 이게 어떤 코드구나 네이밍으로 정리되어 가독성이 좋다.
2) 테스트 클래스 어노테이션 추상화
이제 어노테이션으로 테스트 클래스의 반복 되는 부분을 줄여 보자!
우리는 보통 테스트 코드를 작성할때,
테스트로 돌아가기 위한 @SpringBootTest 정의,
테스트 코드에서 repository 로 데이터 조작시 원복을 위한 @Transactional,
특정 환경으로 돌아가게 하기 위한 @ActiveProfiles 를 작성한다.

@Transactional
@SpringBootTest(
    classes = CommerceApiApp.class,
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@ActiveProfiles("test")

이러한 코드는 테스트 코드를 작성할때마다 필요하므로 해당 코드를 어노테이션으로 만들어 재사용 해보자!
어노테이션은 @Retention 과 @Target 어노테이션으로 만들수 있다.
Retention 은 언제까지 해당 정보를 유지 할지 결정 하는것으로
Source 는 컴파일 후 사라지고, Class 는 class 파일로 존재, Runtion 으로 하면 실행중에도 유지 되도록 할 수 있다.
여기서는 실행중에도 동작 해야 하므로 Runtime 으로 설정 한다.
다른 경우는 사실 어떤 경우에 사용하는지 감이 안잡힌다.. 나중에 어노테이션을 만들 경우가 있을경우 사용하면서 익히도록 해보자.
그리고 @Target 의 경우 이 어노테이션을 어디에 붙일지 이다.
- 클래스면 type
- 메서드 : method
- field : 필드
- parameter : 파라미터
우리는 테스트 클래스 상단에 붙일 예정이니 type으로 지정 한다.
이제 위에 결정한대로 아래와 같이 UserServiceTest 어노테이션을 만들어 보자!

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Transactional
@SpringBootTest(
    classes = CommerceApiApp.class,
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@ActiveProfiles("test")
public @interface UserServiceTest {
}

이제 Test 클래스에 가서 @UserServiceTest 어노테이션을 붙여 본다.

@UserServiceTest
@DisplayName("POST /user")
public class POST_specs {

실행해보면 정상적으로 코드가 돌아가는것을 볼  수 있다.

3) @MethodSource
이제 우리가 전 시간에 배운 @ParameterizedTest 와 @ValueSource 를 보자!

@ParameterizedTest
    @ValueSource(strings = {
        "1243",
        "*&^%",
        "test1234"
    })

@ValueSource 에 테스트 변수를 여러 곳에서 사용한다면 한번에 해당 소스를 정의해두고 재 사용하면 코드가 간단해진다.
그렇게 처리 할 수 있는것이 MethodSource 이다!
예시를 통해 알아보자!
@ValuseSource 에 있던 패스워드를 아래와 같이 UserSource 라는 클래스를 만들어
VaildPasswords 라는 static 메서드를 만든다. (@methodSource 에는 static 만 허용 한다.)

public class UserSource {
    static Stream<Arguments> validPasswords () {
        return Stream.of(
            Arguments.of("1243", false),
            Arguments.of("*&^%", false),
            Arguments.of("test1234", true)
        );
    }
}

첫번째 매개 변수에는 패스워드를 넣고 두번째는 기대하는 검증 결과를 적는다.
이제 테스트 케이스로 가서 아래와 같이 경로를 적고 # 뒤에 메소드 명을 적어두고, 매개 변수로 정의한 값이 넘어 가도록 password 와 expected 를 받는다.

    @ParameterizedTest
    @MethodSource("test.commerce.sources.UserSource#validPasswords")
    void id_잘못된파라미터_400_리턴(String password, boolean expected, @Autowired TestRestTemplate client) {

이제 테스트 코드를 실행하면 아래와 같이 2개의 파라미터가 정상적으로 넘어가는것을 볼 수 있다.

해당 방식을 쓰면 여러 테스트 코드에서 동일하게 사용하는 케이스를 미리 source 로 정의하여
반복하게 사용하는곳에 재 사용하며 코드를 간결하게 작성 할 수 있다.

728x90
Comments