5

I start to learn Kotlin and Mockito, so I code a simple module to test it.

AccountData_K.kt:

open class AccountData_K {
var isLogin: Boolean = false
var userName: String? = null

    fun changeLogin() : Boolean {
        return !isLogin
    }
}

AccountDataMockTest_K.kt:

class AccountDataMockTest_K {
    @Mock
    val accountData = AccountData_K()

    @Before
    fun setupAccountData() {
        MockitoAnnotations.initMocks(this)
    }

    @Test
    fun testNotNull() {
        assertNotNull(accountData)
    }

    @Test
    fun testIsLogin() {
        val result = accountData.changeLogin()
        assertEquals(result, true)
    }

    @Test
    fun testChangeLogin() {        
        `when`(accountData.changeLogin()).thenReturn(false)
        val result = accountData.changeLogin()
        assertEquals(result, false)
    }
}

And when I run the test, it reports an exception about the testChangeLogin() method:

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
2. inside when() you don't call method on mock but on some other object.
3. the parent of the mocked class is not public.
   It is a limitation of the mock engine.

at com.seal.materialdesignwithkotlin.AccountDataMockTest_K.testChangeLogin(AccountDataMockTest_K.kt:57)
...

I doubt why the method is not a method call on a mock...

So please help me, thanks.

Maciej Kowalski
  • 25,605
  • 12
  • 54
  • 63
David_Jiang
  • 73
  • 1
  • 5
  • Possible duplicate of [Is it possible to use Mockito with Kotlin without open the class?](http://stackoverflow.com/questions/36536727/is-it-possible-to-use-mockito-with-kotlin-without-open-the-class) – Jayson Minard Sep 07 '16 at 16:00

3 Answers3

9

By default Kotlin's classes and members are final. Mockito is not able to mock final classes nor methods. To use Mockito you need to open the method you wish to mock:

open fun changeLogin() : Boolean {
    return !isLogin
}

Further reading

PS. In my humble opinion, as long as you keep you interfaces small through i.e ISP, a test code that uses Mockito or other mocking framework is rarely more readable and easy to understand than hand written fakes/stubs.

Community
  • 1
  • 1
miensol
  • 39,733
  • 7
  • 116
  • 112
  • Thank you, and sorry for my superficial knowledge of Kotlin which cause the simple problem. Your further reading links are very useful. Wish you have a good day. – David_Jiang Sep 07 '16 at 13:29
4

As @miensol mentions, your problem occurs because the classes are final by default in Kotlin. The error message isn't very clear although this part mentions final as one of the possible causes:

  1. you stub either of: final/private/equals()/hashCode() methods.

There is a project specifically to help deal with Kotlin "final by default" in unit testing with Mockito. For JUNIT, you can use the kotlin-testrunner which is an easy way to make any Kotlin test automatically open up classes for testing as they are loaded by the classloader. Usage is simple, just add one annotation of @RunWith(KotlinTestRunner::class), for example:

@RunWith(KotlinTestRunner::class)
class MyKotlinTestclass {
   @Test 
   fun test() {
   ...
   }
}

This is thoroughly covered in the article Never say final: mocking Kotlin classes in unit tests

This covers your use case in an automatic way by allowing all classes to be mocked that otherwise would not be allowed.

Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • Thanks for offering a project which is easy and simple to solve the problem. Why I choose another answer because he tells me why. It's not means your better way to solve the problem is useless, just merely I prefer to know where I am wrong. So wish you have a good day. – David_Jiang Sep 07 '16 at 13:35
0

For anybody reaching this post , you can use kotlin-allopen library to open your classes for testing :

First add the dependency in your build.gradle (project) file:

dependencies {
        classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
    }

After that apply the plugin in your build.gradle (app mobdule) file:

apply plugin: 'kotlin-allopen'

Then specify the list of annotations that will make classes open:

allOpen {
    annotation('com.example.myproject.OpenForTesting')
}

And define the annotation in a file named OpenForTesting(file must be in location com.example.myproject.OpenForTesting) in your project:

 /**
     * This annotation allows us to open some classes for mocking purposes while they are final in
     * release builds.
     */
    @Target(AnnotationTarget.ANNOTATION_CLASS)
    annotation class OpenClass

    /**
     * Annotate a class with [OpenForTesting] if you want it to be extendable in debug builds.
     */
    @OpenClass
    @Target(AnnotationTarget.CLASS)
    annotation class OpenForTesting

And use this annotation for every class which you want to be open

@OpenForTesting

Here is the Kotlin official documentation about All-open: https://kotlinlang.org/docs/reference/compiler-plugins.html

Nabzi
  • 1,823
  • 1
  • 16
  • 26