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

image-loader.svg

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

В рамках пятничной статьи предлагаю посмотреть на интересный способ создания моков в 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  proxy() = Proxy.newProxyInstance(T::class.java.classLoader, arrayOf(T::class.java), { _, _, _ -> null }) as T

мы получаем возможность писать так 

val user1 = proxy()
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() {
    override fun getDisplayName() = "John Doe"
    override fun getEmailAddress() = "jdoe@example.com"
    override fun toString() = "$displayName <$emailAddress>"
}

© Habrahabr.ru