Kotlin'de bir veritabanı bağlantısını veya gömülü bir elasticsearch sunucusunu başlatma / durdurma gibi birim test kaynaklarını nasıl yönetebilirim?


94

Kotlin JUnit testlerimde gömülü sunucuları başlatmak / durdurmak ve testlerimde kullanmak istiyorum.

JUnit @Beforeaçıklamasını test sınıfımdaki bir yöntemde kullanmayı denedim ve iyi çalışıyor, ancak yalnızca bir kez yerine her test durumunu çalıştırdığı için doğru davranış değil.

Bu nedenle, @BeforeClassaçıklamayı bir yöntem üzerinde kullanmak istiyorum , ancak bir yönteme eklemek, statik bir yöntemde olması gerektiğini söyleyen bir hataya neden oluyor. Kotlin statik yöntemlere sahip görünmüyor. Aynısı statik değişkenler için de geçerli, çünkü test senaryolarında kullanmak için gömülü sunucuya bir referans tutmam gerekiyor.

Peki bu gömülü veritabanını tüm test durumlarım için bir kez nasıl oluşturabilirim?

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

    ...
}

Not: Bu soru kasıtlı olarak yazar tarafından yazılır ve yanıtlanır ( Kendi Kendine Cevaplanan Sorular ), böylece yaygın olarak sorulan Kotlin konularının cevapları SO'da mevcuttur.


2
JUnit 5, bu kullanım durumu için statik olmayan yöntemleri destekleyebilir, bkz. Github.com/junit-team/junit5/issues/419#issuecomment-267815529 ve Kotlin geliştiricilerinin bu tür iyileştirmelerle ilgilendiğini göstermek için yorumumu + 1'lemekten çekinmeyin.
Sébastien Deleuze

Yanıtlar:


156

Birim testi sınıfınız, bir grup test yöntemi için paylaşılan bir kaynağı yönetmek için genellikle birkaç şeye ihtiyaç duyar. Ve KOTLIN içinde kullanabileceğiniz @BeforeClassve @AfterClassolmayan deney sınıfında değil, onun içindeki arkadaşı nesne ile birlikte @JvmStaticaçıklama .

Bir test sınıfının yapısı şöyle görünür:

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
}  

Yukarıdakiler göz önüne alındığında, aşağıdakileri okumalısınız:

  • tamamlayıcı nesneler - Java'daki Class nesnesine benzer, ancak statik olmayan sınıf başına tek bir
  • @JvmStatic - Java birlikte çalışması için dış sınıfta bir tamamlayıcı nesne yöntemini statik bir yönteme dönüştüren bir açıklama
  • lateinit- variyi tanımlanmış bir yaşam döngüsüne sahip olduğunuzda bir mülkün daha sonra başlatılmasına izin verir
  • Delegates.notNull()- lateinitokunmadan önce en az bir kez ayarlanması gereken bir özellik yerine kullanılabilir .

Burada, Kotlin için gömülü kaynakları yöneten daha kapsamlı test sınıfları örnekleri verilmiştir.

İlki Solr-Undertow testlerinden kopyalanır ve değiştirilir ve test senaryoları çalıştırılmadan önce bir Solr-Undertow sunucusunu yapılandırır ve başlatır. Testler çalıştıktan sonra, testler tarafından oluşturulan tüm geçici dosyaları temizler. Ayrıca, testler çalıştırılmadan önce ortam değişkenlerinin ve sistem özelliklerinin doğru olmasını sağlar. Test senaryoları arasında, geçici olarak yüklenmiş Solr çekirdeklerini kaldırır. 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
}

Ve gömülü bir veritabanı olarak yerel olarak AWS DynamoDB'yi başlatan başka bir başlangıç ​​(Gömülü AWS DynamoDB-local'den biraz kopyalanıp değiştirildi ). Bu test java.library.pathbaşka bir şey olmadan önce hacklemelidir, aksi takdirde yerel DynamoDB (ikili kitaplıklarla sqlite kullanarak) çalışmayacaktır. Ardından, tüm test sınıfları için paylaşılacak bir sunucu başlatır ve testler arasında geçici verileri temizler. 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

}

NOT: örneklerin bazı kısımları şu şekilde kısaltılmıştır:...


0

Kaynakları testlerde önce / sonra geri aramalarla yönetmenin, tabii ki, avantajları vardır:

  • Testler "atomik" tir. Bir test, tüm geri aramalarla bir bütün olarak yürütülür Biri, testlerden önce bir bağımlılık hizmetini çalıştırmayı ve tamamlandıktan sonra kapatmayı unutmaz. Düzgün yapılırsa, yürütme geri aramaları herhangi bir ortamda çalışacaktır.
  • Testler bağımsızdır. Harici veri veya kurulum aşaması yoktur, her şey birkaç test sınıfında yer alır.

Bazı eksileri de var. Bunlardan önemli bir tanesi, kodu kirletmesi ve kodu tek sorumluluk ilkesini ihlal etmesidir. Testler artık yalnızca bir şeyi test etmekle kalmıyor, aynı zamanda ağır bir başlatma ve kaynak yönetimi de gerçekleştiriyor. Bazı durumlarda sorun olmayabilir (bir yapılandırmaObjectMapper gibi ), ancak java.library.pathbaşka bir işlemi (veya işlem içi gömülü veritabanlarını) değiştirmek veya üretmek o kadar da masum değildir.

Neden bu hizmetleri testiniz için 12factor.net'te açıklandığı gibi "enjeksiyon" için uygun bağımlılıklar olarak ele almıyorsunuz ?

Bu şekilde bağımlılık hizmetlerini test kodunun dışında bir yerde başlatır ve başlatırsınız.

Günümüzde sanallaştırma ve kapsayıcılar neredeyse her yerde ve çoğu geliştiricinin makinesi Docker'ı çalıştırabiliyor. : Ve uygulamanın en bir dockerized sürümüne sahip Elasticsearch , DynamoDB , PostgreSQL vb vb. Docker, testlerinizin ihtiyaç duyduğu harici hizmetler için mükemmel bir çözümdür.

  • Bir geliştirici tarafından her test yapmak istediğinde manuel olarak çalıştırılan bir betik olabilir.
  • Oluşturma aracı tarafından çalıştırılan bir görev olabilir (örneğin, Gradle'ın bağımlılıkları tanımlamak için harika dependsOnve DSL'si vardır finalizedBy). Elbette bir görev, geliştiricinin kabuk çıkışları / işlem yürütmelerini kullanarak manuel olarak yürüttüğü aynı betiği çalıştırabilir.
  • Test yürütmeden önce IDE tarafından çalıştırılan bir görev olabilir . Yine aynı betiği kullanabilir.
  • Çoğu CI / CD sağlayıcısının bir "hizmet" kavramı vardır - yapınıza paralel olarak çalışan ve normal SDK / bağlayıcı / API aracılığıyla erişilebilen harici bir bağımlılık (süreç) vardır: Gitlab , Travis , Bitbucket , AppVeyor , Semaphore , ...

Bu yaklaşım:

  • Test kodunuzu başlatma mantığından kurtarır. Testleriniz sadece test edecek ve daha fazlasını yapmayacaktır.
  • Kodu ve verileri ayırır. Yeni bir test durumu eklemek artık yerel araç setiyle bağımlılık hizmetlerine yeni veriler eklenerek yapılabilir. Yani SQL veritabanları için SQL kullanacaksınız, Amazon DynamoDB için CLI'yi tablolar oluşturmak ve öğeleri yerleştirmek için kullanacaksınız.
  • "Ana" uygulamanız başladığında açıkça bu hizmetleri başlatmadığınız bir üretim koduna daha yakındır.

Elbette kusurları var (temelde başladığım ifadeler):

  • Testler daha "atomik" değil. Bağımlılık hizmeti bir şekilde test yürütmeden önce başlatılmalıdır. Başlama şekli farklı ortamlarda farklı olabilir: geliştiricinin makinesi veya CI, IDE veya oluşturma aracı CLI.
  • Testler bağımsız değildir. Artık tohum verileriniz bir görüntünün içinde bile paketlenmiş olabilir, bu nedenle değiştirmek farklı bir projenin yeniden oluşturulmasını gerektirebilir.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.