[Spring Boot] 테스트 코드?

mingTato
9 min readNov 25, 2023

--

1. 왜 테스트 코드를 작성해야 하는가?

테스트 코드를 작성하기 전에는 왜 테스트 코드를 작성해야 하는지를 먼저 이해해야 합니다. 이해하지 않는다면, 테스트코드를 작성하는 일이 귀찮기 때문에 작성하지 않을 것 입니다.

1.1 궁극적 목표는 테스트

코드를 작성하는 목적은 잘 작동하고 깔끔한 코드를 얻기 위함입니다. 테스트를 한다는건, 코드가 정상적으로 작동하는지 검사하는 방법 중 하나입니다. 테스트 코드를 작성한다면, 코드를 테스트하기 쉽게 만들 수 있습니다. 이를 통해 테스트가 용이해지며, 그 과정에서 코드도 자연스럽게 깔끔 해질 수 있습니다.

1.2 시간의 단축

테스트 코드를 작성하는 것이 귀찮고 오랜 시간이 걸릴 것이라고 생각할 수 있지만, 실제로는 시간을 단축할 수 있는 경우가 많습니다. 테스트 코드를 작성하지 않고 코드의 작동을 검증하려 한다면 결국에는 비용이 많이 드는 문제가 발생할 수 있습니다. 테스트 코드는 개발 초기에 작성하면서 버그를 미리 발견하고 수정함으로써 나중에 발생할 수 있는 큰 문제를 예방하고, 시간을 절약할 수 있습니다.

1.3 장점

위에서 비교를 통해 알 수 있듯, 테스트코드를 통한 장점은 다음과 같습니다.

  1. 서버를 실행하는 등의 시간을 절약할 수 있다.
  2. 필요한 데이터를 미리 기입하고, 테스트가 끝나고 정리하는 등의 행동을 하지 않아도 된다.
  3. 단위테스트의 경우 수십ms 이기 때문에 테스트가 매우 빠르다.
  4. 문서로서의 역할이 가능하다. -> 테스트 코드는, 개발자가 작성한 메소드가 어떻게 동작했으면, 어떤 결과를 반환했으면, 하는 것을 작성한 것이기 때문에 처음 코드를 보는 개발자들이 테스트 코드를 통해서, 코드의 동작을 조금더 수월하게 이해할 수 있습니다.
  5. 깔끔한 인터페이스를 얻어낼 수 있다.

2. 스프링부트의 테스트 코드?

테스트 코드는 test 디렉터리에서 작업합니다. 프로젝트에 이미 test 디렉터리가 있어서 거기서 작업 해주었습니다. 테스트 코드를 작성하기 이전에, 테스트 코드에도 다양한 패턴이 있습니다. 그중 제가 사용할 패턴은 given-when-then 패턴인데요. 알아보고 넘어가겠습니다.

2.1 given-when-then 패턴

given-when-then 패턴은 테스트 코드를 세 단계로 구분해 작성하는 방식을 말합니다.

  1. given은 테스트 실행을 준비하는 단계
  2. when은 테스트를 진행하는 단계
  3. then은 테스트 결과를 검증하는 단계

따라서 Given-When-Then = [준비 — 실행- 검증] 입니다. 테스트 코드를 작성 시에 준비/실행/검증 의 세 부분으로 나누기만 하면 됩니다.

예를 들어 새로운 메뉴를 저장하는 코드를 테스트한다고 가정 했을 때, 테스트 코드를 다음과 같이 작성 할 수 있습니다.

@DisplayName("새로운 메뉴를 저장한다.")
@Test
public void saveMenuTest() {
// given : 메뉴를 저장하기 위한 준비 과정
Menu americano = Menu.builder()

// when : 실제로 메뉴를 저장
final long savedId = menuService.save(americano);

// then : 메뉴가 잘 추가되었는지 검증
final Menu savedMenu = menuService.findById(savedId).get();
assertThat(savedMenu.getName()).isEqualTo(name);
assertThat(savedMenu.getPrice()).isEqualTo(price);
} .name("아메리카노")
.price(2000 .build();

2.2 테스트 코드 작성

우선 프로젝트엔, 다음과 같이 user가 만들어져 있습니다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(updatable = false)
private Long id; //TODO: 타입 int로 변경

@Column(length = 50, unique = true)
@NotNull
private String email;

@Column(length = 100)
@NotNull
private String password;

@Column(length = 50)
@NotNull
private String username;

@Column(length = 100)
private String profileUrl;

@Column(length = 100)
private String address;

또한, 테스트 하고 싶은 함수는 다음과 같습니다.

@Transactional(readOnly = true)
public UserInfoResponse getUserInfo(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
return UserInfoResponse.builder()
.id(user.getId())
.email(user.getEmail())
.username(user.getUsername())
.image(user.getProfileUrl())
.address(user.getAddress())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();
}

테스트 코드의 작성은 given-when-then 패턴을 이용하겠습니다.

  1. 테스트 대상 행위를 정합니다. (User 객체에 라는 getUserInfo()메소드에 매개변수로 Long 타입의 userId을 전달하여 호출하면)

2. 기대하는 결과를 작성합니다. (userId가 같은 user정보를 반환해야합니다.)

3. 두 문장을 결합해 테스트 코드로 작성한다.

위 3가지 순서를 지키면 다음과 같은 코드로 작성할 수 있습니다.

@Test
public void testGetUserInfo() {
Long userId = 1L;
User user = User.builder()
.id(userId)
.email("test@example.com")
.username("testuser")
.profileUrl("profile.jpg")
.address("Test Address")
.build();
when(userRepository.findById(userId)).thenReturn(Optional.of(user));

UserInfoResponse userInfoResponse = userService.getUserInfo(userId);

assertEquals(userId, userInfoResponse.getId());
assertEquals("test@example.com", userInfoResponse.getEmail());
assertEquals("testuser", userInfoResponse.getUsername());
assertEquals("profile.jpg", userInfoResponse.getImage());
assertEquals("Test Address", userInfoResponse.getAddress());
assertNotNull(userInfoResponse.getCreatedAt());
assertNotNull(userInfoResponse.getUpdatedAt());
}

테스트 코드를 작성은 내가 작성한 코드(메서드)가 어떤식으로 동작하기를 원하는 지를 코드로 표현하는 것입니다.

테스트 코드를 작성함에 있어 신경 써야하는 부분은 수 많은 예외 상황에 대한 테스트 코드를 생각하고, 테스트 커버리지를 높이고, 테스트 코드의 가독성을 높이고, 아래 설명드릴 FIRST원칙을 지키며, 남들에게 도움이 되는 테스트코드를 작성하는 것 이라고 생각합니다.

3. 테스트 기본 원칙?

공부를 하다보니 테스트 기본 원칙이라는 것들이 있었습니다. 다음 내용은 이를 소개하고자 합니다.

3.1 일곱 테스팅 원칙(Seven Testing Principles)

각 항목에 대한 자세한 내용은 Seven Testing Principles 문서를 참고했습니다.

  • 테스팅은 결함의 존재를 보여주는 것이다.
  • 완벽한 테스트는 불가능하다.
  • 테스트 구성은 가능한 빠르게 시작한다.
  • 결함은 군집되어 있다.
  • 살충제 역설(Pesticide Paradox): 비슷한 테스트가 반복되면 새로운 결함을 발견할 수 없다.
  • 테스팅은 문맥에 의존적이다.
  • 오류 부재의 궤변: 사용되지 않는 시스템이나 사용자의 기대에 부응하지 않는 기능의 결함을 찾고 수정하는 것은 의미가 없다.

3.2 F.I.R.S.T 원칙

유닛 테스트를 구성하기 위해서 F.I.R.S.T 원칙을 따릅니다. 각 항목에 대한 자세한 내용은 F.I.R.S.T Principles 문서를 참고했습니다.

  • Fast: 유닛 테스트는 빨라야 한다. — 단위 테스트는가능한 빠르게 실행되어야 합니다. 실행함에 있어 너무 느려 테스트 실행을 꺼리게 된다면 잘못된 단위테스트입니다.
  • Isolated: 다른 테스트에 종속적인 테스트는 절대로 작성하지 않는다. — 실제로 테스트를 돌리면, 소스코드의 순서대로 동작하는게 아니라 자기 마음대로 동작하는데 이 순서를 지정해서 돌리라는 뜻입니다.
  • Repeatable: 테스트는 실행할 때마다 같은 결과를 만들어야 한다. — 만약 결과가 다르다면, 외부 요소가 테스트에 개입했기 때문에 발생하는 것일 가능성이 높습니다.
  • Self-validating: 테스트는 스스로 결과물이 옳은지 그른지 판단할 수 있어야 한다. — 쉽게 설명하면 테스트 코드를 명확하게 짜라는 뜻입니다.
  • Timely: 유닛 테스트는 프로덕션 코드가 테스트를 성공하기 직전에 구성되어야 한다. 테스트 드리븐 개발(TDD) 방법론에 적합한 원칙이지만 실제로 적용되지 않는 경우도 있다. — 원래 개발하듯이 소스코드 다 짜놓고 나중에 테스트 코드를 짜는게 아니고 테스트 코드와 개발 소스코드를 함께 짜라는 뜻이 됩니다.
    그렇게 테스트 코드가 먼저가 되는 작업 방식을 TDD(Test-Driven Development)라고 부른다.

3.3 유닛 테스트

유닛 테스트는 아래 기본 가이드라인 항목에 따라 작성합니다.

  • 퍼블릭 메소드를 테스트한다.
  • 테스트 결과에 영향을 미치는 디펜던시 객체는 모킹한다.
  • 디스크 관련 디펜던시는 가능한 사용을 피한다.
  • 네트워크 관련 디펜던시는 사용하지 않는다.

🌦️프로젝트 소개글

--

--