본문 바로가기
Android/Test

JUnit Test Rule 알아보기

by 봄석 2019. 11. 1.

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

 

Statement (JUnit API)

abstract  void evaluate()           Run the action, throwing a Throwable if anything goes wrong.

junit.org

 

 

12. Custom Rule

 

https://stefanbirkner.github.io/system-rules/

 

System Rules

System.in The TextFromStandardInputStream rule helps you to create tests for classes which read from System.in. You specify the lines provided by System.in, by calling provideLines(String...). The example's class under test reads two numbers from System.in

stefanbirkner.github.io

 

 

 

Reference

https://github.com/junit-team/junit4/wiki/Rules

 

junit-team/junit4

A programmer-oriented testing framework for Java. Contribute to junit-team/junit4 development by creating an account on GitHub.

github.com

 

 

Sample Code 보러가기

https://github.com/qjatjr1108/Junit4-5_Sample/blob/master/README.md

 

 

댓글