14

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

qbait
  • 2,934
  • 2
  • 22
  • 39
  • 3
    An annotation cannot make a function be private. It can help Android Studio warn you about using that function from things other than test code, but that's about it. – CommonsWare Aug 26 '18 at 22:53
  • 4
    You can't directly test private methods, and you can't make a method private any other way than the keyword `private`. Either make them `internal` or only test public API. – Louis Wasserman Aug 26 '18 at 23:19
  • 2
    Testing a private function is a really bad practice. Always test the public api; don't tie your test to implementations details. – Mik378 Aug 27 '18 at 08:05
  • 1
    For the record: splitting by "," is like 5% of what a CSV parser does for you. Seriously: don't do such things. Use an existing robust CSV parser library. You invent the wheel, and you will repeat all the mistakes that people run into when doing that. Believe me, I have been there to. Parsing *arbitrary* CSV correctly is **hard**. – GhostCat Aug 28 '18 at 09:20
  • 1
    I know @GhostCat. That's the interview assignment :) – qbait Aug 28 '18 at 14:12
  • OK, that makes sense then ;-) ... and my answer for sure doesn't prevent other folks from coming by and answering on the technical side of things! – GhostCat Aug 28 '18 at 14:33
  • did you try the mockk lib? it is able to test private methods – deviant Feb 05 '19 at 11:14

3 Answers3

4

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.

GhostCat
  • 127,190
  • 21
  • 146
  • 218
  • 12
    what if you still want to test that this private function works correctly? – Ilya Kovalyov Mar 08 '20 at 21:27
  • it is subject to change, but I still want it to behave properly – Filip Pranklin Mar 23 '20 at 11:27
  • 2
    Then you should test the usage paths that lead to that private detail. – GhostCat Mar 23 '20 at 11:31
  • 4
    The does not answer the question, OP was not asking whether or not it is a good idea to test private methods, but rather how to do so. – Attila Aug 30 '20 at 22:24
  • @Attila There are always other people that give the "technical" answer to such questions. But remember: content here is not only written for the OP asking the question, but for all future readers. And thing is: it is a **bad** idea to test private methods. But unfortunately: a lot of people do **not understand** that part. You can write your own answer, and explain to people how to correctly shot themselves into their feet. I prefer to tell them: avoid shooting yourself in the foot, because that is a bad thing to do. What is more helpful in the end, the readers can decide themselves. – GhostCat Aug 31 '20 at 06:56
  • And just to be really clear: I am not saying that you shouldnt test for the functionality that private methods implement. But the correct place would be to have decent unit tests for the public interface and ensure that those tests lead to "full" coverage of your production code. But making the private methods themselves the "target" of a distinct test is simply a bad idea. Doing so means creating bad designs, and spending time in places where it should not be spent. – GhostCat Aug 31 '20 at 06:59
  • 1
    @GhostCat, I appreciate the rational behind your answer, it does make sense indeed and brings added value. However, shouldn't be your "non technical/direct" answer added as a comment only to the OP question rather than a proper answer to the question then? – Attila Aug 31 '20 at 08:39
  • @Attila By definition, comments on this site are volatile. There is no guarantee that someone doesn't flag them (no longer needed, or some other reason) ... and then some moderator deletes the comment. So, whenever I want to say something I consider "worthy to last", I am using an answer over comments. – GhostCat Aug 31 '20 at 08:43
3

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

Filip Pranklin
  • 147
  • 1
  • 10
0

You can use java reflections:

To test the method:

  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) )
  }
Piotr
  • 158
  • 2
  • 4