CHAPTER09. JUnit으로 테스팅하기 (Junit4 기준)
JUnit 테스트
- TDD(Test-Driven Development)를 구현하기 위한 대표적인 테스팅 프레임워크
- Q & A가 모두 존재하는 상황에서 풀이과정을 만들면서 A가 나오는지를 확인하기 위한 과정
- 모든 함수와 모든 가능한 A에 대해서 작성하면 좋지만 쉽지 않다
- 특히 초기 개발 단계에서 메소드 시그니처가 변경되는 일이 자주 발생할수 있고, 아예 별로 클래스로 분리해버린다거나 코드 구조를 자주 바꾸는 상황이 생길 수 있는데 이때 모든 테이스를 변경하는게 생각보다 쉽지 않다
Q1. JUnit 테스트를 통해 얻는 가치는 무엇인가
- 개인적으로 생각하는 가장 큰 가치는 유지보수의 용이성이 아닐까
- 코드를 작성하다보면 작년의 나, 저번달의 나에게 '왜 이렇게 짰어?..' 라고 묻는 경우가 있다. 혹은 그 당시 개발했던 요건이 기억나지 않는다던가
- 하지만 완성해놓은 테스트 코드는 답을 알고 있기 때문에 그 결과가 나오도록 수정만 하면된다
- 소스 리팩토링 시에도 모든 테스트 코드가 통과하도록 리팩토링 해나가면 맞게 수정했는지 의심할 필요가 없어진다. (물론 통합테스트에서 문제가 생길 가능성은 예외다)
- 요건 변경으로 Answer가 바뀌었다면 테스트 코드의 expect value들만 변경하고 코드를 수정하면 자신감 있게 코드를 수정할 수 있다.
- 특히 남이 짜던 코드를 받았을때 테스트 코드가 있을 때와 없을때를 가정한다면...
Q2. JUnit 테스트는 어떻게 실행하는가
1. IDE 활용
- 어지간한 IDE는 UI로 Run|Debug 버튼을 제공해주니 버튼눌러서 실행하면된다.
2. cli 활용
- cli 에서도 junit.jar 의존성을 추가해서 실행이 가능하다.
- 매우 귀찮으니까 하지말자(차라리 maven을 써서 돌려라)
# java compile with junit & hamcrest library(useless junit ^4.9.0)
javac -cp .:/Users/sangjunlee/.m2/repository/junit/junit/4.12/junit-4.12.jar:/Users/sangjunlee/.m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar ChapterNineTest.java
## run
java -cp .:/Users/sangjunlee/.m2/repository/junit/junit/4.12/junit-4.12.jar:/Users/sangjunlee/.m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar org.junit.runner.JUnitCore ChapterNineTest
3. maven 등 빌드 툴 활용
# Run all test
mvn clean test
# Run specific test class
mvn clean test -Dtest=com.mbio.book.ChapterNineTest
# Run sepcific test method
mvn clean test -Dtest=com.mbio.book.ChapterNineTest#testAssertArrayEquals
JUnit 테스트 생명 주기
- Junit 4 기준
Q3. JUnit 테스트를 실행할 때 어떤 일이 일어나는가
1.BeforeClass
- public static void no-arg method 에서만 쓰여야 한다
- JVM class loading과 무관하게 JUnit 이 명시적으로 Test Class instance가 호출되기 전에 한번 만 수행해준다.
- 생성 비용이 비싼 static resource 초기화에 사용한다.
- Junit5의 @BeforeAll 과 동일
2.Before
- public void no-arg method 에서만 쓰여야 한다
- 모든 test 어노테이션 전에 실행되는 어노테이션
- 코드 중복 방지를 위해서 쓰거나, 테스트 전 리소스 초기화 등을 위해 사용한다.
- JUnit5의 @BeforeEach 와 동일
3.Test
- 실제 테스틀 위한 public void mehtod
- 언제 어떤 test 어노테이션이 실행될 지 알 수 없으므로, 모든 테스트 메소드는 의존성 없이 독립적으로 작성되어야 한다
4.After
- Before와 거의 유사하며 모든 test 어노테이션 수행 후에 수행된다
5.AfterClass
- BeforeClass 와 거의 유사하며 static resouce 해제에 사용
JUnit LifeCycle 확인
@BeforeClass static method invoked.
Constructor invoked. Instance: com.mbio.book.ChapterNineTest@42110406
@Before method invoked. Instance: com.mbio.book.ChapterNineTest@42110406
test method name : testAssertNotNull
@After method invoked. Instance: com.mbio.book.ChapterNineTest@42110406
Constructor invoked. Instance: com.mbio.book.ChapterNineTest@30946e09
@Before method invoked. Instance: com.mbio.book.ChapterNineTest@30946e09
test method name : testAssertNotSame
@After method invoked. Instance: com.mbio.book.ChapterNineTest@30946e09
@AfterClass static method invoked.
JUnit 사용의 좋은 예
Q4. 테스트가 성공인지 어떻게 증명할 수 있는가
- JUnit의 핵심인 Assert를 이용해 예상값과 일치하는지 확인
assertEquals : 두 개의 객체가 자신들의 equals 메소드에 따라 같은지 비교
assertArrayEquals : 두 배열에 같은 값이 있는지 검증
assertTrue & assertFalse : 주어진 상태를 Boolean 예상 값과 비교
assertNotNull : 객체가 null이 아님
assertThat : 전달된 객체가 Matcher 조건에 맞는지 검증
Sample Code
public class ChapterNineTest {
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertTrue() {
assertTrue("failure - should be true", true);
}
@Test
public void testAssertFalse() {
assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
assertSame("should be same", aNumber, aNumber);
}
// JUnit Matchers assertThat
@Test
public void testAssertThatBothContainsString() {
assertThat("albumen", both(containsString("a")).and(containsString("b")));
}
@Test
public void testAssertThatHasItems() {
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
}
@Test
public void testAssertThatEveryItemContainsString() {
assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
}
@Test(expected = IllegalAccessException.class)
public void testExpected() throws IllegalAccessException {
throw new IllegalAccessException();
}
// Core Hamcrest Matchers with assertThat
@Test
public void testAssertThatHamcrestCoreMatchers() {
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.<Integer>either(equalTo(3)).or(equalTo(4))));
assertThat(new Object(), not(sameInstance(new Object())));
}
}
Q5. 어떻게 특정 예외를 예상할 수 있는가
@Test
anotation에 expected를 추가해서 에러 발생을 정상 테스트로 만들 수 있음- Custom Exception을 만들었거나, 원치 않는 값이 입력되었을때 정상적으로 에러를 뱉어내는지 검증하고 싶으면 사용한다
expected example
@Test(expected = IllegalAccessException.class)
public void testExpected() throws IllegalAccessException {
System.out.println(getCurrentMethodName());
throw new IllegalAccessException();
}
Q6. 테스트가 예상했던 시간 안에 완료되지 않으면 테스트가 실패하게 만들 수 있는가
@Test
anotation에 timeout 속성을 추가해서 할 수 있음
@Test(timeout = 1000L)
public void testTimeout() {
try {
Thread.sleep(1001);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Q7. @RunWith 어노테이션은 어떻게 작동하는가
@RunWith
anotation은 Runner를 상속받은 class와 함께 사용되는 Class수준의 어노테이션- 개별적으로 아래의 Runner를 구현해 별개의 TestRunner를 작성할 수 있다.
- 실행할 테스트 목록을 가져오는 getDescription 과 실제 테스틀 수행할 run 메소드를 작성해서 개별 테스트 러너를 만들고 RunWith 어노테이션으로 실행이 가능하다.
public abstract class Runner implements Describable {
/*
* (non-Javadoc)
* @see org.junit.runner.Describable#getDescription()
*/
public abstract Description getDescription();
/**
* Run the tests for this runner.
*
* @param notifier will be notified of events while tests are being run--tests being
* started, finishing, and failing
*/
public abstract void run(RunNotifier notifier);
/**
* @return the number of tests to be run by the receiver
*/
public int testCount() {
return getDescription().testCount();
}
}
Q8. 실행 중인 테스트들을 사용자화하려면 어떻게 해야 하는가
- Q7로 대체
목으로 의존성 제거하기
Q9. 단위 테스트와 통합 테스트의 차이는 무엇인가
- 단위 테스트란 의존성 없이 독립적으로 하나의 기능 단위별(함수별, 클래스별 등) 개발 코드가 정상적으로 동작하는지를 테스트 하는 것
- 통합 테스트란 DB, MQ, 서버 호출 등 여러 환경과 통합된 테스트를 수행하는 것
- 하지만 완전히 의존성 없는 테스트가 불가능 하기 때문에 고안된 것이 바로 Mock을 이용하는 방법이다. 대표적으로 Mockito 라이브러리가 사용된다.
Q10. 테스트의 의미가 더 잘 표현되게 하려면 어떤 방법을 사용해야 하는가
- Hamcrest를 이용하면 좀더 가독성 있는 Matchers 표현이 가능하다.
- 모국어가 영어면 책읽듯이 읽을거같다
@Test
public void testAssertThatHamcrestCoreMatchers() {
System.out.println(getCurrentMethodName());
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.<Integer>either(equalTo(3)).or(equalTo(4))));
assertThat(new Object(), not(sameInstance(new Object())));
}
행위 주도 개발을 이용해 시스템 테스트 만들기
Q11. 행위 주도 개발이란 무엇인가
- BDD(Behavior-Driven Development) 행위 주도 개발
- 시나리오를 기반으로 테스트 케이스 작성
- 비 개발자가 봐도 읽을 수 있을 정도로 이해할 수 있는 수준으로 작성
- 테스트 케이스가 그자체로 요구사항이 되도록
- 가장 대표적인 무료 라이브러리로는 cucumber가 있다
기본 패턴
Feature : 테스트에 대상의 기능/책임을 명시
Scenario : 테스트 목적에 대한 상황을 설명
Given : 시나리오 진행에 필요한 값을 설정
When : 시나리오를 진행하는데 필요한 조건을 명시
Then : 시나리오를 완료했을 때 보장해야 하는 결과를 명시