Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock package-level function in Kotlin with PowerMock

I have a file with some package-level functions in Kotlin.

//Logger.kt

fun info(tag : String, message : String){
...
}

fun error{....}

I'm testing functions of a class that call functions of this kotlin file, and I would like to mock them. I know that package-level functions are like static methods in Java, so I've been thinking of using PowerMock.

//MyClass: Class that calls Logger.kt functions
class MyClass {

   fun myFunction(){
       info("TAG", "Hello world!")
   }

}

Any ideas?

like image 370
jmartinalonso Avatar asked Mar 16 '18 09:03

jmartinalonso


2 Answers

There is a workaround you can use to mock top-level function from Kotlin.

Explanation

@PrepareForTest annotation does have 2 params to provide the context (classes) where you want to mock something or where you mocking stuff in going to be used.

The first param is value if type Class<?>[]: here you can provide an array of classes. E.g:

@PrepareForTest(Class1::class, Class2::class, Class3::class, QueryFactory::class)

The second param fullyQualifiedNames of type String[]: here you can provide an array with the fully qualified name of the classes. E.g:

@PrepareForTest(Class1::class, fullyQualifiedNames = arrayOf("x.y.z.ClassName"))

Let's say that we have a Kotlin file named "MyUtils.kt" which contains only top-level functions. As you know you cannot reference the MyUtilsKt class from Kotlin files, but from Java you can. It means that there is static class generated (I do not have enough knowledge yet to provide you with more details about this) and it has a fully qualified name.

Solution

This solution is not perfect. I implemented it on our code-base and it seems to work. For sure it can be improved.

  1. I created a Kotlin file called TopLevelFunctionClass.kt where I added the fully qualified names of the "classes" which contain only top-level functions.

internal const val MyUtilsKt = "com.x.y.z.MyUtilsKt"

Unfortunately, I had to hardcode the name since an annotation argument must be a compile-time constant.

  1. I updated the @PrepareForTest annotation of my test class as following:

    @RunWith(PowerMockRunner::class)
    @PrepareForTest(Class1::class, Class2::class, Class4::class,
    fullyQualifiedNames = [MyUtilsKt]) // the string constant declared in TopLevelFunctionClass.kt
    
  2. I updated the test method as following:

    Top-level function in MyUtils.kt:

    internal fun testMock(): Int {
        return 4
    }
    

    The test method:

    @Test
    fun myTestMethod() {
        ...
        mockStatic(Class.forName(MyUtilsKt)) // the string constant declared in TopLevelFunctionClass.kt
        `when`(testMock()).thenReturn(10)
        assertEquals(10, testMock()) // the test will successfully pass.
    }
    

Side effect: In case you rename the kotlin file which contains the top-level functions you have to change also the constant defined in TopLevelFunctionClass.kt. A possible solution to avoid the renaming problem is to add: @file:JvmName("The name you want for this file"). In case you'll have 2 files with the same name you'll get a duplication JVM class name error.

like image 100
David Rauca Avatar answered Sep 19 '22 08:09

David Rauca


You can use PowerMock for this. As you already pointed out, Kotlin generates a static Java class for your top level functions in the file Logger.kt, named LoggerKt.java. You can change it by annotating the file with @file:JvmName(“...“), if you like. Therefore you can do it like this:

@RunWith(PowerMockRunner.class)
@PrepareForTest(LoggerKt.class)
public class MyClassTest {

    @Test
    public void testDoIt() {
        PowerMockito.mockStatic(LoggerKt.class);

        MyClass sut = new MyClass();
        sut.myFunction(); //the call to info(...) is mocked.
    }
}

I tried to make it work in Kotlin, but I didn't find a way to make the from Kotlin generated Logger Java class available as a class literal to be able to use it for the @PrepareForTest annotation. Although it is possible to reference the generated Java class in Kotlin.

like image 29
Philipp Hofmann Avatar answered Sep 22 '22 08:09

Philipp Hofmann