Em meus testes Kotlin JUnit, quero iniciar / parar servidores incorporados e usá-los em meus testes.
Tentei usar a @Before
anotação JUnit em um método em minha classe de teste e funciona bem, mas não é o comportamento correto, pois executa todos os casos de teste em vez de apenas uma vez.
Portanto, desejo usar a @BeforeClass
anotação em um método, mas adicioná-la a um método resulta em um erro dizendo que deve ser em um método estático. Kotlin não parece ter métodos estáticos. E o mesmo se aplica a variáveis estáticas, porque preciso manter uma referência ao servidor incorporado para uso nos casos de teste.
Então, como faço para criar esse banco de dados incorporado apenas uma vez para todos os meus casos de teste?
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
...
}
Observação: esta pergunta foi escrita e respondida intencionalmente pelo autor ( Perguntas auto-respondidas ), de modo que as respostas aos tópicos mais comuns do Kotlin estejam presentes no SO.
fonte
Respostas:
Sua classe de teste de unidade geralmente precisa de algumas coisas para gerenciar um recurso compartilhado para um grupo de métodos de teste. E em Kotlin você pode usar
@BeforeClass
e@AfterClass
não na classe de teste, mas sim dentro de seu objeto companheiro junto com a@JvmStatic
anotação .A estrutura de uma classe de teste seria assim:
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 }
Diante do exposto, você deve ler sobre:
@JvmStatic
- uma anotação que transforma um método de objeto complementar em um método estático na classe externa para interoperabilidade Javalateinit
- permite que umavar
propriedade seja inicializada mais tarde, quando você tiver um ciclo de vida bem definidoDelegates.notNull()
- pode ser usado em vez delateinit
para uma propriedade que deve ser definida pelo menos uma vez antes de ser lida.Aqui estão exemplos mais completos de classes de teste para Kotlin que gerenciam recursos incorporados.
O primeiro é copiado e modificado dos testes Solr-Undertow e, antes que os casos de teste sejam executados, configura e inicia um servidor Solr-Undertow. Após a execução dos testes, ele limpa todos os arquivos temporários criados pelos testes. Ele também garante que as variáveis de ambiente e as propriedades do sistema estejam corretas antes da execução dos testes. Entre os casos de teste, ele descarrega todos os núcleos Solr carregados temporariamente. O teste:
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 }
E outro iniciando o AWS DynamoDB local como um banco de dados incorporado (copiado e modificado ligeiramente de Executando o AWS DynamoDB-local incorporado ). Este teste deve hackear
java.library.path
antes que qualquer coisa aconteça ou o DynamoDB local (usando sqlite com bibliotecas binárias) não será executado. Em seguida, ele inicia um servidor para compartilhar para todas as classes de teste e limpa os dados temporários entre os testes. O teste: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 }
NOTA: algumas partes dos exemplos são abreviadas com
...
fonte
Gerenciar recursos com callbacks antes / depois em testes, obviamente, tem seus prós:
Também tem alguns contras. Uma delas é que polui o código e faz com que o código viole o princípio da responsabilidade única. Os testes agora não apenas testam algo, mas executam uma inicialização pesada e gerenciamento de recursos. Pode ser bom em alguns casos (como configurar um
ObjectMapper
), mas modificarjava.library.path
ou gerar outros processos (ou bancos de dados embutidos em processo) não são tão inocentes.Por que não tratar esses serviços como dependências para seu teste qualificado para "injeção", conforme descrito por 12factor.net .
Dessa forma, você inicia e inicializa os serviços de dependência em algum lugar fora do código de teste.
Hoje em dia, a virtualização e os contêineres estão em quase todos os lugares e a maioria das máquinas dos desenvolvedores pode executar o Docker. E a maior parte do aplicativo tem uma versão dockerized: ElasticSearch , DynamoDB , PostgreSQL e assim por diante. Docker é uma solução perfeita para serviços externos que seus testes precisam.
dependsOn
efinalizedBy
DSL para definir dependências). Uma tarefa, é claro, pode executar o mesmo script que o desenvolvedor executa manualmente usando shell-outs / execs de processo.Esta abordagem:
Claro, ele tem suas falhas (basicamente, as declarações das quais comecei):
fonte