使用Specs2调用Scala中另一个对象的测试对象

使用Specs2调用Scala中另一个对象的测试对象,scala,unit-testing,dependency-injection,specs2,Scala,Unit Testing,Dependency Injection,Specs2,我正在处理一个已经用Scala编写了一些遗留代码的项目。当我发现它不那么容易时,我被指派为它的一个类编写一些单元测试。以下是我遇到的问题: 我们有一个对象,比如Worker和另一个对象来访问数据库,比如DatabaseService,它还扩展了其他类(我认为这并不重要,但仍然如此)。而Worker则被更高的类和对象调用 所以,现在我们有这样的东西: object Worker { def performComplexAlgorithm(id: String) = { va

我正在处理一个已经用Scala编写了一些遗留代码的项目。当我发现它不那么容易时,我被指派为它的一个类编写一些单元测试。以下是我遇到的问题:

我们有一个对象,比如Worker和另一个对象来访问数据库,比如DatabaseService,它还扩展了其他类(我认为这并不重要,但仍然如此)。而Worker则被更高的类和对象调用

所以,现在我们有这样的东西:

object Worker {
    def performComplexAlgorithm(id: String) = {
        val entity = DatabaseService.getById(id)
        //Rest of the algorithm
    }
}
trait DatabaseAbstractService {
    def getById(id: String): SomeEntity
}

object DatabaseService extends SomeOtherClass with DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*complex db query*/}
}

//Probably just create the fake using the mock framework right in unit test
object FakeDbService extends DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*just return something*/}
}

class Worker(val service: DatabaseService) {

    def performComplexAlgorithm(id: String) = {
        val entity = service.getById(id)
        //Rest of the algorithm
    }
}
class AbstractWorker(val service: DatabaseAbstractService)

object Worker extends AbstractWorker(DatabaseService)
我的第一个想法是“嗯,我可能可以用getById方法为DatabaseService创建一个特性”。我真的不喜欢仅仅为了测试而创建一个接口/特性/任何东西的想法,因为我相信这不一定会导致一个好的设计,但是现在让我们先忘掉它吧

现在,若工人是一个类,我可以很容易地使用DI。比如,通过这样的构造函数:

object Worker {
    def performComplexAlgorithm(id: String) = {
        val entity = DatabaseService.getById(id)
        //Rest of the algorithm
    }
}
trait DatabaseAbstractService {
    def getById(id: String): SomeEntity
}

object DatabaseService extends SomeOtherClass with DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*complex db query*/}
}

//Probably just create the fake using the mock framework right in unit test
object FakeDbService extends DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*just return something*/}
}

class Worker(val service: DatabaseService) {

    def performComplexAlgorithm(id: String) = {
        val entity = service.getById(id)
        //Rest of the algorithm
    }
}
class AbstractWorker(val service: DatabaseAbstractService)

object Worker extends AbstractWorker(DatabaseService)
问题是,Worker不是一个类,所以我不能用另一个服务创建它的实例。我可以做类似的事情

object Worker {
    var service: DatabaseAbstractService = /*default*/
    def setService(s: DatabaseAbstractService) = service = s
}
然而,它对我来说几乎没有任何意义,因为它看起来很糟糕,并且导致了一个状态可变的对象,它看起来不太好

问题是,我如何使现有代码易于测试,而不破坏任何东西,也不采取任何可怕的变通办法?我是否有可能或者应该改变现有的代码,以便更容易地进行测试

我正在考虑这样使用扩展:

object Worker {
    def performComplexAlgorithm(id: String) = {
        val entity = DatabaseService.getById(id)
        //Rest of the algorithm
    }
}
trait DatabaseAbstractService {
    def getById(id: String): SomeEntity
}

object DatabaseService extends SomeOtherClass with DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*complex db query*/}
}

//Probably just create the fake using the mock framework right in unit test
object FakeDbService extends DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*just return something*/}
}

class Worker(val service: DatabaseService) {

    def performComplexAlgorithm(id: String) = {
        val entity = service.getById(id)
        //Rest of the algorithm
    }
}
class AbstractWorker(val service: DatabaseAbstractService)

object Worker extends AbstractWorker(DatabaseService)
然后我就可以用不同的服务来模仿工人。然而,我不知道怎么做


对于如何更改当前代码以使其更易于测试,或者如何测试现有代码,我将不胜感激

如果您可以更改
Worker
的代码,您可以将其更改为仍然允许它成为对象,并且还允许通过默认定义的
隐式
交换db服务。这是一个解决方案,我甚至不知道这对您是否可行,但这里是:

case class MyObj(id:Long)

trait DatabaseService{
  def getById(id:Long):Option[MyObj] = {
    //some impl here...
  }
}

object DatabaseService extends DatabaseService


object Worker{
  def doSomething(id:Long)(implicit dbService:DatabaseService = DatabaseService):Option[MyObj] = {
    dbService.getById(id)
  }
}
因此,我们用
getById
方法的具体实现建立了一个特性。然后,我们添加一个该特性的
对象
impl作为要在代码中使用的单例实例。这是一个很好的模式,可以模拟以前仅定义为
对象的对象。然后,我们让
工作者
在其方法上接受一个
隐式
数据库服务
(trait),并给它一个
对象的默认值
数据库服务
,这样常规使用就不必担心满足该要求。然后我们可以这样测试它:

class WorkerUnitSpec extends Specification with Mockito{

  trait scoping extends Scope{
    implicit val mockDb = mock[DatabaseService]
  }

  "Calling doSomething on Worker" should{
    "pass the call along to the implicit dbService and return rhe result" in new scoping{
      mockDb.getById(123L) returns Some(MyObj(123))
      Worker.doSomething(123) must beSome(MyObj(123))
    }
  }
class WorkerUnitSpec extends Specification with Mockito{

  trait scoping extends Scope{
    val mockDb = mock[DatabaseService]
    val testWorker = new Worker(mockDb){}
  }

  "Calling doSomething on Worker" should{
    "pass the call along to the implicit dbService and return rhe result" in new scoping{
      mockDb.getById(123L) returns Some(MyObj(123))
      testWorker.doSomething(123) must beSome(MyObj(123))
    }
  }
}
这里,在我的范围内,我提供了一个隐式模拟的
DatabaseService
,它将取代
doSomething
方法上的默认
DatabaseService
,用于我的测试目的。一旦你这样做了,你就可以开始模拟和测试了

更新

如果您不想采用隐式方法,您可以像这样重新定义
Worker

abstract class Worker(dbService:DatabaseService){
  def doSomething(id:Long):Option[MyObj] = {
    dbService.getById(id)
  }   
}

object Worker extends Worker(DatabaseService)
然后像这样测试它:

class WorkerUnitSpec extends Specification with Mockito{

  trait scoping extends Scope{
    implicit val mockDb = mock[DatabaseService]
  }

  "Calling doSomething on Worker" should{
    "pass the call along to the implicit dbService and return rhe result" in new scoping{
      mockDb.getById(123L) returns Some(MyObj(123))
      Worker.doSomething(123) must beSome(MyObj(123))
    }
  }
class WorkerUnitSpec extends Specification with Mockito{

  trait scoping extends Scope{
    val mockDb = mock[DatabaseService]
    val testWorker = new Worker(mockDb){}
  }

  "Calling doSomething on Worker" should{
    "pass the call along to the implicit dbService and return rhe result" in new scoping{
      mockDb.getById(123L) returns Some(MyObj(123))
      testWorker.doSomething(123) must beSome(MyObj(123))
    }
  }
}

通过这种方式,您可以在抽象的
Worker
类中定义所有重要的逻辑,这就是您测试的重点。为了方便起见,您可以通过代码中使用的对象提供一个singleton
Worker
。有了抽象类,就可以使用构造函数param来指定要使用的数据库服务impl。这在语义上与前面的解决方案相同,但更简洁,因为您不需要在每个方法上都使用隐式。

使用一个方法似乎很好,但实际上我们有很多方法,这意味着我必须将其添加到所有方法中,不是吗?我想我会尝试用一种方法来做这件事,只是为了弄清楚在没有当前设计的情况下是否可能,但我怀疑我是否会将这种隐式引用插入所有方法。
对象
是scala单元测试的祸根。如果您希望能够有效地测试它们,那么在使用它们时需要考虑某些设计因素。这只是一种方法。所以,如果我不想传递隐式引用或更改当前代码,我是否必须忘记UTs或推迟UTs?如果我决定通过代码进行测试,你能给我一些建议吗?需要考虑的“注意事项”是什么?我更新了另一个IML,它可能更适合您的需要。谢谢,我会仔细看看,稍后再试。