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 용 외부 클래스에서 컴패니언 객체 메서드를 정적 메서드로 변환하는 주석lateinit
–var
잘 정의 된 수명주기가있을 때 나중에 속성을 초기화 할 수 있습니다.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
dependsOn
및finalizedBy
DSL이 있습니다). 물론 작업은 개발자가 쉘 아웃 / 프로세스 실행을 사용하여 수동으로 실행하는 것과 동일한 스크립트를 실행할 수 있습니다. - 테스트 실행 전에 IDE에서 실행 하는 작업 일 수 있습니다 . 다시 말하지만 동일한 스크립트를 사용할 수 있습니다.
- 대부분의 CI / CD 제공 업체는 “서비스”라는 개념을 가지고 있습니다. 이는 빌드와 병렬로 실행되고 일반적인 SDK / 커넥터 / API를 통해 액세스 할 수있는 외부 종속성 (프로세스)입니다. Gitlab , Travis , Bitbucket , AppVeyor , Semaphore ,…
이 접근 방식 :
- 초기화 로직에서 테스트 코드를 해제합니다. 귀하의 테스트는 테스트 만 수행하고 더 이상 수행하지 않습니다.
- 코드와 데이터를 분리합니다. 이제 기본 도구 세트를 사용하여 종속성 서비스에 새 데이터를 추가하여 새 테스트 케이스를 추가 할 수 있습니다. 즉, SQL 데이터베이스의 경우 SQL을 사용하고 Amazon DynamoDB의 경우 CLI를 사용하여 테이블을 생성하고 항목을 넣습니다.
- “기본”응용 프로그램이 시작될 때 해당 서비스를 시작하지 않는 프로덕션 코드에 더 가깝습니다.
물론 결함이 있습니다 (기본적으로 내가 시작한 진술).
- 테스트는 더 “원자 적”이 아닙니다. 종속성 서비스는 테스트 실행 전에 어떻게 든 시작해야합니다. 시작 방법은 개발자의 컴퓨터 또는 CI, IDE 또는 빌드 도구 CLI와 같은 다른 환경에서 다를 수 있습니다.
- 테스트는 자체 포함되지 않습니다. 이제 시드 데이터가 이미지 안에 압축 될 수도 있으므로 변경하면 다른 프로젝트를 다시 빌드해야 할 수 있습니다.