【问题标题】:Scala testing with type enrichment使用类型丰富的 Scala 测试
【发布时间】:2016-08-24 14:43:51
【问题描述】:

例如,我已经迷上了类型丰富

object MyImplicits{
  implicit class RichInt(i: Int){
    def complexCalculation: Int = i * 200 
  }
}

我在这样的代码中使用它

object Algorithm{
  def apply(rand: Random) = {
    import MyImplicits._
    rand.nextInt.complexCalculation + 1
  }
}

但是我现在如何隔离和单元测试算法呢?特别是,我想模拟complexCalculation 的实现,如下所示:

class MyAlgorithmTest extends FreeSpec with MockitoSugar{
  import org.mockito.Mockito.when

  "MyApgorithm" {
    "Delegates complex calculation" in {
      val mockRandom = mock[Random]
      when(mockRandom.nextInt()).thenReturn(1)

      // This wouldn't work, but is the kind of thing I'm looking for
      //when(1.complexCalculation).thenReturn(2)
      val expected = 1 * 2 + 1

      val result = MyAlgorithm(mockRandom)
      assert(result === expected)
    }
  }
}

【问题讨论】:

  • 您的问题到底是什么?您是否要确保在测试期间将随机 int 对象转换为 RichInt 对象?还是您不知道如何将断言用于您不知道的随机值?
  • 我相信@Pengin 想模拟complexCalculation 方法——但是因为RichInt 对象的创建是隐式的,所以没有办法为此when 指定RichInt 子句对象。

标签: scala unit-testing dependency-injection mockito scalatest


【解决方案1】:

隐式启用组合,当你有组合时,你通常不需要模拟,因为你可以用实现代替测试。话虽如此,在这种情况下,我不是隐式的忠实粉丝,只是看不到它们带来的价值。我会用老派的作文来解决它(正如我在其他评论中所暗示的那样):

trait Calculation {
  def calculation(i: Int): Int
}

trait ComplexCalculation extends Calculation {
  def calculation(i: Int): Int = i * 200
}

trait MyAlgorithm {
  self: Calculation =>

  def apply(rand: Random) = {
    calculation(rand.nextInt) + 1
  }
}

// somewehre in test package

trait MockCalculation extends Calculation {
  def calculation(i: Int): Int = i * 200
}

//test instance
object MyAlgorithm extends MyAlgorithm with MockCalculation

如果你坚持使用implicits做组合,你可以这样做:

trait Computation {
  def compute(i: Int): Int
}

object prod {
  implicit val comp = new Computation {
    def compute(i: Int): Int = i * 200
  }
}

object test {
  implicit val comp = new Computation {
    def compute(i: Int): Int = i + 2
  }
}

object Algorithm {
  def apply(rand: Random)(implicit comp: Computation) = {
    comp.compute(i) + 1
  }
}

// application site
import prod._

Algorithm(scala.util.Random) // will run * 200 computation

//test

import test._

Algorithm(scala.util.Random) // will run + 2 computation

不过,这不会为您提供用于计算的点语法。我的直觉也反对这种方法,因为这是一种定义行为的非常微妙的方式,并且很容易在导入的内容上犯错。

【讨论】:

  • 我开始怀疑类型丰富的好处。您认为它有多大用处,还是仅用作相当简单操作的糖?
  • “类型丰富”的正式名称为“类型类”-en.wikipedia.org/wiki/Type_class。这是启用“临时多态性”的强大机制,您可以在其中扩展您无法使用新行为控制的类。它们还提供了一种非常通用的行为定义方式。看看 scala 集合的实现——在许多情况下,有一个隐含的证据参数(通常称为ev),它暗示您使用类型类来提供行为。也看这里:stackoverflow.com/questions/5408861/…
  • 我不认为类型丰富与类型类是一回事,但我知道它们通常一起使用,以使语法更好。但这是对我的问题的一个很好的回答,即如果测试一个使用类型类的函数,可以手动提供一个“模拟”版本,而不是让编译器只从范围内。
【解决方案2】:

RichInt.scala

trait RichInt {
  def complexCalculation: Int
}

class RichIntImpl(i: Int) extends RichInt {
  def complexCalculation = i * 200
}

算法.scala

import scala.util.Random

class Algorithm(enrich: Int => RichInt) {
  implicit val _enrich = enrich
  def apply(rand: Random) = {
    rand.nextInt.complexCalculation + 1
  }
}

object Algorithm extends Algorithm(new RichIntImpl(_))

算法测试.scala

import org.scalatest.FreeSpec
import scala.util.Random
import org.mockito.Mockito._

class AlgorithmTest extends FreeSpec with MockSugar {

  "MyApgorithm should" - {
    "Delegate the complex calculation" in {
      val mockRandom = mock[Random]
      when(mockRandom.nextInt()) thenReturn 1

      val algorithm = new Algorithm(
        enrich = mocking[Int => RichInt] { enrich =>
          when(enrich(1)).thenReturnMocking { richInt =>
            when(richInt.complexCalculation).thenReturn(2)
          }
        }
      )

      val expected = 3

      assert(algorithm(mockRandom) === expected)
    }
  }
}

MockSuger.scala

import org.scalatest.mockito.MockitoSugar
import org.mockito.stubbing.OngoingStubbing

// More sugars to make our tests look better.
trait MockSugar extends MockitoSugar {

  def mocking[T <: AnyRef : Manifest](behavior: T => Unit): T = {
    val m = mock[T]
    behavior(m)
    m
  }

  implicit class RichOngoingStubbing[T <: AnyRef : Manifest](stub: OngoingStubbing[T]) {
    def thenReturnMocking(behavior: T => Unit) = {
      val m = mock[T]
      val s = stub.thenReturn(m)
      behavior(m)
      s
    }
  }
}

【讨论】:

  • 所有的模拟糖都在融化我的大脑。您能看出为什么这种方法比我尝试使用特征的答案更好吗?
  • 您的解决方案的主要问题是它没有利用模拟库的优势。如果你能找到一个廉价的表达式来模拟你的测试用例的真实计算,那似乎没问题。但对于更复杂的场景,您可能必须自己实现一些模拟实用程序。
  • 我现在明白了。虽然如果这是最好的方法,它让我质疑类型丰富的优雅 - 测试变得非常难以阅读。
【解决方案3】:

以下使用scalatest api。模拟测试运行良好,隐式类转换正常。

// Implicit.scala in src/main/scala

package implicittesting
import scala.util.Random

object MyImplicits{
  implicit class RichInt(i: Int){
    def complexCalculation: Int = 200*i  // make this complex :)
  }
}

object Algorithm{
  var current = 1
  def apply(rand: Random) = {
    import MyImplicits._
    current = rand.nextInt 
    current.complexCalculation + 100
  }
}


// ImplicitSuite.scala in src/main/test

package implicittesting

import org.scalatest.FunSuite


import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner

@RunWith(classOf[JUnitRunner])
class DeleteSuite extends FunSuite {
  import MyImplicits._
  test("algorithm implicit class conversion test") {
    assert(Algorithm(scala.util.Random) == Algorithm.current.complexCalculation + 200)
    println(Algorithm.current)
  }
}

【讨论】:

  • 我会担心 var,但无论如何,我希望模拟出“复杂计算”逻辑。
【解决方案4】:

这是我想出的最好的。我愿意承认这看起来很疯狂。

import org.scalatest.FreeSpec
import org.scalatest.mockito.MockitoSugar
import scala.util.Random

trait MyImplicits {
  implicit class RichInt(i: Int){
    def complexCalculation: Int = complexCalculationImpl(i)
  }

  def complexCalculationImpl(i: Int) = i * 200
}

trait MyAlgorithm extends MyImplicits {
  def apply(rand: Random) = {
    rand.nextInt.complexCalculation + 1
  }
}

//Implementation for use
object MyAlgorithm extends MyAlgorithm

class MyAlgorithmTest extends FreeSpec with MockitoSugar{
  import org.mockito.Mockito.when

  "MyApgorithm should" - {
    "Delegate the complex calculation" in {
      val mockRandom = mock[Random]
      when(mockRandom.nextInt()).thenReturn(1)

      val instance = new MyAlgorithm {
        override def complexCalculationImpl(i: Int) = i * 2
      }

      val expected = 3 // Note we don't expect 201

      assert(instance(mockRandom) === expected)
    }
  }
}

【讨论】:

  • 在这种情况下是否有充分的理由使用隐式?你已经成功地在一个模块中分离了复杂的计算,为什么不在测试中混合不同的 trait 实现呢?
猜你喜欢
  • 1970-01-01
  • 2019-03-04
  • 2011-12-26
  • 1970-01-01
  • 2011-10-12
  • 1970-01-01
  • 2023-03-05
  • 1970-01-01
  • 2011-12-03
相关资源
最近更新 更多