Распределение макетов с использованием внедрения зависимостей в Scala - PullRequest
0 голосов
/ 13 марта 2019

Я пишу распределенное приложение, в котором я хочу провести модульное тестирование логики приложения отдельно от аспекта распространения. У меня есть класс Number, чей метод printIP зависит от глобальной переменной, которая является IP-адресом машины: Config.ip.

object Config {
    val ip = "192.168.0.0"
}

case class Number(value: Int) {
    def increment = Number(value + 1)
    def printIP = println(Config.ip)
}

В производстве разные экземпляры Number находятся на разных машинах и поэтому имеют разные IP-адреса. При тестировании логики приложения я хочу имитировать разные IP-адреса:

class LogicTest extends FlatSpec {
    val instance1 = Number(1)
    instance1.printIP // prints "192.168.0.0"
    val instance2 = Number(2)
    instance2.printIP // also prints "192.168.0.0"
}

Естественно, оба экземпляра печатают один и тот же IP-адрес при тестировании на одном компьютере. Как я могу проверить свою логику приложения локально, имитируя разные IP-адреса для этих экземпляров.

Я не хочу передавать IP-адрес в качестве аргумента класса в Number, потому что это изменит интерфейс Number.

Я попытался добавить getIp метод к Number, который я могу переопределить в своих модульных тестах:

case class Number(value: Int) {
    def increment = Number(value + 1)
    def getIp = Config.ip
    def printIP = println(getIp)
}

class LogicTest extends FlatSpec {
    val instance1 = new Number(1) { override def getIp = "192.168.0.1" }
    instance1.printIP // prints "192.168.0.1"
    val instance2 = new Number(2) { override def getIp = "192.168.0.2" }
    instance2.printIP // prints "192.168.0.2"
}

Сначала, похоже, это работает. Однако, когда я increment экземпляр, он возвращает новый, и я теряю переопределенный метод getIp:

class LogicTest extends FlatSpec {
    var instance1 = new Number(1) { override def getIp = "192.168.0.1" }
    instance1.printIP // prints "192.168.0.1" (OK)

    val instance2 = new Number(2) { override def getIp = "192.168.0.2" }
    instance2.printIP // prints "192.168.0.2" (OK)

    val instance3 = instance1.increment
    instance3.printIP // prints "192.168.0.0" (NOT OK)

    val instance4 = instance2.increment
    instance4.printIP // prints “192.168.0.0” (NOT OK)
}

Я также взглянул на шаблон Cake для внедрения зависимостей в Scala (http://jonasboner.com/real-world-scala-dependency-injection-di/),, но не вижу, как его можно применить к моему случаю.


Обновление @Dima: изменяет внешний вид интерфейса, когда реплицированные объекты вложены. Предположим следующий искусственный пример:

trait Config { def ip: String }
object Config extends Config { val ip = "127.0.0.1" }

case class Number(value: Int)(implicit config: Config = Config) {
   def getIp = config.ip
}

case class NestedNumber(value: Int)(nestedNum: Number = Number(value))
NestedNumber(5)

Программист может создать NestedNumber, предоставив целое число, и класс автоматически создаст вложенное число с этим значением. Опять же, теперь мы хотим внедрить наш объект конфигурации так, чтобы мы могли модульно тестировать логику приложения отдельно от аспекта распространения.

case class NestedNumber(value: Int)(config: Config = config)(nestedNum: Number = Number(value)(config))
NestedNumber(5)()()

Проблема в том, что нам нужно передать объект config при создании nestedNum. Следовательно, нам нужно несколько списков аргументов. Теперь, внезапно, программисту нужно указать 3 списка аргументов, два из которых пусты, вместо одного аргумента.


Обновление 2 @Dima. Распространены реплицированные типы данных внутри других реплицируемых типов данных. Например, в литературе по CRDT положительно-отрицательный счетчик состоит из двух счетчиков только для роста. Вот что я на самом деле делаю:

type IP = String
case class GCounter(val increments: Map[IP, Int] = Map())(implicit val config: Config) {
    val ownIP: IP = config.ip // will be used to increment our entry in the map
}

case class PNCounter(p: GCounter = GCounter(), n: GCounter = GCounter())(implicit config: Config) {
    val ownIP: IP = config.ip
}

Так что теперь мы можем сделать PNCounter:

trait Config { def ip: IP }
implicit object Config extends Config { val ip = "192.168.0.1" }

// In production
val pn = PNCounter()
pn.ownIP   // "192.168.0.1" (OK)
pn.p.ownIP // "192.168.0.1" (OK)

// Now suppose we send the replica to a remote actor with IP address "192.168.0.9"
case class ReceiveCounter(replica: PNCounter)
remoteActor ! ReceiveCounter(pn) // message send in Akka

// On the receiver's side
receivedMsg.replica.ownIP // "192.168.0.1" (NOT OK, should be 192.168.0.9)

// When testing on one machine
object TestConfig extends Config { val ip = "127.0.0.1" }
val pnTest = PNCounter()(TestConfig)
pnTest.ownIP // "127.0.0.1"   (OK)
pnTest.p.ownIP     // "192.168.0.1" (NOT OK)

1 Ответ

1 голос
/ 14 марта 2019

Передача аргумента - это правильный способ сделать это (по сути, это означает «внедрение зависимостей»).

Вы можете сделать аргумент неявным (и / или присвоить ему значение по умолчанию), чтобы сохранить (внешний вид) интерфейса:

trait Config { def ip: String }
object Config extends Config { val ip = "127.0.0.1" }

case class Number(value: Int)(implicit config: Config = Config) {
   def getIp = config.ip
}

describe("Number") {
   it("uses IP from given config") {
     implicit val config = mock[Config] 
     when(config.ip).thenReturn("foo") 
     Number(123).ip shouldBe "foo"
     verify(config).ip
   }
}
...