Русский
Русский
English
Статистика
Реклама

Тестирование KotlinJS фреймворки, корутины и все-все-все

Kotlin - блестящий проект. Изначально задуманный как просто JVM-язык, впоследствии он получил поддержку компиляции под все мейнстримные платформы, среди которых -- JavaScript.

Вводная. У меня есть пет-проект -- сайт и API-платформа для комьюнити по игре Elite: Dangerous. Бэкенд - на Kotlin/JVM (Ktor+Hibernate), фронтенд - на Kotlin/JS (KVision+Fomantic UI). О пет-проекте я расскажу как-нибудь потом, а о фронте поподобрнее.

  • KVision - фронтэнд-фреймворк для Kotlin, объединяющий в себе идеи из различных десктопных фреймворков (от Swing и JavaFX до WinForms и Flutter) и синтаксические возможности Kotlin, например, DSL-билдеры.

  • Fomantic-UI - форк Semantic-UI, компонентного веб-фреймворка для HTML/JS, который можно сравнить с Bootstrap, только Fomantic будет поинтереснее.

Не так давно я загорелся мыслью связать эти два мира и написать библиотеку для KVision, которая бы, как минимум, облегчила написание KVision-страниц с Fomantic-элементами. И, как подобается open source проекту, я планировал покрыть библиотеку тестами. Вот об этом приключении и будет эта статья.

Код

В первую очередь определимся с задачей. Есть у нас на руках следующий код простой Fomantic-кнопки:

package com.github.kam1sh.kvision.fomanticimport pl.treksoft.kvision.html.*import pl.treksoft.kvision.panel.SimplePanelopen class FoButton(text: String) : Button(text = text, classes = setOf("ui", "button")) {    var primary: Boolean = false        set(value) {            if (value) addCssClass("primary") else removeCssClass("primary")            field = value        }}fun SimplePanel.foButton(    text: String,    init: (FoButton.() -> Unit)? = null): FoButton {    val btn = FoButton(text)    init?.invoke(btn)    add(btn)    return btn}    

И есть парочка тестов:

package com.github.kam1sh.kvision.fomanticimport kotlin.test.BeforeTestimport kotlin.test.Testimport kotlin.test.assertEqualsimport kotlinx.browser.documentimport kotlinx.coroutines.*import pl.treksoft.kvision.panel.ContainerTypeimport pl.treksoft.kvision.panel.Rootclass FoButtonTest {    lateinit var kvapp: Root    @BeforeTest    fun before() {        kvapp = Root("kvapp", containerType = ContainerType.NONE, addRow = false)            }    @Test    fun genericButtonTest() {        kvapp.foButton("Button")        assertEqualsHtml("""...""")    }    @Test    fun buttonPrimaryTest() {        val btn = kvapp.foButton("Button") { primary = true }        assertEqualsHtml("""...""")        btn.primary = false        assertEqualsHtml("""...""")    }}fun assertEqualsHtml(expected: String, message: String? = null) {    val actual = document.getElementById("kvapp")?.innerHTML    assertEquals(expected, actual, message)}

Другими словами: "вшиваем" KVision в div с id=kvapp, создаём кнопку и потом ассертим её HTML-код.

Вопрос. Откуда должен взяться первоначальный div? Можно просто добавить HTML-код через какой-нибудь document.body?.insertAdjacentHTML(...), но что, если нам очень-очень хочется добавить прямо свою страничку вроде такой?

<source lang="html"><!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>    <link rel="stylesheet" type="text/css" href="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/fomantic-ui@2.8.7/dist/semantic.min.css">    <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/fomantic-ui@2.8.7/dist/semantic.min.js"></script></head><body>    <main>        <div id="kvapp">        </div>    </main></body></html></source>

You've lost karma

Давайте сначала обратимся к разделу тестирования документации Kotlin/JS.
For browser projects, it downloads and installs the Karma test runner with other required dependencies; for Node.js projects, the Mocha test framework is used.
Ага. Karma и Mocha. Также говорится, что конфигурируется Karma через js-скрипты в папке karma.config.d.

После поиска по документации Karma по конфигам, приходим методом проб и ошибок к такому конфиг-скрипту:

// karma.config.d/page.jsconfig.set({  customContextFile: "../../../../../src/test/resources/test.html"})

Файл test.html, в моём случае, находится по пути src/test/resources/test.html. Из-за того, что Karma запускается в каталоге build/js/packages/kvision-fomantic-test/node_modules, нам для начала надо подняться на пять каталогов повыше.

Всё готово, верно? Запускаем ./gradlew browserTest, ииии... получаем Disconnected (0 times), because no message in 30000 ms.

Это всё потому, что наша HTML-страница несколько отличается от оригинальной, в которой выполняется особый JS-код. Оригинал можно посмотреть в build/js/node_modules/karma/static/context.html.

Копируем недостающий код в место сразу перед main-блоком:

<!-- The scripts need to be in the body DOM element, as some test running frameworks need the body     to have already been created so they can insert their magic into it. For example, if loaded     before body, Angular Scenario test framework fails to find the body and crashes and burns in     an epic manner. --><script src="context.js"></script><script type="text/javascript">    // Configure our Karma and set up bindings    %CLIENT_CONFIG%    window.__karma__.setupContext(window);    // All served files with the latest timestamps    %MAPPINGS%</script><!-- Dynamically replaced with <script> tags -->%SCRIPTS%<!-- Since %SCRIPTS% might include modules, the `loaded()` call needs to be in a module too.This ensures all the tests will have been declared before karma tries to run them. --><script type="module">    window.__karma__.loaded();</script><script nomodule>    window.__karma__.loaded();</script>

Запускаем, и... прикона, работает.

Корутины мои корутины

Но это всё более-менее просто. А если у нас есть корутины? Типа, HTTP-клиент Ktor или просто нам надо добавить задержку. Вот в Python мы бы просто повесили модификатор async, поставили к pytest плагин pytest-async, и всё.

Попробуем навесить suspend на функцию теста.

> Task :compileTestKotlinJs FAILED

e: ...src/test/kotlin/com/github/kam1sh/kvision/fomantic/FoButtonTest.kt: (44, 5): Unsupported [suspend test functions]

- Gradle

Хоба. Низзя. Тикет ещё не закрыт.

Ладно, тогда выполним весь код теста в runBlocking {}. Однако...

runBlocking эксклюзивен для Kotlin/JVM.

Это проблема, тоже есть тикет, правда, закрытый, мол, это by design. В качестве решения предлагается использовать GlobalScope.promise, и в принципе с ним можно написать runBlocking в одну строку:

fun runBlocking(block: suspend (scope: CoroutineScope) -> Unit) = GlobalScope.promise { block(this) }

Однако этого недостаточно. Если вы напишете такой код, у вас в определённый момент тесты начнут отваливаться по таймауту. Можно в Karma поднять таймаут следующим способом:

config.set({  client: {    mocha: {      timeout: 9000    }  }})

Но вечно поднимать таймауты не получится. Это просто workaround.

Mocha, из которой, на самом деле, и запускается код тестов, имеет два решения:

  • Делать функцию тестов, которые принимают done-колбэк, и дёргать колбэк по завершении теста.

  • Делать функцию теста такой, чтобы она возвращала Promise.

К сожалению, если вы попробуете добавить колбэк в объявление параметров метода, то ничего хорошего вы не получите. Точнее говоря, такой коллбэк в kotlin-test-js не поддерживается.
Второе, увы и ах, тоже не сделать. Ну, тест может возвращать Promise, но в Mocha он как бы не будет передан.

Что нам, собственно, остаётся? Как связать два мира -- мир корутин и мир тестирования?

Встречайте Волшебника из Канзаса - Kotest. Его разработчики всё за нас решили.

Давайте попробуем. Меняем только зависимости и исходники тестов, конфигурация будет прежней.

// build.gradle.ktstestImplementation("io.kotest:kotest-assertions-core-js:4.3.2")testImplementation("io.kotest:kotest-framework-api-js:4.3.2")testImplementation("io.kotest:kotest-framework-engine:4.3.2")
class FoButtonTest : FunSpec({    var kvapp: Root? = null    beforeEach {        kvapp = Root("kvapp", containerType = ContainerType.NONE, addRow = false)    }    test("generic button") {        kvapp!!.foButton("Button")        assertEqualsHtml("""...""")    }    test("primary button") {        val btn = kvapp!!.foButton("Button") { primary = true }        assertEqualsHtml("""...""")        btn.primary = false        delay(200)        assertEqualsHtml("""...""")    }})

Про kotest я много рассказывать не буду, ограничусь тем, что FunSpec -- лишь один из возможных стилей написания тестов.

Если внимательно присмотреться, то можно заметить, что во втором тесте есть delay() из корутин и при этом нигде нет модификатора suspend.

А он есть:

Это пока что всё, что я могу рассказать про kotest. Я ещё буду развивать библиотеку, и когда она будет готова, я анонсирую её выход в дискорде Fomantic и слаке Kotlin/kvision. И параллельно буду познавать kotest.

Если вам этого было мало, то приношу извинения: я хотел ещё здесь показать вывод ./gradlew --debug browserTest, но подготовка этого материала и так достаточно затянулась из-за появления личной жизни, так что если интересно -- созерцайте отладочные логи Gradle сами.

Ну и что? Ну и ничего. Кушайте Kotlin/JS, запивайте kotest-ом и берегите себя.

Источник: habr.com
К списку статей
Опубликовано: 17.01.2021 00:16:40
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Тестирование веб-сервисов

Kotlin

Kotlin-js

Kotest

Kvision

Mocha

Karma

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru