유닛 테스트(Unit Test), 통합 테스트(Integration Test), 기능 테스트(Funcional Test)란? (feat.JUnit5, AssertJ)

문지원(JiwonMoon)·2022년 9월 28일
0
post-thumbnail

유닛 테스트(Unit Test), 통합 테스트(Integration Test), 기능 테스트(Funcional Test)란?

유닛 테스트(Unit Test)란?

유닛 테스트는 전체 코드 중 작은 부분을 테스트하는 것이다.
(예를 들어, 함수 하나하나 개별로 테스트 코드를 작성하는 것)
만약 테스트에 네트워크나 데이터베이스 같은 외부 리소스가 포함된다면 그것은 유닛 테스트가 아니다.

또한, 유닛 테스트는 매우 간단하고 명확하여야 한다.

기본적으로 테스트를 위한 입력 값을 주어서 그에 대한 함수의 출력 값이 정확 한지 아닌지를 판단하는 것이 유닛 테스트라 할 수 있다.

코드의 설계가 별로 좋지 못하다면 유닛 테스트를 작성하기도 어려워진다.

따라서 함수(메소드) 하나하나 테스트 코드를 작성하는 유닛 테스트는 좀 더 나은 코드를 만들 수 있도록 도와준다.

비유하자면, 유닛 테스트는 척추에 비유할 수 있다.

유닛 테스트를 사용한다면 좋은 코드를 디자인할 수 있을 뿐만 아니라 어떤 함수(메소드)에 변화가 생겼을 때 그 함수가 안전하게 수행되는지를 보장해주고 같은 함수(메소드)를 다른 종류의 테스트에서도 적용하기 쉽게 만들어 준다.

유닛 테스트는 빈번히 일어나는 버그를 막는데도 뛰어난 역할을 한다.

코드 일부분에 문제가 있을 경우 아무리 많이 문제 있는 부분을 수정하려 해도 수정한 부분의 코드가 또 문제를 일으키고.. 문제가 문제를 낳는 경우가 대부분이다.

이럴 때 버그가 있는지 없는지 체크하는 유닛 테스트를 만들어 둠으로써 이러한 문제를 쉽게 해결할 수 있다.

통합 테스트(Integration Teset), 기능 테스트(Functional Teset)는 이벤트의 흐름에 이상이 없는지 테스팅하는데 좋은 반면, 유닛 테스트는 어떠한 부분에 문제가 있고 고칠 부분이 어디인지 명확하게 해 줄 때 좋다.

정리하자면, 테스트 중도 개발(test-driven development) 일 때 유닛 테스트는 꼭 작성해야 하며, 코드의 디자인을 개선해주고 나중에 코드의 리팩토링이 필요할 때 어떤 부분을 완전히 분리할 필요 없이 깔끔하게 해준다.

통합 테스트(Integration Test)

통합 테스트는 이름에서 의미하는 바와 같이 각각의 시스템들이 서로 어떻게 상호작용하고 제대로 작동하는지 테스트하는 것을 의미한다.

통합 테스트는 유닛 테스트와 비슷한데, 큰 차이점이 하나 있다. 유닛 테스트는 다른 컴포넌트들과 독립적인 반면 통합 테스트는 그렇지 않다.

예를 들자면 유닛 테스트에서 데이터베이스에 접근하는 코드는 실제 데이터 베이스와 통신하는 것은 아니지만, 통합 테스트는 실제 통신해야 한다.

통합 테스트는 유닛 테스트만으로 충분하다고 느끼지 못할 때 사용된다.

때때로 두 개의 다른 분리된 시스템끼리 잘 통신하고 있는지 증명하고 싶을 때가 있는데(예를 들어 나의 앱과 데이터 베이스가 제대로 상호작용하고 있는지...) 이것을 통합 테스트라 한다.

통합 테스트는 대게 유닛 테스트를 작성하는 것보다 복잡하고 오랜 시간이 걸린다. (데이터베이스를 세팅할 때, 설정 파일을 읽어 오는 과정이라던가 데이터베이스가 잘 세팅되었는지 확인하는 과정이라던가)

따라서 통합 테스트가 꼭 필요한 것이 아니면 유닛 테스트를 작성하는데 집중하는 것이 좋다.

기능 테스트(Funcional Test)

기능 테스트는 E2E 테스트(E2E Test) 혹은 브라우저 테스트(Browser Test)라고 불리며 모두 같은 의미다.

기능 테스트는 어떤 어플리케이션이 제대로 동작하는지 완전한 기능을 테스트하는 것을 의미한다.

예를 들어, 어떤 웹 어플리케이션을 기능 테스트한다고 가정해보면 브라우저 자동화 도구를 사용해 특정한 페이지를 클릭한다던가 하는 것이 기능 테스트라 할 수 있다.

유닛 테스트를 사용하여 개별의 함수가 제대로 동작하는지 확인하고 통합 테스트를 통해 서로 다른 시스템이 잘 상호작용 하는지 확인할 수 있다.

기능 테스트는 이와 완전히 다른 레벨에 있다고 생각하면 된다.

유닛 테스트는 수백 가지가 있을 수 있지만 기능 테스트는 그렇지 못하다.

기능테스트는 작성하기 매우 어렵고 높은 복잡성을 가지고 있기 때문에 많은 시간이 걸린다. (웹 어플리케이션을 가정하면 기능 테스트는 실제 사용자 상호 작용을 시뮬레이션하므로 페이지 로딩 시간조차도 기능 테스트의 한 요인이 된다)

위와 같은 이유로 기능 테스트를 매우 세밀하게 나눠서 하면 좋지 못하다.

대신 기능 테스트는 사용자와 앱의 상호작용을 테스트하고 싶을 때 유용하다. (예를 들어, 회원 가입과 같이 브라우저에서 앱의 특정 흐름을 수동으로 테스트하는 경우)

회원 가입에 대한 기능 테스트라 하면 유저가 회원가입을 마치고 "저희 사이트에 회원 가입하게 된 것을 축하드립니다."라는 페이지를 올바르게 출력해줄 것을 보장해주어야 한다.

정리

  • 유닛 테스트: 함수 하나하나와 같이 코드의 작은 부분을 테스트하는 것
  • 통합 테스트: 서로 다른 시스템들의 상호작용이 잘 이루어 지는지 테스트 하는 것
  • 기능 테스트: 사용자와 어플리케이션의 상호작용이 원할하게 이루어지는지 테스트하는 것

Test 방법

필요한 라이브러리
요즘 Java 유닛(단위)테스트 작성에는 크게 2가지 라이브러리가 사용된다.

JUnit, AssertJ 이란?

  • JUnit5 : 자바 유닛(단위)테스트를 위한 테스팅 프레임워크
  • AssertJ : 자바 테스트를 돕기 위해 다양한 문법을 지원하는 라이브러리

JUnit 만으로도 단위 테스트를 충분히 작성할 수 있다. 하지만 JUnit에서 제공하는 assertEquals()와 같은 메소드는 AssertJ가 주는 메소드에 비해 가독성이 떨어진다. 그렇기 때문에 순수 Java 애플리케이션에서 단위 테스트를 위해 JUnit5와 AssertJ 조합이 많이 사용된다.

given/when/then 패턴

요즘 단위테스트는 거의 given-when-then 패턴으로 작성하는 추세이다. given-when-then 패턴이란 1개의 단위 테스트를 3가지 단계로 나누어 처리하는 패턴으로,
각각의 단계는 다음을 의미한다.

  • given(준비): 어떠한 데이터가 준비되었을 때
  • when(실행): 어떠한 함수를 실행하면
  • then(검증): 어떠한 결과가 나와야 한다.

참고
어떤 메소드가 몇번 호출되었는지를 검사하기 위한 verify 단계도 사용하는 경우가 있는데, 그렇게 실용성이 크지 않으므로 메소드의 호출 횟수가 중요한 테스트에서만 선택적으로 사용하면 될 것 같다.

@DisplayName("로또 번호 갯수 테스트")
@Test
void lottoNumberSizeTest(){
		//given
        
        //when
        
        //then
        
}

@Test는 해당 메소드가 단위 테스트임을 명시하는 어노테이션이다. JUnit은 테스트 패키지 하위의 @Test 어노테이션이 붙은 메소드를 단위 테스트로 인식하여 실행시킨다. 이 상태로 실행하면 테스트 이름이 함수 이름이 default로 지정되는데, 우리는 @DisplayName 어노테이션을 사용하여 읽기 좋은 다른 이름을 부여할 수 있다.

로또 생성기 Java 코드
ex) 다음과 같이 1000원을 주면 1개의 로또를 생성해주는 클래스가 있다고 하자.

public class LottoNumberGenerator {

    public List<Integer> generate(final int money) {
        if (!isValidMoney(money)) {
            throw new RuntimeException("올바른 금액이 아닙니다.");
        }
        return generate();
    }

    private boolean isValidMoney(final int money) {
        return money == 1000;
    }

    private List<Integer> generate() {
        return new Random()
                .ints(1, 45 + 1)
                .distinct()
                .limit(6)
                .boxed()
                .collect(Collectors.toList());
    }

}

위와 같은 로또 번호 생성 코드에 대한 테스트 코드들을 작성해보도록 하자.

  1. 로또 번호 갯수 테스트
  2. 로또 번호 범위 테스트
  3. 잘못된 로또 금액 테스트

1. 로또 번호 갯수 테스트

우선 로또를 생성받기 위해서는 로또 생성기 객체와 금액이 필요하다. 그렇기에 given 단계에서는 LottoNumberGenerator 객체와 금액을 적어주면 된다.

@DisplayName("로또 번호 갯수 테스트")
@Test
void lottoNumberSizeTest() {
    // given
    final LottoNumberGenerator lottoNumberGenerator = new LottoNumberGenerator();
    final int price = 1000;

    // when
    final List<Integer> lottoNumber = lottoNumberGenerator.generate(price);

    // then
    assertThat(lotto.size()).isEqualTo(6);
}

2. 로또 번호 범위 테스트

이번에는 모든 로또 숫자가 1에서 45사이의 숫자인지를 boolean 값으로 검사하므로, AssertJ의 isTrue() 문법이 사용되었다. 그 외에도 isFalse(), isNull(), isNotNull() 등의 메소드가 있다.

@DisplayName("로또 번호 범위 테스트")
@Test
void lottoNumberRangeTest() {
    // given
    final LottoNumberGenerator lottoNumberGenerator = new LottoNumberGenerator();
    final int price = 1000;

    // when
    final List<Integer> lotto = lottoNumberGenerator.generate(price);

    // then
    assertThat(lotto.stream().allMatch(v -> v >= 1 && v <= 45)).isTrue();
}

3. 잘못된 로또 금액 테스트

마지막으로 잘못된 금액이 발생한 경우, Runtime Exception이 발생하는 코드에 대해 테스트를 해야 한다. 예외가 발생하는 경우에는 when 단계에서 assertThrows()로 감싸서 처리를 해야 한다.

@DisplayName("잘못된 로또 금액 테스트")
@Test
void lottoNumberInvalidMoneyTest() {
    // given
    final LottoNumberGenerator lottoNumberGenerator = new LottoNumberGenerator();
    final int price = 2000;

    // when
    final RuntimeException exception = assertThrows(RuntimeException.class, () -> lottoNumberGenerator.generate(price));

    // then
    assertThat(exception.getMessage()).isEqualTo("올바른 금액이 아닙니다.");
}

References (참고 자료)

0개의 댓글