【问题标题】:Unit testing helper or non-interface traits in ScalaScala 中的单元测试助手或非接口特征
【发布时间】:2011-10-26 22:54:49
【问题描述】:

这个问题是关于处理混合非接口特征的类的测试,即包含一些功能的特征。测试时,应将类功能与 mix-in trait 提供的功能隔离开来(据说是单独测试的)。

我有一个简单的Crawler 类,它依赖于一个HttpConnection 和一个HttpHelpers 实用函数集合。现在让我们关注 HttpHelpers。

在 Java 中,HttpHelpers 可能是一个实用程序类,并将其单例作为依赖项传递给 Crawler,手动或使用某些 IoC 框架。测试爬虫很简单,因为依赖很容易模拟。

在 Scala 中,helper trait 似乎是组合功能的首选方式。确实,它更容易使用(扩展时自动导入命名空间的方法,可以使用withResponse ...代替httpHelper.withResponse ...等)。但它对测试有何影响?

这是我想出的解决方案,但不幸的是,它将一些样板文件提升到了测试端。

助手特质:

trait HttpHelpers {
  val httpClient: HttpClient
  protected def withResponse[A](resp: HttpResponse)(fun: HttpResponse => A): A = // ...
  protected def makeGetRequest(url: String): HttpResponse = // ...
}

要测试的代码:

class Crawler(val httpClient: HttpClient) extends HttpHelpers {
  // ...
}

测试:

// Mock support trait
// 1) Opens up protected trait methods to public (to be able to mock their invocation)
// 2) Forwards methods to the mock object (abstract yet)
trait MockHttpHelpers extends HttpHelpers {
  val myMock: MockHttpHelpers
  override def makeGetRequest(url: String): HttpResponse = myMock.makeGetRequest(url)
}

// Create our mock using the support trait
val helpersMock = Mockito.mock(classOf[MockHttpHelpers])

// Now we can do some mocking
val mockRequest = // ...
Mockito when (helpersMock.makeGetRequest(Matchers.anyString())) thenReturn mockRequest

// Override Crawler with the mocked helper functionality
class TestCrawler extends Crawler(httpClient) with MockHttpHelpers {
  val myMock = helpersMock
}

// Now we can test
val crawler = new TestCrawler()
crawler.someMethodToTest()

问题

这种方法确实有效,但是需要为每个辅助 trait 提供一个模拟支持 trait 有点乏味。但是我看不出有任何其他方法可以做到这一点。

  • 这是正确的方法吗?
  • 如果是,能否更有效地实现其目标(语法魔术、编译器插件等)?

欢迎任何反馈。谢谢!

【问题讨论】:

    标签: unit-testing scala traits


    【解决方案1】:

    怎么样:

    val helpers = new HttpHelpers {
      //override or define stuff the trait needs to work properly here
    }
    
    helpers.someMethodToTest
    

    看,Ma,不需要中间特征和模拟库!

    我一直这样做是为了我的特质,我对结果非常满意。

    【讨论】:

    • 您甚至可以导入帮助器以将其成员纳入本地范围。
    • @Hristo:我不太明白你的意思。辅助特征需要混合到用户类中。我将如何通过您的解决方案实现这一目标? (注:我不想测试 trait,我想测试使用 trait 的类)
    • @Daniel:如果我没听错的话,你的意思是用户类没有扩展助手特征,而只是导入它的成员。但是,那些成员不是与外界隔绝,不能嘲笑他们吗?我的目标是单独测试用户类(甚至是帮助者)。
    【解决方案2】:

    您可以编写一个应与 HttpHelpers 混合的 Helper 模拟特征,并使用模拟等效项覆盖其方法:

    trait HttpHelpersMock { this: HttpHelpers =>
    
      //MOCK IMPLEMENTATION
      override protected def withResponse[A](resp: HttpResponse)(fun: HttpResponse => A): A = // ...
    
      //MOCK IMPLEMENTATION
      override protected def makeGetRequest(url: String): HttpResponse = // ...
    }
    

    然后,在测试爬虫时,在实例化时混合模拟特征:

    val crawlerTestee = new Crawler(x) with HttpHelpersMock
    

    mock 方法只会替换实例 crawlerTestee 中的辅助方法。

    编辑:我认为测试一个类如何与一个辅助特征交互并不是一个好主意。在我看来,你应该测试Crawler 的行为而不是它的内部实现细节。实现可以改变,但行为应尽可能保持稳定。我上面描述的过程允许您覆盖辅助方法以使其具有确定性并避免真正的网络,从而帮助和加速测试。

    但是,我认为测试 Helper 本身是有意义的,因为它可以在其他地方重用并且具有正确的行为。

    【讨论】:

    • 这样我会失去使用像 Mockito 这样的模拟框架、指定调用计数等的灵活性。这就是为什么我将 trait 方法重定向到一个可模拟的 trait 实例。所以我可以避免在不同的测试中创建具有不同功能的新特征。更重要的是,如果我只 mock 一个有趣的方法,就不必处理 trait 中的其他方法。
    • 好吧,您可以在测试源路径中拥有一个完整的模拟特征并在任何地方使用它。我不喜欢测试辅助方法和帮助类之间的交互的想法。对我来说,这是一个内部实现细节,爬虫类可以被重写以使其独立于帮助程序而不改变测试本身......
    • 我明白你关于实施细节的观点。您能否也将这方面提升到您的答案中?
    • 想一想,在 Scala 中不模拟 helper trait 大约相当于在 Java 中使用 Helper 类作为直接静态/单例引用(而不是将其作为依赖项传递),这被认为是一些人的不良做法。你能反思一下吗,我对你的意见很感兴趣。
    • 我读到的关于单例的大多数问题都是关于状态共享的。如果您的助手只是方法的无状态“模块”,那么它们只是实现机制。所以你应该只关注其他方法的行为。但是,如果 trait 作为一种受“helpee”对象影响的状态,您应该担心模拟它。
    猜你喜欢
    • 2011-01-28
    • 1970-01-01
    • 2011-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-04
    • 2015-01-07
    • 2017-01-15
    相关资源
    最近更新 更多