TDD - JUnit assert(단언,assertThat,assertEqual,assertTrue,asserrtFalse,is..) 예제

2021. 4. 17. 17:33 테스트 코드/JUnit

 

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

 

JUnit Assert

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

 

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

 

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

 

-assertTrue

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

org.junit.Assert.assertTrue(BooleanExpression);
@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);
}

 

-assertThat

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

   @Test
   public void depositIncreasesBalance() {
      int initialBalance = account.getBalance();
      account.deposit(100);
      assertThat(account.getBalance(), equalTo(99));
   }

 

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

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

 

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

 

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

   @Test
   public void depositIncreasesBalance_hamcrestAssertTrue() {
      account.deposit(50);
      assertThat(account.getBalance() > 0, is(true));
   }

 

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

   @Test
   public void matchesFailure() {
      assertThat(account.getName(), startsWith("xyz"));
   }

 

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

   @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"})));
   }

 

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

 

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

@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"));
 
}

 

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

   @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()));
 
   }

 

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

 

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

 

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

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

 

부동소수점 수를 두 개 비교

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

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

 

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

 

 

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

   @Test
   public void doubles() {
       assertEquals(2.32*3, 6.96, 0.0005);
   }

 

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

   import static org.hamcrest.number.IsCloseTo.*;
   
   @Test
   public void doubles() {
       assertThat(2.32*3, closeTo(6.96, 0.0005));
   }

 

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

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

 

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

   @Test(expected=InsufficientFundsException.class)
   public void throwsWhenWithdrawingTooMuch() {
      account.withdraw(100);
   }

 

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

 

2)옛 방식: try/catch와 fail

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

   @Test
   public void throwsWhenWithdrawingTooMuchTry() {
      try {
         account.withdraw(100);
         fail();
      }
      catch (InsufficientFundsException expected) {
         assertThat(expected.getMessage(), equalTo("balance only 0"));
      }
   }

 

3)새로운 방식: ExpectedException 방식

   @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();
   }

 

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

 

4)예외 무시

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

   @Test
   public void exceptionRule2() {
       
       throw new NullPointerException();
   }

 

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



출처: https://coding-start.tistory.com/259?category=814944 [코딩스타트]