我如何在Scala中注入一个模拟的单例对象?

时间:2013-08-16 16:01:07

标签: scala dependency-injection mocking

我们正在使用Scala 2.10.2,我们正在为我们的DAO使用Slick 1.0.1。我们试图用ScalaMock模拟DAO,我正试图找出一个注入模拟DAO的好方法。我已经使用Java好几年了,但两周前我刚刚开始使用Scala。

现在我们的代码看起来像(忽略任何语法错误,我已经压缩了代码而没有确保它仍然满足类型系统)

abstract class RichTable[T](name: String) 
        extends slick.driver.MySQLDriver.simple.Table[T](name) {
    type ItemType = T
    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
    ...
}

object Users extends RichTable[User]("users") {
    def crypted_password = column[String]("crypted_password")
    ...
}

case class User(id: Option[Int] = None, crypted_password: String) {
    def updatePassword(...) = {
        Users.where(_.id === id).map{e => e.crypted_password}.update("asdf")
    }
}

所有DAO都是继承自RichTable[T]

的单例对象

我们希望能够模拟用户和其他单例DAO对象 - 现在我们所有的单元测试都在访问数据库。但是,我们遇到的问题是如何注入模拟单例对象。到目前为止我们提出的解决方案是:

object DAORepo {
    var usersDAO : Users.type = Users
    var anotherDAO : Another.type = Another
    ...
}

object Users extends RichTable[User]("users") {
    def apply() : Users.type = DAORepos.usersDAO
}

def updatePassword(...) = {
    Users().where(_.id === id).map{e => e.crypted_password}.update("asdf")
}

def test = {
    val mockUsers = mock[Users]
    DAORepo.usersDAO = mockUsers
    // run test using mock repo
}

我们将所有引用从Users更改为Users(),这不会增加过多的混乱。然而,在DAORepo中使用vars的气味很糟糕,我想知道是否有人建议改善这一点。

我已阅读Real-World Scala: Dependency Injection (DI)Component Based Dependency Injection in Scala - 我想我了解如何使用特征来构建DAORepo,例如

trait UsersRepo {
    val usersDAO : Users.type = Users
}

trait DAORepo extends UsersRepo with AnotherRepo { }

trait UsersTestRepo {
    val usersDAO : Users.type = mock[Users]
}

但我仍然不明白我是如何注入新特性的。我可以做类似

的事情
class DAORepoImpl extends DAORepo { }

object DAOWrapper {
    var repo : DAORepo = new DAORepoImpl
}

def test = {
    DAOWrapper.repo = new DAORepoImpl with UsersTestRepo
}

object DAORepo中的单个var替换object DAOWrapper中的二十几个变量,但似乎应该有一个干净的方法来做这个没有任何变量。

1 个答案:

答案 0 :(得分:3)

我不了解你所有的课程和你的特质。

trait UsersRepo {
    val usersDAO : Users.type = Users
}

trait AnotherRepo {
    val anotherDAO : Another.type = Another
}

trait DAORepo extends UsersRepo with AnotherRepo

然后你可以实例化一个真正的RealDAORepo

object RealDAORepo extends DAORepo { }

或嘲笑的

object MockedDAORepo extends DAORepo {
  override val usersDAO : Users.type = mock[Users]
  override val anotherDAO : Another.type = mock[Another]
}

然后在您的应用程序中注入DAORepo,您​​可以使用蛋糕模式和自我类型引用来做到这一点。


我将很快发布一篇关于InfoQ FR的文章,帮助Spring人们理解蛋糕模式。以下是本文的代码示例:

trait UserTweetServiceComponent {
  val userTweetService: UserTweetService
}

trait UserTweetService {
  def createUser(user: User): User
  def createTweet(tweet: Tweet): Tweet
  def getUser(id: String): User
  def getTweet(id: String): Tweet
  def getUserAndTweets(id: String): (User,List[Tweet])
}

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {

  // Declare dependencies of the service here
  self: UserRepositoryComponent 
        with TweetRepositoryComponent =>

  override val userTweetService: UserTweetService = new DefaultUserTweetService

  class DefaultUserTweetService extends UserTweetService {
    override def createUser(user: User): User = userRepository.createUser(user)
    override def createTweet(tweet: Tweet): Tweet = tweetRepository.createTweet(tweet)
    override def getUser(id: String): User = userRepository.getUser(id)
    override def getTweet(id: String): Tweet = tweetRepository.getTweet(id)
    override def getUserAndTweets(id: String): (User,List[Tweet]) = {
      val user = userRepository.getUser(id)
      val tweets = tweetRepository.getAllByUser(user)
      (user,tweets)
    }
  }
}

请注意,这与Spring声明几乎相同:

<bean name="userTweetService" class="service.impl.DefaultUserTweetService">
    <property name="userRepository" ref="userRepository"/>
    <property name="tweetRepository" ref="tweetRepository"/>
</bean>

当你这样做时:

trait MyApplicationMixin
  extends DefaultUserTweetServiceComponent
  with InMemoryUserRepositoryComponent
  with InMemoryTweetRepositoryComponent

它与Spring声明几乎相同(但是你得到了一个类型安全的应用程序上下文):

<import resource="classpath*:/META-INF/application-context-default-tweet-services.xml" />
<import resource="classpath*:/META-INF/application-context-inmemory-tweet-repository.xml" />
<import resource="classpath*:/META-INF/application-context-inmemory-user-repository.xml" />

然后您可以将应用程序用于:

val app = new MyApplicationMixin { }

或者

val app = new MyApplicationMixin { 
   override val tweetRepository = mock[TweetRepository]
}

后者与Spring bean覆盖相同:

<import resource="classpath*:/META-INF/application-context-default-tweet-services.xml" />
<import resource="classpath*:/META-INF/application-context-inmemory-tweet-repository.xml" />
<import resource="classpath*:/META-INF/application-context-inmemory-user-repository.xml" />

 <!-- 
 This bean will override the one defined in application-context-inmemory-tweet-repository.xml
 But notice that Spring isn't really helpful to declare the behavior of the mock, which is much 
 easier with the cake pattern since you directly write code
 -->
<bean id="tweetRepository" class="repository.impl.MockedTweetRepository"/>

回到你的问题,你可以使用蛋糕模式并在你的应用程序中创建服务组件,这取决于你的DAORepo特性。

然后你可以这样做:

trait MyApplicationMixin
      extends DefaultUserServiceComponent
      with AnotherServiceComponent
      with DAORepo

然后:

val app = new MyApplicationMixin { }

或者

val app = new MyApplicationMixin {
    override val usersDAO : Users.type = mock[Users]
    override val anotherDAO : Another.type = mock[Another]
}

构建应用程序后,您可以像这样使用它:

app.userService.createUser(...)

构建的应用程序非常像应用程序上下文