Рубрики
Без рубрики

Тесты базы данных в памяти с помощью Querydsl

Сегодня я пишу об абстрагировании доступа к базе данных с помощью Querydsl таким образом, чтобы облегчить тестирование. С тегами java, программирование, тестирование, обучение.

Написание тестов – важный навык инженера-программиста. Раньше я писал много очень сфокусированных, узких модульных тестов. Тем не менее, я часто обнаруживал, что такие тесты препятствуют рефакторингу и едва помогают в обнаружении регрессий. Были ли такие проблемы вызваны моим неправильным выбором дизайна или они присущи модульным тестам, не является предметом внимания этого поста. Однако дело в том, что в настоящее время я склонен писать более детализированные тесты в стиле интеграции. У такого подхода есть один недостаток: скорость. Например, использование гибернации с полноценной базой данных происходит относительно медленно по сравнению с использованием поддельной реализации репозитория. Сегодня я пишу об абстрагировании доступа к базе данных с помощью Querydsl таким образом, чтобы облегчить тестирование.

Querydsl – это набор библиотек, который, как следует из названия, предоставляет строго типизированный язык, специфичный для конкретной предметной области, для выполнения запросов. Querydsl поддерживает множество технологий доступа к данным, например JDBC, Hibernate, JDO. Следующий пример в Kotlin иллюстрирует, как DSL, созданный на основе класса сущностей, может использоваться для поиска некоторых сущностей через интерфейс JPA:

val queryFactory: JPAQueryFactory = ...
val userEmailToSearch = "alamakota@gmail.com"
val user = queryFactory.query()
    .from(QUser.user)
    .where(QUser.user.email.eq(userEmailToSearch))
    .select(QUser.user)
    .fetchOne()

Одним из важных доступных вариантов является модуль Collections , который предлагает интеграцию с коллекциями POJO и компонентами. Следующий пример в Kotlin показывает, как можно запросить список пользователей:

val users = listOf(userAlan, userBob, userAlice)
val user = CollQuery()
    .from(QUser.user, users)
    .where(QUser.user.email.eq(userEmailToSearch))
    .select(QUser.user)
    .fetchOne()

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

interface EntityQueries {
    fun , TEntity : Any> findFirst(
        qEntity: TQEntity, 
        where: (TQEntity) -> Predicate? = { null }, 
        orderBy: ((TQEntity) -> OrderSpecifier<*>?) = { null }): TEntity? 
}

val queries:EntityQueries = ....

val ala = queries.findFirst(QUser.user, where = { it.email.eq("ala@gmail.com") })
val latestUser = queries.findFirst(QUser.user, orderBy = { it.created.desc() })

Приведенный выше интерфейс позволяет нам более кратко выражать часто используемые запросы.

С помощью Querydsl достаточно легко реализовать интерфейс Entity Queries . Во первых производственная реализация делегирующая JPA фактическую технологию доступа к данным:

class QueryDslDomainQueryFactory(private val queryFactory: JPAQueryFactory) : EntityQueries {
    override fun , TEntity : Any> findFirst(qEntity: TQEntity, where: (TQEntity) -> Predicate?, orderBy: (TQEntity) -> OrderSpecifier<*>?): TEntity? {
        return queryFactory.query()
            .from(qEntity)
            .where(where(qEntity))
            .apply { orderBy(qEntity)?.let { this.orderBy(it) }  }
            .select(qEntity)
            .fetchFirst()
    }
}

Вышесказанное позволяет нам использовать интерфейс Entity Queries вместо JPA, например, в контроллерах Spring, например:

@RestController
class UsersController(private val queries: EntityQueries) {
    @GetMapping("/users")
    fun getByEmail(@RequestParam email: String) = queries.findFirst(QUser.user, where = { it.email.eq(email) })
}

Одним из рекомендуемых Spring способов абстрагирования специфики технологии запросов является использование интерфейсов репозитория например,:

interface UserRepository : Repository {
  fun findByEmail(String email): User?
}

Такой интерфейс был бы волшебным образом реализован Spring runtime и помещен в контекст приложения. На первый взгляд этот подход может показаться привлекательным, поскольку нам не нужно реализовывать интерфейс. Однако существует множество проблем:

  • требуется контекст приложения, который, в свою очередь, медленно загружается
  • нет никаких проверок во время компиляции
  • рефакторинг сложнее без специальной поддержки со стороны IDE
  • фактическое поведение трудно понять без тщательной лекции по документации (что произойдет, если, например, есть несколько пользователей с одним и тем же адресом электронной почты?)

Вызов Entity Queries для поиска пользователей по электронной почте почти так же удобочитаем, как findByEmail но не страдает ни от одного из недостатков, перечисленных выше. Инкапсуляция более сложной логики фильтрации может быть выполнена с помощью простого метода расширения или более сложного Шаблона спецификации .

Мы можем использовать помощники Spring test для упрощения написания тестов, включающих контекст приложения, который позволяет нам вводить например UsersController экземпляр для вызова его методов. Однако такие тесты выполняются сравнительно медленно и, следовательно, приводят к тому, что цикл обратной связи работает намного медленнее. К счастью, абстракцию Entity Queries очень легко реализовать с помощью POJO in-memory collections.

class InMemoryEntityQueries : QueriesBase(), EntityQueries {
    val entities = mutableMapOf, MutableList<*>>()

    override fun , TEntity : Any> findFirst(qEntity: TQEntity, where: (TQEntity) -> Predicate?, orderBy: (TQEntity) -> OrderSpecifier<*>?): TEntity? {
        val entities = entities.getOrPut(qEntity.type, { mutableListOf() }) as List
        return CollQuery()
            .from(qEntity, entities)
            .where(where(qEntity))
            .apply { orderBy(qEntity)?.let { this.orderBy(it) } }
            .select(qEntity)
            .fetchFirst()
    }
}

Приведенная выше реализация выглядит почти точно так же, как и производственная. Конечно, мы можем попытаться извлечь общий код, чтобы сделать вещи более СУХИМИ. Однако наиболее важным наблюдением является то, что мы делегируем реализации Querydsl важную логику фильтрации и упорядочения. Это может повысить нашу уверенность в том, что поддельная реализация ведет себя так же, как и производственная, с той лишь разницей, что фактическое хранилище сущностей.

Учитывая приведенную выше реализацию, теперь мы можем легко заменить зависимость UsersController и создать ее как обычный POJO:

class UsersControllerTests {
    val db = InMemoryEntityQueries()
    val controller = UsersController(db)

    @Test
    fun canFindByEmail(){
        db.entities[User::class.java] =  listOf(User(email = "ala@gmail.com"), User(email = "ola@gmail.com"))

        controller.getByEmail("ola@gmail.com").email.shouldEqual("ola@gmail.com")
        controller.getByEmail("peter@gmail.com").shouldEqual(null)
    }
}

Приведенный выше интерфейс EntityQueries , очевидно, является упрощенной версией. Самая важная недостающая часть – это возможность сохранять объекты. Тем не менее, это несложно реализовать, учитывая реализацию в памяти. Мы можем, например, использовать тот факт, что все наши объекты помечены аннотациями сохранения JPA, чтобы найти поле, помеченное @Id , сгенерируйте идентификатор и назначьте его на основе содержимого переменной entities . Другой подход заключается в том, чтобы пометить все объекты выделенным интерфейсом напр.

interface HasId {
    var id: TId
}

Объект, реализующий HasId , может быть проверен в методе save реализации в памяти и присвоен уникальный идентификатор, например:

fun > save(entity: TEntity) {
    val entities = entities.getOrPut(entity.javaClass, { mutableListOf() }) as List
    if(entity.id == null){
        entity.id = (entities.map { it.id }.max() ?: 0) + 1
    }
    entities += entity
}

Следуя описанному выше подходу, мы можем легко добавить недостающие операции, например, для удаления объекта, и это, в свою очередь, позволяет нам писать еще больше тестов, которые выполняются полностью в памяти. Стоит отметить, что использование реализации базы данных в памяти лучше всего подходит для запросов, которые извлекают, сохраняют или обновляют один или несколько экземпляров. Как только нам нужно использовать функции, естественные для технологии базы данных, например соединения в SQL, нам лучше подключиться к реальной базе данных. В то время как модуль Querydsl collections поддерживает как операции объединения, так и группирования, реализация в памяти часто не эквивалентна реализации в базе данных, особенно в отношении обработки значений null .

Первоначально опубликовано по адресу brightinventions.pl

Автор: Петр Минковский, Инженер-программист @ Яркие Изобретения Электронная почта Stackoverflow Личный блог

Оригинал: “https://dev.to/brightdevs/in-memory-database-tests-with-querydsl-1mij”