如何管理Kotlin中的单元测试资源,如启动/停止数据库连接或嵌入式elasticsearch服务器?

在我的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 ... } 

注意: 这个问题是由作者故意编写和回答的( 自我回答问题 ),所以对于常见的Kotlin主题的答案是在SO中。

你的单元测试类通常需要一些东西来管理一组测试方法的共享资源。 而在Kotlin中,您可以不在测试类中使用@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() – 而不是lateinitlateinit一个应该至少设置一次的属性。

以下是管理嵌入式资源的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本地启动(从运行AWS DynamoDB-local嵌入中稍微复制和修改)。 此测试必须在发生任何事情之前破解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 } 

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