[unit-testing] Kotlin에서 데이터베이스 연결 또는 내장형 Elasticsearch 서버 시작 / 중지와 같은 단위 테스트 리소스를 어떻게 관리하나요?

Kotlin JUnit 테스트에서 임베디드 서버를 시작 / 중지하고 테스트 내에서 사용하고 싶습니다.

@Before내 테스트 클래스의 메서드에 JUnit 주석을 사용해 보았지만 제대로 작동하지만 한 번이 아닌 모든 테스트 케이스를 실행하기 때문에 올바른 동작이 아닙니다.

따라서 @BeforeClass메서드에 주석 을 사용하고 싶지만 메서드에 추가하면 정적 메서드에 있어야한다는 오류가 발생합니다. Kotlin에는 정적 메서드가없는 것 같습니다. 그런 다음 정적 변수에도 동일하게 적용됩니다. 테스트 케이스에서 사용하기 위해 임베디드 서버에 대한 참조를 유지해야하기 때문입니다.

그렇다면 모든 테스트 사례에 대해이 내장 데이터베이스를 한 번만 생성하려면 어떻게해야합니까?

class MyTest {
    @Before fun setup() {
       // works in that it opens the database connection, but is wrong 
       // since this is per test case instead of being shared for all
    }

    @BeforeClass fun setupClass() {
       // what I want to do instead, but results in error because 
       // this isn't a static method, and static keyword doesn't exist
    }

    var referenceToServer: ServerType // wrong because is not static either

    ...
}

참고 : 이 질문은 작성자 ( Self-Answered Questions ) 가 의도적으로 작성하고 답변 하므로 일반적으로 묻는 Kotlin 주제에 대한 답변이 SO에 있습니다.



답변

단위 테스트 클래스는 일반적으로 테스트 메서드 그룹에 대한 공유 리소스를 관리하기 위해 몇 가지 사항이 필요합니다. 그리고 코 틀린에 당신이 사용할 수있는 @BeforeClass@AfterClass되지 테스트 클래스에서, 오히려 그것의 내 동반자 객체 과 함께 @JvmStatic주석 .

테스트 클래스의 구조는 다음과 같습니다.

class MyTestClass {
    companion object {
        init {
           // things that may need to be setup before companion class member variables are instantiated
        }

        // variables you initialize for the class just once:
        val someClassVar = initializer()

        // variables you initialize for the class later in the @BeforeClass method:
        lateinit var someClassLateVar: SomeResource

        @BeforeClass @JvmStatic fun setup() {
           // things to execute once and keep around for the class
        }

        @AfterClass @JvmStatic fun teardown() {
           // clean up after this class, leave nothing dirty behind
        }
    }

    // variables you initialize per instance of the test class:
    val someInstanceVar = initializer()

    // variables you initialize per test case later in your @Before methods:
    var lateinit someInstanceLateZVar: MyType

    @Before fun prepareTest() {
        // things to do before each test
    }

    @After fun cleanupTest() {
        // things to do after each test
    }

    @Test fun testSomething() {
        // an actual test case
    }

    @Test fun testSomethingElse() {
        // another test case
    }

    // ...more test cases
}

위의 내용이 주어지면 다음에 대해 읽어야합니다.

  • 동반 객체-Java 의 Class 객체와 유사하지만 정적이 아닌 클래스 당 단일 항목
  • @JvmStatic -Java interop 용 외부 클래스에서 컴패니언 객체 메서드를 정적 메서드로 변환하는 주석
  • lateinitvar잘 정의 된 수명주기가있을 때 나중에 속성을 초기화 할 수 있습니다.
  • Delegates.notNull()lateinit읽기 전에 적어도 한 번 설정해야하는 속성 대신 사용할 수 있습니다 .

다음은 임베디드 리소스를 관리하는 Kotlin 용 테스트 클래스의 전체 예시입니다.

첫 번째는 Solr-Undertow 테스트 에서 복사 및 수정되며 테스트 케이스가 실행되기 전에 Solr-Undertow 서버를 구성하고 시작합니다. 테스트가 실행 된 후 테스트에서 생성 된 모든 임시 파일을 정리합니다. 또한 테스트를 실행하기 전에 환경 변수와 시스템 속성이 올바른지 확인합니다. 테스트 케이스 사이에 임시로로드 된 Solr 코어를 언로드합니다. 시험:

class TestServerWithPlugin {
    companion object {
        val workingDir = Paths.get("test-data/solr-standalone").toAbsolutePath()
        val coreWithPluginDir = workingDir.resolve("plugin-test/collection1")

        lateinit var server: Server

        @BeforeClass @JvmStatic fun setup() {
            assertTrue(coreWithPluginDir.exists(), "test core w/plugin does not exist $coreWithPluginDir")

            // make sure no system properties are set that could interfere with test
            resetEnvProxy()
            cleanSysProps()
            routeJbossLoggingToSlf4j()
            cleanFiles()

            val config = mapOf(...)
            val configLoader = ServerConfigFromOverridesAndReference(workingDir, config) verifiedBy { loader ->
                ...
            }

            assertNotNull(System.getProperty("solr.solr.home"))

            server = Server(configLoader)
            val (serverStarted, message) = server.run()
            if (!serverStarted) {
                fail("Server not started: '$message'")
            }
        }

        @AfterClass @JvmStatic fun teardown() {
            server.shutdown()
            cleanFiles()
            resetEnvProxy()
            cleanSysProps()
        }

        private fun cleanSysProps() { ... }

        private fun cleanFiles() {
            // don't leave any test files behind
            coreWithPluginDir.resolve("data").deleteRecursively()
            Files.deleteIfExists(coreWithPluginDir.resolve("core.properties"))
            Files.deleteIfExists(coreWithPluginDir.resolve("core.properties.unloaded"))
        }
    }

    val adminClient: SolrClient = HttpSolrClient("http://localhost:8983/solr/")

    @Before fun prepareTest() {
        // anything before each test?
    }

    @After fun cleanupTest() {
        // make sure test cores do not bleed over between test cases
        unloadCoreIfExists("tempCollection1")
        unloadCoreIfExists("tempCollection2")
        unloadCoreIfExists("tempCollection3")
    }

    private fun unloadCoreIfExists(name: String) { ... }

    @Test
    fun testServerLoadsPlugin() {
        println("Loading core 'withplugin' from dir ${coreWithPluginDir.toString()}")
        val response = CoreAdminRequest.createCore("tempCollection1", coreWithPluginDir.toString(), adminClient)
        assertEquals(0, response.status)
    }

    // ... other test cases
}

그리고 또 다른 시작 AWS DynamoDB는 임베디드 데이터베이스로 로컬입니다 ( Running AWS DynamoDB-local Embedded 에서 약간 복사 및 수정 됨 ). 이 테스트는 java.library.path다른 일 이 일어나기 전에 먼저 해킹해야합니다 . 그렇지 않으면 로컬 DynamoDB (바이너리 라이브러리와 함께 sqlite 사용)가 실행되지 않습니다. 그런 다음 모든 테스트 클래스에 대해 공유 할 서버를 시작하고 테스트간에 임시 데이터를 정리합니다. 시험:

class TestAccountManager {
    companion object {
        init {
            // we need to control the "java.library.path" or sqlite cannot find its libraries
            val dynLibPath = File("./src/test/dynlib/").absoluteFile
            System.setProperty("java.library.path", dynLibPath.toString());

            // TEST HACK: if we kill this value in the System classloader, it will be
            // recreated on next access allowing java.library.path to be reset
            val fieldSysPath = ClassLoader::class.java.getDeclaredField("sys_paths")
            fieldSysPath.setAccessible(true)
            fieldSysPath.set(null, null)

            // ensure logging always goes through Slf4j
            System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.Slf4jLog")
        }

        private val localDbPort = 19444

        private lateinit var localDb: DynamoDBProxyServer
        private lateinit var dbClient: AmazonDynamoDBClient
        private lateinit var dynamo: DynamoDB

        @BeforeClass @JvmStatic fun setup() {
            // do not use ServerRunner, it is evil and doesn't set the port correctly, also
            // it resets logging to be off.
            localDb = DynamoDBProxyServer(localDbPort, LocalDynamoDBServerHandler(
                    LocalDynamoDBRequestHandler(0, true, null, true, true), null)
            )
            localDb.start()

            // fake credentials are required even though ignored
            val auth = BasicAWSCredentials("fakeKey", "fakeSecret")
            dbClient = AmazonDynamoDBClient(auth) initializedWith {
                signerRegionOverride = "us-east-1"
                setEndpoint("http://localhost:$localDbPort")
            }
            dynamo = DynamoDB(dbClient)

            // create the tables once
            AccountManagerSchema.createTables(dbClient)

            // for debugging reference
            dynamo.listTables().forEach { table ->
                println(table.tableName)
            }
        }

        @AfterClass @JvmStatic fun teardown() {
            dbClient.shutdown()
            localDb.stop()
        }
    }

    val jsonMapper = jacksonObjectMapper()
    val dynamoMapper: DynamoDBMapper = DynamoDBMapper(dbClient)

    @Before fun prepareTest() {
        // insert commonly used test data
        setupStaticBillingData(dbClient)
    }

    @After fun cleanupTest() {
        // delete anything that shouldn't survive any test case
        deleteAllInTable<Account>()
        deleteAllInTable<Organization>()
        deleteAllInTable<Billing>()
    }

    private inline fun <reified T: Any> deleteAllInTable() { ... }

    @Test fun testAccountJsonRoundTrip() {
        val acct = Account("123",  ...)
        dynamoMapper.save(acct)

        val item = dynamo.getTable("Accounts").getItem("id", "123")
        val acctReadJson = jsonMapper.readValue<Account>(item.toJSON())
        assertEquals(acct, acctReadJson)
    }

    // ...more test cases

}

참고 : 예제의 일부는...


답변

테스트에서 전 / 후 콜백으로 리소스를 관리하는 것은 분명히 장점이 있습니다.

  • 테스트는 “원자 적”입니다. 테스트는 모든 콜백을 사용하여 전체적으로 실행됩니다. 테스트 전에 종속성 서비스를 시작하고 완료 후에 종료하는 것을 잊지 마십시오. 제대로 수행되면 실행 콜백이 모든 환경에서 작동합니다.
  • 테스트는 독립적입니다. 외부 데이터 나 설정 단계가 없으며 모든 것이 몇 가지 테스트 클래스에 포함됩니다.

단점도 있습니다. 그중 한 가지 중요한 것은 코드를 오염시키고 코드가 단일 책임 원칙을 위반하게 만든다는 것입니다. 이제 테스트는 무언가를 테스트 할뿐만 아니라 무거운 초기화 및 리소스 관리를 수행합니다. 어떤 경우에는 (를 구성하는ObjectMapper 것과 같이) 괜찮을 수 있지만 java.library.path다른 프로세스 (또는 프로세스 내 임베디드 데이터베이스)를 수정 하거나 생성하는 것은 그렇게 결백하지 않습니다.

설명처럼 왜, “주사”에 대한 테스트 자격에 대한 의존성으로 이러한 서비스를 취급하지 12factor.net .

이렇게 하면 테스트 코드 외부에서 종속성 서비스시작하고 초기화 할 수 있습니다.

오늘날 가상화 및 컨테이너는 거의 모든 곳에 있으며 대부분의 개발자 컴퓨터에서 Docker를 실행할 수 있습니다. 그리고 대부분의 애플리케이션에는 Elasticsearch , DynamoDB , PostgreSQL 등 고정 된 버전 이 있습니다. Docker는 테스트에 필요한 외부 서비스를위한 완벽한 솔루션입니다.

  • 개발자가 테스트를 실행하려고 할 때마다 수동으로 실행되는 스크립트 일 수 있습니다.
  • 빌드 도구에 의해 실행되는 작업 일 수 있습니다 (예 : Gradle에는 종속성 정의를위한 DSL dependsOnfinalizedByDSL이 있습니다). 물론 작업은 개발자가 쉘 아웃 / 프로세스 실행을 사용하여 수동으로 실행하는 것과 동일한 스크립트를 실행할 수 있습니다.
  • 테스트 실행 전에 IDE에서 실행 하는 작업 일 수 있습니다 . 다시 말하지만 동일한 스크립트를 사용할 수 있습니다.
  • 대부분의 CI / CD 제공 업체는 “서비스”라는 개념을 가지고 있습니다. 이는 빌드와 병렬로 실행되고 일반적인 SDK / 커넥터 / API를 통해 액세스 할 수있는 외부 종속성 (프로세스)입니다. Gitlab , Travis , Bitbucket , AppVeyor , Semaphore ,…

이 접근 방식 :

  • 초기화 로직에서 테스트 코드를 해제합니다. 귀하의 테스트는 테스트 만 수행하고 더 이상 수행하지 않습니다.
  • 코드와 데이터를 분리합니다. 이제 기본 도구 세트를 사용하여 종속성 서비스에 새 데이터를 추가하여 새 테스트 케이스를 추가 할 수 있습니다. 즉, SQL 데이터베이스의 경우 SQL을 사용하고 Amazon DynamoDB의 경우 CLI를 사용하여 테이블을 생성하고 항목을 넣습니다.
  • “기본”응용 프로그램이 시작될 때 해당 서비스를 시작하지 않는 프로덕션 코드에 더 가깝습니다.

물론 결함이 있습니다 (기본적으로 내가 시작한 진술).

  • 테스트는 더 “원자 적”이 아닙니다. 종속성 서비스는 테스트 실행 전에 어떻게 든 시작해야합니다. 시작 방법은 개발자의 컴퓨터 또는 CI, IDE 또는 빌드 도구 CLI와 같은 다른 환경에서 다를 수 있습니다.
  • 테스트는 자체 포함되지 않습니다. 이제 시드 데이터가 이미지 안에 압축 될 수도 있으므로 변경하면 다른 프로젝트를 다시 빌드해야 할 수 있습니다.


답변