Testing private methods in Kotlin

Viewed 21517

How to test private methods in Kotlin? I tried to add @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) from androidx.annotation.VisibleForTesting but it doesn’t make my function private

This is how I’m using it

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun doSomething() {}

[EDIT]

I understand that I shouldn't test private methods, but it's now always trivial. What about below case.

I have a CsvReader class

class CsvReader(private val inputStream: InputStream, private val separator: String = "\t") {
    fun read(): List<String> {
        return read(inputStream.bufferedReader())
    }
    private fun read(bufferedReader: BufferedReader): List<String> {
        val line = bufferedReader.use { it.readLine() } // `use` is like try-with-resources in Java
        return parse(line)
    }
    private fun parse(line: String): List<String> {
        return line.split(separator)
    }
}

And I wrote tests for it

class CsvReaderTest {
    private val stream = mock<InputStream>()
    private val reader = CsvReader(stream)
    private val bufferedReader = mock<BufferedReader>()
    @Test
    fun read() {
        whenever(bufferedReader.readLine()).thenReturn("Jakub\tSzwiec")
        reader.read(bufferedReader) shouldEqual listOf("Jakub", "Szwiec")
    }
    @Test
    fun readWhenEmpty() {
        whenever(bufferedReader.readLine()).thenReturn("")
        reader.read(bufferedReader) shouldEqual listOf("")
    }
    @Test
    fun throwIOExceptionWhenReadingProblems() {
        whenever(bufferedReader.readLine()).thenThrow(IOException::class.java)
        val read = { reader.read(bufferedReader) }
        read shouldThrow IOException::class
    }
}

Unfortunately, for tests I need to call private function fun read(bufferedReader: BufferedReader): List<String> because when mocking File, file.bufferedReader gives NullPointerException Unable to mock BufferedWriter class in junit

3 Answers

You can use java reflections:

To test the method:

class ClassUnderTest {
      private fun printGreetings(name: String): String {
        return "Hello, $name"
      }
}

That's enough:

  private val classUnderTest = spyk(ClassUnderTest()) 

  @Test
    fun `should return greetings`() {
      val method = classUnderTest.javaClass.getDeclaredMethod("printGreetings", String::class.java)
      method.isAccessible = true
      val parameters = arrayOfNulls<Any>(1)
      parameters[0] = "Piotr"

      assertEquals("Hello, Piotr", method.invoke(classUnderTest, *parameters) )
  }

like this:

fun callPrivate(objectInstance: Any, methodName: String, vararg args: Any?): Any? {
        val privateMethod: KFunction<*>? =
            objectInstance::class.functions.find { t -> return@find t.name == methodName }

        val argList = args.toMutableList()
        (argList as ArrayList).add(0, objectInstance)
        val argArr = argList.toArray()

        privateMethod?.apply {
            isAccessible = true
            return call(*argArr)
        }
            ?: throw NoSuchMethodException("Method $methodName does not exist in ${objectInstance::class.qualifiedName}")
        return null
    }

you need to pass instance of the object you want the method to be called on, method name and required arguments

There is only one answer to this: don't even try.

You write source code to communicate intent. If you make something private then that something is an internal implementation detail. It is subject to change the very next second.

Related