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

Testing object which calls another object in Scala using Specs2

我正在处理一个已经有一些用 Scala 编写的遗留代码的项目。当我发现它并不那么容易时,我被赋予了一项任务,为其中一个 classes 编写一些单元测试。这是我遇到的问题:

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

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

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

我的第一个想法是 'Well, I can probably make a trait for DatabaseService with the getById method'。我真的不喜欢为了测试而创建 interface/trait/whatever 的想法,因为我相信它不一定会导致一个好的设计,但让我们暂时忘记它。

现在,如果 Worker 是 class,我可以轻松使用 DI。比如说,通过这样的构造函数:

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

问题是,Worker 不是 class,所以我不能用其他服务创建它的实例。我可以做类似

的事情
object Worker {
    var service: DatabaseAbstractService = /*default*/
    def setService(s: DatabaseAbstractService) = service = s
}

但是,它对我来说几乎没有任何意义,因为它看起来很糟糕并且会导致对象具有可变状态,这看起来不太好。

问题是,我怎样才能使现有代码易于测试而不破坏任何东西并且不做任何糟糕的变通办法?是否有可能或者我应该更改现有代码以便我可以更轻松地对其进行测试?

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

class AbstractWorker(val service: DatabaseAbstractService)

object Worker extends AbstractWorker(DatabaseService)

然后我可以以某种方式创建 Worker 的模拟但具有不同的服务。但是,我不知道该怎么做。

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

如果您可以更改 Worker 的代码,您可以将其更改为仍然允许它是一个对象,并且还允许通过具有默认定义的 implicit 交换数据库服务.这是一个解决方案,我什至不知道这对你是否可行,但它是:

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方法。然后我们添加该特征的 object 实现作为单例实例以在代码中使用。这是一个很好的模式,可以模拟以前只定义为 object 的内容。然后,我们让 Worker 在它的方法上接受一个 implicit DatabaseService (特征)并给它一个默认值 object 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))
    }
  }

在这里,在我的范围内,我制作了一个隐式模拟 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{
    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 class 中定义所有重要的逻辑,这就是您测试的重点。为方便起见,您通过代码中使用的对象提供单例 Worker。有了抽象 class ,您就可以使用构造函数参数来指定要使用的数据库服务实现。这在语义上与之前的解决方案相同,但它更清晰,因为您不需要在每个方法上都隐含。