W moich testach Kotlin JUnit chcę uruchamiać / zatrzymywać wbudowane serwery i używać ich w moich testach.
Próbowałem użyć @Before
adnotacji JUnit w metodzie 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ć @BeforeClass
adnotacji w metodzie, ale dodanie jej do metody powoduje błąd mówiący, że musi ona znajdować się w metodzie statycznej. Wydaje się, że Kotlin nie ma metod statycznych. A potem to samo dotyczy zmiennych statycznych, ponieważ muszę zachować odniesienie do wbudowanego serwera w celu wykorzystania w przypadkach testowych.
Jak więc 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: to pytanie jest celowo napisane i udzielone przez autora ( pytania z własną odpowiedzią), tak aby odpowiedzi na często zadawane tematy Kotlin były obecne w SO.
źródło
Odpowiedzi:
Twoja klasa testów jednostkowych zwykle potrzebuje kilku rzeczy do zarządzania współdzielonym zasobem dla grupy metod testowych. A w Kotlinie możesz używać
@BeforeClass
i@AfterClass
nie w klasie testowej, ale raczej w jej obiekcie towarzyszącym wraz z@JvmStatic
adnotacją .Struktura klasy testowej wyglądałaby 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 pod uwagę powyższe, warto przeczytać o:
@JvmStatic
- adnotacja, która zamienia metodę obiektu towarzyszącego w metodę statyczną w klasie zewnętrznej na potrzeby współpracy języka Javalateinit
- umożliwiavar
późniejszą inicjalizację właściwości, gdy masz dobrze zdefiniowany cykl życiaDelegates.notNull()
- można użyć zamiastlateinit
dla właściwości, która powinna być ustawiona przynajmniej raz przed odczytaniem.Oto pełniejsze przykłady klas testowych dla Kotlin, które zarządzają zasobami osadzonymi.
Pierwsza jest kopiowana i modyfikowana z testów Solr-Undertow , a przed uruchomieniem przypadków testowych konfiguruje i uruchamia serwer Solr-Undertow. Po uruchomieniu testów czyści wszystkie pliki tymczasowe utworzone przez testy. Zapewnia również poprawność zmiennych środowiskowych i właściwości systemu przed uruchomieniem testów. Pomiędzy testami zwalnia wszelkie tymczasowo obciążone 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 }
I kolejny startowy AWS DynamoDB lokalnie jako osadzona baza danych (skopiowana i nieznacznie zmodyfikowana z wbudowanego Uruchamiania AWS DynamoDB-local ). Ten test musi
java.library.path
zhakować, zanim cokolwiek się stanie, w przeciwnym razie lokalny DynamoDB (używając sqlite z bibliotekami binarnymi) nie zostanie uruchomiony. Następnie uruchamia serwer do współdzielenia dla wszystkich klas testowych i czyści tymczasowe dane 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ładów są skracane
...
źródło
Zarządzanie zasobami z wywołaniami zwrotnymi przed / po w testach ma oczywiście swoje zalety:
Ma też pewne wady. Jednym z ważnych z nich jest to, że zanieczyszcza kod i sprawia, że narusza on zasadę pojedynczej odpowiedzialności. Testy teraz nie tylko testują coś, ale wykonują ciężką inicjalizację i zarządzanie zasobami. W niektórych przypadkach może to być w porządku (np. Podczas konfigurowania
ObjectMapper
), ale modyfikowaniejava.library.path
lub tworzenie innych procesów (lub wbudowanych baz danych w procesie) nie jest tak niewinne.Dlaczego nie traktować tych usług jako zależności dla twojego testu kwalifikującego się do „wstrzyknięcia”, jak opisano w 12factor.net .
W ten sposób uruchamiasz i inicjalizujesz usługi zależności gdzieś poza kodem testowym.
Obecnie wirtualizacja i kontenery są prawie wszędzie, a większość maszyn programistów jest w stanie uruchomić Dockera. Większość aplikacji ma wersję dokeryzowaną: Elasticsearch , DynamoDB , PostgreSQL i tak dalej. Docker to idealne rozwiązanie dla usług zewnętrznych, których potrzebują Twoje testy.
dependsOn
ifinalizedBy
DSL do definiowania zależności). Zadanie może oczywiście wykonać ten sam skrypt, który programista wykonuje ręcznie przy użyciu powłok / procesów.To podejście:
Oczywiście ma swoje wady (w zasadzie stwierdzenia, od których zacząłem):
źródło