Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I manage unit test resources in Kotlin, such as starting/stopping a database connection or an embedded elasticsearch server?

In my Kotlin JUnit tests, I want to start/stop embedded servers and use them within my tests.

I tried using the JUnit @Before annotation on a method in my test class and it works fine, but it isn't the right behaviour since it runs every test case instead of just once.

Therefore I want to use the @BeforeClass annotation on a method, but adding it to a method results in an error saying it must be on a static method. Kotlin doesn't appear to have static methods. And then the same applies for static variables, because I need to keep a reference to the embedded server around for use in the test cases.

So how do I create this embedded database just once for all of my test cases?

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      ... } 

Note: this question is intentionally written and answered by the author (Self-Answered Questions), so that the answers to commonly asked Kotlin topics are present in SO.

like image 226
Jayson Minard Avatar asked Feb 22 '16 12:02

Jayson Minard


People also ask

How do you do unit testing on Kotlin?

Test structure Arrange/Given - in which we will prepare all needed data required to perform test. Act/When - in which we will call single method on tested object. Assert/Then - in which we will check result of the test, either pass or fail.

Should unit tests connect to database?

Unit tests should never connect to a database. By definition, they should test a single unit of code each (a method) in total isolation from the rest of your system. If they don't, then they are not a unit test.

What is unit testing in Android Kotlin?

Unit tests run on the Java virtual machine (JVM). For this reason, tests run faster, and one does not have to wait for the emulator or physical device to boot up to test the logic implemented. This article will serve as a guide on how to write local unit tests in Android using Kotlin.


1 Answers

Your unit test class usually needs a few things to manage a shared resource for a group of test methods. And in Kotlin you can use @BeforeClass and @AfterClass not in the test class, but rather within its companion object along with the @JvmStatic annotation.

The structure of a test class would look like:

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 }   

Given the above, you should read about:

  • companion objects - similar to the Class object in Java, but a singleton per class that is not static
  • @JvmStatic - an annotation that turns a companion object method into a static method on the outer class for Java interop
  • lateinit - allows a var property to be initialized later when you have a well defined lifecycle
  • Delegates.notNull() - can be used instead of lateinit for a property that should be set at least once before being read.

Here are fuller examples of test classes for Kotlin that manage embedded resources.

The first is copied and modified from Solr-Undertow tests, and before the test cases are run, configures and starts a Solr-Undertow server. After the tests run, it cleans up any temporary files created by the tests. It also ensures environment variables and system properties are correct before the tests are run. Between test cases it unloads any temporary loaded Solr cores. The test:

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 } 

And another starting AWS DynamoDB local as an embedded database (copied and modified slightly from Running AWS DynamoDB-local embedded). This test must hack the java.library.path before anything else happens or local DynamoDB (using sqlite with binary libraries) won't run. Then it starts a server to share for all test classes, and cleans up temporary data between tests. The test:

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  } 

NOTE: some parts of the examples are abbreviated with ...

like image 164
Jayson Minard Avatar answered Oct 23 '22 23:10

Jayson Minard