Нужен ли Mockito, если у вас Kotlin?

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Салют, коллеги.

В рамках пятничной статьи предлагаю посмотреть на интересный способ создания моков в 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>"
}
Источник: https://habr.com/ru/post/590379/


Интересные статьи

Интересные статьи

Мы в Dropbox считаем, что управление инцидентами — это центральный элемент нашей системы по обеспечению надёжности. И хотя мы также используем проактивные методы, такие к...
Привет! Это ликбез про то, как, имея обычные навыки Win-пользователя, создать удалённый рабочий стол на Windows. Сразу скажу, что большей части аудитории Хабра это покажется детс...
Совсем недавно вышла статья про новый инструмент Playwright, одним из авторов которого является Андрей Лушников. Нам выпала возможность пообщаться с Андреем и задать все вопросы, которые внимател...
Элита программисты или нет, достойны они сверхвысоких зарплат и заслуживают ли особого отношения — вопрос спорный. Очевидно только то, что программист — профессия немасштабируемая, поэтому сравне...
Глава Tesla Илон Маск на днях разослал сотрудникам электронное сообщение, в котором говорилось о том, что если не урезать расходы компании, то через 10 месяцев у Tesla Inc. закончатся деньги....