2016-08-24 28 views
11

stałem dołączone do wzbogacenia typu, na przykładScala typu badania z wzbogacaniem

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

którego używam w kodzie jak tego

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

Ale w jaki sposób można wyizolować i testów jednostkowych Algorytm teraz ? W szczególności chciałbym mock przeróbka complexCalculation, coś takiego:

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) 
    } 
    } 
} 
+0

Jaki jest dokładnie Twój problem? Czy chcesz, aby Twój losowy obiekt int został przekonwertowany na obiekt RichInt podczas testowania? A może nie wiesz, jak użyć asercji dla losowej wartości, której nie znasz? – Samar

+1

Wierzę, że @Pengin chce kpić z metody 'complexCalculation' - ale ponieważ tworzenie obiektu' RichInt' jest niejawne, nie ma możliwości określenia klauzuli 'when' dla tego obiektu' RichInt'. – virsox

Odpowiedz

2

Implicity umożliwiają kompozycję, a gdy masz kompozycję, zwykle nie potrzebujesz makiet, ponieważ możesz zastąpić implementację testowaniem. Biorąc to pod uwagę, nie jestem wielkim fanem implikacji w tym przypadku, po prostu nie widzę wartości, jaką przynoszą. Chciałbym go rozwiązać ze starego składu szkoły (jak zasugerował w swoim drugim komentarzu):

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 

Jeśli nalegać na użyciu implicits zrobić kompozycję, można to zrobić:

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 

to won” t daje ci jednak składnię punktową do obliczeń. Moje podejście jest również sprzeczne z tym podejściem, ponieważ jest to bardzo subtelny sposób definiowania zachowań i łatwo jest popełnić błąd przy wprowadzaniu importu.

+0

Zaczynam wątpić w zalety wzbogacania typu. Czy widzisz dla niego dużą użyteczność, czy jest to przydatne tylko jako cukier dla dość prostych operacji? – Pengin

+0

"Wzbogacanie typu" ma oficjalną nazwę "typ klasy" - https://en.wikipedia.org/wiki/Type_class. Jest to potężny mechanizm umożliwiający "polimorfizm ad-hoc", w którym można rozszerzać klasy, których nie kontrolujesz przy użyciu nowego zachowania. Zapewniają również bardzo ogólny sposób definiowania zachowania. Peek w implementacji scala collections - w wielu przypadkach istnieje domyślny parametr dowodowy (konwencjonalnie zwany 'ev'), który podpowiada ci, że klasa typu służy do zapewnienia zachowania. Zobacz także: http://stackoverflow.com/questions/5408861/what-are-type-classes-in-scala-useful-for – Tim

+0

i tutaj http://danielwestheide.com/blog/2013/02/06/ the-neofytes-guide-to-scala-part-12-type-classes.html – Tim

0

Poniższa używa scalatest API. Testy próbne działają dobrze, a niejawna konwersja klas dzieje się dobrze.

// 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) 
    } 
} 
+0

Byłbym zaniepokojony var, ale w każdym razie miałem nadzieję, że wyśmieję logikę "complexCalculation". – Pengin

0

To najlepsze, co wymyśliłem. Jestem skłonny przyznać, że to wydaje się szalone.

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) 
    } 
    } 
} 
+0

Czy istnieje dobry powód, aby używać implicite w tym przypadku? Pomyślnie oddzieliłeś złożone obliczenia w module, dlaczego nie mieszasz tylko różnych implementacji cech w teście? – Tim

1

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 
    } 
    } 
} 
+0

Cała sztuczka cukru topi mój mózg. Czy widzisz powód, dla którego to podejście jest lepsze niż moja próba odpowiedzi za pomocą cech? – Pengin

+0

Głównym problemem związanym z rozwiązaniem jest to, że nie korzysta on z zalet szyderczej biblioteki. Wydaje się, że można znaleźć tanie wyrażenie, które naśladuje prawdziwe obliczenia dla twojego przypadku testowego. Jednak w przypadku bardziej złożonych scenariuszy może być konieczne samodzielne zaimplementowanie niektórych narzędzi szyderczych. – thirstycrow

+0

Widzę to teraz. Chociaż jest to najlepszy sposób na zrobienie tego, to kwestionuję elegancję wzbogacania typu - testy stają się bardzo trudne do odczytania. – Pengin