Kotlin跨平台的自动化测试方案

我来帮你实现一个Kotlin跨平台的自动化测试方案,支持Android和iOS。

项目结构

kotlin-multiplatform-test/

├── shared/

│  ├── src/

│  │  ├── commonMain/

│  │  ├── commonTest/

│  │  ├── androidMain/

│  │  ├── androidTest/

│  │  ├── iosMain/

│  │  └── iosTest/

│  └── build.gradle.kts

├── androidApp/

│  ├── src/

│  └── build.gradle.kts

├── iosApp/

│  └── iosApp.xcodeproj

└── build.gradle.kts

1. 根目录 build.gradle.kts

<kotlin>

buildscript {    repositories {        gradlePluginPortal()        google()        mavenCentral()    }    dependencies {        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")        classpath("com.android.tools.build:gradle:8.1.0")    }}allprojects {    repositories {        google()        mavenCentral()    }}

2. Shared模块 build.gradle.kts

<kotlin>

plugins {    kotlin("multiplatform")    kotlin("native.cocoapods")    id("com.android.library")}kotlin {    android {        compilations.all {            kotlinOptions {                jvmTarget = "1.8"            }        }    }        iosX64()    iosArm64()    iosSimulatorArm64()        cocoapods {        summary = "Shared module for testing"        homepage = "Link to the Shared Module homepage"        ios.deploymentTarget = "14.1"        framework {            baseName = "shared"        }    }        sourceSets {        val commonMain by getting {            dependencies {                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")            }        }                val commonTest by getting {            dependencies {                implementation(kotlin("test"))                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")            }        }                val androidMain by getting {            dependencies {                implementation("androidx.test.espresso:espresso-core:3.5.1")                implementation("androidx.test:runner:1.5.2")                implementation("androidx.test:rules:1.5.0")                implementation("androidx.test.uiautomator:uiautomator:2.2.0")            }        }                val androidUnitTest by getting {            dependencies {                implementation("junit:junit:4.13.2")                implementation("org.mockito:mockito-core:5.3.1")                implementation("org.robolectric:robolectric:4.10")            }        }                val iosMain by getting        val iosTest by getting    }}android {    namespace = "com.example.shared"    compileSdk = 34    defaultConfig {        minSdk = 24        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"    }}

3. 跨平台测试接口

commonMain/kotlin/TestInterface.kt

<kotlin>

package com.example.testingexpect class PlatformTestRunner {    fun runUITest(testCase: UITestCase)    fun findElementById(id: String): TestElement?    fun findElementByText(text: String): TestElement?    fun takeScreenshot(name: String)}expect class TestElement {    fun click()    fun setText(text: String)    fun getText(): String    fun isDisplayed(): Boolean    fun swipe(direction: SwipeDirection)}enum class SwipeDirection {    UP, DOWN, LEFT, RIGHT}data class UITestCase(    val name: String,    val steps: List<TestStep>)sealed class TestStep {    data class Click(val elementId: String) : TestStep()    data class Input(val elementId: String, val text: String) : TestStep()    data class Verify(val elementId: String, val expectedText: String) : TestStep()    data class Wait(val seconds: Int) : TestStep()    data class Swipe(val direction: SwipeDirection) : TestStep()}

4. Android实现

androidMain/kotlin/AndroidTestRunner.kt

<kotlin>

package com.example.testingimport androidx.test.espresso.Espressoimport androidx.test.espresso.action.ViewActionsimport androidx.test.espresso.assertion.ViewAssertionsimport androidx.test.espresso.matcher.ViewMatchersimport androidx.test.platform.app.InstrumentationRegistryimport androidx.test.uiautomator.UiDeviceimport androidx.test.uiautomator.UiSelectorimport android.view.Viewimport org.hamcrest.Matcherimport java.io.Fileactual class PlatformTestRunner {    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())        actual fun runUITest(testCase: UITestCase) {        testCase.steps.forEach { step ->            when (step) {                is TestStep.Click -> {                    findElementById(step.elementId)?.click()                }                is TestStep.Input -> {                    findElementById(step.elementId)?.setText(step.text)                }                is TestStep.Verify -> {                    val element = findElementById(step.elementId)                    assert(element?.getText() == step.expectedText)                }                is TestStep.Wait -> {                    Thread.sleep(step.seconds * 1000L)                }                is TestStep.Swipe -> {                    swipeScreen(step.direction)                }            }        }    }        actual fun findElementById(id: String): TestElement? {        return try {            val element = device.findObject(UiSelector().resourceId(id))            if (element.exists()) AndroidTestElement(element) else null        } catch (e: Exception) {            null        }    }        actual fun findElementByText(text: String): TestElement? {        return try {            val element = device.findObject(UiSelector().text(text))            if (element.exists()) AndroidTestElement(element) else null        } catch (e: Exception) {            null        }    }        actual fun takeScreenshot(name: String) {        val file = File(            InstrumentationRegistry.getInstrumentation().targetContext.filesDir,            "$name.png"        )        device.takeScreenshot(file)    }        private fun swipeScreen(direction: SwipeDirection) {        val width = device.displayWidth        val height = device.displayHeight                when (direction) {            SwipeDirection.UP -> device.swipe(width / 2, height * 3 / 4, width / 2, height / 4, 10)            SwipeDirection.DOWN -> device.swipe(width / 2, height / 4, width / 2, height * 3 / 4, 10)            SwipeDirection.LEFT -> device.swipe(width * 3 / 4, height / 2, width / 4, height / 2, 10)            SwipeDirection.RIGHT -> device.swipe(width / 4, height / 2, width * 3 / 4, height / 2, 10)        }    }}actual class TestElement(private val uiObject: androidx.test.uiautomator.UiObject) {    actual fun click() {        uiObject.click()    }        actual fun setText(text: String) {        uiObject.setText(text)    }        actual fun getText(): String {        return uiObject.text ?: ""    }        actual fun isDisplayed(): Boolean {        return uiObject.exists()    }        actual fun swipe(direction: SwipeDirection) {        when (direction) {            SwipeDirection.UP -> uiObject.swipeUp(10)            SwipeDirection.DOWN -> uiObject.swipeDown(10)            SwipeDirection.LEFT -> uiObject.swipeLeft(10)            SwipeDirection.RIGHT -> uiObject.swipeRight(10)        }    }}

5. iOS实现

iosMain/kotlin/IOSTestRunner.kt

<kotlin>

package com.example.testingimport platform.XCTest.*import platform.Foundation.*import kotlinx.cinterop.*actual class PlatformTestRunner {    private val app = XCUIApplication()        init {        app.launch()    }        actual fun runUITest(testCase: UITestCase) {        testCase.steps.forEach { step ->            when (step) {                is TestStep.Click -> {                    findElementById(step.elementId)?.click()                }                is TestStep.Input -> {                    findElementById(step.elementId)?.setText(step.text)                }                is TestStep.Verify -> {                    val element = findElementById(step.elementId)                    assert(element?.getText() == step.expectedText)                }                is TestStep.Wait -> {                    NSThread.sleepForTimeInterval(step.seconds.toDouble())                }                is TestStep.Swipe -> {                    swipeScreen(step.direction)                }            }        }    }        actual fun findElementById(id: String): TestElement? {        val element = app.descendantsMatchingType(XCUIElementTypeAny)            .matchingIdentifier(id)            .firstMatch                return if (element.exists()) IOSTestElement(element) else null    }        actual fun findElementByText(text: String): TestElement? {        val predicate = NSPredicate.predicateWithFormat("label == %@", text)        val element = app.descendantsMatchingType(XCUIElementTypeAny)            .matchingPredicate(predicate)            .firstMatch                return if (element.exists()) IOSTestElement(element) else null    }        actual fun takeScreenshot(name: String) {        val screenshot = XCUIScreen.mainScreen.screenshot()        // Save screenshot logic    }        private fun swipeScreen(direction: SwipeDirection) {        when (direction) {            SwipeDirection.UP -> app.swipeUp()            SwipeDirection.DOWN -> app.swipeDown()            SwipeDirection.LEFT -> app.swipeLeft()            SwipeDirection.RIGHT -> app.swipeRight()        }    }}actual class TestElement(private val xcuiElement: XCUIElement) {    actual fun click() {        xcuiElement.tap()    }        actual fun setText(text: String) {        xcuiElement.typeText(text)    }        actual fun getText(): String {        return xcuiElement.label ?: ""    }        actual fun isDisplayed(): Boolean {        return xcuiElement.exists() && xcuiElement.hittable    }        actual fun swipe(direction: SwipeDirection) {        when (direction) {            SwipeDirection.UP -> xcuiElement.swipeUp()            SwipeDirection.DOWN -> xcuiElement.swipeDown()            SwipeDirection.LEFT -> xcuiElement.swipeLeft()            SwipeDirection.RIGHT -> xcuiElement.swipeRight()        }    }}

6. 测试用例示例

commonTest/kotlin/LoginTest.kt

<kotlin>

package com.example.testingimport kotlin.test.Testclass LoginTest {    private val testRunner = PlatformTestRunner()        @Test    fun testLoginFlow() {        val loginTest = UITestCase(            name = "Login Flow Test",            steps = listOf(                TestStep.Click("login_button"),                TestStep.Input("username_field", "testuser@example.com"),                TestStep.Input("password_field", "password123"),                TestStep.Click("submit_button"),                TestStep.Wait(2),                TestStep.Verify("welcome_text", "Welcome, Test User!")            )        )                testRunner.runUITest(loginTest)        testRunner.takeScreenshot("login_success")    }        @Test    fun testScrollAndClick() {        val scrollTest = UITestCase(            name = "Scroll Test",            steps = listOf(                TestStep.Swipe(SwipeDirection.UP),                TestStep.Wait(1),                TestStep.Click("bottom_element"),                TestStep.Verify("result_text", "Clicked!")            )        )                testRunner.runUITest(scrollTest)    }}

7. 页面对象模式

commonMain/kotlin/PageObject.kt

<kotlin>

package com.example.testing.pagesabstract class BasePage(protected val testRunner: PlatformTestRunner) {    abstract fun verify()}class LoginPage(testRunner: PlatformTestRunner) : BasePage(testRunner) {    private val usernameField = "username_field"    private val passwordField = "password_field"    private val loginButton = "login_button"        override fun verify() {        assert(testRunner.findElementById(loginButton)?.isDisplayed() == true)    }        fun login(username: String, password: String): HomePage {        testRunner.findElementById(usernameField)?.setText(username)        testRunner.findElementById(passwordField)?.setText(password)        testRunner.findElementById(loginButton)?.click()                return HomePage(testRunner)    }}class HomePage(testRunner: PlatformTestRunner) : BasePage(testRunner) {    private val welcomeText = "welcome_text"        override fun verify() {        assert(testRunner.findElementById(welcomeText)?.isDisplayed() == true)    }        fun getWelcomeMessage(): String {        return testRunner.findElementById(welcomeText)?.getText() ?: ""    }}

8. 运行测试

Android

<bash>

./gradlew :androidApp:connectedAndroidTest

iOS

<bash>

xcodebuild test -workspace iosApp.xcworkspace -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 14'

这个实现提供了:

跨平台的测试APIAndroid和iOS的具体实现页面对象模式支持基本的UI操作(点击、输入、滑动等)截图功能可扩展的测试框架

你可以根据具体需求进一步扩展功能。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容