En mis pruebas de Kotlin JUnit, quiero iniciar / detener servidores integrados y usarlos dentro de mis pruebas.
Intenté usar la @Before
anotación JUnit en un método en mi clase de prueba y funciona bien, pero no es el comportamiento correcto ya que ejecuta todos los casos de prueba en lugar de solo una vez.
Por lo tanto, quiero usar la @BeforeClass
anotación en un método, pero agregarla a un método da como resultado un error que dice que debe estar en un método estático. Kotlin no parece tener métodos estáticos. Y luego lo mismo se aplica a las variables estáticas, porque necesito mantener una referencia al servidor integrado para usar en los casos de prueba.
Entonces, ¿cómo creo esta base de datos integrada solo una vez para todos mis casos de prueba?
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
...
}
Nota: esta pregunta fue escrita y respondida intencionalmente por el autor ( Preguntas auto- respondidas ), de modo que las respuestas a los temas de Kotlin más frecuentes estén presentes en SO.
fuente
Respuestas:
Su clase de prueba unitaria generalmente necesita algunas cosas para administrar un recurso compartido para un grupo de métodos de prueba. Y en Kotlin puede usar
@BeforeClass
y@AfterClass
no en la clase de prueba, sino dentro de su objeto complementario junto con la@JvmStatic
anotación .La estructura de una clase de prueba se vería así:
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 }
Dado lo anterior, debe leer sobre:
@JvmStatic
- una anotación que convierte un método de objeto complementario en un método estático en la clase externa para la interoperabilidad de Javalateinit
- permite que unavar
propiedad se inicialice más tarde cuando tiene un ciclo de vida bien definidoDelegates.notNull()
- se puede usar en lugar delateinit
para una propiedad que debe establecerse al menos una vez antes de leerse.A continuación, se muestran ejemplos más completos de clases de prueba para Kotlin que administran recursos integrados.
El primero se copia y modifica de las pruebas de Solr-Undertow , y antes de ejecutar los casos de prueba, se configura e inicia un servidor Solr-Undertow. Una vez ejecutadas las pruebas, limpia los archivos temporales creados por las pruebas. También garantiza que las variables de entorno y las propiedades del sistema sean correctas antes de que se ejecuten las pruebas. Entre los casos de prueba, descarga los núcleos Solr cargados temporalmente. La prueba:
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 }
Y otro que inicia AWS DynamoDB local como una base de datos integrada (copiada y modificada ligeramente de Ejecución de AWS DynamoDB local integrado ). Esta prueba debe piratear
java.library.path
antes de que suceda cualquier otra cosa o DynamoDB local (usando sqlite con bibliotecas binarias) no se ejecutará. Luego, inicia un servidor para compartir con todas las clases de prueba y limpia los datos temporales entre las pruebas. La prueba: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: algunas partes de los ejemplos se abrevian con
...
fuente
La gestión de recursos con devoluciones de llamada antes / después en las pruebas, obviamente, tiene sus ventajas:
También tiene algunas desventajas. Uno importante de ellos es que contamina el código y hace que el código viole el principio de responsabilidad única. Las pruebas ahora no solo prueban algo, sino que realizan una inicialización pesada y una gestión de recursos. Puede estar bien en algunos casos (como configurar un
ObjectMapper
), pero modificarjava.library.path
o generar otros procesos (o bases de datos integradas en proceso) no es tan inocente.¿Por qué no tratar esos servicios como dependencias para su prueba elegibles para "inyección", como lo describe 12factor.net ?
De esta manera, inicia e inicializa los servicios de dependencia en algún lugar fuera del código de prueba.
Hoy en día, la virtualización y los contenedores están en casi todas partes y la mayoría de las máquinas de los desarrolladores pueden ejecutar Docker. Y la mayor parte de la aplicación tienen una versión dockerized: Elasticsearch , DynamoDB , PostgreSQL y así sucesivamente. Docker es una solución perfecta para los servicios externos que necesitan sus pruebas.
dependsOn
yfinalizedBy
DSL para definir dependencias). Una tarea, por supuesto, puede ejecutar el mismo script que el desarrollador ejecuta manualmente usando shell-outs / ejecutivos de procesos.Este enfoque:
Por supuesto, tiene sus defectos (básicamente, las declaraciones de las que he comenzado):
fuente