是否可以在 Scala 中模拟/覆盖依赖项/导入?
Is it possible to mock / override dependencies / imports in Scala?
我有一些代码如下所示:
package org.samidarko.actors
import org.samidarko.helpers.Lib
class Monitoring extends Actor {
override def receive: Receive = {
case Tick =>
Lib.sendNotification()
}
}
有没有办法从 ScalaTest mock/stub Lib
像 proxyquire for节点?
我听说我可以使用依赖注入,但我不想那样做
我唯一的选择是将我的库作为 class 参数传递吗?
class Monitoring(lib: Lib) extends Actor {
有什么建议可以让它更容易测试吗?谢谢
编辑:
Xavier Guihot的回答是一个有趣的问题解决方法,但我选择更改代码以进行测试。
我将 Lib
作为参数传递,并且我正在使用 mockito 进行模拟,这使得代码比隐藏范围更容易测试和维护。
此答案仅使用 scalatest,不影响源代码:
基本解:
假设你有这个 src class(你想测试的那个,你想模拟它的依赖):
package com.my.code
import com.lib.LibHelper
class MyClass() {
def myFunction(): String = LibHelper.help()
}
和这个库依赖项(你想在测试 MyClass 时模拟/覆盖它):
package com.lib
object LibHelper {
def help(): String = "hello world"
}
想法是在您的测试文件夹中创建一个 class,它将 override/shadow 库。 class 将与您要模拟的对象具有相同的名称和相同的包。在 src/test/scala/com/external/lib
中,您可以创建包含此代码的 LibHelper.scala
:
package com.lib
object LibHelper {
def help(): String = "hello world - overriden"
}
这样你就可以用通常的方式测试你的代码了:
package com.my.code
import org.scalatest.FunSuite
class MyClassTest extends FunSuite {
test("my_test") {
assert(new MyClass().myFunction() === "hello world - overriden")
}
}
改进的解决方案允许为每个测试设置模拟行为:
前面的代码清晰简单,但 LibHelper 的模拟行为对于所有测试都是相同的。并且可能希望 LibHelper 的一种方法产生不同的输出。因此,我们可以考虑在 LibHelper 中设置一个可变变量,并在每次测试之前更新该变量,以设置 LibHelper 的所需行为。 (这仅在 LibHelper 是一个对象时有效)
阴影 LibHelper(src/test/scala/com/external/lib 中的那个)应替换为:
package com.lib
object LibHelper {
var testName = "test_1"
def help(): String =
testName match {
case "test_1" => "hello world - overriden - test 1"
case "test_2" => "hello world - overriden - test 2"
}
}
最严重的 class 应该变成:
package com.my.code
import com.lib.LibHelper
import org.scalatest.FunSuite
class MyClassTest extends FunSuite {
test("test_1") {
LibHelper.testName = "test_1"
assert(new MyClass().myFunction() === "hello world - overriden - test 1")
}
test("test_2") {
LibHelper.testName = "test_2"
assert(new MyClass().myFunction() === "hello world - overriden - test 2")
}
}
非常重要的精度,因为我们使用的是全局变量,所以必须强制 scalatest 按顺序(而不是并行)进行 运行 测试。关联的 scalatest 选项(将包含在 build.sbt 中)是:
parallelExecution in Test := false
不是一个完整的答案(因为我不太了解 AOP),但是为了让您朝着正确的方向前进,这可以通过 Java 称为 AspectJ 的库来实现:
https://blog.jayway.com/2007/02/16/static-mock-using-aspectj/
https://www.cakesolutions.net/teamblogs/2013/08/07/aspectj-with-akka-scala
伪代码示例(不赘述):
class Mock extends MockAspect {
@Pointcut("execution (* org.samidarko.helpers.Lib.sendNotification(..))")
def intercept() {...}
}
此方法的低级基础是动态代理:https://dzone.com/articles/java-dynamic-proxy。但是,您也可以模拟静态方法(也许您必须将单词 static
添加到模式中)。
我有一些代码如下所示:
package org.samidarko.actors
import org.samidarko.helpers.Lib
class Monitoring extends Actor {
override def receive: Receive = {
case Tick =>
Lib.sendNotification()
}
}
有没有办法从 ScalaTest mock/stub Lib
像 proxyquire for节点?
我听说我可以使用依赖注入,但我不想那样做
我唯一的选择是将我的库作为 class 参数传递吗?
class Monitoring(lib: Lib) extends Actor {
有什么建议可以让它更容易测试吗?谢谢
编辑:
Xavier Guihot的回答是一个有趣的问题解决方法,但我选择更改代码以进行测试。
我将 Lib
作为参数传递,并且我正在使用 mockito 进行模拟,这使得代码比隐藏范围更容易测试和维护。
此答案仅使用 scalatest,不影响源代码:
基本解:
假设你有这个 src class(你想测试的那个,你想模拟它的依赖):
package com.my.code
import com.lib.LibHelper
class MyClass() {
def myFunction(): String = LibHelper.help()
}
和这个库依赖项(你想在测试 MyClass 时模拟/覆盖它):
package com.lib
object LibHelper {
def help(): String = "hello world"
}
想法是在您的测试文件夹中创建一个 class,它将 override/shadow 库。 class 将与您要模拟的对象具有相同的名称和相同的包。在 src/test/scala/com/external/lib
中,您可以创建包含此代码的 LibHelper.scala
:
package com.lib
object LibHelper {
def help(): String = "hello world - overriden"
}
这样你就可以用通常的方式测试你的代码了:
package com.my.code
import org.scalatest.FunSuite
class MyClassTest extends FunSuite {
test("my_test") {
assert(new MyClass().myFunction() === "hello world - overriden")
}
}
改进的解决方案允许为每个测试设置模拟行为:
前面的代码清晰简单,但 LibHelper 的模拟行为对于所有测试都是相同的。并且可能希望 LibHelper 的一种方法产生不同的输出。因此,我们可以考虑在 LibHelper 中设置一个可变变量,并在每次测试之前更新该变量,以设置 LibHelper 的所需行为。 (这仅在 LibHelper 是一个对象时有效)
阴影 LibHelper(src/test/scala/com/external/lib 中的那个)应替换为:
package com.lib
object LibHelper {
var testName = "test_1"
def help(): String =
testName match {
case "test_1" => "hello world - overriden - test 1"
case "test_2" => "hello world - overriden - test 2"
}
}
最严重的 class 应该变成:
package com.my.code
import com.lib.LibHelper
import org.scalatest.FunSuite
class MyClassTest extends FunSuite {
test("test_1") {
LibHelper.testName = "test_1"
assert(new MyClass().myFunction() === "hello world - overriden - test 1")
}
test("test_2") {
LibHelper.testName = "test_2"
assert(new MyClass().myFunction() === "hello world - overriden - test 2")
}
}
非常重要的精度,因为我们使用的是全局变量,所以必须强制 scalatest 按顺序(而不是并行)进行 运行 测试。关联的 scalatest 选项(将包含在 build.sbt 中)是:
parallelExecution in Test := false
不是一个完整的答案(因为我不太了解 AOP),但是为了让您朝着正确的方向前进,这可以通过 Java 称为 AspectJ 的库来实现:
https://blog.jayway.com/2007/02/16/static-mock-using-aspectj/
https://www.cakesolutions.net/teamblogs/2013/08/07/aspectj-with-akka-scala
伪代码示例(不赘述):
class Mock extends MockAspect {
@Pointcut("execution (* org.samidarko.helpers.Lib.sendNotification(..))")
def intercept() {...}
}
此方法的低级基础是动态代理:https://dzone.com/articles/java-dynamic-proxy。但是,您也可以模拟静态方法(也许您必须将单词 static
添加到模式中)。