Можно ли имитировать/переопределять зависимости/импорт в Scala?

У меня есть код, который выглядит так:

package org.samidarko.actors

import org.samidarko.helpers.Lib

class Monitoring extends Actor {

  override def receive: Receive = {
    case Tick =>
       Lib.sendNotification()
    }
}

Есть ли способ макет/заглушка Lib из ScalaTest, как с proxyquire для nodejs?

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

Моя единственная альтернатива - передать мою библиотеку в качестве параметра класса?

class Monitoring(lib: Lib) extends Actor {

Любые советы, чтобы сделать его более тестируемым? Спасибо

ИЗМЕНИТЬ:

Ответ Xavier Guihot представляет собой интересный подход к проблеме, но я решил изменить код для целей тестирования.

Я передаю Lib как параметр и издеваюсь над mockito, это упрощает тестирование кода и поддерживать, чем затенение масштаба.


person samidarko    schedule 01.03.2018    source источник


Ответы (2)


Этот ответ использует только scalatest и не влияет на исходный код:

Базовое решение:

Допустим, у вас есть этот класс src (тот, который вы хотите протестировать и для которого вы хотите имитировать зависимость):

package com.my.code

import com.lib.LibHelper

class MyClass() {
  def myFunction(): String = LibHelper.help()
}

и эта зависимость библиотеки (которую вы хотите имитировать/переопределить при тестировании MyClass):

package com.lib

object LibHelper {
  def help(): String = "hello world"
}

Идея состоит в том, чтобы создать класс в вашей тестовой папке, который будет переопределять/затенять библиотеку. Класс будет иметь то же имя и тот же пакет, что и тот, который вы хотите имитировать. В src/test/scala/com/external/lib вы можете создать LibHelper.scala, который содержит этот код:

package com.lib

object LibHelper {
  def help(): String = "hello world - overriden"
}

И таким образом вы можете протестировать свой код обычным способом:

package com.my.code

import org.scalatest.FunSuite

class MyClassTest extends FunSuite {
  test("my_test") {
    assert(new MyClass().myFunction() === "hello world - overriden")
  }
}

Улучшенное решение, которое позволяет настраивать поведение макета для каждого теста:

Предыдущий код ясен и прост, но имитируемое поведение LibHelper одинаково для всех тестов. И может потребоваться, чтобы метод LibHelper производил разные результаты. Таким образом, мы можем рассмотреть возможность установки изменяемой переменной в LibHelper и обновления переменной перед каждым тестом, чтобы установить желаемое поведение LibHelper. (Это работает, только если LibHelper является объектом)

Затеняющий LibHelper (тот, что находится в src/test/scala/com/external/lib) следует заменить на:

package com.lib

object LibHelper {

  var testName = "test_1"

  def help(): String =
    testName match {
      case "test_1" => "hello world - overriden - test 1"
      case "test_2" => "hello world - overriden - test 2"
    }
}

И самым маленьким классом должен стать:

package com.my.code

import com.lib.LibHelper

import org.scalatest.FunSuite

class MyClassTest extends FunSuite {
  test("test_1") {
    LibHelper.testName = "test_1"
    assert(new MyClass().myFunction() === "hello world - overriden - test 1")
  }
  test("test_2") {
    LibHelper.testName = "test_2"
    assert(new MyClass().myFunction() === "hello world - overriden - test 2")
  }
}

Очень важная точность, так как мы используем глобальную переменную, обязательно заставить scalatest запускать тест последовательно (не параллельно). Связанный параметр scalatest (который должен быть включен в build.sbt):

parallelExecution in Test := false
person Xavier Guihot    schedule 01.03.2018
comment
Хороший. Я пытался скрыть LibHelper, но создал объект в тестовой области перед созданием экземпляра класса. Есть ли способ заглушить результаты LibHelper, чтобы сделать ваше решение более гибким? - person samidarko; 01.03.2018
comment
Вы показали мне, как я могу затенить область действия, заменив и Object другим объектом. Мне было интересно, можно ли заглушить / смоделировать методы, чтобы сделать их более динамичными, например, давать разные ответы от одного теста к другому? - person samidarko; 01.03.2018
comment
@samidarko Обновил ответ; надеюсь это поможет. - person Xavier Guihot; 02.03.2018
comment
Интересное решение. Спасибо - person samidarko; 02.03.2018
comment
@XavierGuihot Как убедиться, что тест вызывает затененный объект, а не исходный? разве это не зависит от загрузчика классов? - person Assaf Mendelson; 08.08.2018
comment
Я думаю, что при выполнении тестов поведение по умолчанию состоит в том, чтобы сначала искать класс в источниках тестов, а затем искать в источниках проекта и внешних зависимостях. Я никогда не был свидетелем обратного. - person Xavier Guihot; 08.08.2018

Не полный ответ (поскольку я не очень хорошо знаю АОП), но чтобы направить вас в правильном направлении, это возможно с помощью библиотеки Java под названием AspectJ:

https://blog.jayway.com/2007/02/16/static-mock-using-aspectj/

https://www.cakesolutions.net/teamblogs/2013/08/07/aspectj-with-akka-scala

Пример в псевдокоде (не вдаваясь в подробности):

class Mock extends MockAspect {
    @Pointcut("execution (* org.samidarko.helpers.Lib.sendNotification(..))")
    def intercept() {...}

}

Низкоуровневые основы этого подхода — динамические прокси: https://dzone.com/articles/java-dynamic-proxy. Однако вы также можете имитировать статические методы (возможно, вам придется добавить слово static в шаблон).

person dk14    schedule 01.03.2018