
Салют, коллеги.
В рамках пятничной статьи предлагаю посмотреть на интересный способ создания моков в 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>" }
