gpt4 book ai didi

unit-testing - 如何在 Kotlin 中管理单元测试资源,例如启动/停止数据库连接或嵌入式 Elasticsearch 服务器?

转载 作者:IT老高 更新时间:2023-10-28 13:26:10 25 4
gpt4 key购买 nike

在我的 Kotlin JUnit 测试中,我想启动/停止嵌入式服务器并在我的测试中使用它们。

我尝试在我的测试类中的方法上使用 JUnit @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

...
}

注意: 这个问题是作者有意编写和回答的 (Self-Answered Questions),因此常见的 Kotlin 主题的答案出现在 SO 中。

最佳答案

您的单元测试类通常需要一些东西来管理一组测试方法的共享资源。在 Kotlin 中,您可以不在测试类中使用 @BeforeClass@AfterClass,而是在其 companion object 中使用连同 @JvmStatic annotation .

测试类的结构如下:

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
}

鉴于以上内容,您应该阅读以下内容:

  • companion objects - 类似于 Java 中的 Class 对象,但每个类都有一个非静态的单例
  • @JvmStatic - 将伴随对象方法转换为 Java 互操作外部类上的静态方法的注释
  • lateinit - 当您有一个明确定义的生命周期时,允许稍后初始化 var 属性
  • Delegates.notNull() - 可以代替 lateinit 用于在读取前应至少设置一次的属性。

以下是管理嵌入式资源的 Kotlin 测试类的更完整示例。

第一个是从Solr-Undertow tests复制和修改的,在运行测试用例之前,配置并启动 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

}

注意:示例的某些部分缩写为 ...

关于unit-testing - 如何在 Kotlin 中管理单元测试资源,例如启动/停止数据库连接或嵌入式 Elasticsearch 服务器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35554076/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com