Scala тестирование с использованием типа обогащения
Я привязался к обогащению типов, например
object MyImplicits{
implicit class RichInt(i: Int){
def complexCalculation: Int = i * 200
}
}
Который я использую в коде, подобном этому
object Algorithm{
def apply(rand: Random) = {
import MyImplicits._
rand.nextInt.complexCalculation + 1
}
}
Но как я могу выделить и unit test Алгоритм сейчас? В частности, я хотел бы высмеять реализацию 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)
}
}
}
Ответы
Ответ 1
Implicits позволяет создавать композицию, а когда у вас есть композиция, вам обычно не нужны mocks, потому что вы можете заменить реализацию для тестирования. Это, как говорится, я не большой поклонник implicits в этом случае, просто не вижу ценности, которую они приносят. Я бы решил это со старой школьной композицией (как намекнул мой другой комментарий):
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
Это не даст вам точечного синтаксиса для вычисления. Моя кишка также идет вразрез с этим подходом, потому что это очень тонкий способ определения поведения, и легко ошибиться с тем, что импортировать.
Ответ 2
RichInt.scala
trait RichInt {
def complexCalculation: Int
}
class RichIntImpl(i: Int) extends RichInt {
def complexCalculation = i * 200
}
Algorithm.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(_))
AlgorithmTest.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
В следующем примере используется скалярный 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)
}
}
Ответ 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)
}
}
}