JUnit Test Rule 알아보기
JUnit Test Rule에 대하여 알아보도록 하겠습니다.
JUnit의 Rule이란??
Rule은 테스트 클래스에서 동작 방식을 재정의하거나 쉽게 추가하는 것을 말합니다.
사용자는 기존의 Rule을 재사용하거나 확장하는 것이 가능합니다.
JUnit Rule 종류
JUnit은 사용할 수 있는 여러 가지 Rule이 존재합니다. (아래의 표 참고)
여러 가지 Rule에 대하여 자세히 알아보겠습니다.
기본 Rule 클래스
규칙 이름 설명
Rule | Description |
TemporaryFolder | 임시폴더 관리. 테스트 후 삭제 |
ExternalResources | 자원(DB, 파일, 소켓) 관리 |
ErrorCollector | 지속적 테스트 실패 수집 |
Verifier | 별개 조건 확인 (vs assert*) |
TestWatcher | 테스트 인터셉터 (starting, succeeded, failed, finished…) |
TestName | 테스트 메소드명을 알려줌 |
Timeout | 테스트 클래스 전역 timeout 설정 (vs @Timeout) |
ExpectedException | 예외 직접 확인 (vs @Expected) |
DisableOnDebug | Rule 디버그 비활성화 데코레이터 |
RuleChain | 복수 Rule chaining 복합체 |
ClassRule | 테스트슈트 전체에 Rule 적용 |
1. TemporaryFoler Rule
- 임시 폴더, 파일들을 생성할 수 있습니다.
- 테스트가 모두 끝난 후 삭제합니다.
- 기본적으로 resource를 삭제하지 못하는 경우 어떠한 exception도 반환하지 않습니다.
class HasTempFolderTest {
@get:Rule
val folder = TemporaryFolder()
@Test
fun testUsingTempFolder() {
val createdFile = folder.newFile("myfile.txt")
val createdFolder = folder.newFolder("subfolder")
println(createdFile)
println(createdFolder)
Assert.assertEquals(2,folder.root.list().size)
}
}
아래처럼 임시저장소에 저장되는 것을 알 수 있습니다.
//result
/var/folders/8_/pk7f5n_x6j5fsyn6dftvnpz40000gn/T/junit7812537062205433850/myfile.txt
/var/folders/8_/pk7f5n_x6j5fsyn6dftvnpz40000gn/T/junit7812537062205433850/subfolder
2. ExternalResources Rule
- 외부 Resource(DB connect, File, Socket) 초기화 /반환을 관리합니다.
- 특정 자원을 다른 테스트 케이스에서 재사용할 때 유용합니다.
class UsesExternalResourceTest {
internal var myServer = Server()
@get:Rule
val resource: ExternalResource = object : ExternalResource() {
@Throws(Throwable::class)
override fun before() {
myServer.connect()
}
override fun after() {
myServer.disconnect()
}
}
@Test
fun testFoo() {
val user = Client(1)
user.run(myServer)
Assert.assertEquals(1, myServer.activeUser.size)
}
}
3. ErrorCollector Rule
- 에러가 발생하더라도 지속적으로 테스트를 진행하게 도와주는 Rule입니다.
class UsesErrorCollectorTwiceTest {
@get:Rule
val collector = ErrorCollector()
@Test
fun example() {
collector.addError(Throwable("first thing went wrong"))
collector.addError(Throwable("second thing went wrong"))
collector.checkThat("a", equalTo("b"))
collector.checkThat(1, equalTo(2))
println("Test continues even if an error occurs")
}
}
collector에 error를 담으면 test를 진행하면서 발생했던 모든 error의 결과를 알 수 있습니다.
//result
Test continues even if an error occurs
java.lang.Throwable: first thing went wrong
at k.bs.junit.test_rule.errorcollector.UsesErrorCollectorTwiceTest.example(UsesErrorCollectorTwiceTest.kt:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
....
java.lang.Throwable: second thing went wrong
at k.bs.junit.test_rule.errorcollector.UsesErrorCollectorTwiceTest.example(UsesErrorCollectorTwiceTest.kt:16)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
...
java.lang.AssertionError:
Expected: "b"
but: was "a"
Expected :b
Actual :a
<Click to see difference>
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.junit.Assert.assertThat(Assert.java:956)
at org.junit.rules.ErrorCollector$1.call(ErrorCollector.java:65)
at org.junit.rules.ErrorCollector.checkSucceeds(ErrorCollector.java:78)
at org.junit.rules.ErrorCollector.checkThat(ErrorCollector.java:63)
...
java.lang.AssertionError:
Expected: <2>
but: was <1>
Expected :<2>
Actual :<1>
<Click to see difference>
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.junit.Assert.assertThat(Assert.java:956)
at org.junit.rules.ErrorCollector$1.call(ErrorCollector.java:65)
at org.junit.rules.ErrorCollector.checkSucceeds(ErrorCollector.java:78)
at org.junit.rules.ErrorCollector.checkThat(ErrorCollector.java:63)
at org.junit.rules.ErrorCollector.checkThat(ErrorCollector.java:54)
....
4. Verifier Rule
- 테스트 자체를 검증하는 assert 와는 다르게 , 테스트 케이스 실행 후 만족해야 하는 환경조건이나 Global조건(객체들의 종합 상태)을 검사하는 데 사용됩니다.
- 즉 ,**테스트 실행할 때마다 실행되며 사용자 정의 검증 로직을 추가로 넣어 특정 조건을 만족하는지 검증하는 데 사용됩니다.**
class VerifierRuleTest {
private var MAX_AGE = 25
internal var peopleWithAgeGreaterThanMaxAge: MutableList<Person> = ArrayList()
@get:Rule
var verifier: Verifier = object : Verifier() {
public override fun verify() {
assertThat(peopleWithAgeGreaterThanMaxAge.size, CoreMatchers.equalTo(0))
}
}
@Test
fun personTest1() {
val person = Person.Builder()
.name("Frank")
.age(20)
.build()
if (person.age > MAX_AGE) {
peopleWithAgeGreaterThanMaxAge.add(person)
}
}
@Test
fun personTest2() {
val person = Person.Builder()
.name("Angela")
.age(30)
.build()
if (person.age > MAX_AGE) {
peopleWithAgeGreaterThanMaxAge.add(person)
}
}
}
매 테스트 케이스를 수행할 때마다 추가로 verify()가 수행됩니다.
두 번째 테스트 케이스에서 age는 30이 넘으므로 test 가 실패합니다.
5. TestWatcher
- 테스트 Interceptor (starting, succeeded, failed , finished)을 intercept
class WatchermanTest {
@get:Rule
val watchman: TestRule = object : TestWatcher() {
fun applying(base: Statement, description: Description): Statement {
return super.apply(base, description)
}
override fun succeeded(description: Description) {
watchedLog += description.displayName + " " + "success!\n"
}
override fun failed(e: Throwable, description: Description) {
watchedLog += description.displayName + " " + e.javaClass.simpleName + "\n"
}
override fun skipped(e: AssumptionViolatedException, description: Description) {
watchedLog += description.displayName + " " + e.cause + "\n"
}
override fun starting(description: Description) {
super.starting(description)
}
override fun finished(description: Description) {
super.finished(description)
}
}
@Test
fun fails() {
fail()
}
@Test
fun test_success() {
}
companion object {
private var watchedLog = ""
@AfterClass
@JvmStatic
fun teardown() {
println(watchedLog)
}
}
}
테스트 정보를 남기는 코드를 분리하여 기록할 수 있습니다.
6. TestName
- 테스트 메서드명을 얻을 수 있습니다.
class NameRuleTest {
@get:Rule
val name = TestName()
@Test
fun testA() {
assertEquals("testA", name.methodName)
}
@Test
fun testB() {
assertEquals("testB", name.methodName)
}
}
7. Timeout
- 하나의 테스트가 통과하기 위한 timeout을 설정할 수 있습니다.
class HasGlobalTimeout {
@get:Rule
val globalTimeout: TestRule = Timeout.millis(20)
@Test
fun testInfiniteLoop1() {
log += "ran1"
while (true) {
}
}
@Test
fun testInfiniteLoop2() {
log += "ran2"
while (true) {
}
}
companion object {
var log: String = ""
}
}
테스트 케이스마다 timeout을 설정하여 timeout시 에러를 발생시킵니다.
8. ExpectedException
- 예외 직접 확인할 수 있습니다.
- Error 메시지도 검증이 가능합니다.
class HasExpectedException {
@get:Rule
val thrown = ExpectedException.none()
@Test
fun throwsNothing() {
}
@Test
fun throwsNullPointerException() {
thrown.expect(NullPointerException::class.java)
throw NullPointerException()
}
@Test
fun throwsNullPointerExceptionWithMessage() {
thrown.expect(NullPointerException::class.java)
thrown.expectMessage("happened?")
thrown.expectMessage(startsWith("What"))
throw NullPointerException("What happened?")
}
}
9. ClassRule
- TestSuite의 클래스마다 적용할 수 있는 Rule입니다.
*TestSuite란 테스트할 클래스가 하나가 아니라 여럿이라면 묶어서 테스트하는 것입니다.
*
@RunWith(Suite::class)
@Suite.SuiteClasses(A::class, B::class)
class UsesExternalResourceTest {
companion object {
val myServer = Server()
@get:ClassRule
@JvmStatic
val resource: ExternalResource = object : ExternalResource() {
@Throws(Throwable::class)
override fun before() {
myServer.connect()
}
override fun after() {
myServer.disconnect()
}
}
}
}
@RunWith(Suite::class) , @Suite. SuiteClasses(..)를 이용하여
A 클래스의 테스트 케이스와 B클래스의 테스트 케이스를 모두 수행합니다.
10. Rule Chaine
- 여러 개의 Rule을 chain으로 적용할 수 있습니다.
class UseRuleChainTest {
@get:Rule
val chain: TestRule = RuleChain
.outerRule(LoggingRule("outer rule"))
.around(LoggingRule("middle rule"))
.around(LoggingRule("inner rule"))
@Test
fun example() {
assertTrue(true)
}
}
사용자 정의로 생성한 LoggingRule을 체인 형식으로 적용하였습니다.
LoggingRule은 각 테스트 전후로 시작… 끝…. 로그 메시지를 출력하는 Rule로 입니다.
자세한 내용은 아래에서 보도록 하겠습니다.
//result
start: outer rule
start: middle rule
start: inner rule
end: inner rule
end: middle rule
end: outer rule
RuleChain 클래스 method
Method | Description |
emptyRuleChain() | TestRule없이 리턴합니다. RuleChain선언의 시작이 될 수 있습니다. |
outerRule(TestRule outerRule) |
emptyRuleChain().around(outerRule) |
around(TestRule encloseRule) | 기존의 Rule체인을 감까 새로운 룰을 추가합니다. |
위의 예제처럼 around로 감싼 룰들은 아래와 같은 구조를 띄게 됩니다.
(outer ( middle (inner()))
11. Custom Rule
- Custom 한 rule을 생성하여 사용할 수 있습니다.
- TestRule Interface을 구현하여 사용합니다.
class LoggingRule(private val name: String) : TestRule {
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
try {
println("start: $name")
base.evaluate()
} finally {
println("end: $name")
}
}
}
}
}
TestRule interface는 apply함수를 가지고 있습니다.
Stetement : Rule이 사용되는 Junit Runtime 내의 테스트들을 나타냅니다.
base.evaluate()은 테스트 케이스의 실행입니다.
아래와 같이 테스트 전 후로 로그를 찍어주는 Custom TestRule인 것입니다.
println("start: $name") //테스트 전 로그출력
base.evaluate() //테스트실행
println("end: $name") //테스트 후 로그 출력
https://junit.org/junit4/javadoc/4.12/org/junit/runners/model/Statement.html
12. Custom Rule
https://stefanbirkner.github.io/system-rules/
Reference
https://github.com/junit-team/junit4/wiki/Rules
Sample Code 보러가기
https://github.com/qjatjr1108/Junit4-5_Sample/blob/master/README.md
'Android > Test' 카테고리의 다른 글
@BeforeClass, @AfterClass ,@Before,@After 순서 알아보기 (808) | 2019.11.01 |
---|---|
JUnit 테스트 순서 제어하기 (0) | 2019.11.01 |
Mockito-Kotlin Sample로 자세히 알아보기 (0) | 2019.10.28 |
Mockito-Kotlin 알아보기 (0) | 2019.10.28 |
Test Double(테스트 더블)알아보기 (33) | 2019.10.28 |
댓글