使用Specs2调用Scala中另一个对象的测试对象
我正在处理一个已经用Scala编写了一些遗留代码的项目。当我发现它不那么容易时,我被指派为它的一个类编写一些单元测试。以下是我遇到的问题: 我们有一个对象,比如Worker和另一个对象来访问数据库,比如DatabaseService,它还扩展了其他类(我认为这并不重要,但仍然如此)。而Worker则被更高的类和对象调用 所以,现在我们有这样的东西:使用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
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
类中定义所有重要的逻辑,这就是您测试的重点。为了方便起见,您可以通过代码中使用的对象提供一个singletonWorker
。有了抽象类,就可以使用构造函数param来指定要使用的数据库服务impl。这在语义上与前面的解决方案相同,但更简洁,因为您不需要在每个方法上都使用隐式。使用一个方法似乎很好,但实际上我们有很多方法,这意味着我必须将其添加到所有方法中,不是吗?我想我会尝试用一种方法来做这件事,只是为了弄清楚在没有当前设计的情况下是否可能,但我怀疑我是否会将这种隐式引用插入所有方法。对象
是scala单元测试的祸根。如果您希望能够有效地测试它们,那么在使用它们时需要考虑某些设计因素。这只是一种方法。所以,如果我不想传递隐式引用或更改当前代码,我是否必须忘记UTs或推迟UTs?如果我决定通过代码进行测试,你能给我一些建议吗?需要考虑的“注意事项”是什么?我更新了另一个IML,它可能更适合您的需要。谢谢,我会仔细看看,稍后再试。