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

杰森·米纳德(Jayson Minard):

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

    ...
}

注意: 这个问题是作者故意写和回答的(“ 自我回答的问题”),因此SO中提供了常见Kotlin主题的答案。

杰森·米纳德(Jayson Minard):

您的单元测试类通常需要一些东西来管理一组测试方法的共享资源。在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互操作的外部类上将伴随对象方法转换为静态方法
  • 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作为嵌入式数据库(从“ 运行AWS DynamoDB-local Embedded”略微复制和修改)。此测试必须先进行hack,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

}

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

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

运行单元测试时是否可以阻止嵌入式服务器启动?

如何在单个 Postgres 服务器中为多个数据库使用嵌入式 Debezium?

如何在 Mockito 单元测试期间生成 H2 嵌入式数据库?

如何确定数据库服务器或嵌入式数据库是否合适

如何在python中对数据库连接pymysql进行单元测试?

是否可以在进程中启动Zookeeper服务器实例,例如用于单元测试?

结合服务器数据库和嵌入式数据库电子/反应应用

单元测试,如何让spring管理服务不污染数据库

JUnit测试与嵌入式Tomcat服务器,如何指定HTTP和HTTPS连接器自动端口?

如何在Undertow嵌入式服务器中登录文件?

如何在Spark嵌入式Web服务器中启用HTTP / 2

以后是否可以在客户端-服务器db环境中使用嵌入式Derby数据库?

.Net客户端无法访问嵌入式Firebird数据库服务器

我应该如何在无服务器环境中管理postgres数据库句柄?

如何在不连接数据库的情况下在 nestjs 和 typeorm 中编写单元测试

无法为 Kafka 启动嵌入式服务器以进行 Junit 测试

弹簧启动测试和嵌入式弹性服务器

在春季启动中使用嵌入式数据库进行测试

Java嵌入式数据库(java db / derby)连接管理

无法启动嵌入式Jetty服务器

启动嵌入式Jetty服务器的最短代码

嵌入式码头:资源文件夹中的服务器html文件

如何在Mule单元测试中伪造SFTP服务器?

如何在无服务器Mocha插件中模拟用于单元测试的功能

如何在Cordapp中对服务和控制器(kotlin)进行单元测试?

如何在使用bpchar的h2数据库中执行单元测试?

在多线程套接字服务器中管理数据库连接

当数据库位于服务器文件夹之外时,如何启动Mongoose连接?

如何连接到 ActiveMQ Artemis 嵌入式服务器?