본 포스팅은 프로그래머스 미니 데브 코스를 공부하며
학습을 기록하기 위한 목적으로 작성된 글입니다.
단위테스트 시 사용하는 용어로 테스트 대상을 분류해보자.
SUT
Method
테스트 방법론
TDD
BDD
어떤 방법론이 옳다고 할 수는 없다.
가장 중요한 건 테스트 코드가 존재하는 것이다.
비즈니스룰은 테스트 코드만으로 파악될 수도 있기 때문이다.
intelliJ의 create Test를 통해 테스트 클래스를 쉽게 생성할 수 있다.
inteliJ에서 test클래스를 작성하면
main과 같은 패키지 구조로 테스트 클래스가 생성되고
assert문에 대한 static import가 자동으로 생성된다. 👇
import static org.junit.jupiter.api.Assertions.*;
그래서 굳이 static문을 입력하지 않아도 쉽게 테스트 코드를 작성할 수 있게 된다.
(static import 미사용 시 메소드를 Assertions.assertEquals(...)
의 형식으로 사용해야 함)
테스트 코드
결과
테스트가 성공적으로 수행되면 green light를 볼 수 있다.
그런데 테스트 수행 시
method명
만 봐서는 테스트 결과를 직관적으로 파악하기 힘들 때가 있다.
이 때 @DisplayName
를 이용할 수 있다.
코드
결과
이모지도 넣을 수 있다. 너무 귀여워~🎃
이번에는 Unhappy path, 에러가 발생하는 테스트를 진행해보겠다.
테스트 코드 추가
@Test
@DisplayName("할인 금액은 마이너스가 될 수 없다.")
void testWithMinus() {
assertThrows(IllegalArgumentException.class, () -> new FixedAmountVoucher(UUID.randomUUID(), -100));
}
assertThrows()
메소드의
첫 번째 인자에 IllegalArgumentException.class
를,
두 번째 인자에는 에러가 발생해야 하는 람다식을 적어준다.
코드
결과
코드를 실행시켜보면 첫 번째 인자에 해당하는 예외가 발생하지 않았기 때문에 다음과 같은 오류가 발생한다. IllegalArgumentException
이 발생하지 않았다는 소리이다.
org.opentest4j.AssertionFailedError: Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.
FixedAmountVoucher에 로직 추가
public FixedAmountVoucher(UUID voucherId, long amount) {
// 추가한 로직 ------------------------
if (amount < 0) throw new IllegalArgumentException("Amount should be positive");
// -----------------------------------
this.voucherId = voucherId;
this.amount = amount;
}
결과
로직을 설정하고 다시 테스트를 실행하면 IllegalArgumentException
이 발생해서 테스트가 성공한다.
만일 개발 중 테스트가 계속 실패하는데 현재 당장 테스트를 고칠 수 없는 상황이라면
@Disabled를 사용할 수 있다.
코드
@Test
@DisplayName("할인 금액은 마이너스가 될 수 없다.")
@Disabled // 추가
void testWithMinus() {
assertThrows(IllegalArgumentException.class, () -> new FixedAmountVoucher(UUID.randomUUID(), -100));
}
결과
@BeforeAll
클래스가 생성되기 전 맨 처음 한 번만 실행된다.
@BeforeEach
각 메서드가 실행되기 직전에 매번 호출된다.
FixedAmountVoucherTest에 코드 추가
class FixedAmountVoucherTest {
// 코드 추가---------------------------------------
private static final Logger logger = LoggerFactory.getLogger(FixedAmountVoucherTest.class);
@BeforeAll
static void setup() {
logger.info("@BeforeAll - 단 한 번 실행");
}
@BeforeEach
void init() {
logger.info("@BeforeEach - 매 테스트마다 실행");
}
//------------------------------------------------
@Test
@DisplayName("기본적인 assertEqual 테스트 🎃")
void testAssertEqual() {
assertEquals(2, 1 + 1); // unexpected: 기대되는 값, actual: 판별해야하는 값
}
@Test
@DisplayName("주어진 금액만큼 할인을 해야한다.")
void testDiscount() {
var sut = new FixedAmountVoucher(UUID.randomUUID(), 100);
assertEquals(900, sut.discount(1000));
}
@Test
@DisplayName("할인 금액은 마이너스가 될 수 없다.")
void testWithMinus() {
assertThrows(IllegalArgumentException.class, () -> new FixedAmountVoucher(UUID.randomUUID(), -100));
}
}
log가 제대로 출력되지 않을 시 logback.xml파일을 확인하고
logback-test.xml복사본을 만들어 root레벨을 debug단계로 수정해준다.
결과
(+) 추가로, @Before과는 반대로 코드가 실행된 후에 호출되는 @AfterAll, @AfterEach도 있다.
@AfterAll
모든 메소드가 실행된 뒤 마지막에 한 번만 실행된다.
@AfterEach
각 메서드가 실행된 직후에 매번 호출된다.
Ex ) 할인금액(amount)가 판매금액(beforeDiscount)보다 크다면?
코드
결과
테스트를 실행해보면 기대값과 결과값이 다르므로 코드가 잘못됐다는 것을 알 수 있다.
이번에도 FixedAmountVoucher의 코드를 수정해보자.
FixedAmountVoucher
수정 전
public long discount(long beforeDiscount) {
return beforeDiscount - amount;
}
수정 후
public long discount(long beforeDiscount) {
var discountedAmount = beforeDiscount - amount;
return (discountedAmount < 0) ? 0 : discountedAmount;
}
수정 후 결과
테스트 코드를 작성해나가면서 기존에 미처 생각하지 못했던 부분을 발견하는 게 중요하다.
예외상황2
Ex ) 할인금액으로 큰 금액을 입력 시 허용해줘야할까?
코드
class FixedAmountVoucherTest {
@Test
@DisplayName("유효한 할인 금액으로만 생성할 수 있다.")
void testVoucherCreation() { // 여러 개의 테스트 동시 실행
assertAll("FixedAmountVoucher creation",
() -> assertThrows(IllegalArgumentException.class, () -> new FixedAmountVoucher(UUID.randomUUID(), 0)),
() -> assertThrows(IllegalArgumentException.class, () -> new FixedAmountVoucher(UUID.randomUUID(), -100)),
() -> assertThrows(IllegalArgumentException.class, () -> new FixedAmountVoucher(UUID.randomUUID(), -100))
);
}
}
결과
FixedAmountVoucher
수정 전
private final UUID voucherId;
private final long amount;
public FixedAmountVoucher(UUID voucherId, long amount) {
if (amount < 0) throw new IllegalArgumentException("Amount should be positive");
this.voucherId = voucherId;
this.amount = amount;
}
수정 후
private static final long MAX_VOUCHER_AMOUNT = 10000; // 프로그램에서 허용하는 amount의 최대값
private final UUID voucherId;
private final long amount;
public FixedAmountVoucher(UUID voucherId, long amount) { // 각각의 예외 메세지 정확하게 기입
if (amount < 0) throw new IllegalArgumentException("Amount should be positive");
if (amount == 0) throw new IllegalArgumentException("Amount should not be zero");
if (amount > MAX_VOUCHER_AMOUNT) throw new IllegalArgumentException("Amount should be less than %d".formatted(MAX_VOUCHER_AMOUNT) );
this.voucherId = voucherId;
this.amount = amount;
}
실행결과
hamcrest 클래스는 다양한 매치룰을 쉽게 작성하고 테스트할 수 있는 매처(Matcher)들에 대한 라이브러리이다. (테스트 프레임워크가 아니다.)
hamcrest 클래스의 매처를 사용하면
테스트 검증을 위한 junit의 Assertion클래스와 연계하여
junit의 assert 조건코드를 더 가독성있게 바꿀 수 있다.
spring.starter가 추가되어있다면 hamcrest도 자동으로 추가되어있다.
따라서 따로 dependency를 추가할 필요는 없다.
코드
위 코드들의 결과는 모두 동일하다.
equalTo()
equalTo()는 org.hamcrest.Matchers 클래스에서 제공하는 메소드이다.
is()
코드
코드
역시 둘은 동일한 기능을 한다.
hamcrest는 collection에 대한 테스트에 있어 편리한 기능을 제공하기도 한다.
리스트에 대한 검증은 hamcrest를 사용하는 가장 큰 이유이다.
만약 배열의 크기를 assertEquall()로 구해야한다면
배열의 사이즈를 구한 뒤 어떤 값과 같은지 비교해야하는 코드를 작성해야할 것이다.
그러나 assertThat()에서는 hasSize()를 사용할 수 있다.
코드
hasSize()
Collection의 크기 비교 시 사용
everyItem()
Collection의 아이템 전체를 순회하면서 테스트한다.
greaterThan()
입력한 값보다 더 큰 아이템이 있는지 비교
collection 내부에 어떤 아이템이 존재하는지도 확인할 수 있다.
새로 알게 된 것
- static import 란?
import문을 static으로 가져오는 구문이다.
Junit에서의 테스트 코드 작성 시 사용하면 가독성을 높일 수 있다.
참고자료 : 📌 kasania 님의 [Java] Static import에 대한 관찰
@DisplayName
테스트 코드의 가독성을 높일 수 있는 어노테이션.
함수명에 대한 설명을 한글로 입력할 수 있다는 장점이 있다.
@Nested 와 함께 사용하면 더욱 좋다.
📌 참고자료 : 뱀귤 님의 [Spring] JUnit 5 에서 @Nested 와 @DisplayName 으로 가독성 있는 테스트 코드 작성하기
Assertion.assertThrows()
두 번째 인자를 실행해서 첫 번째 인자와 같은 예외 타입인지 검사한다.
assertThrows()문의첫 번째 인자
에는 예외 발생 시의 예외 클래스를 적어줘야 한다.
assertThrows()문의두 번째 인자
는 실행가능한 Executable executable
📌 참고자료 : Covenent 님의 완벽정리! Junit5로 예외 테스트하는 방법
@Disabled
테스트 클래스 또는 메소드를 비활성화 할 수 있다.
📌 참고자료 : heejeong Kwon 님의 [JUnit] JUnit5 사용법 - 기본
assertAll()
assertAll()은 여러 테스트를 한 번에 검증할 수 있다.
단, assertAll() 내에서 검증하는 모든 테스트를 통과해야만 한다.
참고자료 : 📌 코동이 님의 AssertAll
assertThat
hamcrest 라이브러리를 통합하며 assertion에 있어 더 나은 방법을 제공하는 메소드이다.
hamcrest가 static 메서드로 제공하는 여러 matcher를 사용할 수 있다.
참고자료 : 📌 JongMin 님의 Unit Test에서 AssertThat을 사용하자
rf
https://www.lesstif.com/java/hamcrest-junit-test-case-18219426.html