자바와 JUnit 단위 테스트 - 1부

 

자바와 JUnit 단위테스트 - 1부

자바와 JUnit을 활용한 실용주의 단위 테스트 책을 보고 공부하며 정리한 내용입니다.

인텔리제이 테스트 설정

  1. 자바 프로젝트 생성

  2. test 폴더 생성 후 폴더 지정

    setting1

  3. 테스트할 클래스에서 오른쪽 -> Go To -> Test (Create New Test…)

  4. Junit4 -> Fix -> OK

  5. 테스트 코드 작성 후 테스트

테스트 코드 생성 단축키

⇧ + ⌘ + T

단위 테스트

  • 모듈이나 애플리케이션 안에 있는 개별적인 코드 단위가 예상대로 작동하는지 확인하는 방법
  • 메서드 단위의 테스트로 어플리케이션이 기대한 방식대로 잘 동작함을 증명하며 버그를 조기에 잡아내는 것을 목적으로 한다.
  • 테스트 코드의 문제 발생 여부로 작성한 소스코드의 오류를 더 빠르게 잡아내고 수정할 수 있다.
  • 코드 리팩토링 시 테스트 코드로 로직이 변경되지 않음을 보장 할 수 있다.
  • 코드의 퀄리티를 높일 수 있다.

JUnit4

실행 준비 단언

테스트 코드 생성 -> 테스트할 클래스 이름 + Test

import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

public class classNameTest{
  @Test
  public void test(){
    // 준비 (Arrange)
    // 테스트를 하기 위한 설정 객체 생성 및 데이터 세팅
    
    // 실행 (Act)
    // 검증할 코드 실행
    
    // 단언 (Assert)
    // 기대 값과 비교하여 검증
  }
}

@Before 메서드로 테스트 초기화

테스트에서 중복된 코드가 있을 때 공통적으로 초기화 하는 방법

import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

public class classNameTest{
  private Test test;

  @Before
  public void create(){
    test = new Test();
  }
  
  @Test
  public void test1(){
   	// test 사용
  }
  
  @Test
  public void test2(){
    // test 사용
  }
}
JUnit 동작 순서
  1. Test 클래스의 인스턴스 생성
  2. @Before 메서드 호출하여 공통 메소드 초기화
  3. 테스트 메소드 실행
  4. 테스트가 여러개 일 경우 1~3 반복…

테스트 클래스에 static 필드가 있을 경우 새로운 인스턴스를 생성해도 상태가 공유되므로 사용 주의

어떻게 테스트 코드를 작성할까?

  • 반복문, if문과 복잡한 조건문을 본다.
  • 데이터 변형들을 고려하여 조건문에 어떠한 영향을 미치는지 고려한다. null, 0 등
  • 단순한 행복경로(happy path)가 아닌 상황들을 고려. 예외, 오류 조건

JUnit 단언

단언

어떤 조건이 참인지 검증하는 방법. 단언한 조건이 참이 아니면 테스트는 실패(failure)를 보고한다.

JUnit에 포함된 전통적인 스타일의 단언과 표현력이 좋은 햄크레스트(Hamcrest) 라이브러리를 사용하는 단언이 있다.

JUnit의 기본적인 단언

import static org.junit.Assert.*;

assertTrue

@Test
public void test(){
	int a = 10;
  assertTrue(a > 5);
}

assertThat은 명확한 값을 비교

assertThat() 정적 메서드는 햄크레스트 단언의 예이다.

  • 첫 번째 인자는 실제(actual) 표현식으로 검증하고자 하는 값

  • 두 번째 인자는 매처(matcher)로 실쩨 값과 표현식의 결과를 비교

    매처는 테스트의 가독성을 크게 높혀준다.

    ex) assertThat(account.getBalance(), equalTo(100));

    계좌 잔고가 100과 같아야 한다.

햄크레스트 매처 import
import static org.hamcrest.CoreMatchers.*;

햄크레스트 단언은 실패할 경우 일반적인 단언보다 오류메시지에서 더 많은 정보를 알 수 있다.

중요 햄크레스트 매처

equalTo(), is()

자바 배열, 컬렉션 객체 비교 시 사용

assertThat(account.getName(), equalTo("yoon"));
assertThat(account.getName(), is("yoon"));
not()

어떤 것을 부정하는 단언

assertThat(account.getName(), not(equalTo("yoon")));
nullValue(), notNullValue()

null 값이나 null이 아닌 값을 검사할 때 사용

assertThat(account.getName(), is(not(nullValue()));
assertThat(account.getName(), is(notNullValue()));

null이 아닌 값을 자주 검사하는 것은 설계 문제이거나 지나친 걱정이므로 많은 경우에 이러한 검사는 불필요 하다.

부동 소수점 수를 두 개 비교

isCloseTo 매처 사용. 부동 소수점은 정확한 수를 표현할 수 없기 때문에 근사치를 구해야 한다.

import static org.hamcrest.number.IsCloseTo.*;
// ...
assertThat(2.32 * 3, closeTo(6.96, 0.0005));

단언 설명

모든 JUnit 단언에는 message라는 선택적 첫 번째 인자가 있다. message 인자는 단언의 근거를 설명한다.

@Test
public void testWithWorthlessAssertionComment(){
	account.deposit(50);
	assertThat("account balance is 100", account.getBalance(), equalTo(50));
}

설명이 있는 주석문을 선호한다면 단언 메시지를 추가 할 수 있다. 하지만 더 좋은 방법은 테스트 코드 자체만으로 이해할 수 있게 작성하는 것이다.

테스트 이름 변경, 의미 있는 상수, 변수 이름 개선, 복잡한 초기화 작업을 의미 있는 이름을 가진 도우미 메서드로 추출, 가독성이 우수한 햄크레스트 단언 사용 등의 방법을 활용하는 것이 테스트를 훨씬 좋게 만든다.

예외를 기대하는 세 가지 방법

애너테이션 사용 (단순)

@Test(expected=UserException.class)
public void test(){
	// ...
}

해당 exception 이 발생하면 테스트는 통과한다.

try/catch와 fail (구)

try {
	// ...메소드 예외 발생 시
	fail();
} catch(UserException expected){ 
}

예외가 발생하면 catch로 넘어가고 테스트가 종료되어 통과

ExpectedException 규칙 (신)

import org.junit.rules.*;

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void exceptionRule(){
  thrown.expect(UserException.class);
  thrown.expectMessage("예외 메시지");
  
  // ...
}

테스트를 실행할 때 발생할 수 있는 예외를 알려주고 등록된 예외가 발생해도 테스트를 통과한다.

예외 무시

발생하는 예외를 다 던져버린다. 예외가 발생하더라도 junit에서 테스트 오류로 보고한다.

@Test
public void test() throws IOException {

}

테스트 조직

AAA로 테스트 일관성 유지하기

Arrange - Act - Assert + (After)

준비(Arrange): 테스트 코드를 실행하기 전에 시스템이 적절한 상태에 있는지 확인

실행(Act): 테스트 코드를 실행. 보통은 단일 메서드 호출

단언(Assert): 실행한 코드가 기대한 대로 동작하는지 확인

+

사후(After): 테스트를 실행할 때 어떤 자원을 할당했다면 잘 정리(clean up) 되었는지 확인

동작 테스트 vs 메서드 테스트

테스트를 작성할 때는 클래스 동작에 집중해야 하며 개별 메서드를 테스트한다고 생각하면 안된다. 단위 테스트를 작성할 때는 먼저 전체적인 시각에서 시작하고 개별 메서드를 테스트하는 것이 아니라 클래스의 종합적인 동작을 테스트해야 한다.

테스트와 프로덕션 코드의 관계

JUnit 테스트는 검증 대상인 프로덕션 코드와 같은 프로젝트에 위치할 수 있다. 하지만 테스트는 주어진 프로젝트 안에서 프로덕션 코드와 분리해야한다.

테스트와 프로덕션 코드 분리

  • 테스트를 프로덕션 코드와 같은 디렉터리 및 패키지에 넣기

    구현하기 쉽지만 어느 누구도 실제 시스템에서 이렇게 하지 않는다.

  • 테스트를 별도 디렉터리로 분리하지만 프로덕션 코드와 같은 패키지에 넣기

    대부분의 회사에서 선택하는 정책. 이클립스, 메이븐 같은 도구는 이 모델을 권장한다.

    |---src
    |    |---package
    |           |---UserClass.java
    |
    |---test
    |    |---package
    |           |---UserClassTest.java
    
  • 테스트를 별도의 디렉터리와 유사한 패키지에 유지하기

    |---src
    |    |---package
    |           |---UserClass.java
    |
    |---test
    |    |---test
    |          |---package2
    |                 |---UserClassTest.java
    

내부 데이터 노출 vs 내부 동작 노출

테스트를 위해 내부 데이터를 노출하는 것은 테스트와 프로덕션 코드 사이에 과도한 결합을 초래한다.

내부 행위를 테스트 하려는 것은 설계에 문제가 있는 것이다. 단일 책임 원칙(SRP, Single Responsobility Principle)을 어기는 내부 메서드일 수 있다. SRP는 어떤 클래스가 작고 단일 목적을 가져야 함을 의미하며, 가장 좋은 해결책은 흥미로운 private 메서드를 추출하여 다른 클래스로 이동시키는 것이다. 그렇게 하면 그 클래스의 유용한 public 메서드가 될것이다.

집중적인 단일 목적 테스트의 가치

다수의 케이스를 별도의 JUnit 테스트 메서드로 분리하세요. 각각에는 검증하는 동작을 표현하는 이름을 붙이세요.

테스트를 분리하면…
  • 단언이 실패했을 때 실패한 테스트 이름이 표시되기 때문에 어느 동작에서 문제가 있는지 빠르게 파악할 수 있다.
  • 실패한 테스트를 해독하는 데 필요한 시간을 줄일 수 있다.
  • 모든 케이스가 실행되었음을 보장할 수 있다. 단언이 실패하면 메서드는 중단된다.

문서로서의 테스트

단위 테스트는 우리가 만드는 클래스에 대한 지속적이고 믿을 수 있는 문서 역할을 해야한다. 테스트는 코드 자체로 쉽게 설명할 수 없는 가능성들을 알려준다.

일관성 있는 이름으로 테스트 문서화

테스트 이름은 테스트하려는 맥락을 제안하기 보다는 어떤 맥락에서 일련의 행동을 호출했을 때 어떤 결과가 나오는지를 명시한다. 좀 더 분명한 이름은 프로그래머가 테스트 내용이 무엇인지 파악하도록 도와준다.

doingSomOperationGeneratesSomeResult

어떤 동작을 하면 어떤 결과가 나온다.

someResultOccursUnderSomCondition

어떤 결과는 어떤 조건에서 발생한다.


행위 주도 개발(BDD - Behavior-Driven Development)의 given-when-then

givenSomeContextWhenDoingSomeBehaviorThenSomeResultOccurs

주어진 조건에서 어떤 일을 하면 어떤 결과가 나온다.

이 테스트 이름은 읽기에 길고 복잡할 수 있기에 givenSomeContext를 제거한다.

whenDoingSomeBehaviorThenSomeResultOccurs

어떤 일을 하면 어떤 결과가 나온다.

어떤 형식이든 일관성을 유지하는 것이 중요하다. 주요 목표는 테스트코드가 다른사람에게 의미 있게하는 만드는 것이다.

테스트를 의미 있게 만들기

  • 지역 변수 이름 개선하기
  • 의미 있는 상수 도입하기
  • 햄크레스트 단언 사용하기
  • 커다란 테스트를 작게 나누어 집중적인 테스트 만들기
  • 테스트 군더더기들을 도우미 메서드와 @Before 메서드로 이동하기

테스트 이름과 코드를 재작업하여 부가적으로 주석을 넣지 않고도 스토리를 알 수 있도록 만드세요.

@Before과 @After, 공통 초기화와 정리 더 알기

연관된 메서드 집합에 대해 더 많은 테스트를 추가하면 상당한 테스트 코드가 같은 초기화 부분을 가진다는 것을 알게 될것이다. @Before 메서드를 활용하여 중복된 코드를 막자.

여러 개의 @Before 메서드는 실행 순서를 보장하지 않으며 순서가 필요하다면 하나의 메서드로 결합해야 한다. 각각의 테스트 코드가 실행되기 전에 @Before 메서드가 호출되어 초기화 작업을 하고 테스트가 진행된다.

@After 메서드는 테스트에 발생하는 부산물을 정리하는 역할을 한다. 예를들어 열려있는 데이터베이스와 연결을 종료하는 역할을 한다.

@BeforeClass와 @AfterClass 애너테이션

@BeforeClass와 @AfterClass 애너테이션은 테스트 코드들의 시작과 종료에 한 번만 실행된다.

녹색이 좋다. 테스트를 의미있게 유지

보통 모든 테스트가 항상 통과한다고 기대해야 한다. 실패하면 곧바로 고쳐 모든 테스트가 항상 통과하도록 하자.

테스트를 빠르게

테스트 코드에 데이터베이스처럼 느린 자원을 통제하는 부분이 없다면 수 초안에 수천 개의 테스트를 실행하는 것이 가능하다. 전체 테스트를 항상 실행하는 것은 어렵지 않으므로 특수한 경우가 아니라면 전부 수행하여 녹색을 유지하자.

견딜 수 있는 많큼 많은 테스트를 실행하세요.

테스트 제외 @Ignore(“~~”)

다수의 테스트의 실패가 발생했을 때 @Ignore 애너테이션으로 다른 테스트를 무시한다. 문제가 있는 테스트에 하나씩 집중하여 해결을 빠르게 하자.