在 Scala 中使用依赖注入模拟分发
Mock distribution using dependency injection in Scala
我正在编写一个分布式应用程序,我想在其中独立于分布方面对应用程序逻辑进行单元测试。我有一个 class 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 地址作为 class 参数传递给 Number
,因为这会更改 Number
的接口。
我尝试向 Number
添加一个 getIp
方法,我可以在我的单元测试中覆盖它:
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
一个实例时,它 returns 一个新实例并且我失去了覆盖的 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)
}
我还查看了 Scala 中用于依赖注入的 Cake 模式 (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
,而 class 将自动创建一个具有该值的嵌套数字。同样,我们现在想要注入我们的配置对象,以便我们可以独立于分发方面对应用程序逻辑进行单元测试。
case class NestedNumber(value: Int)(config: Config = config)(nestedNum: Number = Number(value)(config))
NestedNumber(5)()()
问题是我们在创建nestedNum
时需要传递config
对象。因此,我们需要多个参数列表。现在,程序员突然需要指定 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)
传递一个参数 是 正确的方法(这基本上就是 "dependency injection" 的意思)。
您可以使参数隐式(and/or 给它一个默认值)以保留接口(的外观):
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
}
}
我正在编写一个分布式应用程序,我想在其中独立于分布方面对应用程序逻辑进行单元测试。我有一个 class 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 地址作为 class 参数传递给 Number
,因为这会更改 Number
的接口。
我尝试向 Number
添加一个 getIp
方法,我可以在我的单元测试中覆盖它:
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
一个实例时,它 returns 一个新实例并且我失去了覆盖的 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)
}
我还查看了 Scala 中用于依赖注入的 Cake 模式 (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
,而 class 将自动创建一个具有该值的嵌套数字。同样,我们现在想要注入我们的配置对象,以便我们可以独立于分发方面对应用程序逻辑进行单元测试。
case class NestedNumber(value: Int)(config: Config = config)(nestedNum: Number = Number(value)(config))
NestedNumber(5)()()
问题是我们在创建nestedNum
时需要传递config
对象。因此,我们需要多个参数列表。现在,程序员突然需要指定 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)
传递一个参数 是 正确的方法(这基本上就是 "dependency injection" 的意思)。
您可以使参数隐式(and/or 给它一个默认值)以保留接口(的外观):
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
}
}