Scala - 为使用数据库连接扩展特征/类的对象/单例编写单元测试

Posted

技术标签:

【中文标题】Scala - 为使用数据库连接扩展特征/类的对象/单例编写单元测试【英文标题】:Scala - write unit tests for objects/singletons that extends a trait/class with DB connection 【发布时间】:2014-03-24 19:43:53 【问题描述】:

单元测试相关问题

在测试扩展具有 DB 连接(或任何其他“外部”调用)的另一个特征/类的 scala 对象时遇到问题

在我的项目中的任何地方使用带有数据库连接的单例使得单元测试不是一种选择,因为我无法覆盖/模拟数据库连接

这导致更改我的设计仅在明显需要成为对象的情况下用于测试目的

有什么建议吗?

代码 sn-p 用于不可测试的代码:

object How2TestThis extends SomeDBconnection 

  val somethingUsingDB = 
    getStuff.map(//some logic)
  

  val moreThigs 
    //more things
  



trait SomeDBconnection 
  import DBstuff._
  val db = connection(someDB)  
  val getStuff = db.getThings

【问题讨论】:

【参考方案1】:

    其中一个选项是使用 cake 模式来根据需要要求一些 DB 连接和特定于 mixin 的实现。例如:

    import java.sql.Connection
    
    // Defines general DB connection interface for your application
    trait DbConnection 
      def getConnection: Connection
    
    
    // Concrete implementation for production/dev environment for example
    trait ProductionDbConnectionImpl extends DbConnection 
      def getConnection: Connection = ???
    
    
    // Common code that uses that DB connection and needs to be tested.
    trait DbConsumer 
      this: DbConnection =>
    
      def runDb(sql: String): Unit = 
        getConnection.prepareStatement(sql).execute()
      
    
    
    ...
    
    // Somewhere in production code when you set everything up in init or main you
    // pick concrete db provider
    val prodDbConsumer = new DbConsumer with ProductionDbConnectionImpl
    prodDbConsumer.runDb("select * from sometable")
    
    ...
    
    // Somewhere in test code you mock or stub DB connection ...
    val testDbConsumer = new DbConsumer with DbConnection  def getConnection = ??? 
    testDbConsumer.runDb("select * from sometable")
    

    如果您必须使用单例/Scala object,您可以使用 lazy val 或一些 init(): Unit 方法来设置连接。

    另一种方法是使用某种注射器。例如查看电梯代码:

    package net.liftweb.http
    
    /**
     * A base trait for a Factory.  A Factory is both an Injector and
     * a collection of FactorMaker instances.  The FactoryMaker instances auto-register
     * with the Injector.  This provides both concrete Maker/Vender functionality as
     * well as Injector functionality.
     */
    trait Factory extends SimpleInjector
    

    然后在你的代码中的某个地方你像这样使用这个供应商:

    val identifier = new FactoryMaker[MongoIdentifier](DefaultMongoIdentifier) 
    

    然后在您实际上必须访问 DB 的地方:

    identifier.vend
    

    您可以在测试中提供替代提供程序,方法是在您的代码周围加上:

    identifier.doWith(mongoId)  <your test code> 
    

    可以方便地与 specs2 Around 上下文一起使用,例如:

    implicit val dbContext new Around 
      def around[T: AsResult](t: => T): Result = 
        val mongoId = new MongoIdentifier 
          def jndiName: String = dbName
        
        identifier.doWith(mongoId) 
          AsResult(t)
        
      
    
    

    这很酷,因为它是在 Scala 中实现的,没有任何特殊的字节码或 JVM hack。

    如果您认为前 2 个选项过于复杂并且您有一个小应用程序,您可以使用 Properties file/cmd args 让您知道您是在测试模式还是生产模式下运行。这个想法再次来自 Lift :)。您可以自己轻松地实现它,但在这里您如何使用 Lift Props 来实现它:

    // your generic DB code:
    val jdbcUrl: String = Props.get("jdbc.url", "jdbc:postgresql:database")
    

    你可以有 2 个 props 文件:

    production.default.props

    jdbc.url=jdbc:postgresql:database

    test.default.props

    jdbc.url=jdbc:h2

    Lift 会自动检测运行模式Props.mode 并选择正确的道具文件来读取。您可以使用 JVM cmd args 设置运行模式。

    因此,在这种情况下,您可以连接到内存数据库,也可以只读取运行模式并在代码中相应地设置您的连接(模拟、存根、未初始化等)。

    使用常规 IOC 模式 - 通过构造函数参数将依赖项传递给类。不要使用object。除非您使用特殊的依赖注入框架,否则这很快就会变得不方便。

一些建议:

object 用于不能有替代实现的东西,如果只有这个实现可以在所有环境中工作。对常量和纯 FP 无副作用代码使用 object。在最后一刻使用单例进行连接 - 就像一个带有 main 的类,而不是在许多组件依赖它的代码深处,除非它没有副作用或者它使用诸如可堆叠/可注入供应商提供者之类的东西(参见 Lift )。

结论:

您不能模拟对象或覆盖其实现。您需要将代码设计为可测试的,上面列出了一些选项。使用易于组合的部分使您的代码具有灵活性是一种很好的做法,这不仅是为了测试目的,也是为了可重用性和可维护性。

【讨论】:

相关问题:***.com/questions/21969225/… 问题是我不想要任何数据库连接(不是测试不是 h2)用于我的单元测试我想模拟一个数据库调用并返回一个 json 结果,例如这个解决方案是一个很好的集成测试但不适用于方法单元测试 前 2 个选项可让您在测试中模拟数据库/连接。如果你使用 Cake 模式,你只需混合一个模拟数据库,如果你使用某种注入器,那么你可以注入任何类型的模拟数据库。

以上是关于Scala - 为使用数据库连接扩展特征/类的对象/单例编写单元测试的主要内容,如果未能解决你的问题,请参考以下文章

创建一个扩展抽象类的对象并将其添加到 Scala 中类型抽象类的序列中

Scala 中的特征和抽象方法覆盖

对象可以在scala中扩展抽象类吗?

具有不同生命周期的对象的 Scala 蛋糕模式

大数据笔记(二十四)——Scala面向对象编程实例

Scala中的构造器与对象文末加群学习哦