Объект тестирования, который вызывает другой объект в Scala с использованием Specs2

Я работаю над проектом, в котором уже есть устаревший код, написанный на Scala. Мне дали задание написать несколько модульных тестов для одного из его классов, когда я обнаружил, что это не так просто. Вот проблема, с которой я столкнулся:

У нас есть объект, скажем, Worker и другой объект для доступа к базе данных, скажем, DatabaseService, который также расширяет другой класс (я не думаю, что это имеет значение, но все же). Рабочий, в свою очередь, вызывается высшими классами и объектами.

Итак, прямо сейчас у нас есть что-то вроде этого:

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

Моим первым, хотя, было: «Ну, я, вероятно, могу сделать трейт для DatabaseService с помощью метода getById». Мне не очень нравится идея создавать интерфейс/черту/что-то еще только ради тестирования, потому что я считаю, что это не обязательно приведет к хорошему дизайну, но давайте пока забудем об этом.

Теперь, если бы 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, но с другим сервисом. Однако я не понял, как это сделать.

Я был бы признателен за любые советы относительно того, как изменить текущий код, чтобы сделать его более тестируемым, или протестировать существующий.


person Vlad Stryapko    schedule 02.09.2015    source источник


Ответы (1)


Если вы можете изменить код для 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, который заменит 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 через объект, который используется в коде для удобства. Наличие абстрактного класса позволяет вам использовать параметр конструктора, чтобы указать, какую службу базы данных следует использовать. Это семантически то же самое, что и предыдущее решение, но оно чище, поскольку вам не нужны неявные значения для каждого метода.

person cmbaxter    schedule 02.09.2015
comment
Кажется довольно хорошим с одним методом, но на самом деле у нас их много, так что это означает, что мне придется добавить это ко всем методам, не так ли? Думаю, я попытаюсь сделать это с помощью одного метода, просто чтобы выяснить, возможно ли это вообще без текущего дизайна, но я сомневаюсь, что буду вставлять такую ​​неявную ссылку на все методы. - person Vlad Stryapko; 02.09.2015
comment
objects — проклятие модульного тестирования в scala. При их использовании необходимо учитывать определенные особенности дизайна, если вы хотите иметь возможность эффективно их тестировать. Это только один подход. - person cmbaxter; 02.09.2015
comment
Итак, должен ли я забыть о UT или отложить их, если я не хочу передавать неявную ссылку или изменять текущий код? Не могли бы вы дать несколько советов относительно того, что мне придется делать, если я решу сделать код тестируемым? Что это за «соображения», которые необходимо учитывать? - person Vlad Stryapko; 02.09.2015
comment
Я обновил другой импл, который может лучше соответствовать вашим потребностям. - person cmbaxter; 02.09.2015
comment
Спасибо, посмотрю повнимательнее и чуть позже попробую. - person Vlad Stryapko; 02.09.2015