【问题标题】:Good practices of writing and unit testing Utility methods in scalaScala 中编写和单元测试实用方法的良好实践
【发布时间】:2019-02-18 10:15:21
【问题描述】:

在特定情况下,您需要有一些跨不同类所需的实用方法。为了解决这种情况,您创建一个 Util 对象,在其中放置所有这些方法

object AggregatorUtil {
  def aggregateValues(list : List[BigDecimal]) = //some logic...
}

// Import everything in the Utilities object
import AggregatorUtil._

然后导入类中需要的任何 util 成员。然而,这样做的缺点是,由于您的所有方法都在单例对象中,并且 使用实用方法模拟类的对象和单元测试方法变得很棘手。

为了再次解决这个问题,我想到的唯一解决方案是将功能提取到特征中,然后模拟特征。

如果还有其他方法可以处理和测试 util 方法,请告诉我,哪种方法更简洁。

提前致谢!!!

注意:-我在我的项目中使用 scalatest 和 mockito。

【问题讨论】:

  • 为什么很难测试对象中的方法?或者您的意思是测试使用该 import AggregatorUtil._ 的方法
  • 你为什么要在这里使用模拟?嘲笑本身并不是某种目标——有时它可能是一种必要的邪恶,但任何时候你都可以避免它,你绝对应该避免它。
  • 我同意@TravisBrown,如果这些实用程序函数是经过适当测试的小实用程序、纯函数并在另一个方法中使用,那么就是一个私人调用,您的单元测试不应该知道它们存在。现在,如果它们是访问外部资源的依赖项或者它们进行一些复杂的计算,那么它们不应该被建模为对象中的实用程序,而是作为依赖项,这将是模拟的适当使用
  • 我不同意“尽可能”避免嘲笑的观点。在没有任何外部依赖或复杂逻辑的情况下不模拟完全琐碎的东西当然是有意义的,但这只是常识。除此之外,我想说,应该模拟出可以模拟的一切。话虽如此,实际的问题听起来像是一个 XY 问题:he only solution that came to mind was Extracting the functionality out to a trait - 你有解决方案,但正在要求另一个解决方案,甚至没有提及它有什么问题。只是好奇吗?
  • @ChaitanyaWaikar 不,我认为,否决是因为基于意见的(又称“最佳实践”)和“好奇心”问题在这里是题外话。查看How To Ask,了解哪些问题被视为“好”。

标签: scala mockito scalatest


【解决方案1】:

如果您需要进行模拟,则将这一切置于模拟特征中是前进的方向。如果嘲笑是不必要的,请避免它。不必要的嘲笑是……不必要的。你只会在没有额外价值的事情上浪费时间和精力。

当您在其他文件中有复杂的功能或功能并希望将其视为black box 并假设它按预期工作时,最好使用模拟(然后您通常会单独对这些东西进行单元测试)。但是,如果您可以避免它并使用函数的 实际 功能,您将对应用程序的功能有更真实的了解,并且会更快地发现新的错误/重大更改(如果您已模拟功能而忘记更新你的模拟,你可能不会发现你引入的任何新错误)。

当您在 MVC 应用程序(例如 Scala Play 微服务)中模拟对数据库的调用时,需要进行模拟的一个很好的例子是。您显然不想在测试代码时运行实际的数据库,因此您通常会模拟出连接器层并从连接器函数返回虚拟/模拟数据。

你不会嘲笑的一个例子是这样的:

trait MyTrait {
  def toInt(str: String): Int
}

val mockedTrait = mock[MyTrait]
when(mockedTrait.toInt(eq("3")).thenReturn(3)

这是一个有点愚蠢的例子,但我认为它清楚地解释了这一点 - 做这样的事情会很荒谬。嘲笑并不总是答案。

【讨论】:

    【解决方案2】:

    我主要使用 Test-Implementation 进行模拟,我发现它更具可读性,而且您不必学习模拟框架。

    这里是一个例子:

    界面:

    trait DataRepo {
    
      def persist(data: DataObject): Future[DataObject]
    
      def idents(): Future[List[String]]
    
      def insertData(dataCont: DataObject): Future[Int]
      ...
    }
    

    模拟界面:

    object DataRepoMock extends DataRepo {
    
      def persist(data: DataObject): Future[DataObject] = ??? // only implement when needed
    
      def idents(): Future[List[String]] = Future.successful((0 to 10).map(_=>Random.nextInt(100)))
    
      def insertData(dataCont: DataObject): Future[Int] = Future.successful(Random.nextInt(100))
      ...
    }
    

    您还可以使用所有 Scala 的好东西,例如模式匹配,让您的 Mock 对输入做出不同的反应。

    这是一个例子,这不仅仅是我自己使用的;): EPFLx: scala-reactiveXLecture 2.5 测试 Actor 系统

      def fakeGetter(url:String, depth: Int):Props =
        Props(new Getter(url, depth){
          override def webClient: WebClient = FakeWebClient
        })
    

    【讨论】:

    • 当您可以使用模拟框架并以声明性方式声明其行为时,为什么要通过实现复杂的模拟类来满足不同场景的所有需求来浪费时间并在测试中引入可能的错误在每个测试/场景的基础上?
    • @Bruno 我添加了一个示例,很好地展示了这种方法有意义的情况。
    • 我的意思是你有很多行代码,如果你必须满足一个以上的测试,可能有逻辑来选择正确的答案,并且该代码远离测试本身(因为它必须重复使用)妨碍其可红色性。如果您使用模拟框架,您可以在测试设置中以声明性方式(仅 2 行)创建模拟和存根返回值,因此您阅读测试更清晰,整体代码更短,而不是容易出错
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-06
    • 2020-05-28
    相关资源
    最近更新 更多