코알못

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

JAVA

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

코린이s 2026. 3. 8. 15:25

저번 시간에 이어서 테스트 코드 품질을 개선하는 방법에 대해서 알아보자!

이번 시간에는 아래 방법에 대해서 예시를 통해 하나씩 보도록 한다.

  • TestRestTemplate.postForObject
  • TestRestTemplate.exchange
  • AssertJ.isCloseTo
  • @DirtiesContext
  • TestFixture
  • TestDSL

우리는 TestRestTemplate.postForEntity 를 이전 시간에 사용했다.

그러나 우리가 응답의 헤더나 상태코드가 필요 없고 응답 데이터만 필요할 때가 있다.

그럴때 사용할 수 있는 메서드는 postForObject 이다!

아래와 같이 예제 코드를 작성해보자.

Users users = Users.builder()
            .name("코린이")
            .id("test")
            .password(password)
            .build();
            
Users response = client
            .postForObject("/users", users, Users.class);
System.out.println(response.getName());

그러면 바로 응답 데이터인 Users로 받게 되어 아래와 같이 getBody 를 호출하지 않아도 바로 받아 처리 할 수 있어

간단한 요청 테스트에 사용할 수 있다.

Users users = response.getBody();


다음으로는 TestRestTemplate.exchange 에 대해서 알아보자!

exchange() 는 HTTP Header, Method, Entity 등을 모두 지정할 수 있는 메서드이다.

기존에 사용하던 postForEntity 나 postForObject 의 경우 따로 지정 할 수 없었다.
그래서 원하는 대로 지정해서 사용하고자 하면 exchange를 사용하면 된다!

아래 예시와 같이 Header 과 요청값을 정의하고 exchange 에 넣는다.

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer token");

Users users = Users.builder()
            .name("코린이")
            .id("test")
            .password(password)
            .build();
HttpEntity<Users> request = new HttpEntity<>(users, headers);

ResponseEntity<Users> response = client.exchange(
    "/users",
    HttpMethod.POST,
    request,
    Users.class
);

System.out.println(res01.getBody().getName());

실행해보면 아래와 같이 정상적으로 출력됨을 확인 할 수 있다.


그리고 검증 기능 하나 더 알아 보자면..

DB에 저장되는 시간을 검증할 때는 현재 시각과 약간의 차이가 발생할 수 있다.

따라서 정확히 같은 시간을 비교하면 테스트가 실패할 수 있다.

이때 AssertJ의 isCloseTo 를 사용하면 된다.

우선 우리가 regDate 라는 칼럼을 생성하지 않았으므로 아래와 같이 생성한다.

@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Getter
@Entity
public class Users {

    @Id
    String userId;
    String name;
    String password;
    @CreatedDate
    LocalDateTime regDate;
}

@EntityListeners 가 자동으로 @CreatedDate 가 붙은 컬럼에 생성일을 추가하여 Insert 쿼리를 날린다.

참고 사항으로 만약 Spring 이 아니라 DB에서 자동 생성 되도록 하려면 아래와 같이 수정한다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Getter
@Entity
public class Users {

    @Id
    String userId;
    String name;
    String password;
    @Column(insertable = false, updatable = false)
    LocalDateTime regDate;
}

@Column(insertable = false, updatable = false) 을 지정하여 업데이트와 임의로 데이터 추가 안되도록 정의하고

@CreateDate 와 @EntityListeners 를 제거 한다.

그리고 DB 의 regDate 칼럼 생성시 아래와 같이 Default 값 자동으로 들어가도록 설정 해야 한다.

reg_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP

이제 테스트 코드에 Regdate 값을 isCloseTo 로 비교하는 문을 넣는다.

@Test
void 유저_생성(){
    // Arrange
    LocalDateTime now = LocalDateTime.now();
    Users users = UserFixture.createUser();

    // Act
    ResponseEntity<Users> response = userDSL.회원생성(users);

    // Assert
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getBody()).isNotNull();
    assertThat(response.getBody()).isNotNull();
    assertThat(response.getBody().getRegDate()).isCloseTo(now, within(1, ChronoUnit.SECONDS));
}

해당 코드는 상단에 테스트 코드 실행시 현재 시간을 저장한 now 와, DB의 regdate 값의 1s 이내 차이 까지는

허용 하겠다는 의미로 코드 실행 시점부터 instert 까지 ms 정도 차이가 있을수 있으니

1s 의 차이는 테스트 통과로 지정한다!

그럼 아래와 같이 테스트 코드가 통과하는것을 볼 수 있다.


그리고 만약 테스트 실행중에 같이 사용하는 클래스가 서로 영향을 주어 실패한다고 하면..

아래와 같이 @DirtiesContext 를 테스트 코드 위에 정의 한다!

@SpringBootTest
@DirtiesContext
class ServiceTest {
...}

 

스프링에서 bean 생성은 싱글톤 이라서 컨테이너에 빈 하나만 생성되고 계속적으로 사용된다.

그러므로 테스트마다 다음 테스트에 영향을 주면 안되는 경우에 @DistiesContext 를 정의하여

매번 빈을 생성하여 테스트 끼리 공유 되지 않도록 할 수 있다!


이제 테스트 픽스처(Test Fixture)에 대해서 알아보자!

테스트 픽스처(Test Fixture) 는 테스트를 수행하기 위해 미리 준비해두는 데이터나 객체를 의미한다.

테스트에서 반복적으로 사용하는 객체 생성 코드를 한 곳에 모아서 관리하면 테스트 코드의 가독성이 좋아진다.

예시로 알아보자!

우리는 테스트 진행할때 테스트 할 더미 유저를 많이 생성했다.

이를 미리 준비해두고 사용하는것이 더 좋을것 같아 유저 생성하는 메서드를 테스트 픽스처로 만들어 보도록 한다!

유저 픽스쳐를 만들고 static 으로 유저 생성하는 메소드를 만든다.

package test.commerce.fixture;

import commerce.command.Users;

public class UserFixture {

    public static Users createUser() {
        return Users.builder()
            .name("코린이")
            .id("test")
            .password("test")
            .build();
    }
}

아래 코드는 이전 시간에 만든 테스트 코드 이다. 

@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);
}

 

유저 생성하는 코드를 테스트 픽스쳐로 변경하면 코드 가독성 향상이 되고

나중에 유저 생성하는 테스트 케이스가 있다면 중복 정의 하지 않고 재사용 가능하다.

@Test
void 비밀번호검증(
    @Autowired TestRestTemplate client
) {
    // Arrange
    Users users = UserFixture.createUser();

    // 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);
}

매번 사용하는 코드를 미리 준비하는것이 TestFixture 라는것을 알아봤다.


이제 알아볼것은 Test DSL 이다.

Test DSL 은 Test Domain Spectific Language 로 영어 그대로 해석해 보면 도메인에 맞게 만든 언어 라는 의미 이다.

즉, 테스트 코드를 읽기 쉽게 만드는것으로 '테스트 코드의 가독선 개선'을 위해 만들어진 개념이다.

유저 생성 예시를 통해 알아보자!

@Test
void 유저_생성(
    @Autowired TestRestTemplate client
) {
    // Arrange
    Users users = UserFixture.createUser();

    // 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);
}

 Act 부분의 유저 생성 부분을 직관적으로 알 수 있게 DSL 로 만들어 보자

그럼 UserDSL 을 먼저 생성하여 유저 생성 메서드를 만든다.

package test.commerce.dsl;

import commerce.command.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

@Component
public class UserDSL {

    @Autowired
    TestRestTemplate testRestTemplate;

    public ResponseEntity<Users> 회원생성(Users users) {
        return testRestTemplate.postForEntity("/users", users, Users.class);
    }
}

 그리고 해당 DSL 을 사용하기 위해서는 해당 빈을 스캔해줘야하는데

우리 실습 같은 경우에는 @SpringTest 에 스캔할 클래스를 지정했다.

그러므로 UserDSL 도 스캔해야하니 지정하도록 한다.

@SpringBootTest(
    classes = {CommerceApiApp.class, UserDSL.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)

그리고 테스트 코드에 회원 생성하는 부분을 DSL 로 변경한다.

@Test
void 유저_생성(
@Autowired UserDSL userDSL){
    // Arrange
    Users users = UserFixture.createUser();

    // Act
    ResponseEntity<Users> response = userDSL.회원생성(users);
    
    // Assert
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getBody()).satisfies(this::assertName);
    assertThat(response.getBody()).satisfies(this::assertPassword);
}

 

이렇게 변경하면 행위에 대한 부분을 알기 쉽게 표현하여 가독성이 좋아진다.

이처럼 데이터를 미리 셋팅하고 준비하는것이 TestFixture 라고 하면,

테스트 코드 케이스 중 행위에 대한 반복이 이뤄 지게 되는데, 이를 누구나 알기 쉽게 정의 해두는것은 Test DSL 이라고 한다.

두개 개념이 비슷해서 헷갈릴 수 있다.  몇가지 더 예시를 통해 알아보자!

우선 구조는 아래와 같이 보통 두게 되며

핵심은 데이터 준비는 Fixture, 행동은 DSL, 검증은 테스트 본문에 두는 것이다.

src
 └─ test
     └─ java
         ├─ fixture
         │   ├─ UserFixture
         │   └─ OrderFixture
         │
         ├─ dsl
         │   ├─ UserApiDsl
         │   └─ OrderApiDsl
         │
         └─ user
             └─ UserApiTest

유저 생성 관련해서 Fixture 를 아래와 같이 둔다. 보면 데이터 준비 관련 내용이다.

public class UserFixture {

    public static UserCreateRequest 회원가입요청내용() {
        return new UserCreateRequest(
                "tester",
                "tester@test.com",
                "password123"
        );
    }

}
public class OrderFixture {

    public static OrderCreateRequest 주문요청내용(Long userId) {
        return new OrderCreateRequest(
                userId,
                "상품1",
                2
        );
    }
}

DSL의 경우 테스트에 대한 행위를 둔다.

@Component
public class UserApiDsl {

    private final TestRestTemplate restTemplate;

    public UserApiDsl(TestRestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public UserResponse 회원가입(UserCreateRequest request) {
        return restTemplate.postForObject(
                "/users",
                request,
                UserResponse.class
        );
    }
}
@Component
public class OrderApiDsl {

    private final TestRestTemplate restTemplate;

    public OrderApiDsl(TestRestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public OrderResponse 주문생성(OrderCreateRequest request) {
        return restTemplate.postForObject(
                "/orders",
                request,
                OrderResponse.class
        );
    }
}

그리고 테스트 코드를 아래와 같이 작성 할 수 있다.

@Test
void 주문_생성시_상품명_정상리턴() {
	
    UserCreateRequest request = UserFixture.회원가입요청내용();
    
    UserResponse user = userApiDsl.회원가입(request);

    OrderCreateRequest orderRequest =
            OrderFixture.주문요청내용(user.getId());

    OrderResponse order =
            orderApiDsl.주문생성(orderRequest);

    assertThat(order.getProductName()).isEqualTo("상품1");
}

가독성 좋은 코드가 되며, 대상 데이터 준비는 Fixture, 행위 관련 내용은 DSL 에 정의 되어 있는것을 알 수 있다.

그리고 테스트를 하다 보면 다양한 입력값이 필요하다.
이때 매번 값을 직접 만드는 것은 번거롭기 때문에 임의 값 생성 클래스를 만들어 사용하는 것이 좋다.

패스워드와 이름을 자동 생성하는 제너레이터를 만든다.

package test.commerce.ganerator;

import java.util.UUID;

public class DataGenerator {
    public static String randomPassword() {
        return UUID.randomUUID() + "Password!";
    }

    public static String randomName() {
        return "user-" + UUID.randomUUID();
    }
}

 그리고 Fixture 에 매개변수를 받으면 해당 매개변수로 생성, 그렇지 않으면 랜덤 생성하는 메서드를 만든다.

package test.commerce.fixture;

import commerce.command.Users;
import test.commerce.ganerator.DataGenerator;

public class UserFixture {

    public static Users createUser() {
        return Users.builder()
            .name(DataGenerator.randomName())
            .id("test")
            .password(DataGenerator.randomPassword())
            .build();
    }

    public static Users createUser(String name, String password) {
        return Users.builder()
            .name(name)
            .id("test")
            .password(password)
            .build();
    }
}

 

이제 테스트 코드를 아래와 같이 돌리면

@Test
void 유저_생성(){
    // Arrange
    Users users = UserFixture.createUser();

    // Act
    ResponseEntity<Users> response = userDSL.회원생성(users);

    // Assert
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getBody()).isNotNull();
    assertThat(response.getBody()).isNotNull();
}

정상으로 돌아가는것을 볼 수 있다.

끝!

728x90
Comments