我一直在尝试采用Cake Pattern,但我很难适应这种编程风格,特别是在涉及单元测试时。
让我们假设我有以下业务对象:
trait Vet {
def vaccinate(pet: Pet)
}
trait PetStore { this: Vet =>
def sell(pet: Pet) {
vaccinate(pet)
// do some other stuff
}
}
现在,我想在模拟Vet的功能时测试PetStore。如果我正在使用合成,我正在创建一个模拟[Vet]并将其传递给PetStore构造函数,然后像在Java世界中那样编写模拟。但是,我找不到人们如何用蛋糕模式做到这一点。
一种可能的解决方案是根据预期的使用情况对每个测试用例实施vaccinate(),但这不允许我验证模拟被正确调用,不允许我使用匹配器等
那么 - 人们如何使用Cake Pattern和模拟对象?
答案 0 :(得分:6)
我在阅读这篇博文后开始使用蛋糕模式:https://github.com/precog/staticsite/blob/master/contents/blog/Existential-Types-FTW/index.md这种方法与大多数Cake Pattern帖子的不同之处在于使用了存在类型而不是自我类型。
我已经使用这个模式几个月了,它似乎运行良好,因为我可以在我想要的时候指定一个模拟。它确实具有更多的依赖注入感觉,但它具有使您的代码具有特征的所有好处。
我使用存在类型的问题的混蛋版本将是这样的:
case class Pet(val name: String)
trait ConfigComponent {
type Config
def config: Config
}
trait Vet {
def vaccinate(pet: Pet) = {println ("Vaccinate:" + pet)}
}
trait PetStoreConfig {
val vet: Vet
}
trait PetStore extends ConfigComponent {
type Config <: PetStoreConfig
def sell(pet: Pet) {
config.vet.vaccinate(pet)
// do some other stuff
}
}
您可以将它们整合到您的应用中
class MyApp extends PetStore with PetStoreConfig {
type Config = MyApp
def config = this
val vet = new Vet{}
sell(new Pet("Fido"))
}
scala> new MyApp
Vaccinate:Pet(Fido)
res0: MyApp = MyApp@668dd96c
您可以通过创建VetLike的实例来单独测试组件,还可以使用PetStore测试创建VetLike模拟器。
//Test VetLike Behavior
scala> val vet = new Vet{}
scala> vet.vaccinate(new Pet("Fido"))
Vaccinate:Pet(Fido)
//Test Petstore Behavior
class VetMock extends Vet {
override def vaccinate(pet: Pet) = println("MOCKED")
}
class PetStoreTest extends PetStore with PetStoreConfig {
type Config = PetStoreTest
def config = this
val vet = new VetMock
val fido = new Pet("Fido")
sell(fido)
}
scala> new PetStoreTest
MOCKED
答案 1 :(得分:4)
这是一个很好的问题。我们得出的结论是,它无法完成,至少与我们习惯的方式不同。可以使用存根而不是模拟,并以蛋糕方式混合存根。但这比使用模拟更多的工作。
我们有两个Scala团队和一个团队采用了蛋糕模式,使用存根而不是模拟,而另一个团队坚持使用类和依赖注入。现在我已经尝试了两种,我更喜欢DI与模拟,因为它更容易测试。并且可以说也更容易阅读。
答案 2 :(得分:2)
我找到了一种方法,将Scalamock与Scalatest一起用于单元测试和蛋糕模式&#39;模块。
起初,我遇到了很多问题(包括this),但我相信我在下面提出的解决方案是可以接受的。如果您有任何疑虑,请告诉我。
这就是我设计你的例子的方式:
trait VetModule {
def vet: Vet
trait Vet {
def vaccinate(pet: Pet)
}
}
trait PetStoreModule {
self: VetModule =>
def sell(pet: Pet)
}
trait PetStoreModuleImpl extends PetStoreModule {
self: VetModule =>
def sell(pet: Pet) {
vet.vaccinate(pet)
// do some other stuff
}
}
然后将测试定义如下:
class TestPetstore extends FlatSpec with ShouldMatchers with MockFactory {
trait PetstoreBehavior extends PetStoreModule with VetModule {
object MockWrapper {
var vet: Vet = null
}
def fixture = {
val v = mock[Vet]
MockWrapper.vet = v
v
}
def t1 {
val vet = fixture
val p = Pet("Fido")
(vet.vaccinate _).expects(p)
sell(p)
}
def vet: Vet = MockWrapper.vet
}
val somePetStoreImpl = new PetstoreBehavior with PetStoreModuleImpl
"The PetStore" should "vaccinate an animal before selling" in somePetStoreImpl.t1
}
使用此设置,您将面临“不利条件”。你必须在你写的每个测试中调用val vet = fixture
。另一方面,人们可以轻松地创建另一个“实施”。测试,例如,
val someOtherPetStoreImpl = new PetstoreBehavior with PetStoreModuleOtherImpl
答案 3 :(得分:1)
虽然这是一个老问题,但我正在为未来的读者添加我的答案。我相信这个SO帖子 - How to use mocks with the Cake Pattern - 请求和回答相同的事情。
我成功地遵循了弗拉基米尔·马特维耶夫给出的答案(这是撰写本文时的最佳答案)