
테스트 코드에는 일련의 로직을 넣지 말자
ds_chanin
·2025. 2. 23. 02:19
들어가며
테스트 코드의 미덕은 무엇일까~
프로덕션 코드의 안정성을 비롯해 여러가지 장점이 있겠지만
프로덕션 코드가 어떻게 동작하는지 설명하는 문서로써의 역할도 톡톡히 한다고 볼 수 있다.
다시말해 누군가가 읽어야 하는 대상이라는 말이다.
누군가가 읽어야 한다면 읽는데 부담이 없어야 하니 이해를 방해하는 요소는 적으면 적을수록 좋다.
그런데 테스트 코드를 작성하다보면 유혹에 넘어가 이해를 방해하는 요소를 작성하는 경우가 생기곤 한다.
물론 코드를 작성하는 당시 '나'는 해당 코드를 잘 이해할수 있을지 모르지만
한달이 지나고 두달이 지난뒤 그 코드를 다시 읽는 '나'는 이전의 '나'와 거의 다른사람이다.
결국 내가 작성한 코드도 쉽게 이해하지 못 할 수 있다.
여기서 이해를 방해하는 요소는 테스트 코드에 일련의 로직을 첨가하는 경우라고 보고
이야기를 나누어 보고자 한다~
배드 케이스 1
아래와 같은 RacingCar라는 객체가 있다고 하자
RacingCar의 정책은 다음과 같다.
- 임의의 숫자를 입력받아 숫자의 크기에 따라 움직임에 변화가 생긴다.
- 입력받은 숫자의 크기가 4보다 크면 전진하고 그렇지 않으면 전진하지 않는다.
public class RacingCar {
private int position = 0; // 초기 위치는 0
// 자동차가 주어진 숫자에 따라 이동하는 메서드
public void move(int number) {
if (number >= 4) {
position++; // 4 이상이면 전진
}
}
// 현재 위치 반환
public int getPosition() {
return position;
}
}
상수 표현을 해주는게 좋지만 지금 이야기하고자 하는 부분이 아니니 넘어가자
그리고 이를 검증하기 위한 테스트 코드를 아래와 같이 작성하면 좋지 않다.
class RacingCarTest {
@Test
void testCarMovementWithIfElse() {
RacingCar car = new RacingCar();
int randomNumber = generateRandomNumber(); // 가정된 랜덤 숫자 생성
car.move(randomNumber);
if (randomNumber >= 4) {
assertThat(car.getPosition()).isEqualTo(1); // 이동해야 함
} else {
assertThat(car.getPosition()).isEqualTo(0); // 이동하지 않아야 함
}
}
// 랜덤 숫자 생성 메서드 (테스트에서는 비효율적)
private int generateRandomNumber() {
return (int) (Math.random() * 10); // 0 ~ 9 랜덤 숫자 반환
}
}
위 코드는 다음과 같은 문제가 있다.
- if-else로 인해 테스트가 어떠한 케이스를 검증하려는 것인지 명확하지 않다.
- randomNumber라는 불확실한 값이 생성되고 검증함에 따라 특정 케이스가 실패하는지 아닌지 알 수 없다.
- 테스트 결과가 확정적이지 않아 재현이 된다 보장할 수 없다.
- 유지보수 포인트가 증가하였다.
테스트 코드도 운영코드 못지않게 유지보수의 대상이다.
유지보수가 쉬우려면 코드는 간결하게 표현되어야 한다.
따라서 이를 고치는 방법에는 두가지 방법이 있을것 같다.
해피 케이스 1-1
명확하게 두개의 케이스로 나누어 표현하는것이 가장 직관적인 방법이 될 수 있다.
class RacingCarTest {
@Test
void shouldMoveWhenRandomNumberIsFourOrGreater() {
RacingCar car = new RacingCar();
car.move(4); // 4 이상일 때 이동해야 함
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void shouldNotMoveWhenRandomNumberIsLessThanFour() {
RacingCar car = new RacingCar();
car.move(3); // 3 이하일 때 이동하지 않아야 함
assertThat(car.getPosition()).isEqualTo(0);
}
}
이제 테스트는 독립적이고 확정적인 상태를 갖는다.
유지보수는 하기 쉬워졌으며 테스트 코드만 보고도 이 테스트에서 무엇을 확인하려는지 보다 명확해졌다.
해피 케이스 1-2
그러나 위와 같이 테스트를 작성하면
중복코드가 너무 많이 생기는것 아닌가~?
DRY(Don't repeat yourself) 를 위반하는거 아니야~?
라고 생각할 수 있다.
그러나 테스트 코드에서는 이렇게 반복된 코드가 마냥 중복된 코드는 아니다.
그러나 아무리 생각해도 위와 같은 코드가 두벌이나 작성되는게 거슬릴수 있다.
그럴때는 Junit을 사용한다면 ParameterizedTest를 이용하여 하나의 테스트에 given을 달리하여 하나의 테스트로 표현해볼 수 있다.
class RacingCarTest {
@DisplayName("자동차는 입력받은 숫자에 따라 움직임이 변화한다.")
@ParameterizedTest
@CsvSource({
"4, 1",
"3, 0"
})
void testCarMovement(int input, int expectedPosition) {
RacingCar car = new RacingCar();
car.move(input);
assertThat(car.getPosition()).isEqualTo(expectedPosition);
}
}
이렇게 표현하면 두개의 테스트 케이스를 하나의 테스트 코드로 간결하게 표현할 수 있다.
이때 종종 실수하는 모습을 보는데 가능한 경계값만 테스트하자.
굳~~이 경계값이 4를 벗어난 값까지 모두 테스트해줄 필요는 없다. (ex. 0, 10..)
주의!
그런데 위에서 말했듯 반복된 코드가 마냥 안좋은것은 아니다 오히려 이렇게 ParameterizedTest를 통해 표현함으로써 안좋아지는 경우도 있다.
특히 너무 많은 매개변수를 한 번에 넣으면 가독성이 떨어지고, 어떤 케이스를 검증하는지 파악하기 어려워지는 문제가 발생한다.
- ParameterizedTest에서 다루는 매개변수가 많아지면 각 값이 어떠한 의미를 가지는지 설명하기 어렵고 파악하기도 어렵다.
- 새로운 테스트 케이스를 추가하려면 기존 테스트 케이스는 어떻게 이루어져 있는지 파악해야한다. (테스트 케이스가 중복될 수 있기 때문에)
- 하나의 테스트에서 여러가지를 검증하기 때문에 테스트의 목적을 파악하기 힘들어진다.
- 가독성이 안좋아지고 유지보수하기 어려워진다.
따라서 뭐든지 정도가 중요하다. 과하지 않게 적당한 경우에 사용하자.
언제나 코드를 작성한 뒤 정말 최선인지 확인하고 리팩토링 하도록 하자.
마치며
그런데 if-else 쓰지 말라고 글을 썻다고 제발 for-loop나 다른 로직을 첨부하는 행위는 하지말자..
알잘딱깔센으로다가 간결하게 작성하도록 하자.
테스트 케이스를 간결하게 작성하지 못하겠으면 프로덕션 코드를 의심하자.
프로덕션 코드가 테스트를 하기 힘들게 설계가 되어있을 확률이 백만퍼센트 아닐까?
'스터디 > 코드리뷰' 카테고리의 다른 글
하드코딩한 매직넘버는 상수로써 표현하자 (0) | 2025.02.21 |
---|---|
테스트도 단일책임원칙을 지키자 (0) | 2025.02.19 |
생성자 내부에서 너무 많은 일을 하지 말자 (0) | 2025.02.19 |
Exception을 테스트 할 때 거짓 음성을 주의하자 (0) | 2025.02.18 |
코드 리뷰 요청에 정성을 담아보자 (0) | 2025.02.18 |