Цель статьи заключается в том, чтобы показать какие возникают трудности при использовании Spock с Kotlin, какие есть пути их разрешения и ответить на вопрос, стоит ли использовать Spock, если вы разрабатываете на Kotlin. Подробности под катом.
Я работаю в компании, в которой практикуется экстремальное программирование. Одним из основных приёмов экстремального программирования, который мы используем в повседневной работе, является TDD (test-driven development). Это означает, что прежде чем изменять код, мы пишем тест, который покрывает желаемое изменение. Таким образом, мы регулярно пишем тесты и имеем покрытие кода тестами близкое к 100%. Это предъявляет определенные требования к выбору тестового фрэймворка: одно дело — писать тесты раз в неделю, совсем другое — делать это каждый день.
Мы занимаемся разработкой на Kotlin и в определенный момент в качестве основного фрэймворка мы выбрали Spock. С того момента прошло около 6 месяцев, чувство эйфории и новизны прошло, поэтому статья является своего рода ретроспективой, в которой я попытаюсь рассказать, с какими трудностями мы столкнулись за это время и как мы их разрешали.
В первую очередь нужно разобраться, какие фрэймворки позволяют тестировать Kotlin и какие преимущества дает именно Spock по сравнению с ними.
Одним из достоинств Kotlin является его совместимость с Java, что позволяет использовать для тестирования любые Java фрэймворки, такие как Junit, TestNG, Spock и т.д. В то же время есть фрэймворки разработанные специально для Kotlin такие как Spek и Kotest. Почему мы выбрали имеено Spock?
Я бы выделил следующие достоинства:
Это очень поверхностное сравнение (если это вообще можно назвать сравнением), но это примерно те причины, по которым мы выбрали Spock. Естественно, в последующие недели и месяцы мы столкнулись с некоторыми трудностями.
Тут я сразу оговорюсь, что проблемы, которые описаны ниже, не являются уникальными для Spock, они будут актуальны для любого Java-фрэймворка и связаны они, в основном, с совместимостью с Kotlin.
Проблема
В отличие от Java, все классы Kotlin по умолчанию имеют модификатор
Поэтому если у вас есть сервис:
и вы попытаетесь создать mock этого сервиса в Groovy:
, то вы получите ошибку:
Решение:
Проблема
В Kotlin аргументы функции или конструктора могут иметь значения по умолчанию, которые используются в том случае, если аргумент функции не указан при её вызове. Трудность заключается в том, что байт-код Java теряет значения аргументов по умолчанию и имена параметров функции и поэтому при вызове функции или конструктора Kotlin из Spock требуется явно указывать все значения.
Рассмотрим класс
Для того, чтобы создать экземпляр этого класса в Groovy Spock тесте, придется передать в конструктор значения для всех аргументов:
Решение:
Это может быть довольно узкой областью, но если вы планируете использовать рефлексию в своих тестах, то стоит особенно хорошо подумать, стоит ли использовать Groovy. В нашем проекте мы используем много аннотаций и, чтобы не забыть аннотировать определенные классы или определенные поля аннотациями, мы пишем тесты с использованием рефлексии. Тут-то и возникают трудности.
Проблема
Классы и аннотации написаны на Kotlin, логика, связанная с тестами и рефлексией на Groovy. Из-за этого в тестах получается мешанина из Java Reflection API и Kotlin Reflection:
Конвертация Java классов в Kotlin:
Вызов Kotlin extension-функций как статических методов:
Трудночитаемый код, где будут использоваться типы Kotlin:
Конечно, это не является чем-то непосильным или невозможным. Вопрос только в том, а не проще ли это сделать при помощи Kotlin фрэймворка для тестирования, где можно будет использовать extension-функции и чистый Kotlin Reflection??
Если вы планируете использовать корутины, то это еще один повод задуматься о том, не хотите ли вы выбрать Kotlin фрэймворк для тестирования.
Проблема
Если создать
, то она скомпилируется следующим образом:
, где Continuation — это Java-класс, который инкапсулирует логику по выполнению корутины. Вот тут и возникает проблема, как создать объект этого класса? Также, в Groovy недоступен runBlocking. И поэтому вообще не очень ясно, как тестировать код с корутинами в Spock.
За полгода использования Spock он принес нам больше хорошего, чем плохого и о выборе мы не жалеем: тесты легко читать и поддерживать, писать параметризованные тесты — одно удовольствие. Ложкой дегтя в нашем случае оказалась рефлексия, но она используется всего в нескольких местах, то же самое с корутинами.
При выборе фрэймворка для тестирования проекта, который разрабатывается на Kotlin стоит хорошо подумать, какие функции языка вы планируете использовать. В случае если вы пишете простое CRUD-приложение, Spock будет идеальным решением. Используете корутины и рефлексию? Тогда лучше посмотрите на Spek или Kotest.
Материал не претендует на полноту, поэтому если вы сталкивались с другими трудностями при использовании Spock и Kotlin — пишите в комментариях.
Я работаю в компании, в которой практикуется экстремальное программирование. Одним из основных приёмов экстремального программирования, который мы используем в повседневной работе, является TDD (test-driven development). Это означает, что прежде чем изменять код, мы пишем тест, который покрывает желаемое изменение. Таким образом, мы регулярно пишем тесты и имеем покрытие кода тестами близкое к 100%. Это предъявляет определенные требования к выбору тестового фрэймворка: одно дело — писать тесты раз в неделю, совсем другое — делать это каждый день.
Мы занимаемся разработкой на Kotlin и в определенный момент в качестве основного фрэймворка мы выбрали Spock. С того момента прошло около 6 месяцев, чувство эйфории и новизны прошло, поэтому статья является своего рода ретроспективой, в которой я попытаюсь рассказать, с какими трудностями мы столкнулись за это время и как мы их разрешали.
Почему именно Spock?
В первую очередь нужно разобраться, какие фрэймворки позволяют тестировать Kotlin и какие преимущества дает именно Spock по сравнению с ними.
Одним из достоинств Kotlin является его совместимость с Java, что позволяет использовать для тестирования любые Java фрэймворки, такие как Junit, TestNG, Spock и т.д. В то же время есть фрэймворки разработанные специально для Kotlin такие как Spek и Kotest. Почему мы выбрали имеено Spock?
Я бы выделил следующие достоинства:
- Во-первых, Spock написан на Groovy. Хорошо это или плохо — судите сами. Про Groovy можно почитать статейку вот тут. Лично меня в Groovy привлекает лаконичный синтаксис, наличие динамической типизации, встроенный синтаксис для списков, обычных и ассоциативных массивов, а также совершенно сумасшедшие преобразования типов.
- Во-вторых, Spock уже содержит mock-фрэймворк (MockingApi) и assertion-библиотеку;
- Еще Spock отлично подходит для написания параметризованных тестов:
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 7
0 | 0 | 0
}
Это очень поверхностное сравнение (если это вообще можно назвать сравнением), но это примерно те причины, по которым мы выбрали Spock. Естественно, в последующие недели и месяцы мы столкнулись с некоторыми трудностями.
Проблемы, возникающие при тестировании Kotlin при помощи Spock и их решения
Тут я сразу оговорюсь, что проблемы, которые описаны ниже, не являются уникальными для Spock, они будут актуальны для любого Java-фрэймворка и связаны они, в основном, с совместимостью с Kotlin.
1. final по умолчанию
Проблема
В отличие от Java, все классы Kotlin по умолчанию имеют модификатор
final
, что предотвращает дальнейшее создание потомков и переопределение методов. Проблема тут заключается в том, что при создании mock-объектов какого-либо класса, большинство mock-фрэймворков пытаются создать proxy-объект, который как раз и является потомком исходного класса и переопредяет его методы. Поэтому если у вас есть сервис:
class CustomerService {
fun getCustomer(id: String): Customer {
// Some logic
}
}
и вы попытаетесь создать mock этого сервиса в Groovy:
def customerServiceMock = Mock(CustomerService)
, то вы получите ошибку:
org.spockframework.mock.CannotCreateMockException: Cannot create mock for class CustomerService because Java mocks cannot mock final classes
Решение:
- Первым, что приходит на ум, является использование ключевого слова
open
, которое позволит другим классам наследоваться от данного. Недостатками такого решение будет очевидное «загрязнение» кода, а также предоставление возможности расширять класс, который, возможно, вовсе не предназначен для расширения; - Вторым решением будет использование all-open плагина для компилятора. Этот плагин делает классы и их члены открытыми на этапе компиляции. Этот плагин необходим в том числе для использования Spring Framework и Hibernate, потому что они полагаются на использование cglib(Code Generation Library);
- Также, можно использовать одну из mock-библиотек, разработанных специально для Kotlin (например, mockk). Однако, этот метод нельзя назвать предпочтительным, так как Spock уже содержит свою собственную библиотеку Spock MockingApi.
2. Значения аргументов по умолчанию
Проблема
В Kotlin аргументы функции или конструктора могут иметь значения по умолчанию, которые используются в том случае, если аргумент функции не указан при её вызове. Трудность заключается в том, что байт-код Java теряет значения аргументов по умолчанию и имена параметров функции и поэтому при вызове функции или конструктора Kotlin из Spock требуется явно указывать все значения.
Рассмотрим класс
Customer
, который имеет конструктор с 2-мя обязательными полями email
и name
и опциональными полями, имеющими значения по умолчанию:data class Customer(
val email: String,
val name: String,
val surname: String = "",
val age: Int = 18,
val identifiers: List<NationalIdentifier> = emptyList(),
val addresses: List<Address> = emptyList(),
val paymentInfo: PaymentInfo? = null
)
Для того, чтобы создать экземпляр этого класса в Groovy Spock тесте, придется передать в конструктор значения для всех аргументов:
new Customer("john.doe@gmail.com", "John", "", 18, [], [], null)
Решение:
- Первое решение проблемы — использование аннотации
@JvmOverloads
, которая создаст перегруженные конструкторы или перегруженные функции. - Также, можно использовать отдельный groovy класс — фабрику объектов, чтобы делегировать логику по созданию и популяции объектов:
class ModelFactory {
static def getCustomer() {
new Customer(
"john.doe@gmail.com",
"John",
"Doe",
18,
[nationalIdentifier],
[address],
paymentInfo
)
}
static def getAddress() { new Address(/* arguments */) }
static def getNationalIdentifier() { new NationalIdentifier(/* arguments */) }
static def getPaymentInfo() { new PaymentInfo(/* arguments */) }
}
3. Java Reflection vs Kotlin Reflection
Это может быть довольно узкой областью, но если вы планируете использовать рефлексию в своих тестах, то стоит особенно хорошо подумать, стоит ли использовать Groovy. В нашем проекте мы используем много аннотаций и, чтобы не забыть аннотировать определенные классы или определенные поля аннотациями, мы пишем тесты с использованием рефлексии. Тут-то и возникают трудности.
Проблема
Классы и аннотации написаны на Kotlin, логика, связанная с тестами и рефлексией на Groovy. Из-за этого в тестах получается мешанина из Java Reflection API и Kotlin Reflection:
Конвертация Java классов в Kotlin:
def kotlinClass = new KClassImpl(clazz)
Вызов Kotlin extension-функций как статических методов:
ReflectJvmMapping.getJavaType(property.returnType)
Трудночитаемый код, где будут использоваться типы Kotlin:
private static boolean isOfCollectionType(KProperty1<Object, ?> property) {
// Some logic
}
Конечно, это не является чем-то непосильным или невозможным. Вопрос только в том, а не проще ли это сделать при помощи Kotlin фрэймворка для тестирования, где можно будет использовать extension-функции и чистый Kotlin Reflection??
4. Coroutines
Если вы планируете использовать корутины, то это еще один повод задуматься о том, не хотите ли вы выбрать Kotlin фрэймворк для тестирования.
Проблема
Если создать
suspend
функцию:suspend fun someSuspendFun() {
// Some logic
}
, то она скомпилируется следующим образом:
public void someSuspendFun(Continuation<? super Unit> $completion) {
// Some logic
}
, где Continuation — это Java-класс, который инкапсулирует логику по выполнению корутины. Вот тут и возникает проблема, как создать объект этого класса? Также, в Groovy недоступен runBlocking. И поэтому вообще не очень ясно, как тестировать код с корутинами в Spock.
Итоги
За полгода использования Spock он принес нам больше хорошего, чем плохого и о выборе мы не жалеем: тесты легко читать и поддерживать, писать параметризованные тесты — одно удовольствие. Ложкой дегтя в нашем случае оказалась рефлексия, но она используется всего в нескольких местах, то же самое с корутинами.
При выборе фрэймворка для тестирования проекта, который разрабатывается на Kotlin стоит хорошо подумать, какие функции языка вы планируете использовать. В случае если вы пишете простое CRUD-приложение, Spock будет идеальным решением. Используете корутины и рефлексию? Тогда лучше посмотрите на Spek или Kotest.
Материал не претендует на полноту, поэтому если вы сталкивались с другими трудностями при использовании Spock и Kotlin — пишите в комментариях.
nshipyakov
Спасибо за статью. Я правильно понимаю, что для написания тестов используется груви?
roma0297 Автор
Да, Spock — это Groovy фрэймворк