【问题标题】:How to a mock a function call within a function?如何在函数中模拟函数调用?
【发布时间】:2019-08-08 21:16:16
【问题描述】:

我正在为 PlayFramework Scala 应用程序创建单元测试,并遇到了一个我需要测试的函数,它会调用命令行接口。这个 cli 调用无法在我们的测试环境中运行,所以我想模拟一下。

class Foo @Inject()(val bar: Bar, val a: A, val b: B...) {
    def testThis(...) = {
        ...
        callCommandLine
        ...
    }
}

class Bar() {
    def callCommandLine(s: String): String = {
        ...
    }
}

以下是我尝试过的

class FooSpec() {
    "testFoo" in {
        val foo = app.injector.instanceOf[Foo]
        val result = testThis(...)

        val bar = mock[Bar]
        val mockedOutput = "fake cmd line result"
        when(bar.callCommandLine(anyString)).thenReturn(mockedOutput)

        result mustBe mockedOutput
    }
}

我明白为什么我的测试不起作用,但我不知道我需要做什么才能使它起作用。我应该将模拟的 bar 类注入 foo 吗?

【问题讨论】:

    标签: scala playframework mockito scalatest


    【解决方案1】:

    假设你的代码实际上是

    class Foo @Inject()(val bar: Bar, val a: A, val b: B...) {
        def testThis(...) = {
            ...
            bar.callCommandLine()  // <-- difference here
            ...
        }
    }
    

    您面临的障碍正在出现,因为您正在使用“实际”应用程序及其依赖注入容器来构造Foo。在“实际”应用程序中,Bar 显然绑定到实际的 Bar 实例,而不是您在测试中创建的模拟实例。

    要解决此问题,您有两种选择:

    1. 手动创建 Foo 的实例:
    "testFoo" in {
        val mockedBar = mock[Bar]
        when(mockedBar.callCommandLine(anyString)).thenReturn(...)
        val foo = new Foo(mockedBar, mock[A], mock[B], ...)
        foo.testThis shouldBe "expectedResult"
    }
    

    这种方式简单直接,但它完全模拟了其他依赖项(AB 等)。在大多数情况下,这是可接受的(甚至是理想的)结果,因为它允许独立于依赖行为测试 Foo 中的代码。

    缺点很明显 - 这不是一个集成测试,因此它涵盖的内容较少(即 AB 行为),并且不测试要在生产中使用的实际组件,因为它不涉及以任何方式进行依赖注入。

    就我个人而言,我建议采用这种方式 - 它会创建一个更“独立”或“正交”的测试,并允许在其所有依赖项的不同行为下测试 Foo

    1. 创建一个特定于测试的依赖注入容器并在那里模拟Bar
    val mockedBar = mock[Bar]
    val app = new GuiceApplicationBuilder()
      .overrides(bind[Bar].toInstance(mockedBar))
      .build()
    
    "testFoo" in {
        when(mockedBar.callCommandLine(anyString)).thenReturn(...)
        val foo = app.injector.instanceOf[Foo]
        foo.testThis shouldBe "expectedResult"
    }
    

    注意这个 sn-p 不会在使用后重置模拟,但使用 beforeEach 应该很容易做到这一点。更好的方法是为每个测试创建一个 mockedBar 的新实例,但为简洁起见,我将在此省略。

    PlayFramework documentation 中有一节介绍了这个特定的用例。

    这种方法更像是集成测试——除了Bar 之外的所有依赖项都使用实际实现,具有它带来的所有优点和缺点。

    【讨论】:

    • 这是正确的答案,但我想补充一点,以便在每次测试后重置模拟,您可以使用 github.com/mockito/…
    • @Bruno +1,但mockito itself 不鼓励重置模拟
    • 我知道,但有时你需要连接它们来播放(比如测试你的 JSON api),这是处理它的不那么样板的方式。我完全同意,如果组件可以独立于框架进行测试,您的第一个解决方案会更好
    猜你喜欢
    • 2021-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-01
    • 2011-04-18
    • 2021-12-17
    相关资源
    最近更新 更多