SSAFY를 통해 총 3번의 프로젝트를 진행했지만 기획, 발표를 위한 작업을 제외하면 거의 1개월 만에 프로젝트 하나를 완성해야 했기에 구현을 중심으로 진행했다.
프로젝트가 끝나고 항상 다음 프로젝트에는 TestCode를 적용해야지... 하고 생각은 하지만 매번 구현하기 바빠서 Test는 생략...
늦게나마 Test에 대해 공부하고 적용하여 코드의 품질 개선에 대해 신경 써보고자한다.
테스트코드는 기본적으로는 내가 구성한 로직을 확인하기 위함이다.
매번 눈으로 확인하거나 사람이 확인하기에는 번거로울 뿐더러 그 내용을 신뢰할 수 없다.테스트코드를 작성하는 만큼 시간이 더 걸리는 것은 사실이지만 매번 로직을 검사하거나, 코드를 확장 or 변경하였을 때 타 기능에 영향은 없는지 확인할 수 있기 때문에 궁극적으로는 더 효율적일 수 있다.
따라서, 코드 변경에 용이하고 코드의 품질을 높일 수 있는 테스트 코드 작성은 거의 필수라고 볼 수 있다.
Junit과 AssertJ는 프로젝트를 생성하면 보통 일반적으로 test가 dependecy되어있기 때문에 따로
의존을 할 필요는 없다.
dependencies {
// Spring boot
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Database
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
테스트를 위해 간단한 Americano, CafeLatte 클래스를 만든다.
public class Americano implements Beverage{
@Override
public int getPrice() {
return 4500;
}
@Override
public String getName() {
return "Americano";
}
}
public class CafeLatte implements Beverage{
@Override
public int getPrice() {
return 5000;
}
@Override
public String getName() {
return "Cafe Latte";
}
}
간단한 기능을 가진 키오스크를 만들어준다.
@Getter
public class CafeKiosk {
private final List<Beverage> beverageList = new ArrayList<>();
public void add(Beverage beverage) {
beverageList.add(beverage);
}
public void remove(Beverage beverage) {
beverageList.remove(beverage);
}
public void clear() {
beverageList.clear();
}
public int calculateTotalPrice() {
int totalPrice = 0;
for (Beverage beverage : beverageList) {
totalPrice += beverage.getPrice();
}
return totalPrice;
}
public Order createOrder() {
return new Order(LocalDateTime.now(), beverageList);
}
만들어 놓은 Americano와 latte로 키오스크가 제대로 동작하는지 Test코드를 작성한다.
Test코드의 경우 Junit과 AssertJ 두 가지 모두 assertThat 메서드가 존재하지만 체이닝 메서드가 가능한 AssertJ의 assertThat을 사용하여 테스트 코드를 작성하였다.
import static org.assertj.core.api.Assertions.assertThat;
class CafeKioskTest {
@Test
void add() {
CafeKiosk cafeKiosk = new CafeKiosk();
cafeKiosk.add(new Americano());
// 아메리카노를 추가한 후 size가 1인가
assertThat(cafeKiosk.getBeverageList()).hasSize(1);
// 주문 리스트에서 0번째 음료의 가격이 4500인가
assertThat(cafeKiosk.getBeverageList().get(0).getPrice()).isEqualTo(4500);
//주문한 음료의 이름이 Americano인가
assertThat(cafeKiosk.getBeverageList().get(0).getName()).isEqualTo("Americano");
}
@Test
void remove() {
CafeKiosk cafeKiosk = new CafeKiosk();
CafeLatte cafeLatte = new CafeLatte();
cafeKiosk.add(cafeLatte);
// 추가 하였을 때 크기가 1인가
assertThat(cafeKiosk.getBeverageList()).hasSize(1);
cafeKiosk.remove(cafeLatte);
// 지우고 난 후 크기가 0인가 + kiosk가 비워져있는가
assertThat(cafeKiosk.getBeverageList()).hasSize(0);
assertThat(cafeKiosk.getBeverageList()).isEmpty();
}
@Test
void clear() {
CafeKiosk cafeKiosk = new CafeKiosk();
cafeKiosk.add(new Americano());
cafeKiosk.add(new CafeLatte());
// 라떼와 아메리카노 추가 후 주문메뉴가 2개가 되었는가
assertThat(cafeKiosk.getBeverageList()).hasSize(2);
cafeKiosk.clear();
// clear로 비워졌는가
assertThat(cafeKiosk.getBeverageList().isEmpty());
}
}
세 가지 테스트 케이스는 모두 통과되었다. 직접 print문을 찍어 확인하지도 않았고, 들어간 음료를 일일이 확인하여 검증하지도 않았다. 수정하거나 추가적인 사항이 요구될 때, 기본 로직에 대한 점검을 매번 확인하지 않더라도 테스트 코드가 해결해 줄 것이다.
여기서 한 종류의 음료 여러 잔을 한 번에 담는 기능을 추가해달라는 요구사항이 들어왔다고 가정하자. 이 때 가장 우선되어야하는 것은 바로 질문하기이다.
예외상황에 대해 암묵적으로 합의 되었다고 생각하거나, 생각하지 못한 상황이 존재할 수 있기 때문이다.
테스트 케이스는 해피 케이스와, 예외 케이스 두 가지로 분류된다.
아메리카노 한 개 주문 후, 한 개가 담겨있는지 or 모두 삭제되었는지는 해피케이스에 대한 내용이다.
예를 들어 화면에서 아메리카노 0잔을 입력했을 때, 어떻게 처리할 것인지, 음수로 했을 때는 어떻게 할 것인지는 상식적인 로직은 아니지만 이러한 예외케이스에 대한 처리가 모두 이루어져야 꼼꼼한 테스트와 프로덕션 코드가 완성될 수 있다.
꼼꼼한 테스트 코드를 위해서는 경계값 테스트가 필요하다.
예를 들어 아메리카노가 3잔 이상이면 10% 할인이라는 조건을 만족해야한다고 했을 때, 4잔일 때 or 5잔일 때를 테스트하기보다는 경계값인 3에 대한 해피 케이스 테스트를 작성하는 것이 좋다.
예외 케이스의 경우에는 0,1보다는 2잔 일 때를 테스트를 하는것이 효용성이 더 좋다.
public void add(Beverage beverage, int count) {
if (count <= 0) {
throw new IllegalStateException("음료를 1잔 이상 주문하셔야 합니다.");
}
for (int i = 0; i < count; i++) {
beverageList.add(beverage);
}
}
@Test
void addSeveralBeverages() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano, 2);
// 해피 케이스 테스트
assertThat(cafeKiosk.getBeverageList().get(0)).isEqualTo(americano);
assertThat(cafeKiosk.getBeverageList().get(1)).isEqualTo(americano);
}
@Test
void addZeroBeverages() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
/* 예외 케이스 테스트
* 예외발생하는 메서드를 람다식으로 입력
* hasMessage를 사용해 에러 메세지 기입 가능
* */
assertThatThrownBy(() -> cafeKiosk.add(americano, 0))
.isInstanceOf(IllegalStateException.class)
.hasMessage("음료는 1잔 이상 주문하셔야합니다.");
}
IF문으로 흐름을 분리하여 작성하는 것은 좋지않은 테스트코드이다. 따라서 Zero인 경우와 그렇지 않은 경우를 나누어 테스트를 진행한다.
루프나 조건문이 들어갈 경우 제어의 흐름이 여러 개로 분리되어 모든 경우를 대비해서 코드를 작성해야한다. 이 때, IF문으로 로직을 작성하게되면 가독성이 떨어질 뿐 아니라 다른 사람이 코드를 봤을 때 한번에 흐름을 예측하기가 어렵다.
또한, 항상 동일한 결과를 보장해야하는 TestCode에서 IF문으로 인해 신뢰성을 보장하기 어려울 수 있기에 TestCode는 상황을 명확하게 설정하고, 해당 결과를 예측하여 검증하는 방향으로 작성해야한다.