Салют, коллеги.
В рамках пятничной статьи предлагаю посмотреть на интересный способ создания моков в Kotlin, без использования сторонних библиотек.
Я занимаюсь разработкой аддонов для Atlassian-стека в компании Stiltsoft и, из-за технических ограничений, до сих пор (да в 2021 году и, скорее всего, в ближайшие пару лет) вынужден использовать Java 8. Но, чтоб не отставать от прогрессивного человечества, внутри компании мы пробуем Kotlin, пишем на нем тесты и разные экспериментальные продукты.
Однако, вернемся к тестам. Часто у нас есть интерфейс из предметной области, нам не принадлежащий, но который активно используется нашим кодом. Причем у самого интерфейса много разных методов, но в каждом сценарии используем их буквально по паре штук. Например, интерфейс ApplicationUser.
public interface ApplicationUser {
String getKey();
String getUsername();
String getEmailAddress();
String getDisplayName();
long getDirectoryId();
boolean isActive();
}
В разных тестах нам нужен объект типа ApplicationUser с разным набором предустановленных полей, где-то надо displayName и emailAddress, где-то только username и так далее.
В общем случае, нам нужен способ "на лету" создавать объекты, реализующие определенный интерфейс, с возможностью произвольного переопределения методов этого объекта.
Самое простое решение - анонимные классы.
ApplicationUser user = new ApplicationUser() {
@Override
public String getDisplayName() {
return "John Doe";
}
@Override
public String getEmailAddress() {
return "jdoe@example.com";
}
@Override
public String toString() {
return getDisplayName() + " <" + getEmailAddress() + ">";
}
@Override
public String getKey() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public long getDirectoryId() {
return 0;
}
@Override
public boolean isActive() {
return false;
}
};
Очевидный недостаток простого решения, совершенно безумное количество строк. Можно немного схитрить и написать абстрактный класс дефолтно реализующий все методы
public abstract class AbstractApplicationUser implements ApplicationUser {
@Override
public String getKey() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public long getDirectoryId() {
return 0;
}
@Override
public boolean isActive() {
return false;
}
@Override
public String getEmailAddress() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
}
и потом использовать его.
ApplicationUser user = new AbstractApplicationUser() {
@Override
public String getDisplayName() {
return "John Doe";
}
@Override
public String getEmailAddress() {
return "jdoe@example.com";
}
@Override
public String toString() {
return getDisplayName() + " <" + getEmailAddress() + ">";
}
};
Это улучшит ситуацию со строками, но класс-обертку придется написать на каждую сущность такого плана.
Более продвинутый вариант - использовать специализированную библиотеку.
ApplicationUser user = mock(ApplicationUser.class);
when(user.getDisplayName()).thenReturn("John Doe");
when(user.getEmailAddress()).thenReturn("jdoe@example.com");
String toString = user.getDisplayName() + " <" + user.getEmailAddress() + ">";
when(user.toString()).thenReturn(toString);
C количеством строк тут уже порядок, но код стал более "тяжелым" для восприятия и, на мой вкус, не очень красивым.
Я предлагаю альтернативный план: собрать решение из существующих фич Kotlin. Но сначала, небольшое теоретическое отступление про делегаты.
Один из юзкейсов делегирования, навесить какой-то дополнительный функционал на "чужой" объект, причем незаметным для конечного пользователя способом.
Например, мы отдаем объект ApplicationUser`a наружу, но хотим отправлять какое-то событие, каждый раз как у него вызовут метод getEmailAddress(). Для этого делаем свой объект, реализующий интерфейс ApplicationUser
public class EventApplicationUser implements ApplicationUser {
private ApplicationUser delegate;
public EventApplicationUser(ApplicationUser delegate) {
this.delegate = delegate;
}
@Override
public String getEmailAddress() {
System.out.println("send event");
return delegate.getEmailAddress();
}
@Override
public String getDisplayName() {
return delegate.getDisplayName();
}
@Override
public String getKey() {
return delegate.getKey();
}
@Override
public String getUsername() {
return delegate.getUsername();
}
@Override
public long getDirectoryId() {
return delegate.getDirectoryId();
}
@Override
public boolean isActive() {
return delegate.isActive();
}
}
Используется такая конструкция следующим образом
public ApplicationUser method() {
ApplicationUser user = getUser();
return new EventApplicationUser(user);
}
Так вот, в Kotlin есть встроенная поддержка для такого использования делегата. И вместо простыни кода в стиле
@Override
public String someMethod() {
return delegate.someMethod();
}
Можно сделать так
class EventApplicationUser(private val user: ApplicationUser) : ApplicationUser by user {
override fun getEmailAddress(): String {
println("send event")
return user.emailAddress
}
}
И этот код будет работать точно так же как и его джавовский собрат. Важный момент, синтаксис делегирования работает и для анонимных классов, т.е. можно делать вот так, без предварительной подготовки классов-оберток
val user = object : ApplicationUser by originalUser {
override fun getEmailAddress(): String {
println("send event")
return originalUser.emailAddress
}
}
Теперь надо лишь как-то подготовить объект originalUser, реализующий дефолтное поведение. Тут нам пригодится возможность создать динамический прокси.
Написав простую инлайн функцию
inline fun <reified T> proxy() = Proxy.newProxyInstance(T::class.java.classLoader, arrayOf(T::class.java), { _, _, _ -> null }) as T
мы получаем возможность писать так
val user1 = proxy<ApplicationUser>()
val user2: ApplicationUser = proxy()
Обе строки делают одно и то же, создают динамический прокси для интерфейса ApplicationUser.
Разница, чисто синтаксическая, в первом случае мы явно параметризуем нашу функцию proxy() и компилятор понимает, что результат будет типа ApplicationUser, во втором случае мы откровенно говорим, что хотим переменную типа ApplicationUser и компилятор понимает чем надо параметризовать функцию proxy().
Остается только свести все вместе
val user = object : ApplicationUser by proxy() {
override fun getDisplayName() = "John Doe"
override fun getEmailAddress() = "jdoe@example.com"
override fun toString() = "$displayName <$emailAddress>"
}
Здесь мы создаем анонимный объект с интерфейсом ApplicationUser, тут же все методы делегируем в свежесозданный мок и переопределяем только нужное, без всяких оберток/заготовок под каждую сущность, естественным образом.
p. s. Идеально, конечно было бы снять ограничение на интерфейсы и разрешить делать что-то в таком духе, но тут уже нужна поддержка со стороны компилятора
val user = proxy<ApplicationUser>() {
override fun getDisplayName() = "John Doe"
override fun getEmailAddress() = "jdoe@example.com"
override fun toString() = "$displayName <$emailAddress>"
}
Borz
а можно просто использовать mockk:
или вообще иерархию замокировать: https://mockk.io/#hierarchical-mocking
UPD: и обратите ещё внимание на kotest и faker
dimskiy
Автор предлагает не использовать моковый фреймворк, а вы предлагаете использовать другой моковый фреймворк, еще более многословный и поощряющий говнокод (тест статики, например)
Borz
предлагаю в Kotlin использовать mockk вместо mockito. И ни слова не написал что подход автора плохой - просто у автора свой путь. Когда понадобится не просто замокировать объект, а сделать ещё множество всего, тогда и будет видна разница предлагаемого решения или использования "многословного" фреймворка.
статику тоже иногда надо замокировать - когда вы тестируете код выше её. Тот же "object" вполне себе статика на класс но при этом, вполне себе законная конструкция в Kotlin
ЗЫ: на примерах уровня HelloWorld любое использование фреймворков кажется избыточным
dimskiy
Я подчеркнул только бессмысленность вашего примера. Очевидно, что фреймворк доя тестов удобнее чем его отсутствие. Что касается статики - имхо, если для тестов вам приходится ее мокать, то почти наверняка вы сделали что-то не так
Borz
смысл в примере в том, что как аргументация для статьи была озвучена фраза `код стал более "тяжелым" для восприятия` при использовании сторонней библиотеки, которая изначально не была создана под Kotlin
e_Hector Автор
Да, но в таком варианте растет количество "не естественного" синтаксиса, имею в виду конструкции библиотеки
every
,returns
вместо "родных" языковыхoverride
Плюс, сходу не понятно, как сделать собственное поведение для мокируемого метода?
Например, как будет выглядет вот такой мок, но сделанный на
mockk
p.s. Вариант
moсkk
выглядит лучше, чемmockito
, но концептуальные претензии такие жеBorz
как в вашем подходе проверить, что метод, к примеру isFork, был вызван не более одного раза?
loltrol
Очевидно же что писать свой велосипед с еще более весёлыми конструкциями. С вероятностью 99% при переходе проекта с уровня hello world на уровень решения прикладных задач выйдет или каша, или решение ничем не "уступающее" в концептуальном плане mokk.
Иногда люди забывают, что в большинстве случаев платят за успешное выполнение бизнес задачи, а не за красивые идейные синтаксические конструкции. Ну а если бизнес позволяет выкатить свой велик - ну ладно, отлично же, можно и побаловаться.
e_Hector Автор
Никак :)
На всякий случай, я не предлагаю выкинуть Mockito и ему подобные инструменты и заменить на подход из статьи.
Я за то, чтобы не тащить к себе хоть и крутой, но избыточный инструмент.