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 용 외부 클래스에서 컴패니언 객체 메서드를 정적 메서드로 변환하는 주석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
}
참고 : 예제의 일부는...
테스트에서 전 / 후 콜백으로 리소스를 관리하는 것은 분명히 장점이 있습니다.
- 테스트는 "원자 적"입니다. 테스트는 모든 콜백을 사용하여 전체적으로 실행됩니다. 테스트 전에 종속성 서비스를 시작하고 완료된 후에 종료하는 것을 잊지 마십시오. 제대로 수행되면 실행 콜백은 모든 환경에서 작동합니다.
- Tests are self-contained. There is no external data or setup phases, everything is contained within a few test classes.
It has some cons too. One important of them is that it pollutes the code and makes the code violate single responsibility principle. Tests now not only test something, but perform a heavyweight initialization and resource management. It can be ok in some cases (like configuring an ObjectMapper
), but modifying java.library.path
or spawning another processes (or in-process embedded databases) are not so innocent.
Why not treat those services as dependencies for your test eligible for "injection", like described by 12factor.net.
This way you start and initialize dependency services somewhere outside of the test code.
Nowadays virtualization and containers are almost everywhere and most developers' machines are able to run Docker. And most of the application have a dockerized version: Elasticsearch, DynamoDB, PostgreSQL and so on. Docker is a perfect solution for external services that your tests need.
- It can be a script that runs is run manually by a developer every time she wants to execute tests.
- It can be a task run by build tool (e.g. Gradle has awesome
dependsOn
andfinalizedBy
DSL for defining dependencies). A task, of course, can execute the same script that developer executes manually using shell-outs / process execs. - It can be a task run by IDE before test execution. Again, it can use the same script.
- Most CI / CD providers have a notion of "service" — an external dependency (process) that runs in parallel to your build and can be accessed via it's usual SDK / connector / API: Gitlab, Travis, Bitbucket, AppVeyor, Semaphore, …
This approach:
- Frees your test code from initialization logic. Your tests will only test and do nothing more.
- Decouples code and data. Adding a new test case can now be done by adding new data into dependency services with it's native toolset. I.e. for SQL databases you'll use SQL, for Amazon DynamoDB you'll use CLI to create tables and put items.
- Is closer to a production code, where you obviously do not start those services when your "main" application starts.
Of course, it has it's flaws (basically, the statements I've started from):
- Tests are not more "atomic". Dependency service must be started somehow prior test execution. The way it is started may be different in different environments: developer's machine or CI, IDE or build tool CLI.
- Tests are not self-contained. Now your seed data may be even packed inside an image, so changing it may require rebuilding a different project.
'programing tip' 카테고리의 다른 글
제네릭 메서드는 어떻게, 언제, 어디서 구체화됩니까? (0) | 2020.10.27 |
---|---|
dplyr에서 조인 할 때 x 및 y에 대한 열 이름을 지정하는 방법은 무엇입니까? (0) | 2020.10.27 |
파이썬 MySQLDB IN 절에서 사용하기 위해 목록을 파열 (0) | 2020.10.27 |
IIS에서 Mercurial과 hgwebdir을 어떻게 설정합니까? (0) | 2020.10.27 |
jQuery는 DOM 노드를 얻습니까? (0) | 2020.10.27 |