Kotest 테스트 팩토리(Test Factories)

때로는 일반 테스트 세트를 작성한 다음 특정 입력에 재사용하고 싶을 때가 있다. Kotest에서는 하나 이상의 스펙에 포함될 수 있는 테스트를 생성하는 테스트 팩토리를 통해 이를 수행할 수 있다.

개요

자체 컬렉션 라이브러리를 만들고 싶다고 가정해 보자. 다소 진부한 예이지만 문서화 목적에 잘 부합하는 예이다.

ListVector라는 두 가지를 구현할 IndexedSeq 인터페이스를 만들 수 있다.

interface IndexedSeq<T> {

    // returns the size of t
    fun size(): Int

    // returns a new seq with t added
    fun add(t: T): IndexedSeq<T>

    // returns true if this seq contains t
    fun contains(t: T): Boolean
}

List 구현을 테스트하고 싶다면 이렇게 하면 된다:

class ListTest : WordSpec({

   val empty = List<Int>()

   "List" should {
      "increase size as elements are added" {
         empty.size() shouldBe 0
         val plus1 = empty.add(1)
         plus1.size() shouldBe 1
         val plus2 = plus1.add(2)
         plus2.size() shouldBe 2
      }
      "contain an element after it is added" {
         empty.contains(1) shouldBe false
         empty.add(1).contains(1) shouldBe true
         empty.add(1).contains(2) shouldBe false
      }
   }
})

이제 Vector를 테스트하려면 테스트를 복사하여 붙여넣어야 한다. 더 많은 구현과 더 많은 테스트를 추가하면 테스트 스위트가 파편화되고 동기화되지 않을 가능성이 높다.

이 문제는 IndexedSeq을 매개변수로 받아들이는 테스트 팩토리를 생성하여 해결할 수 있다.

테스트 팩토리를 생성하기 위해 funSpec, wordSpec 등과 같은 빌더 함수를 사용한다. 빌더 함수는 각 스펙 스타일에 대해 존재한다.

따라서 이전 테스트를 테스트 팩토리로 변환하려면 다음을 수행하면 된다:

fun <T> indexedSeqTests(name: String, empty: IndexedSeq<T>) = wordSpec {
   name should {
      "increase size as elements are added" {
         empty.size() shouldBe 0
         val plus1 = empty.add(1)
         plus1.size() shouldBe 1
         val plus2 = plus1.add(2)
         plus2.size() shouldBe 2
      }
      "contain an element after it is added" {
         empty.contains(1) shouldBe false
         empty.add(1).contains(1) shouldBe true
         empty.add(1).contains(2) shouldBe false
      }
   }
}

그런 다음 이를 사용하려면 이를 하나의 스펙(또는 여러 스펙)에 한 번 이상 포함해야 한다.

class IndexedSeqTestSuite : WordSpec({
   include(indexedSeqTests("vector"), Vector())
   include(indexedSeqTests("list"), List())
})

테스트 클래스에는 일반적인 인라인 테스트뿐만 아니라 여러 가지 유형의 팩토리가 포함될 수 있다. 예를 들어:

class HugeTestFile : FunSpec({

   test("first test") {
     // test here
   }

   include(factory1("foo"))
   include(factory2(1, 4))

   test("another test") {
     //  testhere
   }
})

포함된 각 테스트는 개별적으로 정의된 것처럼 테스트 출력 및 보고서에 나타낸다.

Listeners

테스트 팩토리는 일반적인 테스트 전후의 콜백을 지원한다. 팩토리에 추가된 모든 콜백은 해당 팩토리가 포함된 스펙에 차례로 추가된다.

단, 해당 팩토리에서 생성된 테스트에만 콜백이 적용된다. 즉, 자체 수명 주기 메서드가 있는 독립형 팩토리를 만들 수 있으며 다른 팩토리 또는 스펙에 정의된 수명 주기 메서드와 충돌하지 않는다는 확신을 가질 수 있다.

예를 들어:

val factory1 = funSpec {
  beforeTest {
     println("Executing $it")
  }
  test("a") {  }
  test("b") {  }
}

class LifecycleExample : FunSpec({
   include(factory1)
   test("c")
   test("d")
})

테스트 스위트를 실행하면 다음과 같은 내용이 출력된다:

Executing a
Executing b

보시다시피, factory1에 추가된 beforeTest 블록은 해당 팩토리에 정의된 테스트에만 적용되며, 추가된 스펙에 정의된 테스트에는 적용되지 않는다.


참조




최종 수정 : 2024-04-21