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

时间:2014-02-23 08:17:37

标签: unit-testing scala mocking typesafe

单元测试相关问题

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

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

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

有什么建议吗?

不可测试代码的代码段:

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 个答案:

答案 0 :(得分:4)

  1. 其中一个选项是使用cake模式来根据需要进行某些数据库连接和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方法设置连接。

  2. 另一种方法是使用某种注射器。例如,查看Lift代码:

    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) {}
    

    然后在您实际需要访问数据库的地方:

    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黑客攻击。

  3. 如果您认为前2个选项过于复杂且您的应用程序较小,则可以使用Properties file / cmd args告诉您是否在测试或生产模式下运行。这个想法再次来自Lift :)。您可以自己轻松地实现它,但是在这里您可以使用Lift Props

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

    您可以拥有2个道具文件:

    • production.default.props

      jdbc.url=jdbc:postgresql:database

    • test.default.props

      jdbc.url=jdbc:h2

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

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

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

  5. 一些建议:

    object用于无法替代实现的内容,以及此唯一实现是否适用于所有环境。使用object表示常量和纯FP非副作用代码。在最后一刻使用单例连接 - 就像一个main的类,而不是代码深处的某个地方,许多组件依赖它,除非它没有副作用或者使用类似可堆叠/可注入的供应商提供者(见Lift)。

    结论:

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