В моих тестах Kotlin JUnit я хочу запускать / останавливать встроенные серверы и использовать их в своих тестах.
Я попытался использовать @Before
аннотацию JUnit для метода в моем тестовом классе, и он работает нормально, но это неправильное поведение, поскольку он запускает каждый тестовый пример, а не только один раз.
Поэтому я хочу использовать @BeforeClass
аннотацию для метода, но добавление ее к методу приводит к ошибке, в которой говорится, что она должна быть в статическом методе. Kotlin, похоже, не имеет статических методов. То же самое относится и к статическим переменным, потому что мне нужно сохранить ссылку на встроенный сервер для использования в тестовых примерах.
Итак, как мне создать эту встроенную базу данных только один раз для всех моих тестовых случаев?
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
...
}
Примечание: этот вопрос намеренно написан, и автор отвечает на него ( вопросы с ответами), так что ответы на часто задаваемые темы Kotlin присутствуют в SO.
источник
Ответы:
Классу модульного тестирования обычно требуется несколько вещей для управления общим ресурсом для группы методов тестирования. И в Котлин вы можете использовать
@BeforeClass
и@AfterClass
не в тестовом классе, а в пределах его объекта - компаньона вместе с@JvmStatic
аннотацией .Структура тестового класса будет выглядеть так:
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 }
Учитывая вышесказанное, вам следует прочитать о:
@JvmStatic
- аннотация, которая превращает метод сопутствующего объекта в статический метод внешнего класса для взаимодействия с Javalateinit
- позволяетvar
инициализировать свойство позже, когда у вас есть четко определенный жизненный циклDelegates.notNull()
- может использоваться вместоlateinit
свойства, которое должно быть установлено хотя бы один раз перед чтением.Вот более полные примеры тестовых классов для Kotlin, которые управляют встроенными ресурсами.
Первый копируется и изменяется из тестов Solr-Undertow , и перед запуском тестовых случаев настраивает и запускает сервер Solr-Undertow. После запуска тестов он очищает все временные файлы, созданные тестами. Это также обеспечивает правильность переменных среды и свойств системы перед запуском тестов. Между тестами он выгружает все временно загруженные ядра Solr. Тест:
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 }
И еще один запускающий локальный AWS DynamoDB в качестве встроенной базы данных (скопирован и немного изменен из « Запуск локального встроенного AWS DynamoDB» ). Этот тест должен быть взломан,
java.library.path
прежде чем что-то еще произойдет, иначе локальный DynamoDB (с использованием sqlite с двоичными библиотеками) не будет работать. Затем он запускает общий сервер для всех тестовых классов и очищает временные данные между тестами. Тест: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 }
ПРИМЕЧАНИЕ: некоторые части примеров сокращены до
...
источник
У управления ресурсами с помощью обратных вызовов до / после в тестах, очевидно, есть свои плюсы:
У него тоже есть минусы. Одним из важных из них является то, что это загрязняет код и заставляет его нарушать принцип единой ответственности. Теперь тесты не только проверяют что-то, но и выполняют тяжелую инициализацию и управление ресурсами. В некоторых случаях это может быть нормально (например, настройка
ObjectMapper
), но изменениеjava.library.path
или создание других процессов (или встроенных баз данных) не так уж и невинно.Почему бы не рассматривать эти службы как зависимости для вашего теста, подходящего для «инъекции», как описано на 12factor.net .
Таким образом вы запускаете и инициализируете службы зависимостей где-то вне тестового кода.
В настоящее время виртуализация и контейнеры есть почти везде, и большинство машин разработчиков могут запускать Docker. И у большинства приложений есть докерированные версии: Elasticsearch , DynamoDB , PostgreSQL и так далее. Docker - идеальное решение для внешних сервисов, которые нужны вашим тестам.
dependsOn
иfinalizedBy
DSL для определения зависимостей). Задача, конечно же, может выполнять тот же сценарий, который разработчик выполняет вручную с помощью командных интерпретаторов / исполняемых файлов процессов.Этот подход:
Конечно, у него есть недостатки (в основном те утверждения, с которых я начал):
источник