You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Studio中TestNG+Gradle测试任务构建成功但未执行

测试框架未执行测试问题排查与修复

问题背景

基于Java、TestNG、Appium、Gradle搭建的Android测试项目,运行自定义testRun任务时构建显示成功,但实际未执行任何测试,Gradle报告更新但无测试结果,使用Android模拟器运行测试。

项目结构

project-root
│
├── .gradle
├── .idea
├── app
│   ├── apk
│   ├── build
│   ├── reports
│   ├── screenshots
│   └── src
│       ├── main
│       │   ├── java
│       │   │   ├── com
│       │   │   │   ├── utils
│       │   │   │   │   └── (some classes)
│       │   │   │   └── instagrid
│       │   │   │       └── application
│       │   │   │           └── (some classes - test steps)
│       │   │   └── res
│       │   └── test
│       │       ├── java
│       │       │   └── regression
│       │       │       ├── (your test classes)
│       │       │       └── regression.xml
│       │       └── res
├── gradle
└── (other project files)

关键配置文件

regression.xml

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="instagrid" preserve-order="true">
    <test name="Regression">
        <classes>
            <class name="regression.LandingPageTests"/>
            <class name="regression.RegistrationTests"/>
            <class name="regression.LoginTests"/>
            <class name="regression.ProfileTests"/>
            <class name="regression.BatteryDetailsTests"/>
            <class name="regression.MapTests"/>
            <class name="regression.TranslationsTests"/>
            <class name="regression.HomePageTests"/>
            <!--<class name="regression.PairingTests"/>-->
       </classes>
   </test>
</suite>

build.gradle

plugins {
    id 'com.android.application'
}

apply from: "testsetup.gradle"

android {
    namespace 'com.example.instagrid'
    compileSdk 33

    defaultConfig {
        applicationId "com.example.instagrid"
        minSdk 33
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    sourceSets {
        main.java.srcDirs = ['src/main/java']
        main.resources.srcDirs = ['src/main/res']
        test.java.srcDirs = ['src/test/java']
        test.resources.srcDirs = ['src/test/res']
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'io.appium:java-client:7.3.0'
    implementation 'org.testng:testng:7.4.0'
    testImplementation project(path: ':app')
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    // Other dependencies
    implementation 'org.jsoup:jsoup:1.16.1'
}

task smokeTests(type: Test) {
    useTestNG() {
        useDefaultListeners = true
        includeGroups 'smoke'
    }
    filter {
        includeTestsMatching 'regression.xml'
    }
}

testsetup.gradle

import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent

ext {
    env = 'uat'
    defaultTestPackage = "regression"
}

tasks.withType(Test) {
    classpath += files (
            android.sourceSets.main.java.srcDirs,
            android.sourceSets.test.java.srcDirs
    )
    useTestNG() {
        includeGroups project.hasProperty("groups") ? "$groups" : "none"
    }
    testLogging {
        showStandardStreams = true
        events  = [TestLogEvent.STARTED,
                   TestLogEvent.FAILED,
                   TestLogEvent.PASSED,
                   TestLogEvent.SKIPPED,
                   TestLogEvent.STANDARD_OUT,
                   TestLogEvent.STANDARD_ERROR]
        exceptionFormat = TestExceptionFormat.FULL
        showCauses = true
        showExceptions = true
        showStackTraces = true
        showStandardStreams = true

        afterTest { desc, result ->
            println "\nTest executed ${desc.name} [${desc.className}] with result: ${result.resultType}"
        }

        afterSuite { desc, result ->
            if (!desc.parent) { // will match the outermost suite
                def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
                def startItem = '|  ', endItem = '  |'
                def repeatLength = startItem.length() + output.length() + endItem.length()
                println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength))
            }
        }
    }

    enableAssertions = true
    scanForTestClasses = false

    options {
        systemProperties(System.getProperties())
        systemProperty 'env', System.properties['env'] ?: "${env}"
    }
}

def generateTestRunArgs() {
    def resultDir = "$projectDir/app/reports"
    def suiteDir = "$projectDir/src/test/java/regression"
    def regressionXML = "$projectDir/src/test/java/regression.xml"

    def args = ["-d", resultDir, "-configfailurepolicy", "continue", regressionXML]

    if (project.hasProperty("defaultListeners")) {
        args += ["-usedefaultlisteners", "true"]
    } else {
        args += ["-usedefaultlisteners", "false",
                 "-listener", "testng.listeners.TestExecListener"]
    }

    if (project.hasProperty("groups")) {
        args += ["-groups", "$groups"]
    }

    if (project.hasProperty("testclasses")) {
        args += ["-testclass", "$testclasses"]
    }

    if (project.hasProperty("testmethods")) {
        args += ["-methods", "$testmethods"]
    }

    if (project.hasProperty("parallel")) {
        args += ["-parallel", "$parallel"]
    } else {
        args += ["-parallel", "methods"]
    }

    if (project.hasProperty("threads")) {
        args += ["-threadcount", "$threads"]
        args += ["-suitethreadpoolsize", "$threads"]
    } else {
        args += ["-threadcount", "1"]
        args += ["-suitethreadpoolsize", "1"]
    }

    if (project.hasProperty("dataproviderthreads")) {
        args += ["-dataproviderthreadcount", "$dataproviderthreads"]
    } else {
        args += ["-dataproviderthreadcount", "1"]
    }

    return args
}


/**
 * Using the default gradle test task (limited options)
 * Usage:
 * $ gradle clean testRun --tests FeatureATest
 */
task testRun(type: Test) {
    dependsOn "clean"

    classpath += files(
            android.sourceSets.main.java.srcDirs,
            android.sourceSets.test.java.srcDirs
    )

    scanForTestClasses = true
    forkEvery = 1
    useTestNG() {
        useDefaultListeners = true
        includeGroups 'smoke'
    }
    testClassesDirs = files("src/test/java/regression")

    doLast {
        def args = generateTestRunArgs()
        println "TestNG args: " + args
        println "System property env value: ${System.properties.env}"
        println "env default: $env"
    }
}

/**
 * Using the default gradle test task (limited options)
 * Usage:
 * $ gradle testGroups -Pgroups=main
 * $ gradle testGroups -Pgroups=main,unit
 */
task testGroups(type: Test) {
    dependsOn "clean", "classes", "testClasses"

    useTestNG() {
        includeGroups project.hasProperty("groups") ? "$groups" : "none"
    }
    testLogging.events "passed", "skipped", "failed"
    testLogging.showStandardStreams = true
    testLogging.exceptionFormat = "full"
}

执行结果

./gradlew clean testRun -Pgroups=smoke        

> Task :app:testRun

--------------------------------------------------------------------
|  Results: SUCCESS (0 tests, 0 successes, 0 failures, 0 skipped)  |
--------------------------------------------------------------------
TestNG args: [-d, D:\Workspace\instagrid\app/app/reports, -configfailurepolicy, continue, D:\Workspace\instagrid\app/src/test/java/regression.xml, -usedefaultlisteners, false, -listener, testng.listeners.TestExecListener, -groups, smoke, -parallel, methods, -threadcount, 1, -suitethreadpoolsize, 1, -dataproviderthreadcount, 1]
System property env value: null
env default: uat

核心原因分析

  1. 依赖配置错误:TestNG和Appium依赖使用implementation而非testImplementation,导致测试类无法在测试类路径中访问这些库。
  2. 测试类路径与目录配置错误:testRun任务中testClassesDirs指向源码目录src/test/java/regression,而非编译后的class文件目录;classpath仅添加了源码目录,未包含依赖库和编译后的主代码/测试代码class文件。
  3. 全局Test任务配置冲突:tasks.withType(Test)中设置了scanForTestClasses = false,覆盖了testRun任务中scanForTestClasses = true的配置,导致Gradle不扫描测试类。
  4. TestNG套件XML未被加载:generateTestRunArgs方法生成了TestNG参数,但未传递给Gradle的Test任务,Test任务默认不会使用该参数,而是依赖自身的suiteXmlFiles配置。
  5. 系统属性传递问题:systemProperty 'env'未正确传递,导致测试代码可能无法获取环境变量。

修复步骤

1. 修正依赖配置

修改build.gradle中的依赖,将TestNG和Appium改为testImplementation:

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    // 改为testImplementation
    testImplementation 'io.appium:java-client:7.3.0'
    testImplementation 'org.testng:testng:7.4.0'
    testImplementation project(path: ':app')
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    // Other dependencies
    implementation 'org.jsoup:jsoup:1.16.1'
}

2. 调整全局Test任务配置

修改testsetup.gradle中的全局配置,移除scanForTestClasses = false,并补充编译后的class目录到类路径:

tasks.withType(Test) {
    classpath += files (
            android.sourceSets.main.java.srcDirs,
            android.sourceSets.test.java.srcDirs,
            // 添加编译后的class目录
            android.sourceSets.main.output.classesDirs,
            android.sourceSets.test.output.classesDirs
    )
    useTestNG() {
        includeGroups project.hasProperty("groups") ? "$groups" : "none"
    }
    testLogging {
        // 保留原有日志配置...
    }

    enableAssertions = true
    // 移除全局的scanForTestClasses设置,让具体任务自行配置
    // scanForTestClasses = false

    options {
        systemProperties(System.getProperties())
        systemProperty 'env', project.findProperty('env') ?: env
    }
}

3. 修复testRun任务配置

更新testsetup.gradle中的testRun任务,指向正确的class目录并加载TestNG套件XML:

task testRun(type: Test) {
    dependsOn "clean", "testClasses" // 依赖testClasses确保测试代码已编译

    classpath = sourceSets.test.runtimeClasspath // 使用默认测试类路径,包含所有依赖和编译后的类

    scanForTestClasses = true
    forkEvery = 1
    useTestNG() {
        useDefaultListeners = true
        includeGroups project.hasProperty("groups") ? "$groups" : "smoke"
        // 加载TestNG套件XML
        suiteXmlFiles = files("$projectDir/src/test/java/regression/regression.xml")
    }
    // 指向编译后的测试类目录
    testClassesDirs = sourceSets.test.output.classesDirs

    doLast {
        def args = generateTestRunArgs()
        println "TestNG args: " + args
        println "System property env value: ${System.properties['env']}"
        println "env default: $env"
    }
}

4. 确保测试类符合要求

  • 测试类位于src/test/java/regression目录下,类名与regression.xml中的一致。
  • 测试方法添加@Test(groups = "smoke")注解,确保被includeGroups 'smoke'匹配。

验证结果

执行命令./gradlew clean testRun -Pgroups=smoke,预期输出会显示测试执行的详细日志,结果统计中包含实际执行的测试数量:

./gradlew clean testRun -Pgroups=smoke        

> Task :app:testRun

Test executed testLandingPage [regression.LandingPageTests] with result: SUCCESS
...
--------------------------------------------------------------------
|  Results: SUCCESS (5 tests, 5 successes, 0 failures, 0 skipped)  |
--------------------------------------------------------------------
TestNG args: [...]
System property env value: uat
env default: uat

内容的提问来源于stack exchange,提问作者automationguru

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 10:35:56