Unit testing 如何管理Kotlin中的单元测试资源,例如启动/停止数据库连接或嵌入式elasticsearch服务器?
在我的Kotlin JUnit测试中,我想启动/停止嵌入式服务器,并在测试中使用它们 我尝试在测试类中的一个方法上使用JUnitUnit testing 如何管理Kotlin中的单元测试资源,例如启动/停止数据库连接或嵌入式elasticsearch服务器?,unit-testing,junit,kotlin,Unit Testing,Junit,Kotlin,在我的Kotlin JUnit测试中,我想启动/停止嵌入式服务器,并在测试中使用它们 我尝试在测试类中的一个方法上使用JUnit@Before注释,它工作得很好,但这不是正确的行为,因为它运行每个测试用例,而不是只运行一次 因此,我想在方法上使用@BeforeClass注释,但将其添加到方法中会导致错误,说明它必须在静态方法上。Kotlin似乎没有静态方法。静态变量也是如此,因为我需要保留对嵌入式服务器的引用,以便在测试用例中使用 那么,如何为所有测试用例创建一次嵌入式数据库呢 class My
@Before
注释,它工作得很好,但这不是正确的行为,因为它运行每个测试用例,而不是只运行一次
因此,我想在方法上使用@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
...
}
注意:这个问题是作者特意编写和回答的(),因此,常见的Kotlin主题的答案都出现在so中。您的单元测试类通常需要一些东西来管理一组测试方法的共享资源。在Kotlin中,您可以在测试类中使用
@BeforeClass
和@AfterClass
,而不是在测试类中使用,而是在测试类中使用
测试类的结构如下所示:
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中的类对象,但每个类的单例不是静态的
- -在Java互操作的外部类上将伴随对象方法转换为静态方法的注释
- -允许在定义好生命周期后初始化
属性var
- -可代替
用于在读取前至少设置一次的属性lateinit
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
}
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
}
另一个启动AWS DynamoDB local作为嵌入式数据库(从中复制并稍加修改)。此测试必须在其他任何事情发生之前破解java.library.path
,否则本地DynamoDB(使用sqlite和二进制库)将无法运行。然后,它启动一个服务器来共享所有测试类,并清理测试之间的临时数据。测试:
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
}
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
}
类TestAccountManager{
伴星{
初始化{
//我们需要控制“java.library.path”,否则sqlite找不到它的库
val dynLibPath=File(“./src/test/dynlib/”).absoluteFile
setProperty(“java.library.path”,dynLibPath.toString());
//测试黑客:如果我们在系统类加载器中杀死这个值,它将
//在下次访问时重新创建,允许重置java.library.path
val fieldSysPath=ClassLoader::class.java.getDeclaredField(“sys\u路径”)
fieldSysPath.setAccessible(true)
fieldSysPath.set(null,null)
//确保日志记录始终通过Slf4j
System.setProperty(“org.eclipse.jetty.util.log.class”、“org.eclipse.jetty.util.log.Slf4jLog”)
}
private val localDbPort=19444
私有lateinit var localDb:DynamoDBProxyServer
私有lateinit var dbClient:AmazonDynamoDBClient
私有lateinit var dynamo:DynamoDB
@BeforeClass@JvmStatic fun setup()之前{
//不要使用ServerRunner,它是邪恶的,并且没有正确设置端口
//它会将日志记录重置为关闭。
localDb=DynamoDBProxyServer(localDbPort,LocalDynamoDBServerHandler(
LocalDynamoDBRequestHandler(0,true,null,true,true),null)
)
localDb.start()
//即使被忽略,也需要假凭证
val auth=BasicAWSCredentials(“伪造密钥”、“伪造秘密”)
dbClient=AmazonDynamoDBClient(auth)初始化为{
signerRegionOverride=“us-east-1”
setEndpoint(“http://localhost:$localDbPort“)
}
dynamo=DynamoDB(dbClient)
//创建一次表
AccountManagerSchema.createTables(dbClient)
//供调试参考
dynamo.listTables().forEach{table->
println(table.tableName)
}
}
@AfterClass@JvmStatic-fun-teardown(){
dbClient.shutdown()
localDb.stop()
}
}
val jsonMapper=jacksonObjectMapper()
val dynamoMapper:DynamoDBMapper=DynamoDBMapper(dbClient)
@娱乐前准备测试(){
//插入常用的测试数据
setupStaticBillingData(dbClient)
}
@娱乐后清洁测试(){
//删除任何不应在任何测试用例中存活的内容
deleteAllInTable()
deleteAllInTable()
deleteAllInTable()
}
私有内联fun deleteAllInTable(){…}
@测试乐趣testAccountJsonRoundTrip(){
val acct=账户(“123”,…)
发电机驱动程序保存(acct)
val item=dynamo.getTable(“Accounts”).getItem(“id”,“123”)
val acctradeJSON=jsonMapper.readValue(item.toJSON())
资产质量(会计科目、会计科目)
}
//…更多的测试用例
}
注意:示例的某些部分缩写为
..
在测试中使用前/后回调管理资源显然有其优点:
- 测试是“原子的”。一个测试作为一个整体执行,它包含所有回调。在测试和关闭之前,人们不会忘记启动依赖项服务