Scala: 模拟和蛋糕模式

10

我一直在尝试采用蛋糕模式,但在适应这种编程风格方面,特别是在单元测试方面,我遇到了困难。

假设我有以下业务对象:

trait Vet {
  def vaccinate(pet: Pet)
}

trait PetStore { this: Vet =>
  def sell(pet: Pet) {
    vaccinate(pet)
    // do some other stuff
  }
}

现在,我想在模拟Vet函数的情况下测试PetStore。如果我使用组合方式,我将创建一个mock[Vet]并将其传递给PetStore构造函数,然后像在Java世界中那样编写mock。然而,我找不到任何关于如何在蛋糕模式中实现这一点的参考资料。

可能的解决方案之一是根据预期的使用情况在每个测试用例中实现vaccinate(),但这样就无法验证mock是否被正确调用,也无法使用匹配器等。

那么,人们如何在Cake Pattern中使用mock对象呢?

4个回答

6
我在阅读了这篇博客文章后开始使用蛋糕模式:https://github.com/precog/staticsite/blob/master/contents/blog/Existential-Types-FTW/index.md 该方法与大多数蛋糕模式文章不同,它使用存在类型而不是自身类型。
我已经使用这种模式几个月了,看起来效果很好,因为我可以在需要时指定一个模拟。 它确实有一种依赖注入的感觉,但它具有您将代码放入特质中所获得的所有优点。
我使用存在类型的混合版本解决你的问题可能会像这样:
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的实例并创建一个VetLike的模拟来单独测试组件,并在PetStore测试中使用它。

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

这很酷 - 但是我错过了什么吗?你在PetStore中使用Vet类型做什么? - Electric Monk
所以我试图给出一个不使用ConfigComponent trait的例子,但是我做错了什么。无论如何,我已经更新了这个例子并添加了ConfigComponent。希望事情看起来更清楚一些。 - Travis Stevens

4
这是个好问题。我们得出结论,至少不能以我们惯常的方式完成。可以使用 stubs 替代 mocks 并像制作蛋糕一样混合它们。但这比使用 mocks 更费力。
我们有两个 Scala 团队,一个团队采用了 cake 模式,使用 stubs 替代 mocks,而另一个团队则坚持类和依赖注入。现在我尝试了两种方法,我更喜欢使用 DI 和 mocks 进行测试,因为它更简单。而且可以说更容易阅读。

1
这最初是我的想法。然而,随着我越来越多地使用Scala,我已经到了将同一概念业务对象的不同关注点分离到不同特征中以实现可测试性和清晰度的阶段。在这里使用DI会导致过于庞大的对象图和繁琐的应用程序初始化代码。 - Electric Monk
正如人們所說的“你的經驗可能會有所不同”。我們的經驗剛好相反。 - Rick-777

2
我已经找到了一种使用Scalamock和Scalatest进行单元测试“Cake Pattern”模块的方法。
起初,我遇到了许多问题(包括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

1
尽管这是一个老问题,但我为了未来的读者添加我的答案。我相信这篇SO帖子 - 如何在蛋糕模式中使用模拟 - 提出并回答了同样的问题。
我成功地按照弗拉基米尔·马特维耶夫提供的答案(当时是最佳答案)进行了操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接