javakotlinunit-testingjar

Loading a jar file into a kotlin unit test


I'm working on a kotlin application to send data to a third party application. The vendor has provided a jar file that we will need to run in order to actually transmit the data to them (for more details see: How do I execute a jar file as part of a kotlin application?)

While I have figured out how to execute the jar using java.lang.ProcessBuilder, I am struggling to implement a unit test. I wanted to create a test of my logic that calls the jar file without having to worry about the contents of the actual jar. To do that I created a simple java class, packaged it into a jar, and then put that jar into src/test/resources.

In the unit test, I then load the jar as an input stream via a class loader. That input stream then gets written to a tmp file. When I call my function using that tmp file I get an error like this:

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

After some experimenting, I found that I can get around this by passing the absolute file path and the original (non-tmp) file to my function. The problem is, that will only work on my local machine and I need this test to be compatible with a CI pipeline.

So then, how to I load a test resource and get the path and file name without needing a hard coded file path?

My dummy class (HelloTest.jar):

public class Main {
    public static void main(String[] args) throws Exception {
        //TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
        // to see how IntelliJ IDEA suggests fixing it.
        System.out.printf("Hello and welcome!");

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

        for (int i = 1; i <= 5; i++) {
            //TIP Press <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
            // for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
            System.out.println("i = " + i);
        }
    }
}

My kotlin function to run the 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()
    })
}

My unit test:

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

                //this works locally but would fail in a CI build
                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)

                //this throws an access error
                executeJavaJar(".",
                    inputFile.name, listOf("abc")).fold({
                    fail("test execute java jar failed with error: ${it.message}")
                }, {
                        output->
                    println(output)
                    assertNotNull(output)
                    assertThat(output).isNotEmpty
                })
            }
        }
    }
}

Solution

  • As k314159 said in the comments, you are running java -jar in the current directory, but you are not passing the whole path of the jar file to java -jar. The temporary jar file is created in a temporary directory, which probably is not the current directory.

    You can pass the absolutePath of the file instead:

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