[SpringBoot] JUnit을 이용한 테스트 실습

suRan·2022년 7월 25일
1

🍃 SpringBoot

목록 보기
5/24

본 포스팅은 프로그래머스 미니 데브 코스를 공부하며
학습을 기록하기 위한 목적으로 작성된 글입니다.


테스트 시 주의사항

  • ❌테스트 메소드는 추상메소드이면 안된다❌
  • ❌테스트 메소드는 어떤 값도 return 해선 안된다.❌
  • Happy path만 생각하는 것이 아니라, Unhappy path를 생각해야 한다.

단위테스트 시 사용하는 용어로 테스트 대상을 분류해보자.

SUT

  • FixedAmountVoucherTest
  • HamcrestAssertionTest

Method

  • testAssertEqual()
  • testMinusDiscountAmount()
  • testWithMinus()
  • testVoucherCreation()
  • hamcrestTest()
  • hamcrestListMatcherTest()

테스트 방법론

TDD

  • 테스트 케이스부터 작성하면서 코드를 구현하는 방법

BDD

  • 구현을 해놓고 테스트 케이스를 작성하는 방법

어떤 방법론이 옳다고 할 수는 없다.
가장 중요한 건 테스트 코드가 존재하는 것이다.
비즈니스룰은 테스트 코드만으로 파악될 수도 있기 때문이다.


테스트 클래스 생성

intelliJ의 create Test를 통해 테스트 클래스를 쉽게 생성할 수 있다.

  • @Before
    실제 테스트 시작 전 해야할 셋업이 있을 때
  • @After
    리소스 정리 및 클린업 코드 필요 시 사용
  • Member
    테스트 코드 작성을 원하는 멤버 선택


intelliJ의 검색기능을 통해 testMethod 검색 -> 메소드 작성 가능

inteliJ에서 test클래스를 작성하면
main과 같은 패키지 구조로 테스트 클래스가 생성되고
assert문에 대한 static import가 자동으로 생성된다. 👇

import static org.junit.jupiter.api.Assertions.*;

그래서 굳이 static문을 입력하지 않아도 쉽게 테스트 코드를 작성할 수 있게 된다.
(static import 미사용 시 메소드를 Assertions.assertEquals(...)의 형식으로 사용해야 함)




assertEqual()

테스트 코드



결과

테스트가 성공적으로 수행되면 green light를 볼 수 있다.

그런데 테스트 수행 시
method명만 봐서는 테스트 결과를 직관적으로 파악하기 힘들 때가 있다.

이 때 @DisplayName를 이용할 수 있다.



코드



결과

이모지도 넣을 수 있다. 너무 귀여워~🎃




assertThrows()

이번에는 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 클래스의 생성자 메소드에 amount에 대한 로직을 작성해준다.

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

만일 개발 중 테스트가 계속 실패하는데 현재 당장 테스트를 고칠 수 없는 상황이라면
@Disabled를 사용할 수 있다.

코드

    @Test
    @DisplayName("할인 금액은 마이너스가 될 수 없다.")
    @Disabled // 추가
    void testWithMinus() {
        assertThrows(IllegalArgumentException.class, () -> new FixedAmountVoucher(UUID.randomUUID(), -100));
    }


결과



@BeforeAll / @BeforeEach

@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
각 메서드가 실행된 직후에 매번 호출된다.




Testcase에서의 예외 상황 설정

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


수정 후 결과

테스트 코드를 작성해나가면서 기존에 미처 생각하지 못했던 부분을 발견하는 게 중요하다.



assertAll()


예외상황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 클래스

hamcrest 클래스는 다양한 매치룰을 쉽게 작성하고 테스트할 수 있는 매처(Matcher)들에 대한 라이브러리이다. (테스트 프레임워크가 아니다.)

hamcrest 클래스의 매처를 사용하면
테스트 검증을 위한 junit의 Assertion클래스와 연계하여
junit의 assert 조건코드를 더 가독성있게 바꿀 수 있다.

spring.starter가 추가되어있다면 hamcrest도 자동으로 추가되어있다.
따라서 따로 dependency를 추가할 필요는 없다.



assertThat()


새 테스트 클래스 HamcrestAssertionTest를 만든다.

코드

위 코드들의 결과는 모두 동일하다.

  • equalTo()
    equalTo()는 org.hamcrest.Matchers 클래스에서 제공하는 메소드이다.

  • is()



코드



  • anyOf()
    어떤 메소드 호출 시 결과 값이 나뉠 수 있을 때 사용한다.


코드

역시 둘은 동일한 기능을 한다.

  • not()



collection에 대한 테스트

hamcrest는 collection에 대한 테스트에 있어 편리한 기능을 제공하기도 한다.
리스트에 대한 검증은 hamcrest를 사용하는 가장 큰 이유이다.

만약 배열의 크기를 assertEquall()로 구해야한다면
배열의 사이즈를 구한 뒤 어떤 값과 같은지 비교해야하는 코드를 작성해야할 것이다.
그러나 assertThat()에서는 hasSize()를 사용할 수 있다.



코드

  • hasSize()
    Collection의 크기 비교 시 사용

  • everyItem()
    Collection의 아이템 전체를 순회하면서 테스트한다.

  • greaterThan()
    입력한 값보다 더 큰 아이템이 있는지 비교



collection 내부에 어떤 아이템이 존재하는지도 확인할 수 있다.

  • containsInAnyOrder()
    순서가 중요하지 않을 때 사용.
    순서를 포함하지 않고 매개변수로 입력한 아이템들이 존재하는지 확인
  • contains()
    순서가 중요할 때 사용
  • hasItem()
    단 하나의 아이템이 collection과 일치하는지 확인
    hasItem()안에 매처를 넣을 수도 있다.
  • greaterThanOrEqualTo()
    컬렉션 내부에 입력 값보다 크거나 같은 값이 있는지 확인한다.
    매처가 돌면서 개별적으로 매칭을 한다.


새로 알게 된 것

rf
https://www.lesstif.com/java/hamcrest-junit-test-case-18219426.html

profile
개발 공부를 해라

0개의 댓글