在Scala中使用依赖注入进行模拟分发

时间:2019-03-13 16:13:55

标签: scala unit-testing dependency-injection scalatest

我正在编写一个分布式应用程序,在该应用程序中,我要对应用程序逻辑进行独立于分布式方面的单元测试。我有一个类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)
}

我还查看了Scala(http://jonasboner.com/real-world-scala-dependency-injection-di/)中用于依赖项注入的Cake模式,但是我看不到如何将其应用于我的案例。


更新@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 个答案:

答案 0 :(得分:1)

传递参数 是正确的方法(从本质上讲,这就是“依赖注入”的含义)。

您可以使参数隐式(和/或为其提供默认值)以保留接口(的外观):

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