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

如何在Kotlin单元测试中加载Jar文件并适配CI流水线

问题描述

我正在开发一个用Kotlin编写的、向第三方应用发送数据的应用,供应商提供了用于实际传输数据的Jar文件。我已经用java.lang.ProcessBuilder实现了运行Jar的逻辑,但编写单元测试时遇到了问题。

我想在不关心实际Jar内容的前提下测试调用Jar的逻辑,于是制作了一个简单的Java类并打包成Jar,放在src/test/resources目录中。测试时通过类加载器将Jar读为输入流并写入临时文件,但调用执行函数时出现如下错误:

java.lang.Exception: Error: Unable to access jarfile hello15286565382928855962.jar

硬编码本地绝对路径能正常运行,但这种方式在CI流水线中会失效。请问如何不用硬编码路径就能加载测试资源Jar并获取其路径和文件名?


测试用Jar(HelloTest.jar)代码

public class Main {
    public static void main(String[] args) throws Exception {
        System.out.printf("Hello and welcome!");

        if( args.length > 1 ) {
            throw new Exception("Too many args");
        }

        for (int i = 1; i <= 5; i++) {
            System.out.println("i = " + i);
        }
    }
}

Kotlin执行Jar的函数

internal fun executeJavaJar(workingDir:String, jarFile:String, args:List<String>): Either<Throwable, String>
{
   return runCatching {
       val cmdArgs: MutableList<String> = mutableListOf("java", "-jar", jarFile)
       cmdArgs.addAll(args)
       ProcessBuilder(cmdArgs).directory(File(workingDir))
           .redirectOutput(ProcessBuilder.Redirect.PIPE)
           .redirectError(ProcessBuilder.Redirect.PIPE).start()
    }.fold({
        it->
        val output = IOUtils.toString( it.inputStream, Charsets.UTF_8)
        val error = IOUtils.toString( it.errorStream, Charsets.UTF_8)
       if(error.isNotEmpty())
       {
          throw Exception(error)
       }
       else {
           output.right()
       }
    }, {
        error->
        error.left()
    })
}

单元测试代码

@Test
fun `test execute java jar`()
{
    mockContext(300).value().let { context ->
        testInt295DependencyProvider(context).let { dp ->
            javaClass.classLoader.getResourceAsStream("HelloTest.jar").use { stream ->                 

                // 本地能运行但CI会失败
                executeJavaJar("/Users/userA/Documents/workspace/app-event-management/lambda-INT295/src/test/resources",
                    "HelloTest.jar", listOf("abc")).fold({
                    fail("test execute java jar failed with error: ${it.message}")
                }, {
                        output->
                    println(output)
                    assertNotNull(output)
                    assertThat(output).isNotEmpty
                })
            }
        }
    }
}

@Test
fun `test execute java jar with tmp`()
{
    mockContext(300).value().let { context ->
        testInt295DependencyProvider(context).let { dp ->
            javaClass.classLoader.getResourceAsStream("HelloTest.jar").use { stream ->
                assertNotNull(stream)
                val inputFile = createTempFile(prefix = "hello", suffix = ".jar").toFile()
                inputFile.copyInputStreamToFile(stream)

                // 报无法访问Jar的错误
                executeJavaJar(".",
                    inputFile.name, listOf("abc")).fold({
                    fail("test execute java jar failed with error: ${it.message}")
                }, {
                        output->
                    println(output)
                    assertNotNull(output)
                    assertThat(output).isNotEmpty
                })
            }
        }
    }
}

解决方案

问题根源

第二个测试中仅传入inputFile.name(文件名),同时工作目录设为.(当前目录),但系统临时文件默认存储在系统临时目录(如/tmp),并非当前工作目录,因此找不到Jar文件。

具体解决方法

方法1:使用临时文件的绝对路径

直接传入临时文件的绝对路径,不受工作目录限制:

@Test
fun `test execute java jar with tmp fixed`() {
    mockContext(300).value().let { context ->
        testInt295DependencyProvider(context).let { dp ->
            javaClass.classLoader.getResourceAsStream("HelloTest.jar").use { stream ->
                assertNotNull(stream)
                val inputFile = createTempFile(prefix = "hello", suffix = ".jar").apply {
                    copyInputStreamToFile(stream)
                    deleteOnExit() // 测试结束自动清理临时文件
                }

                // 传入临时文件绝对路径,工作目录可保留为当前目录
                executeJavaJar(".", inputFile.absolutePath, listOf("abc")).fold({
                    fail("test execute java jar failed with error: ${it.message}")
                }, { output ->
                    println(output)
                    assertNotNull(output)
                    assertThat(output).isNotEmpty
                })
            }
        }
    }
}

方法2:直接获取测试资源路径(无需写入临时文件)

如果测试资源在编译后以文件形式存在(多数本地/CI环境满足),可通过类加载器获取资源的绝对路径:

@Test
fun `test execute java jar with resource path`() {
    mockContext(300).value().let { context ->
        testInt295DependencyProvider(context).let { dp ->
            javaClass.classLoader.getResource("HelloTest.jar")?.let { resource ->
                val jarFile = File(resource.toURI())
                executeJavaJar(jarFile.parent, jarFile.name, listOf("abc")).fold({
                    fail("test execute java jar failed with error: ${it.message}")
                }, { output ->
                    println(output)
                    assertNotNull(output)
                    assertThat(output).isNotEmpty
                })
            } ?: fail("HelloTest.jar not found in test resources")
        }
    }
}

可选优化:修改执行函数支持File对象

避免路径拼接错误,直接传入File对象:

internal fun executeJavaJar(workingDir: File, jarFile: File, args: List<String>): Either<Throwable, String> {
    return runCatching {
        val cmdArgs = mutableListOf("java", "-jar", jarFile.absolutePath)
        cmdArgs.addAll(args)
        ProcessBuilder(cmdArgs)
            .directory(workingDir)
            .redirectOutput(ProcessBuilder.Redirect.PIPE)
            .redirectError(ProcessBuilder.Redirect.PIPE)
            .start()
    }.fold({ process ->
        val output = IOUtils.toString(process.inputStream, Charsets.UTF_8)
        val error = IOUtils.toString(process.errorStream, Charsets.UTF_8)
        if (error.isNotEmpty()) {
            throw Exception(error)
        } else {
            output.right()
        }
    }, { error ->
        error.left()
    })
}

调用示例:

executeJavaJar(File("."), inputFile, listOf("abc"))

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:15:56