unit-testing - kotlin test
¿Cómo administro los recursos de prueba unitaria en Kotlin, como iniciar/detener una conexión de base de datos o un servidor elástico de búsqueda integrado? (2)
Administrar recursos con devoluciones de llamada antes / después en las pruebas, obviamente, tiene sus ventajas:
- Las pruebas son "atómicas". Una prueba se ejecuta como un todo con todas las devoluciones de llamada. Uno no olvidará iniciar un servicio de dependencia antes de las pruebas y cerrarlo después de que haya terminado. Si se realiza correctamente, las devoluciones de llamada de ejecución funcionarán en cualquier entorno.
- Las pruebas son independientes. No hay datos externos o fases de configuración, todo está contenido dentro de unas pocas clases de prueba.
Tiene algunas desventajas también.
Una de ellas importante 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 de peso pesado y gestión de recursos.
Puede estar bien en algunos casos (como
configurar un
ObjectMapper
), pero modificar
java.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 elegible 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ía de la aplicación tiene una versión acoplada: DynamoDB , DynamoDB , PostgreSQL , etc. Docker es una solución perfecta para los servicios externos que necesitan sus pruebas.
- Puede ser un script que se ejecuta se ejecuta manualmente por un desarrollador cada vez que quiere ejecutar pruebas.
-
Puede ser una tarea ejecutada por la herramienta de compilación (por ejemplo, Gradle tiene una increíble
dependsOn
yfinalizedBy
por DSL para definir dependencias). Una tarea, por supuesto, puede ejecutar el mismo script que el desarrollador ejecuta manualmente usando shell-outs / proceso execs. - Puede ser una tarea ejecutada por IDE antes de la ejecución de la prueba . De nuevo, puede usar el mismo script.
- La mayoría de los proveedores de CI / CD tienen una noción de "servicio": una dependencia externa (proceso) que se ejecuta en paralelo a su compilación y se puede acceder a través de su SDK / conector / API habitual: Gitlab , Travis , Bitbucket , AppVeyor , Semaphore , ...
Este enfoque:
- Libera su código de prueba de la lógica de inicialización. Sus pruebas solo probarán y no harán nada más.
- Desacopla el código y los datos. Ahora se puede agregar un nuevo caso de prueba agregando nuevos datos a los servicios de dependencia con su conjunto de herramientas nativo. Es decir, para las bases de datos SQL usará SQL, para Amazon DynamoDB usará CLI para crear tablas y colocar elementos.
- Está más cerca de un código de producción, donde obviamente no inicia esos servicios cuando se inicia su aplicación "principal".
Por supuesto, tiene sus defectos (básicamente, las declaraciones que he comenzado):
- Las pruebas no son más "atómicas". El servicio de dependencia debe iniciarse de alguna manera antes de la ejecución de la prueba. La forma en que se inicia puede ser diferente en diferentes entornos: máquina del desarrollador o CI, IDE o CLI de herramienta de compilación.
- Las pruebas no son independientes. Ahora sus datos semilla pueden estar incluso empaquetados dentro de una imagen, por lo que cambiarlos puede requerir la reconstrucción de un proyecto diferente.
En mis pruebas Kotlin JUnit, quiero iniciar / detener servidores integrados y usarlos dentro de mis pruebas.
Intenté usar la anotación JUnit
@Before
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 anotación
@BeforeClass
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 incorporado para usar en los casos de prueba.
Entonces, ¿cómo creo esta base de datos incrustada 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 está escrita y respondida intencionalmente por el autor ( Preguntas con respuesta propia ), de modo que las respuestas a los temas de Kotlin más frecuentes están presentes en SO.
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
anotación
@JvmStatic
.
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:
- objetos complementarios : similar al objeto Class en Java, pero un singleton por clase que no es estático
-
@JvmStatic
: una anotación que convierte un método de objeto complementario en un método estático en la clase externa para interoperabilidad Java -
lateinit
: permite que una propiedadvar
se inicialice más adelante cuando tiene un ciclo de vida bien definido -
Delegates.notNull()
: se puede usar en lugar delateinit
para una propiedad que debe establecerse al menos una vez antes de leerse.
Aquí hay 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 que se ejecuten los casos de prueba, configura e inicia un servidor de Solr-Undertow. Después de que se ejecutan las pruebas, limpia todos los archivos temporales creados por las pruebas. También garantiza que las variables de entorno y las propiedades del sistema sean correctas antes de ejecutar 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 inicio de AWS DynamoDB local como una base de datos integrada (copiada y modificada ligeramente de
Ejecución de AWS DynamoDB-local integrado
).
Esta prueba debe hackear el
java.library.path
antes de que ocurra algo más o DynamoDB local (usando sqlite con bibliotecas binarias) no se ejecutará.
Luego, inicia un servidor para compartir todas las clases de prueba y limpia datos temporales entre 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
...