Home > Software engineering >  Is it possible to test arguments in mocked constructor with mockk?
Is it possible to test arguments in mocked constructor with mockk?

Time:08-16

Purpose

I want to test the result of a function of a class. And it returns an instance of an Android class, which should be mocked in a unit test since it seems to be stubbed in any unit test.

Example code

Here's a minimized example code of the simulated android class, AndroidData, and the class to be tested, MyHelper.

/**
 * Should be mocked in a unit test.
 */
class AndroidData(val value: Float)

/**
 * Target class to be tested.
 */
class MyHelper() {
    fun createData(flag: Boolean): AndroidData {
        // simplified logic
        return if (flag) AndroidData(20f) else AndroidData(10f)
    }
}

What I want to achieve is something like this(note that this is incorrect code):

class MyHelperTest : TestCase() {
    @Test
    fun testCreateData() {
        mockkConstructor(AndroidData::class)
        val valueSlot = slot<Float>()
        every { constructedWith<AndroidData>(capture(valueSlot)) }  // Type mismatch in IDE
        val helper = MyHelper()
        
        val returnedData = helper.createData(true)
        assertTrue(returnedData.value >= 15f)
    }
}

I want to test that returned AndroidData instance has a correct value, or matches some conditions. I cannot find any tutorial about this case or any correct way to implement it.

What I tried

These 2 test cases would both fail.

    @Test
    fun testCreateData1() {
        mockkConstructor(AndroidData::class)

        val helper = mockk<MyHelper>(relaxed = true)
        val createdData = helper.createData(true)

        println("createdData=$createdData")  // AndroidData(child of #1#2)
        println("createdData.value=${createdData.value}")  // 0.0

        // test if the argument in AndroidData's constructor was larger than 15
        assertTrue(createdData.value >= 15f)  // assertion failed
    }

    @Test
    fun testCreateData2() {
        mockkConstructor(AndroidData::class)
        // raise error: Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock
        every {
            constructedWith<AndroidData>(LargerThan15Matcher())
        }

        val helper = mockk<MyHelper>(relaxed = true)
        val createdData = helper.createData(true)

        println("createdData=$createdData")  // AndroidData(child of #1#2)
        println("createdData.value=${createdData.value}")  // 0.0

        // test if the argument in AndroidData's constructor was larger than 15
        assertTrue(createdData.value >= 15f)
    }

The testing idea was pretty easy and intuitive for Python unittest in my experience. Yet it seems impossible in Java or Android? I haven't tried the mockito and roboletric library because I was told that mockk provides support on Android projects. Or I just haven't found the correct way to do this, or the whole testing idea is completely wrong? Please end my days of searching and struggle.

Update 0815

With @Karsten Gabriel 's suggestion, I made a bit of mod on the testCreateData2() test case, but still it doesn't work. The createdData seems not really an AndroidData instance and has value 0.0.

    @Test
    fun testCreateData2() {
        mockkConstructor(AndroidData::class)
        // Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock
        every {
            constructedWith<AndroidData>(LargerThan15Matcher()).value
        } returns 1f

        val helper = mockk<MyHelper>(relaxed = true)
        val createdData = helper.createData(true)

        println("createdData=$createdData")  // AndroidData(child of #1#2)
        println("createdData.value=${createdData.value}")  // 0.0

        // test if the argument in AndroidData's constructor was larger than 15
        assertTrue(createdData.value >= 15f)
    }

BTW, the LargerThan15Matcher:

class LargerThan15Matcher : Matcher<Float> {
    override fun match(arg: Float?): Boolean {
        return if (arg == null) false else arg >= 15f
    }
}

CodePudding user response:

It is not really clear to me what you are trying to achieve, but I can try to help you with what I think I understand.

Let's start with your test-case testCreateData1:

  • A relaxed mock in MockK tries to return a "simple value" (mostly "default" values or null if possible) for each function call of the mock.
  • Therefore, your relaxed mock of MyHelper returns AndroidData(0.0) which does not satisfy your assertion.

Regarding test-case testCreateData2:

  • every { ... } alone is not a proper expression, but it is intended to specify what should happen on every .... For instance, if you have a mock m with a function f you could write something like the following:
every { m.f() } returns 42
every { m.f() } answers { callOriginal() }
  • In pretty much the same way, constructedWith<AndroidData>(LargerThan15Matcher()) is not a proper expression, because it is intended to specify what should happen with an AndroidData constructed with an argument matched by the LargerThan15Matcher. For instance, if you want to say that the value of such an object should always return 1 you could write
 every { constructedWith<AndroidData>(LargerThan15Matcher()).value } returns 1f

Update 0815

Your updated test does not work due to an unfortunate combination of mocking:

  • You mock each constructed element of AndroidData.
  • But at the same time you also have a (relaxed) mock of MyHelper which does not call the constructor of AndroidData, but instead it gives you some mock of AndroidData.

There are (at least) two possibilities to solve this:

  1. You do not mock MyHelper but only constructed values of AndroidData:
mockkConstructor(AndroidData::class)
every {
    constructedWith<AndroidData>(LargerThan15Matcher()).value
} returns 20f

val helper = MyHelper()
val createdData = helper.createData(true)

assertTrue(createdData.value >= 15f)
  1. You mock MyHelper and you explicitly define how your mock should work:
val helper = mockk<MyHelper> {
    every { createData(true) } returns AndroidData(20f)
}

val createdData = helper.createData(true)

// test if the argument in AndroidData's constructor was larger than 15
assertTrue(createdData.value >= 15f)

or

val helper = mockk<MyHelper> {
    every { createData(true) } returns mockk {
        every { value } returns 20f
    }
}

val createdData = helper.createData(true)

assertTrue(createdData.value >= 15f)
  • Related