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

时间:2015-09-02 12:38:00

标签: scala unit-testing dependency-injection specs2

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

我们有一个对象,比如,Worker和另一个访问数据库的对象,比如说,DatabaseService也扩展了其他类(我不认为这很重要,但仍然如此)。反过来,工人被更高级的阶级和对象召唤。

所以,现在我们有这样的事情:

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

我的第一个就是'嗯,我可以使用getById方法为DatabaseService制作一个特性'。我不太喜欢创建界面/特性/任何仅仅是为了测试的想法,因为我相信它并不一定会导致一个漂亮的设计,但让我们忘记它现在。

现在,如果工人是一个班级,我可以轻松使用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
    }
}

问题是,工人不是一个班级,所以我不能用另一个服务制作它的实例。我可以做类似

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

然而,它对我来说几乎没有任何意义,因为它看起来很糟糕并导致一个具有可变状态的物体,这看起来并不是很好。

问题是,如何在不破坏任何内容且不做任何可怕的解决方法的情况下,使现有代码易于测试?是否可以或应该更改现有代码,以便我可以更轻松地进行测试?

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

class AbstractWorker(val service: DatabaseAbstractService)

object Worker extends AbstractWorker(DatabaseService)

然后我以某种方式可以创建一个模拟工人但具有不同的服务。但是,我没有弄清楚如何做到这一点。

我对如何更改当前代码以使其更易于测试或测试现有代码表示感谢。

1 个答案:

答案 0 :(得分:3)

如果您可以更改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方法的具体impl的特征。然后我们将该特征的object impl添加为在代码中使用的单例实例。这是一个很好的模式,可以模拟以前只定义为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,可以取代DatabaseService方法的默认doSomething以用于测试目的。一旦你这样做,你就可以开始嘲笑和测试了。

<强>更新

如果您不想采用隐式方法,可以像这样重新定义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类中定义所有重要性逻辑,以及您需要关注测试的内容。为方便起见,您通过代码中使用的对象提供单例Worker。拥有一个抽象类,您可以使用构造函数参数指定要使用的数据库服务impl。这在语义上与之前的解决方案相同,但它更清晰,因为您不需要隐含在每个方法上。