알면 유용한 jUnit 활용
Junit Kung Fu 내용을 보고 JUnit 활용에 대해 공부 할 겸 요약 및 정리한 내용이다.
원글 보기 : Junit Kung Fu
Test Naming
Test의 메소드명은 무조건 test라 이름짓지 말고, should를 활용해서 작성하자.
예를 들어 돈을 입출금하는 작업에 대해서 입금처리를 하는 테스트 메소드을 naming 한다면 아래와 같이 작성 할 수 있다. 입금이라는 작업을 한다는 것은 명확하지만 실제로 어떤 동작에 대해 테스트하는지에 대해서는 불명확하다.
testDeposit()
should를 활용하면 어떤 동작을 테스트하고자 하는 것인지 더 명확하게 표현 할 수 있다.
depositShouldAddAmountToAccountBalance() // 계좌에 금액을 더해야한다.
Test Class의 이름은 이 테스트 클래스가 어떤 상황에서 필요한지를 알 수 있도록 작성한다.
WhenYouCreateACell.class
- aDeadCellShouldBePrintedAsAdot();
- aLiveCellShouldBePrintedAsAsterisk();
...
// Grid에서 새로운 셀을 생성할 때 사용하지 않는 셀은 . (dot)로 표시, 사용하는 셀은 * (asterisk) 로 표시
테스트 클래스명은 해당 테스트가 언제 사용되는지를 명시해주고(When~), 메소드명은 어떤 동작(should)을 테스트하는지 알 수 있도록 작성한다.
테스트 메소드는 일관된 형식으로 작성 한다.
보통 Given - When - Then 또는 Arrange - Act - Assert (AAA) 형식으로 작성한다.
- Arrange 단계에서는 test 할 데이터와 expected 되는 데이터를 준비하도록 한다.
- Act 단계에서는 테스트하고자 하는 부분의 동작을 테스트 한다.
- Assert 단계에서는 Act 단계의 결과 값이 expected 데이터와 일치하는지 확인한다.
테스트 코드는 결과물과 마찬가지 이다.
실제로 실행되는 프로그램이나 서비스만큼 테스트코드 또한 주요한 결과물 중에 하나이다. 따라서 항상 refactoring을 통해서 clean 한 코드를 유지하고, 읽기 쉬운 코드로 작성하도록 해야한다.
Hamcrest 활용하기
import static org.junit.Assert.*;
...
assertEquals(10000, calculatedTax, 0);
보통 실제 값과 기대 값이 일치하는지를 비교할 때 assertEquals를 많이 쓰게 된다.
assertEquals는 첫 번째 인자 값이 기대값이고, 두 번째 인자값이 실제 값이다. 마지막은 오차 허용범위값이다.
나는 매번 사용 할 때마다, 기대 값과 실제 값의 위치가 헷갈린다.
하지만, 두 파라미터의 순서를 바꿔도 테스트하는데 큰 오류는 없기에 순서를 고려하지 않은 경우가 많았다.
import static org.hamcrest.Matchers.*;
...
assertThat(calculatedTax, is(10000));
이 코드는 hamcrest 의 matcher를 사용한 코드이다. assertEquals를 사용한 코드와 동일한 동작을 하는 코드이다. 하지만 첫 번째 코드에 비해 더 명확해보인다. 왜냐하면 cacculatedText is 10000 처럼 쉽게 이해가 되기 때문이다.
assertThat() 은 첫 번째에 실제 값을 적고, 두 번째 인자값은 matcher를 추가한다. 실제 값이 matcher을 통과한 값이면 테스트코드가 통과하게 된다.
뿐만 아니라 is() 과 같은 core matcher들을 사용하면 좀 더 명확하고 쉽게 테스트 코드를 작성 할 수 있으므로 테스트 코드 작성에 적극 활용하자.
String[] colors = new String[] {"red","green","blue"};
String color = "yellow";
assertThat(color, not(isIn(colors)));

위의 코드와 같이 여러 기본 matcher를 혼합하여 사용이 가능하기 때문에 다양한 유형의 assert 를 작성 할 수 있다. 또한 custom matcher를 개발자가 직접 개발 할 수 도 있다.
Core | |
any() | Matches anything |
is() | A matcher that checks if the given objects are equal. |
describedAs() | Adds a descrption to a Matcher |
Logical | |
allOf() | Takes an array of matchers, and all matchers must match the target object. |
anyOf() | Takes an array of matchers, and at least one of the matchers must report that it matches the target object. |
not() | Negates the output of the previous matcher. |
Object | |
equalTo() | A matcher that checks if the given objects are equal. |
instanceOf() | Checks if the given object is of type X or is compatible with type X |
notNullValue() | Tests whether the given object is null or not null. |
nullValue() | Tests whether the given object is null or not null. |
sameInstance() | Tests if the given object is the exact same instance as another. |
더 자세한 내용은 matchers 참고!
Junit Rules 활용하기
The Temporary Folder Rule
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class LoadDynamicPropertiesTest {
// 임시 폴더를 생성한다.
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private File properties;
// 테스트 수행 전 test 데이터를 준비한다.
@Before
public void createTestData() throws IOException {
properties = folder.newFile("messages.properties");
BufferedWriter out = new BufferedWriter(new FileWriter(properties));
// Set up the temporary file
out.close();
}
// 테스트에서 해당 임시 폴더를 사용한다.
@Test
public void shouldLoadFromPropertiesFile() throws IOException {
System.out.println(properties.getAbsolutePath());
}
}
/var/folders/l0/w_28q17x06zgmd2xvs_nq73w0000gn/T/junit6045199402712718193/messages.properties
테스트를 위한 임시 폴더를 생성한다.
임시 폴더 이기때문에 테스트 후 해당 폴더는 삭제된다.
The ErrorController Rule
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import static org.hamcrest.core.StringContains.*;
import static org.hamcrest.CoreMatchers.not;
public class ErrorCollectorTest {
@Rule
public ErrorCollector collector = new ErrorCollector();
@Test
public void testSomething() {
String result = doStuff();
collector.addError(new Throwable("first thing went wrong"));
collector.addError(new Throwable("second thing went wrong"));
collector.checkThat(result, not(containsString("Oh no, not again")));
}
private String doStuff() {
return "Oh no, not again";
}
}
테스트 실행 중 에러가 발생해도 실행을 중단하지 않고, 테스트 에러 발생결과를 출력한다. first thing went wrong, second thing went wrong, Oh no, not again 모두 에러가 발생하는 코드이지만 결과 내용을 보면 에러가 발생하여도 중단하지 않고 모든 에러 리스트를 보여준다.
The TimeOut Rule
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
public class TaxCalculatorDataTest {
@Rule
public Timeout globalTimeout = new Timeout(1000);
@Test
public void testSomething() {
for(;;);
}
@Test
public void testSomethingElse() {
System.out.println("test");
}
}
비기능적인 요구사항으로 정해진 시간 이내에 처리되어야 할 메소드가 있다면, TimeOut Rule을 사용하면 된다. 위의 코드 내용대로 하면 1000ms내에 처리되지 않으면 테스트 코드가 실패하게 된다.
The Verifier Rule
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Verifier;
public class VerifierTest {
private List<String> systemErrorMessages = new ArrayList<String>();
@Rule
public Verifier verifier = new Verifier(){
@Override
protected void verify() throws Throwable {
assertThat(systemErrorMessages.size(), is(0));
}
};
@Test
public void testSomething() {
// ...
systemErrorMessages.add("Oh, bother!");
}
}
각 테스트 메소드에 대해서 verify() 부분의 코드를 체크한다.
해당 테스트 코드는 systemErrorMessage에 내용이 있기 때문에 테스크코드가 실패한다.
The Watchman Rule
public class TaxCalculatorDataTest {
@Rule
public TestWatchman watchman = new TestWatchman(){
public void failed(Throwable e, org.junit.runners.model.FrameworkMethod method) {
System.out.println( method.getName() + " " + e.getClass().getSimpleName());
}
public void succeeded(org.junit.runners.model.FrameworkMethod method) {
System.out.println( method.getName() + " " + "success!");
}
};
@Test
public void fails() {
assertEquals(1, 0);
}
@Test
public void succeeds() {
assertEquals(1, 1);
}
}
각 메소드가 실패하거나 성공했을 때 실행 될 Call을 설정 할 수 있다. fails() 테스트 코드의 경우 실패하는 코드 이므로 failed() 가 실행되며, succeeds() 테스트 코드는 성공하는 코드 이므로 succeeded() 가 실행된다.
jUnit Categories
각 클래스의 테스트를 카테고리별 나누어 실행 그룹을 설정 할 수 있다. 테스트 Annotation 기법으로 쉽게 그룹핑을 할 수 있다.
적용하는 방법 3단계로 나누어진다.
- 카테고리들을 정의한다.
public interface IntegrationTests {}
public interface PerformanceTests {}
public interface PerformanceTests extends IntegrationTests {}
예제에서는 통합테스트, 성능테스트로 카테고리를 지정하였고, extends를 통해 카테고리간의 상속관계도 지정 할 수 있다.
- 테스트 메소드 별로 annotation을 통해 카테고리를 지정해준다.
public class CellTest {
// 성능 카테고리
@Category(PerformanceTests.class)
@Test
public void aLivingCellShouldPrintAsAPlus() {...}
// 통합 카테고리
@Category(IntegrationTests.class)
@Test
public void thePlusSymbolShouldProduceALivingCell() {...}
// 일반 테스트 메소드
@Test
public void theMinusSymbolShouldProduceADeadCell() {...}
}
2-1. 테스트 클래스 전체 메소드에 카테고리를 지정하려면 테스트 클래스 위에 annotation을 작성한다.
@Category(IntegrationTests.class)
public class CellTest {
...
}
3 . 카테고리별 테스트 클래스를 생성하여 카테고리 테스트를 한다.
import org.junit.experimental.categories.Categories.IncludeCategory;
import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Categories.class)
@IncludeCategory(PerformanceTests.class) // 성능 카테고리에 해당하는 테스트만 실행한다.
@ExcludeCategory(PerformanceTests.class) // 성능 카테고리에 해당하는 테스트들은 제외한다.
@SuiteClasses( { CellTest.class, WhenYouCreateANewUniverse.class }) // 어떤 테스트 클래스에 대해서 작업을 수행 할지 정해준다.
public class PerformanceTestSuite {
//....
}