在Kotlin中如何模拟扩展函数

47
如何在测试中使用Mockito或PowerMock来模拟Kotlin扩展函数?由于它们是静态解析的,所以应该将它们作为静态方法调用进行测试还是作为非静态方法?

就像在Java中一样,你要避免使用静态方法。静态方法通常应该被用作简单的工具函数。扩展函数就像静态方法一样,因此它们只应该被用作简单的工具函数。这些函数应该如此简单,以至于不需要或不值得模拟它们。 - nhaarman
你能提供一下你目前尝试过的吗? - Sachin Chandil
只是想知道:您是否还在等待其他答案;或者有什么我可以做的让我的答案值得接受? - GhostCat
6个回答

63
我认为MockK可以帮助你。
它还支持模拟扩展函数。
你可以使用它来模拟对象范围的扩展。
data class Obj(val value: Int)

class Ext {
    fun Obj.extensionFunc() = value + 5
}

with(mockk<Ext>()) {
    every {
        Obj(5).extensionFunc()
    } returns 11

    assertEquals(11, Obj(5).extensionFunc())

    verify {
        Obj(5).extensionFunc()
    }
}

如果您的扩展是模块级的,也就是说它是在一个文件中声明的(而不是在类内部),您应该以这种方式进行模拟:
data class Obj(val value: Int)

// declared in File.kt ("pkg" package)
fun Obj.extensionFunc() = value + 5

mockkStatic("pkg.FileKt")

every {
    Obj(5).extensionFunc()
} returns 11

assertEquals(11, Obj(5).extensionFunc())

verify {
    Obj(5).extensionFunc()
}

通过在扩展声明的包和文件的名称(例如,在示例中为`pkg.File.kt`)中添加`mockkStatic("pkg.FileKt")`行。
在JVM环境中,您可以使用函数引用替换类名。
mockkStatic(Obj::extensionFunc)

请注意,这将模拟整个pkg.FileKt类,而不仅仅是extensionFunc。
这种语法也适用于扩展属性。
val Obj.squareValue get() = value * value

mockkStatic(Obj::squareValue)

更多信息可以在这里找到:网站GitHub

当扩展函数有参数时,这是否有效?验证失败似乎是由于参数不匹配(奇怪的是它似乎期望另一个参数,类型应与您正在扩展的类相同) - hmac
1
完美运行,谢谢!请注意,在使用 verify 时,您需要使用 Mockk 库中的 verify,而不是 Mockito 中的 verify。由于我在同一个文件中都有,所以我通过创建导入别名来解决它:import io.mockk.verify as verifyMockk - Micer
1
这对我没有用,但是稍微不同的方式适用于我的例子: every { any<Obj>().extensionFunc() } 返回11看起来很傻,因为现在结果不取决于参数,但在我的用例中它不必如此,而另一种方式会引发错误。 - LeYAUable
1
你还可以使用mockkStatic(Obj::extensionFunction.declaringKotlinFile.qualifiedName!!)来代替指定文件名为字符串。这样做可以避免在文件重命名或移动时出现奇怪的错误。 - undefined
@Benjamin,你能解释一下如何编写这个带有示例的mockkStatic吗?假设我的抽象类是Context,扩展函数是getActivity(),它在名为ActivityExtension.kt的文件中,其jvm名称为ActivityUtil。 - undefined
@Terry 你可以这样写:mockkStatic(Context::getActivity.declaringKotlinFile.qualifiedName!!) { your test code }。在你的测试代码中,你可以为一个具体的实例模拟getActivity,例如every { myContext.getActivity() } returns ...,或者为任何实例模拟getActivity,例如every { any<Context>().getActivity() } returns ... - undefined

10
实例扩展函数可以通过 mockito-kotlin 的帮助像这样进行存根和验证:
data class Bar(thing: Int)

class Foo {
   fun Bar.bla(anotherThing: Int): Int { ... }
}

val bar = Bar(thing = 1)
val foo = mock<Foo>()

with(foo) {
  whenever(any<Bar>().bla(any()).doReturn(3)
}

verify(foo).apply {
  bar.bla(anotherThing = 2)
}

所以您必须将扩展函数包装在一个类中吗? - VIN

9
首先,Mockito不了解Kotlin特定的语言结构。最终,Mockito会查看字节码。Mockito只能理解在那里找到的内容和类似于Java语言结构的内容。
意思是:为了确保,您可能想使用javap来反编译已编译的类文件,以识别要模拟的方法的确切名称/签名。
显然,当该方法是静态的时,您必须使用PowerMock或JMockit;如果不是,则应优先选择Mockito。
从Java的角度来看,您只需避免模拟静态内容;但当然,现在不同的语言具有不同的想法/概念,事情变得非常有趣。

9
下一个人,请注意,在Java中的扩展函数签名是class FilenameKt { public static ReturnType extensionName(Receiver receiver, Params otherParams)...}。请注意,这是一个翻译,不包括解释或其他内容。 - Kraiden

3
我将使用mockk库。
对于扩展文件,写java名称,如下所示:
@file:JvmName(name = "ExtensionUtils")

package myproject.extension

...

为了快速编码,我创建了带有不同扩展名的文件模拟:

object FastMock {

    fun extension() = mockkStatic("myproject.extension.ExtensionUtils")

    fun listExtension() = mockkStatic("myproject.extension.ListExtensionUtils")

}

在测试中调用此函数:

FastMock.listExtension()
every { itemList.move(from, to) } returns Unit

1
使用Mockk库,
ContextExt.kt文件中给出一个顶级(意味着不在类中声明的)扩展方法,例如:
Context.doSomething(param1: Int, param2: String = "default") : Int {
   // Does something that wouldn't work in your test environment
}

嘲笑它,我们可以做以下几点:
MyTestClass {
   companion object {
       private const val MOCKED_VALUE = 10
   }
   ...

   @Before
   fun before() {
      mockkStatic("com.my.package.ContextExtKt")
      every { any<Context>().doSomething(any(), any()) } returns MOCKED_VALUE
   }
}

有几件事需要注意:
1. 你提供给mockkStatic的参数是文件的第一行声明的包名 + 文件名 + "Kt"。例如,对于包名为com.my.package的文件ContextExt.kt,你需要使用mockkStatic("com.my.package.ContextExtKt")。
2. 无论扩展方法的参数是否有默认值,都要提供所有参数。如果需要,可以使用具体的值,否则可以使用any()。

0
你可以轻松使用:
mockkStatic(YourClass::extensionFunction)
every { any<YourClass>().extensionFunction() } returns YOUR_MOCKED_DATA

e.g

fun String.doSomething() = "SOMETHING"

@Test
fun `test extension functions`() {
    mockkStatic(String::doSomething)
    every { any<String>().doSomething() } returns YOUR_MOCKED_DATA
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接