我正在使用已经有一些用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)
然后我以某种方式可以创建一个模拟工人但具有不同的服务。但是,我没有弄清楚如何做到这一点。
我对如何更改当前代码以使其更易于测试或测试现有代码表示感谢。
答案 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。这在语义上与之前的解决方案相同,但它更清晰,因为您不需要隐含在每个方法上。