-
14-3. 코틀린 테스팅 : 픽스쳐와 설정Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 12. 18. 16:28
14장 - 코틀린 테스팅
14-3. 픽스쳐와 설정
14-3. 픽스쳐와 설정
1) 픽스처 제공하기
테스트 픽스처란 실제 테스트를 진행하기 위해 필요한 환경과 자원을 초기화하고
테스트가 끝나면 정리해야 하는 경우를 말한다.
코테스트에서는 TestListener 인터페이스를 구현해 픽스처를 지정할 수 있다.
TestListener 인터페이스는 BeforeTestListener 등의 개별 픽스처 인터페이스를 한데 모아둔 리스너이다.
package fixture import io.kotest.core.listeners.* import io.kotest.core.spec.Spec import io.kotest.core.spec.materializeAndOrderRootTests import io.kotest.core.spec.style.FunSpec import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.matchers.shouldBe import kotlin.reflect.KClass object SpecLevelListener : TestListener { override suspend fun prepareSpec(kclass: KClass<out Spec>) { println("PrepareSpec(in SpecLevelListener) : ${kclass.qualifiedName}") } override suspend fun beforeSpec(spec: Spec) { println("BeforeSpec: ${spec.materializeRootTests().joinToString { it.testCase.displayName }}") } override suspend fun afterTest(testCase: TestCase, result: TestResult) { println("AfterTest: ${testCase.displayName}") } override suspend fun afterSpec(spec: Spec) { println("AfterSpec: ${spec.materializeAndOrderRootTests().joinToString { it.testCase.displayName }}") } override suspend fun finalizeSpec( kclass: KClass<out Spec>, results: Map<TestCase, TestResult>) { println("FinalizeSpec(in SpecLevelListener): ${kclass.qualifiedName}") } class NumbersTestWithFixture1 : FunSpec() { init { context("Addition") { test("2 + 2") { 2 + 2 shouldBe 4 } test("4 + 4") { 4 + 4 shouldBe 8 } } } override fun listeners() = listOf(SpecLevelListener) } class NumbersTestWithFixture2 : FunSpec() { init { context("Multiplication") { test("2 * 2") { 2 * 2 shouldBe 4 } test("4 * 4") { 4 * 4 shouldBe 16 } } } override fun listeners() = listOf(SpecLevelListener) } } //BeforeSpec: Addition //AfterTest: 2 + 2 //AfterTest: 4 + 4 //AfterTest: Addition //AfterSpec: Addition //BeforeSpec: Multiplication //AfterTest: 2 * 2 //AfterTest: 4 * 4 //AfterTest: Multiplication //AfterSpec: Multiplication
beforeSpec() / beforeTest()의 차이는
beforeTest()는 테스트마다 실행되고 테스트가 활성화될 경우에만 호출되는 반면,
beforeSpec()은 어떤 명세가 인스턴스화될 때 실행된다.(afterTest() / afterSpec()도 동일)
따라서 테스트 함수가 실제 호출될 때 불러야 하는 준비/정리 코드는 beforeTest()/afterTest()를
명세 클래스의 인스턴스마다 하나씩 필요한 준비/정리 코드는 beforeSpec()/afterSpec()을 사용한다.
TestListener에 정의된 prepareSpec, finalizeSpec은
개별 Spec 안에서 listeners를 오버라이드 할 경우 동작하지 않는 것도 확인할 수 있다.
프로젝트 수준의 리스너인 beforeProject()/afterProject() 구현을 하고 싶은 경우
ProjectConfig 타입의 싱글턴 객체에 리스너를 등록해야 한다.
위 예제에서 사용한 코드에서 클래스만 그대로 사용하고
ProjectConfig, beforeProject(), afterProject(), prepareSpec(), finalizeSpec()을 추가한 코드이다.
package fixture import io.kotest.core.config.AbstractProjectConfig import io.kotest.core.listeners.* import io.kotest.core.spec.Spec import io.kotest.core.spec.materializeAndOrderRootTests import io.kotest.core.spec.style.FunSpec import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.matchers.shouldBe import kotlin.reflect.KClass object MyProjectListener : ProjectListener, TestListener { override val name: String = "MyProjectListener" override suspend fun beforeProject() { println("Before project") } override suspend fun afterProject() { println("After project") } override suspend fun prepareSpec(kclass: KClass<out Spec>) { println("PrepareSpec : ${kclass.qualifiedName}") } override suspend fun finalizeSpec(kclass: KClass<out Spec>, results: Map<TestCase, TestResult>) { println("FinalizeSpec : ${kclass.qualifiedName}") } class NumbersTestWithFixture1 : FunSpec() { init { context("Addition") { test("2 + 2") { 2 + 2 shouldBe 4 } test("4 + 4") { 4 + 4 shouldBe 8 } } } override fun listeners() = listOf(MyProjectListener) } class NumbersTestWithFixture2 : FunSpec() { init { context("Multiplication") { test("2 * 2") { 2 * 2 shouldBe 4 } test("4 * 4") { 4 * 4 shouldBe 16 } } } override fun listeners() = listOf(MyProjectListener) } } //Before project //PrepareSpec : fixture.MyProjectListener.NumbersTestWithFixture1 //FinalizeSpec : fixture.MyProjectListener.NumbersTestWithFixture1 //PrepareSpec : fixture.MyProjectListener.NumbersTestWithFixture2 //FinalizeSpec : fixture.MyProjectListener.NumbersTestWithFixture2 //After project
추가로 AutoCloseable 인터페이스를 구현해 자원을 자동으로 해제할 수 있다.
이 기능을 동작시키기 위해서는 자원을 할당할 때 autoClose() 호출로 감싸야 한다.
import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import java.io.FileReader class FileTest : FunSpec() { private val reader = autoClose(FileReader("data.txt")) init { test("Line count") { reader.readLines().isNotEmpty() shouldBe true } } }
2) 테스트 설정
코테스트는 테스트 환경을 설정할 수 있는 여러 수단을 제공한다.
명세 함수가 제공하는 config() 함수를 통해 여러 테스트 실행 파라미터를 지정할 수 있다.
일반적으로 config()가 적용된 테스트 블록의 동작을 변경한다.
config() 함수를 사용해 동작을 변경할 수 있는 파라미터는 아래와 같다.
● invocations : 테스트 실행 횟수. 모든 실행이 성공해야 테스트가 성공한 것으로 간주
간헐적으로 실패하는 비결정적 테스트가 있을 때 유용함
● threads : 테스트를 실행할 스레드 수. invocations가 2 이상일 때만 이 옵션이 유용함
● enabled : 테스트 실행 여부. false로 설정 시 테스트 실행을 비활성화
● timeout : 테스트 실행에 걸리는 최대 시간.
테스트 실행 시간이 이 값을 넘어서면 테스트가 종료되고 실패로 간주. 비결정적 테스트에 유용
아래 코드는 config() 함수를 이용한 코드이다.
import io.kotest.core.spec.style.BehaviorSpec import io.kotest.core.spec.style.ShouldSpec import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import kotlin.time.Duration.Companion.minutes import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) class StringSpecWithConfig : StringSpec({ "2 + 2 should be 4".config(invocations = 10) { (2 + 2) shouldBe 4 } }) @OptIn(ExperimentalTime::class) class ShouldSpecWithConfig : ShouldSpec({ context("Addition") { context("1 + 2") { should("be equal to 3").config(threads = 2, invocations = 100) { (1 + 2) shouldBe 3 } should("be equal to 2 + 1").config(timeout = 1.minutes) { (1 + 2) shouldBe (2 + 1) // 실패하는데 이유를 모르겠음 } } } }) @OptIn(ExperimentalTime::class) class BehaviorSpecWithConfig : BehaviorSpec({ Given("Arithmetic") { When("x is 1") { val x = 1 And("increased by 1") { then("result is 2").config(invocations = 100) { (x + 1) shouldBe 2 } } } } })
각 테스트를 개별적으로 설정하는 방법 외에도 defaultConfig() 함수를 오버라이드해
한 명세에 속한 모든 테스트 케이스의 설정을 한꺼번에 변경하는 방법도 있다.
import io.kotest.core.spec.style.StringSpec import io.kotest.core.test.TestCaseConfig import io.kotest.matchers.shouldBe class StringSpecWithConfig2 : StringSpec({ "2 + 2 should be 4" { (2 + 2 shouldBe 4)} }) { override fun defaultConfig(): TestCaseConfig = TestCaseConfig(invocations = 10, threads = 2) }
코테스트의 기능중 테스트 사이의 테스트 케이스 인스턴스를 공유하는 방법인 격리 모드(Isolation mode)가 있다.
모든 테스트 케이스는 기본적으로 한 번만 인스턴스화되고, 모든 테스트 케이스에 같은 인스턴스를 사용한다.
성능상으로는 유리하지만, 일부 시나리오의 경우 이런 동작을 원하지 않을 수 있다.
즉 테스트 그룹을 시작할 때 마다 인스턴스화하고 싶을 경우 isolationMode 프로퍼티를 오버라이드하면 된다.
이 프로퍼티는 IsolationMode 이넘에 속하는 세가지 상수 중 하나를 지정한다.
● SingleInstance : 테스트 케이스의 인스턴스가 하나만 생성. 디폴트 동작
● InstancePerTest : 문맥이나 테스트 블록이 실행될 때 마다 테스트 케이스의 새 인스턴스 생성
● InstancePerLeaf : (말단에 있는)개별 테스트 블록을 실행하기 전에 테스트가 인스턴스화
앞에서 사용했던 FunSpec 스타일의 테스트 케이스를 이용해 예시를 보자면,
import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe class IncTest : FunSpec() { var x = 0 init { context("Increment") { println("Increment") test("prefix") ++x shouldBe 1 // OK } test("postfix") { println("postfix") x++ shouldBe 0 // Fail } } }
직전 테스트에서 대입한 값이 x 변수에 들어있기 때문에 두 번째 테스트가 실패한다.
이 때 격리 모드를 InstancePerTest로 변경하면, 두 테스트를 모두 성공시킬 수 있다.
격리 모드를 변경하는 코드를 추가한 뒤 다시 실행해보면 정상적으로 동작하는 것을 확인할 수 있다.
import io.kotest.core.config.AbstractProjectConfig import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe class IncTest : FunSpec() { var x = 0 init { context("Increment") { println("Increment") test("prefix") ++x shouldBe 1 // OK } test("postfix") { println("postfix") x++ shouldBe 0 // OK } } } object IncTestProjectConfig : AbstractProjectConfig() { override val isolationMode = IsolationMode.InstancePerTest }
여기까지가 14장 코테스트에 대한 내용이다.
사실 코틀린도 익숙하지 않은데 여기서 테스트로 들어가니 테스트 개념도 명확하게 잡혀있지 않아
익숙하지 않음이 두배가 되어서 어느정도 이해는 했지만, 실제로 사용해보기는 어려워 보인다.
그래도 나중에 가면 테스트 코드는 반드시 필요할 것 이라고 생각하기 때문에
정리해두면 언젠간 쓸 일이 분명이 있을 것 이라고 생각한다.
반응형'Study(종료) > Kotlin 22.09.13 ~ 12.18' 카테고리의 다른 글
코틀린 스터디 회고 (0) 2023.01.03 14-2. 코틀린 테스팅 : 단언문(assertion) (2) 2022.12.17 14-1. 코틀린 테스팅 : 코테스트 명세 (0) 2022.12.16 13-4. 동시성 : 자바 동시성 사용하기 (2) 2022.12.11 13-3. 동시성 : 동시성 통신 (1) 2022.12.11