Scala - напишите модульные тесты для объектов/синглетонов, которые расширяют черту/класс с подключением к БД

Вопрос, связанный с модульным тестом

Обнаружена проблема с тестированием объектов scala, которые расширяют другой трейт/класс, имеющий соединение с БД (или любой другой «внешний» вызов).

Использование синглтона с подключением к БД в любом месте моего проекта делает модульный тест невозможным, потому что я не могу переопределить/издеваться над подключением к БД

Это приводит к изменению моего дизайна только для целей тестирования в ситуациях, когда он явно должен быть объектом.

Какие-либо предложения ?

Фрагмент кода для нетестируемого кода:

object How2TestThis extends SomeDBconnection {

  val somethingUsingDB = {
    getStuff.map(//some logic)
  }

  val moreThigs {
    //more things
  }

}

trait SomeDBconnection {
  import DBstuff._
  val db = connection(someDB)  
  val getStuff = db.getThings
}

person Nimrod007    schedule 23.02.2014    source источник


Ответы (1)


  1. Один из вариантов - использовать шаблон торта, чтобы потребовать некоторого подключения к БД и конкретной реализации миксина по желанию. Например:

    import java.sql.Connection
    
    // Defines general DB connection interface for your application
    trait DbConnection {
      def getConnection: Connection
    }
    
    // Concrete implementation for production/dev environment for example
    trait ProductionDbConnectionImpl extends DbConnection {
      def getConnection: Connection = ???
    }
    
    // Common code that uses that DB connection and needs to be tested.
    trait DbConsumer {
      this: DbConnection =>
    
      def runDb(sql: String): Unit = {
        getConnection.prepareStatement(sql).execute()
      }
    }
    
    ...
    
    // Somewhere in production code when you set everything up in init or main you
    // pick concrete db provider
    val prodDbConsumer = new DbConsumer with ProductionDbConnectionImpl
    prodDbConsumer.runDb("select * from sometable")
    
    ...
    
    // Somewhere in test code you mock or stub DB connection ...
    val testDbConsumer = new DbConsumer with DbConnection { def getConnection = ??? }
    testDbConsumer.runDb("select * from sometable")
    

    Если вам нужно использовать singleton/Scala object, у вас может быть lazy val или какой-либо init(): Unit метод, который устанавливает соединение.

  2. Другим подходом было бы использование какого-либо инжектора. Например, посмотрите на код лифта:

    package net.liftweb.http
    
    /**
     * A base trait for a Factory.  A Factory is both an Injector and
     * a collection of FactorMaker instances.  The FactoryMaker instances auto-register
     * with the Injector.  This provides both concrete Maker/Vender functionality as
     * well as Injector functionality.
     */
    trait Factory extends SimpleInjector
    

    Затем где-то в вашем коде вы используете этого поставщика следующим образом:

    val identifier = new FactoryMaker[MongoIdentifier](DefaultMongoIdentifier) {}
    

    И затем в местах, где вам действительно нужно получить доступ к БД:

    identifier.vend
    

    Вы можете указать альтернативного провайдера в тестах, окружив свой код:

    identifier.doWith(mongoId) { <your test code> }
    

    который можно удобно использовать с контекстом spec2 Around, например:

    implicit val dbContext new Around {
      def around[T: AsResult](t: => T): Result = {
        val mongoId = new MongoIdentifier {
          def jndiName: String = dbName
        }
        identifier.doWith(mongoId) {
          AsResult(t)
        }
      }
    }
    

    Это довольно круто, потому что реализовано на Scala без каких-либо специальных байт-кодов или хаков JVM.

  3. Если вы считаете, что первые 2 варианта слишком сложны, и у вас есть небольшое приложение, вы можете использовать аргументы Properties file/cmd, чтобы узнать, работаете ли вы в тестовом или производственном режиме. И снова идея исходит от Лифта :). Вы можете легко реализовать это самостоятельно, но вот как это сделать с помощью Lift Props:

    // your generic DB code:
    val jdbcUrl: String = Props.get("jdbc.url", "jdbc:postgresql:database")
    

    У вас может быть 2 файла реквизита:

    • production.default.props

      jdbc.url=jdbc:postgresql:database

    • test.default.props

      jdbc.url=jdbc:h2

    Lift автоматически обнаружит режим запуска Props.mode и выберет правильный файл реквизита для чтения. Вы можете установить режим запуска с помощью аргументов JVM cmd.

    Таким образом, в этом случае вы можете либо подключиться к БД в памяти, либо просто прочитать режим запуска и соответствующим образом установить соединение в коде (макет, заглушка, неинициализированный и т. д.).

  4. Используйте обычный шаблон IOC - передайте зависимости через аргументы конструктора в класс. Не используйте object. Это быстро становится неудобным, если только вы не используете специальные фреймворки внедрения зависимостей.

Некоторые предложения:

Используйте object для чего-то, что не может иметь альтернативной реализации, и если только эта реализация будет работать во всех средах. Используйте object для констант и чистого кода FP без побочных эффектов. Используйте синглтоны для связывания вещей в последний момент — например, класс с main, а не где-то глубоко в коде, где от него зависят многие компоненты, если только он не имеет побочных эффектов или не использует что-то вроде стекируемых/вводимых поставщиков поставщиков (см. Лифт).

Вывод:

Вы не можете издеваться над объектом или переопределять его реализацию. Вам нужно спроектировать свой код так, чтобы его можно было тестировать, и некоторые из его параметров перечислены выше. Хорошей практикой является сделать ваш код гибким с помощью легко компонуемых частей не только для целей тестирования, но и для повторного использования и удобства сопровождения.

person yǝsʞǝla    schedule 23.02.2014
comment
Связанный вопрос: stackoverflow.com/questions/21969225/ - person yǝsʞǝla; 23.02.2014
comment
проблема в том, что мне не нужно какое-либо соединение с БД (не тестировать, а не h2) для моего модульного тестирования, я хочу смоделировать вызов БД и вернуть результат json, например, это решение является хорошим 1 для интеграционных тестов, но не для модульного тестирования методов - person Nimrod007; 25.02.2014
comment
Первые 2 варианта позволяют вам издеваться над db/connection в тестах. Если вы используете шаблон Cake, вы просто смешиваете фиктивную базу данных, а если вы используете какой-то инжектор, вы можете вводить любую фиктивную базу данных. - person yǝsʞǝla; 25.02.2014