代码之家  ›  专栏  ›  技术社区  ›  Vlad Stryapko

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

  •  1
  • Vlad Stryapko  · 技术社区  · 9 年前

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

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

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

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

    我的第一个想法是“嗯,我可能可以用getById方法为DatabaseService创建一个特性”。我不太喜欢仅仅为了测试而创建一个接口/特性/任何东西的想法,因为我认为这不一定会带来一个好的设计,但现在我们就忘掉它吧。

    现在,如果Worker是一个类,我可以很容易地使用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不是一个类,所以我不能用另一个服务创建它的实例。我可以做一些类似的事情

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

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

    问题是,我如何在不破坏任何东西和不做任何可怕的变通方法的情况下使现有代码易于测试?是否有可能,或者我应该改为更改现有代码,以便更容易地进行测试?

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

    class AbstractWorker(val service: DatabaseAbstractService)
    
    object Worker extends AbstractWorker(DatabaseService)
    

    然后我可以用不同的服务创建一个Worker的模拟。然而,我不知道该怎么做。

    对于如何更改当前代码以使其更具可测试性或测试现有代码,我将非常感激。

    1 回复  |  直到 9 年前
        1
  •  3
  •   cmbaxter    9 年前

    如果您可以更改代码 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 该特性的impl作为要在代码中使用的单实例。这是一个很好的模式,可以用来嘲笑以前只定义为 对象 。然后,我们 工人 接受 含蓄的 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))
        }
      }
    

    在这里,在我的范围内,我做了一个含蓄的嘲讽 数据库服务 可用,将取代默认值 数据库服务 doSomething 用于我的测试目的。一旦你做到了这一点,你就可以开始模仿和测试了。

    使现代化

    如果你不想采用隐式方法,你可以重新定义 工人 像这样:

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

    通过这种方式,您可以在抽象中定义所有重要的逻辑 工人 类,这是您测试的重点。您提供了一个单例 工人 通过代码中为方便而使用的对象。有了抽象类,我们就可以使用构造函数参数来指定要使用的数据库服务impl。这在语义上与前面的解决方案相同,但它更清晰,因为您不需要每个方法上的隐式。