2016-02-22 8 views
36

W moich testach JUnit w Kotlin, chcę uruchomić/zatrzymać wbudowane serwery i używać ich w ramach moich testów.Jak zarządzać zasobami testów jednostkowych w Kotlin, takimi jak uruchamianie/zatrzymywanie połączenia z bazą danych lub wbudowany serwer elasticsearch?

Próbowałem użyć adnotacji JUnit @Before dla metody w mojej klasie testowej i działa dobrze, ale nie jest to właściwe zachowanie, ponieważ uruchamia każdy przypadek testowy zamiast tylko raz.

Dlatego chcę użyć adnotacji @BeforeClass dla metody, ale dodanie jej do metody powoduje błąd informujący, że musi to być metoda statyczna. Kotlin nie ma statycznych metod. A to samo dotyczy zmiennych statycznych, ponieważ muszę zachować odniesienie do wbudowanego serwera do wykorzystania w przypadkach testowych.

Jak utworzyć tę wbudowaną bazę danych tylko raz dla wszystkich moich przypadków testowych?

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 

    ... 
} 

Uwaga:kwestia ta jest celowo napisany i odpowiedział przez autora (Self-Answered Questions), tak, że odpowiedzi na najczęściej zadawane tematy Kotlin są obecne w SO.

+1

JUnit 5 może wspierać metody mobilnymi dla tego przypadku zastosowania, zobacz https://github.com/ junit-team/junit5/issues/419 # issuecomment-267815529 i nie krępuj się dać +1 mojemu komentarzowi, aby pokazać, że programiści Kotlin są zainteresowani takimi ulepszeniami. –

Odpowiedz

59

Twoja klasa testów jednostkowych zazwyczaj potrzebuje kilku rzeczy do zarządzania wspólnym zasobem dla grupy metod testowych. A w Kotlin można używać @BeforeClass i @AfterClass nie w klasie testowej, ale raczej w jej companion object wraz z @JvmStatic annotation.

Struktura klasy testowej będzie wyglądać następująco:

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 
} 

Biorąc powyższe pod uwagę, należy przeczytać:

  • companion objects - podobny do obiektu klasy w Javie, ale singleton na klasę, która nie jest statyczna
  • @JvmStatic - adnotacja, która zamienia metodę obiektu towarzyszącego w statyczną metodę na zewnętrznej klasie r Java współdziałanie
  • lateinit - pozwala nieruchomość var zostać zainicjowana później, gdy masz dobrze zdefiniowany cykl
  • Delegates.notNull() - może być używany zamiast lateinit dla nieruchomości, które powinny być ustawione przynajmniej raz przed czytać.

Oto pełniejsze przykłady klas testowych dla Kotlin, które zarządzają zasobami osadzonymi.

Pierwsza jest kopiowana i modyfikowana od Solr-Undertow tests, a przed uruchomieniem przypadków testowych konfiguruje i uruchamia serwer Solr-Undertow. Po uruchomieniu testów czyści wszystkie tymczasowe pliki utworzone podczas testów. Zapewnia także poprawność zmiennych środowiskowych i właściwości systemu przed uruchomieniem testów. Między przypadkami testowymi rozładowuje dowolne tymczasowo załadowane rdzenie Solr. 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 
} 

a drugim od AWS DynamoDB lokalny jako wbudowaną bazą danych (kopiowane i nieznacznie modyfikowany Running AWS DynamoDB-local embedded).Ten test musi włamać się do java.library.path zanim cokolwiek innego się nie stanie lub lokalny DynamoDB (używając sqlite z bibliotekami binarnymi) nie będzie działał. Następnie uruchamia serwer do współużytkowania dla wszystkich klas testowych i czyści dane tymczasowe między testami. 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 

} 

UWAGA: niektóre części przykładach skrót z ...

Powiązane problemy