如何在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
相关产品推荐
相关产品推荐

