Web/TDD 2019. 9. 4. 22:20

 

이번 포스팅에서 다루어볼 내용은 테스트 코드를 잘 조직하고 구조화 할 수 있는 JUnit 기능을 살펴볼 것이다.

포스팅에서 다룰 내용은 아래와 같다.

 

  • 준비-실행-단언을 사용하여 테스트를 가시적이고 일관성 있게 만드는 방법
  • 메서드를 테스트하는 것이 아니라 동작을 테스트하여 테스트 코드의 유지 보수성을 높이는 방법
  • 테스트 이름의 중요성
  • @Before와 @After 애너테이션을 활용하여 공통 초기화 및 정리 코드를 설정하는 방법
  • 거슬리는 테스트를 안전하게 무시하는 방법

 

AAA로 테스트 일관성 유지

 

  1. 준비(Arrange) : 테스트 코드를 실행하기 전에 시스템이 적절한 상태에 있는지 확인한다. 객체들을 생성하거나 이것과 의사소통하거나 다른 API를 호출하는 것 등이다. 드물지만 시스템이 우리가 필요한 상태로 있다면 준비 상태를 생략하기도 한다.
  2. 실행(Act) : 테스트 코드를 실행한다. 보통은 단일 메서드를 호출한다.
  3. 단언(Assert) : 실행한 코드가 기대한 대로 동작하는지 확인한다. 실행한 코드의 반환값 혹은 그 외 필요한 객체들의 새로운 상태를 검사한다. 또 테스트한 코드와 다른 객체들 사이의 의사소통을 검사하기도 한다.
  4. 사후(After) : 때에 따라 테스트를 실행할 때 어떤 자원을 할당했다면 잘 정리되었는지 확인해야한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@FunctionalInterface
public interface Scoreable {
   int getScore();
}
 
public class ScoreCollection {
   private List<Scoreable> scores = new ArrayList<>();
   
   public void add(Scoreable scoreable) {
      scores.add(scoreable);
   }
   
   public int arithmeticMean() {
      int total = scores.stream().mapToInt(Scoreable::getScore).sum();
      return total / scores.size();
   }
}
 
public class ScoreCollectionTest {
   @Test
   public void answersArithmeticMeanOfTwoNumbers() {
      //Arrange
      ScoreCollection collection = new ScoreCollection();
      collection.add(() -> 5);
      collection.add(() -> 7);
      
      //Act
      int actualResult = collection.arithmeticMean();
 
      //Assert
      assertThat(actualResult, equalTo(6));
   }
}
 
cs

 

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

테스트를 작성할 때는 클래스 동작에 집중해야 하며 개별 메서드를 테스트한다고 생각하면 안된다. 은행의 ATM을 예로 들면, 그 클래스의 메서드에는 deposit(),withdraw(),getBalance() 메서드가 있다.(예금,출금,잔액조회) 

 

만약 위와 같은 메서드를 가진 클래스가 존재하고, 출금에 관한 테스트 코드를 작성한다고 생각해보자. 출금은 하나의 메서드로 구현이 되어 있다. 그러면 출금을 테스트하기 위해서는 딱 withdraw()라는 단일 메서드만 신경쓰면 될까? 아니다. 출금을 위해서는 계좌개설 이후에 먼저 입금이 되어있어야 가능하다. 여기서 말하고자 하는 바는 테스트 코드를 작성한다는 것은 하나의 단일 메서드를 테스트하기 위한 테스트 코드가 아닌 하나의 동작, 혹은 일련의 동작의 모음을 테스트 하기 위한 코드 작성으로 생각하여 전체적인 시각에서 테스트코드를 시작, 작성해야 한다는 것이다.

 

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

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

 

단위 테스트는 일방향성이다. 테스트 코드는 프로덕션 코드에 의존하지만, 그 반대는 해당하지 않는다. 프로덕션 코드는 테스트 코드의 존재를 모른다. 하지만 테스트를 작성하는 행위가 프로덕션 시스템의 설계에 영향을 주지 않는다는 것은 아니다. 더 많은 단위 테스트를 작성할수록 설계를 변경했을 때 테스트 작성이 훨씬 용이해지는 경우가 늘어날 수 있다.

 

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

프로덕션 소프트웨어를 배포할 때 테스트를 함께 포함할 수 있지만, 대부분 그렇게 하지 않는다. 그렇게 하면 로딩하는 JAR 파일이 커지고 코드 베이스의 공격 표면(Attack surface)도 늘어난다.

 

  • 테스트를 프로덕션 코드와 같은 디렉터리 및 패키지에 넣기 : 구현하기 쉽지만 어느 누구도 실제 시스템에 이렇게 하지 않는다. 이 정책을 쓰면 실제 배포할 때 테스트 코드를 걷어 내는 스크립트가 필요하다. 클래스 이름으로 구별하거나 테스트 클래스 여부를 식별할 수 있는 리플렉션 코드를 작성해야 한다. 테스트를 같은 디렉터리에 유지하면 디렉터리 목록에서 뒤져야 하는 파일 개수도 늘어난다.
  • 테스트를 별도 디렉터리로 분리하지만 프로덕션 코드와 같은 패키지에 넣기 : 대부분은 이것은 선택한다. 이클립스와 메이븐 같은 도구는 이 모델을 권장한다. src 디렉토리와 별개로 클래스 패스에 test 디렉토리를 두고, 실제 테스트할 클래스의 패키지명을 동일하게 만들어 테스트 클래스를 작성한다. 이렇게 디렉토리와 패키지를 구성하면 각 테스트는 검증하고자 하는 대상 클래스와 동일한 패키지를 갖는다. 즉, 테스트 클래스는 패키지 수준의 접근 권한을 가진다.
  • 테스트를 별도의 디렉터리와 유사한 패키지에 유지하기 : test 디렉터리에 검증 클래스와는 조금 다른 패키지를 생성해 테스트 코드를 유지한다. 테스트 코드를 프로덕션 코드의 패키지와 다르게 하면 public 인터페이스만 활용하여 테스트 코드를 작성한다.

 

단일 목적의 테스트 작성

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class ProfileTest {
    
    private Profile profile;
    private Question question;
    private Criteria criteria;
    
    @Before
    public void init() {
        profile = new Profile("Kakao");
        question = new BooleanQuestion(1"Got bonuses?");
        criteria = new Criteria();
    }
 
   @Test
   public void matchAnswerFalseWhenMustMatchCriteriaNotMet() {
      
      profile.add(new Answer(question, Bool.FALSE));
      criteria.add(new Criterion(new Answer(question, Bool.TRUE), Weight.MustMatch));
     
      boolean matches = profile.matches(criteria);
  
      assertFalse(matches);
   }
   
   @Test
   public void matchAnswersTrueForAnyDonCareCriteria() {
       profile.add(new Answer(question, Bool.FALSE));
       criteria.add(new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare));
       
       boolean matches = profile.matches(criteria);
       
       assertTrue(matches);
   }
   
}
 
===============================================================================================
 
public class ProfileTest {
    
    private Profile profile;
    private Question question;
    private Criteria criteria;
    
    @Before
    public void init() {
        profile = new Profile("Kakao");
        question = new BooleanQuestion(1"Got bonuses?");
        criteria = new Criteria();
    }
   @Test
   public void matchAnswerFalseWhenMustMatchCriteriaNotMet() {
      
      profile.add(new Answer(question, Bool.FALSE));
      criteria.add(new Criterion(new Answer(question, Bool.TRUE), Weight.MustMatch));
     
      boolean matches = profile.matches(criteria);
  
      assertFalse(matches);
 
      profile.add(new Answer(question, Bool.FALSE));
      criteria.add(new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare));
       
      boolean matches = profile.matches(criteria);
       
      assertTrue(matches);
   }
   
}
cs

 

만약 위에 같은 테스트 코드가 있는데, 비슷한 테스트 케이스이기에 아래와 같이 테스트 코드를 하나의 메소드로 합쳤다. 이렇게 코드를 리팩토링하면 @Test 애너테이션이 하나이기에 ProfileTest 인스턴스는 하나만 생성되고 @Before 애너테이션이 붙은 초기화코드도 한번만 초기화되면서 두개를 테스트 해볼 수 있다. 하지만 이러한 리팩토링은 아래와 같은 이점을 잃게 된다.

 

테스트를 분리하는 이점

  • Assert가 실패했을 때 실패한 테스트 이름이 표시되기 때문에 어느 동작에서 문제가 있는지 빠르게 파악할 수 있다.
  • 실패한 테스트를 해독하는 데 필요한 시간을 줄일 수 있다. JUnit은 각 테스트를 별도의 인스턴스로 실행하기 때문이다. 따라서 현재 실패한 테스트에 대해 다른 테스트의 영향을 제거할 수 있다.
  • 모든 케이스가 실행되었음을 보장할 수 있다. Assert가 실패하면 현재 테스트 메서드는 중단된다. Assert 실패는 java.lang.AssertionError를 던지기 때문이다.(JUnit은 이것을 잡아 테스트를 실패로 표시한다.) Assert 실패 이후의 테스트 케이스는 실행되지 않는다.

즉, 단일 목적을 가진 테스트로 나누는 것이 좋다.

 

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

테스트 케이스를 단일 메서드로 결합할수록 테스트 이름 또한 일반적이고 의미를 잃어 간다. 좀 더 작은 테스트로 이동할수록 각각은 분명한 행동에 집중한다. 또 각 테스트 이름에 더 많은 의미를 부여할 수 있다. 테스트하려는 맥락을 제안하기 보다는 어떤 맥락에서 일련의 행동을 호출했을 때 어떤 결과가 나오는지를 명시하는 것이 좋다.

 

예제로 행위 주도 개발(BDD, Behavior-Driven Development)에서 말하는 given-when-then 같은 양식을 사용할 수도 있다.

 

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

 

위와 같은 메서드 이름은 너무 길수도 있다. 그러면 givenSomeContext부분은 제거하여 

 

ex)whenDoingSomeBehaviorThenSomeResultOccurs(어떤 일을 하면 어떤 결과가 나온다.)

 

어느 형식이든지 일관성을 유지하는 것이 중요하다. 주요 목표는 테스트 코드를 다른 사람에게 의미 있게 만드는 것이다. 그리고 주석이 없어도 개발자가 봤을 때, 흐름을 이해할 수 있는 혹은 맥락을 이해할 수 있는 테스크 코드를 작성하는 것이 좋다.

 

@Before와 @After 더 알아보기

연관된 행동 집합에 대해 더 많은 테스트를 추가하면 상당한 테스트 코드가 같은 초기화 부분을 가진다. @Before 메서드를 활용하면 공통적인 초기화 코드를 하나의 메서드로 추출할 수 있어 중복된 코드들을 막을 수 있다.

 

JUnit이 @Before와 @Test 메서드를 어떤 순서로 실행하는지 이해하는 것은 중요하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AssertMoreTest {
   
   @Before
   public void createAccount() {
      // ...
   }
   
   @After
   public void closeConnections() {
      // ...
   }
   
   @Test
   public void depositIncreasesBalance() {
      // ...
   }
   
   @Test
   public void hasPositiveBalance() {
      // ...
   }
}
cs

 

만약 위와 같은 테스트 클래스가 존재한다고 생각해보자. 실행 순서는 어떻게 될까?

 

  1. @Before
  2. @Test(depositIncreasesBalance)
  3. @After
  4. @Before
  5. @Test(hasPositiveBalance)
  6. @After

 

  1. @Before
  2. @Test(hasPositiveBalance)
  3. @After
  4. @Before
  5. @Test(depositIncreasesBalance)
  6. @After

테스트 코드는 위와 같은 순서로 실행된다. 사실 @Test는 어떠한 순서로 실행될지 알 수 없다. 물론 @FixMethodOrder(MethodSorters.NAME_ASCENDING)와 같은 애너테이션을 테스트 클래스에 붙여 메서드 이름의 내림차순,오름차순으로 순서를 지정할 수는 있다. 하지만 조금 특이한 것이 보인다. 초기화와 후처리 메서드가 매 테스트 메서드마다 실행된다는 점이다. 이것은 즉, @Test 메서드를 수행할 때마다, 테스트 클래스는 새로운 인스턴스를 생성하여 @Before,@After를 다시 실행한다는 것이다.

 

이것은 JUnit의 특징이며 중요한 성질이다. JUnit은 우리에게 독립된 테스트 실행을 제공해주는 것이다.

 

@BeforeClass와 @AfterClass 애너테이션

해당 애너테이션이 붙은 메서드는 어떤 테스트를 처음 실행하기 전에 한 번만 실행된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class AssertMoreTest {
   @BeforeClass
   public static void initializeSomethingReallyExpensive() {
      // ...
   }
   
   @AfterClass
   public static void cleanUpSomethingReallyExpensive() {
      // ...
   }
   
   @Before
   public void createAccount() {
      // ...
   }
   
   @After
   public void closeConnections() {
      // ...
   }
   
   @Test
   public void depositIncreasesBalance() {
      // ...
   }
   
   @Test
   public void hasPositiveBalance() {
      // ...
   }
}
cs

 

실행 순서는 아래와 같다.

 

  1. @BeforeClass
  2. @Before
  3. @Test
  4. @After
  5. @Before
  6. @Test
  7. @After
  8. @AfterClass

테스트제외

만약 테스트에서 제외하고 싶은 메서드가 있다면 @Ignore 애너테이션을 붙여준다. 그리고 결과를 보면 스킵된 테스트 개수를 볼 수 있다.(왼쪽 상단의 Runs: 2/2 (1 skipped) )

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AssertMoreTest {
   
   @Test
   public void depositIncreasesBalance() {
      // ...
   }
   
   @Ignore
   @Test
   public void hasPositiveBalance() {
      // ...
   }
}
cs

 

 

여기까지 테스트 코드를 어떻게 조직하며 내부적인 JUnit 동작 메커니즘을 다루어보았다. 사실 너무 간단한 내용만 다루어봤지만 이렇게 하나하나 다루다보면 테스트 코드 작성에 익숙해지는 날이 오지 않을까 싶다.

posted by 여성게
:
Web/TDD 2019. 9. 4. 18:30

 

이번 포스팅에서 다루어볼 예제는 JUnit 3단계 준비,실행,단언 단계중 "단언"의 몇 가지 예제를 다루어볼 것이다. JUnit에 내장된 단언 이외에 햄크레스트(Hamcrest) 라이브러리를 활용한 단언 몇가지도 다루어볼 것이다.

 

JUnit Assert

JUnit에서 assert는 테스트에 넣을 수 있는 정적 메서드 호출이다. 각 Assert 구문은 어떤 조건이 참인지 검증하는 방법이다. 단언한 조건이 참이 아니면 테스트는 그 자리에서 멈추고 실패한다.

 

JUnit은 크게 두 가지 Assert 스타일을 제공한다. 전통적인 스타일의 Assert는 JUnit의 원래 버전에 포함되어 있으며, 새롭고 좀 더 표현력이 좋은 햄크레스트라고 알려진 Assert 구문도 있다.

 

두 가지 Assert 스타일은 각자 다른 환경에서 다른 방식으로 제공된다. 두 가지를 섞어서 사용할 수도 있지만 보통 둘 중 한가지를 선택하여 사용하면 좋다.

 

-assertTrue

가장 기본적인 assert 구문이다.

 

1
org.junit.Assert.assertTrue(BooleanExpression);
cs

 

1
2
3
4
5
6
7
8
9
10
11
@Test
public void hasPositiveBalance() {
   account.deposit(50);
   assertTrue(account.hasPositiveBalance());
}
@Test
public void depositIncreasesBalance() {
   int initialBalance = account.getBalance();
   account.deposit(100);
   assertTrue(account.getBalance() > initialBalance);
}
cs

 

-assertThat

명확한 값을 비교하기 위해 사용한다. 대부분 assert 구문은 기대하는 값과 반환된 실제 값을 비교한다.

 

1
2
3
4
5
6
   @Test
   public void depositIncreasesBalance() {
      int initialBalance = account.getBalance();
      account.deposit(100);
      assertThat(account.getBalance(), equalTo(99));
   }
cs

 

assertThas() 정적 메소드는 햄크레스트 assert의 예이다. 햄크레스트 단언의 첫 번째 인자는 실제 표현식, 즉 우리가 검증하고자 하는 값이다. 두 번째 인자는 매처이다. 매처는 실제 값과 표현식의 결과를 비교한다.

equalTo 매처에는 어떤 자바 인스턴스나 기본형 값이라도 넣을 수 있다. 내부적으로는 equals() 메서드를 사용한다. 자바 기본형은 객체형으로 오토박싱되기 때문에 어떤 타입도 비교할 수 있다.

 

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

 

assertTrue와 동일한 햄크레스트 구문은 아래와 같다.

 

1
2
3
4
5
   @Test
   public void depositIncreasesBalance_hamcrestAssertTrue() {
      account.deposit(50);
      assertThat(account.getBalance() > 0, is(true));
   }
cs

 

이제 기타 햄크레스트 assert 구문을 살펴보자.

 

1
2
3
4
   @Test
   public void matchesFailure() {
      assertThat(account.getName(), startsWith("xyz"));
   }
cs

 

위의 코드는 account.getName()의 문자열이 "xyz"로 시작하는지 테스트하는 코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   @Test
   public void comparesArraysFailing() {
      assertThat(new String[] {"a""b""c"}, equalTo(new String[] {"a""b"}));
   }
 
   @Test
   public void comparesArraysPassing() {
      assertThat(new String[] {"a""b"}, equalTo(new String[] {"a""b"}));
   }
 
   @Test
   public void comparesCollectionsFailing() {
      assertThat(Arrays.asList(new String[] {"a"}), 
            equalTo(Arrays.asList(new String[] {"a""ab"})));
   }
 
   @Test
   public void comparesCollectionsPassing() {
      assertThat(Arrays.asList(new String[] {"a"}), 
            equalTo(Arrays.asList(new String[] {"a"})));
   }
cs

 

위 코드는 배열 혹은 리스트 인자들이 동일하게 들어가있는 지 비교하는 assert 구문이다.

 

경우에 따라 is 데코레이터를 추가하여 매처 표현의 가독성을 더 높일 수 있다. is는 디자인패턴의 데코레이터 패턴을 따른다. is는 단지 넘겨받은 매처를 반환할 뿐(즉, 아무것도 하지 않음)이다. 비록 아무일도 하지 않지만 코드의 가독성을 높여줄 수 있다.

 

1
2
3
4
5
6
7
8
@Test
public void variousMatcherTests() {
   Account account = new Account("my big fat acct");
 
   assertThat(account.getName(), is(equalTo("my big fat acct")));
   assertThat(account.getName(), equalTo("my big fat acct"));
 
}
cs

 

위 코드에서 두 줄의 assertThat의 구문이 하는 역할은 동일하다. 하지만 is 표현식이 붙으므로써 가독성을 높혀주는 효과를 줄 수 있다.

 

1
2
3
4
5
6
7
8
9
   @Test
   public void variousMatcherTests() {
      Account account = new Account("my big fat acct");
      
      assertThat(account.getName(), not(equalTo("plunderings")));
      assertThat(account.getName(), is(not(nullValue())));
      assertThat(account.getName(), is(notNullValue()));
 
   }
cs

 

어떠한 결과의 부정이 테스트 성공 결과로 만들고 싶다면 not구문을 넣어주면 된다. 하지만 위에서 보면 null이 아닌 값을 자주 검사하는 테스트 코드가 나온다면 애플리케이션 설계 문제이거나 지나치게 걱정하는 테스트 코드이다. 많은 경우 널 체크는 불필요하고 가치가 없는 테스트일 수 있다.

 

여기서 작은 차이점을 이해해야한다. 만약 테스트 도중에 NPE이 발생한다면, 테스트는 예외를 발생시키고 비교값이 null인 경우에는 테스트가 테스트 실패를 던진다.

 

기타 다른 햄크레스트 매처를 이용하면 아래와 같은 테스트를 진행할 수 있다.

  • 객체 타입을 검사
  • 두 객체의 참조가 같은 인스턴스인지 검사
  • 다수의 매처를 결합하여 둘 다 혹은 둘 중에 어떤 것이든 성공하는지 검사
  • 어떤 컬렉션이 요소를 포함하거나 조건에 부합하는지 검사
  • 어떤 컬렉션이 아이템 몇 개를 모두 포함하는지 검사
  • 어떤 컬렉션에 있는 모든 요소가 매처를 준수하는지 검사

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
   @Test
   public void variousMatcherTests() {
      Account account = new Account("my big fat acct");
      
      assertThat(account.getName(), is(equalTo("my big fat acct")));
 
      assertThat(account.getName(), allOf(startsWith("my"), endsWith("acct")));
 
      assertThat(account.getName(), anyOf(startsWith("my"), endsWith("loot")));
 
      assertThat(account.getName(), not(equalTo("plunderings")));
 
      assertThat(account.getName(), is(not(nullValue())));
      assertThat(account.getName(), is(notNullValue()));
 
      assertThat(account.getName(), isA(String.class));
 
      assertThat(account.getName(), is(notNullValue())); // not helpful
      assertThat(account.getName(), equalTo("my big fat acct"));
   }
 
   @Test
   public void sameInstance() {
      Account a = new Account("a");
      Account aPrime = new Account("a");
      // TODO why needs to be fully qualified??
      assertThat(a, not(org.hamcrest.CoreMatchers.sameInstance(aPrime)));
   }
 
   @Test
   public void moreMatcherTests() {
      Account account = new Account(null);
      assertThat(account.getName(), is(nullValue()));
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void items() {
      List<String> names = new ArrayList<>();
      names.add("Moe");
      names.add("Larry");
      names.add("Curly");
 
      assertThat(names, hasItem("Curly"));
 
      assertThat(names, hasItems("Curly""Moe"));
 
      assertThat(names, hasItem(endsWith("y")));
 
      assertThat(names, hasItems(endsWith("y"), startsWith("C"))); //warning!
 
      assertThat(names, not(everyItem(endsWith("y"))));
   }
cs

 

부동소수점 수를 두 개 비교

컴퓨터는 모든 부동소수점 수를 표현할 수 없다. 자바에서 부동소수점 타입(float & double)의 어떤 수들은 근사치로 구해야 할 수도 있다. 

 

1
2
3
4
   @Test
   public void doubles() {
       assertThat(2.32*3, equalTo(6.96));
   }
cs

 

위의 테스트는 무사히 통과할 수 있을 까?

 

 

위의 결과처럼 테스트는 실패로 끝나버린다. 그렇다면 부동소수점 비교 테스트는 어떻게 진행할까? 

 

1
2
3
4
   @Test
   public void doubles() {
       assertEquals(2.32*36.960.0005);
   }
cs

 

위와 같이 오차값을 넣어서 테스트를 진행한다. 혹은 햄크레스트의 closeTo 단언문을 사용해도 된다.

 

1
2
3
4
5
6
   import static org.hamcrest.number.IsCloseTo.*;
   
   @Test
   public void doubles() {
       assertThat(2.32*3, closeTo(6.960.0005));
   }
cs

 

발생하길 원하는 예외를 기대하는 세 가지 방법

코드가 항상 기대하는 값을 나오길 기대하는 테스트는 완벽하지 않은 테스트일 수 있다. 때에 따라서 특정 상황에서 어떠한 예외가 발생하길 원하는 테스트를 해봐야할 때도 있다. 어떤 클래스가 예외를 던지는 조건을 이해하면 그 클래스를 사용하는 클라이언트 개발자가 훨씬 사용하기 수월할 것이다.

 

1)단순한 방식: 애너테이션 사용

 

1
2
3
4
   @Test(expected=InsufficientFundsException.class)
   public void throwsWhenWithdrawingTooMuch() {
      account.withdraw(100);
   }
cs

 

위의 코드는 잔고가 없는데, 돈을 인출하여 InsufficientFundsException 예외가 발생하는지 테스트 하는 코드이다. 즉, 예외가 발생해야 테스트가 통과하게 되는 것이다.

 

2)옛 방식: try/catch와 fail

발생한 예외를 처리하는 방법으로 try/catch 블록을 활용할 수도 있다. 예외가 발생하지 않으면 org.junit.Assert.fail() 메서드를 호출하여 강제로 실패한다.

 

1
2
3
4
5
6
7
8
9
10
   @Test
   public void throwsWhenWithdrawingTooMuchTry() {
      try {
         account.withdraw(100);
         fail();
      }
      catch (InsufficientFundsException expected) {
         assertThat(expected.getMessage(), equalTo("balance only 0"));
      }
   }
cs

 

3)새로운 방식: ExpectedException 방식

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   @Rule
   public ExpectedException thrown = ExpectedException.none();  
   
   @Test
   public void exceptionRule() {
      thrown.expect(InsufficientFundsException.class); 
      thrown.expectMessage("balance only 0");  
      
      account.withdraw(100);  
   }
   
   @Test
   public void exceptionRule2() {
       thrown.expect(NullPointerException.class);
       
       throw new NullPointerException();
   }
cs

 

위와 같이 public 으로 ExpectedException 인스턴스를 생성한 후에 @Rule 애너테이션을 붙여준다. 그리고 테스트 코드에 예외 룰을 작성해준다. 단순히 expect()만 호출한다면 해당 테스트 코드에서 해당 예외가 발생하면 테스트를 통과시키고, expectMessage()까지 호출하면 예외클래스+예외메시지까지 동일해야 테스트가 성공한다.

 

4)예외 무시

만약 테스트 코드에서 발생하는 예외를 무시하고 싶다면 그냥 예외를 던진다.

 

1
2
3
4
5
6
   @Test
   public void exceptionRule2() {
       
       throw new NullPointerException();
   }
cs

 

여기까지 JUnit의 assert 구문 몇 가지를 다루어봤다. 사실 이것보다 더 많은 구문이 존재하기 때문에 기타 다른 구문은 공식 레퍼런스를 이용하자!

posted by 여성게
: