Легкий способ проверить неприятные вызовы статических методов в Kotlin

1656682696 legkij sposob proverit nepriyatnye vyzovy staticheskih metodov v kotlin

Алексей Федоров

Позвольте мне предположить… Вы наткнулись на какой-то код в Kotlin, использующий какую-то стороннюю библиотеку. API, предоставляемый библиотекой, — это один или несколько статических методов. И вы хотите протестировать какой-нибудь код с помощью этих статических методов. Это больно.

Вы не уверены, как подойти к этой проблеме.

Возможно, вы задаете себе вопрос: «Когда авторы посторонних библиотек прекратят использовать статические методы?»

В любом случае, кто я таков, чтобы рассказывать вам, как тестировать вызовы статических методов в Kotlin?

Последние пять лет я фанатик тестирования и евангелист разработки, управляемый тестированием – они не зря называют меня сотрудником TDD. В момент написания этого я работал с Kotlin в производстве около двух лет.

Вперед!

Вот что я чувствую, когда вижу такие ужасные API:

VqR1mKMGNy9Q80UjC7KRGAB3I4WVgDA428t6
(источник: pexels.com)

Позвольте мне показать вам, что я имею в виду, на грубом примере, с которым недавно имел дело. Библиотека была а newrelic клиент. Чтобы использовать его, мне пришлось вызвать статический метод в каком-нибудь классе. В упрощенном виде это выглядит примерно так:

NewRelicClient.addAttributesToCurrentRequest(“orderId”, order.id)

Мне нужно было изменить, что мы присылаем, и мне пришлось добавить больше атрибутов. Поскольку я хотел быть уверен, что мое изменение ничего не нарушает и делает именно то, что я хочу, мне нужно было написать тест. Для этого кода еще не было теста.

Если вы все еще читаете, я предполагаю, что вы в той же ситуации. Или вы были в прошлом.

Я согласен, что это болезненная ситуация.

Как я должен высмеивать эти вызовы в тесте?

Я знаю, это удручает, что большинство имитационных библиотек не могут имитировать вызовы статических методов. И даже работающие на Java не всегда работают на Kotlin.

Есть библиотеки, которые могут это сделать, например powermock, например. Но знаешь, что? Возможно, вы уже используете mockito или какая-нибудь другая библиотека. Добавление еще одного инструмента насмешки в проект сделает все еще более запутанным и разочаровывающим.

Я знаю, как это раздражает несколько инструментов для одной работы в одной кодовой базе. Это вызывает большую путаницу для всех.

Что ж, эта проблема уже была решена около двух десятилетий назад!

заинтересованы? Приходите прокатиться.

Рефакторинг к Humble Object

Давайте посмотрим на код, с которым мы здесь работаем:

class FulfilOrderService {

    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        NewRelicClient.addAttributesToCurrentRequest(
                "orderId", order.id)
        NewRelicClient.addAttributesToCurrentRequest(
                "orderAmount", order.amount.toString())
                
    }
    
}

Он выполняет различные действия с приказом, чтобы его выполнить, а затем назначает несколько атрибутов текущему запросу newrelic.

Первое, что мы здесь сделаем вместе, это вытащим метод addAttributesToRequest. Мы также хотим параметризировать его с помощью key и value аргументы. Вы можете сделать это вручную или, если вам удалось использовать IntelliJ IDEA, вы можете сделать такой рефакторинг автоматически.

Во как:

  1. Выберите ”orderId” и извлечь локальную переменную. Назовите это key.
  2. Выберите order.id и извлечь локальную переменную. Назовите это value.
  3. Выберите NewRelicClient.addAttributesToCurrentRequest(key, value) и извлечь метод. Назовите это addAttributesToRequest.
  4. IntelliJ выделит этот второй вызов NewRelicClient как дубликат и сообщает, что вы можете заменить его вызовом нового приватного метода. IntelliJ спросит вас, хотите ли вы это сделать. Сделай это.
  5. Встроенные переменные key и value.
  6. Наконец, создайте метод protected вместо private. Несколько я покажу вам, почему метод нужно защищать.
  7. Вы заметите, что IntelliJ подсвечивает protected с упреждением. Это потому, что все классы в Kotlin есть final по умолчанию. Поскольку выпускные классы нельзя продолжать, protected бесполезно. Одним из решений, предлагаемых IntelliJ, является создание класса open. Сделай это. Метод addAttributesToRequest тоже должно стать открытым.

Вот что у вас должно получиться в результате:

open class FulfilOrderService {

    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        addAttributesToRequest("orderId", order.id)
        addAttributesToRequest("orderAmount",
                               order.amount.toString())
                               
    }
    
    protected open fun addAttributesToRequest(key: String,
                                              value: String) {
                                              
        NewRelicClient.addAttributesToCurrentRequest(key, value)
        
    }
    
}

Обратите внимание, что все эти рефакторинги были полностью автоматическими и, следовательно, безопасными для выполнения. Для этого нам не требуются тесты. Защищенный метод позволит нам написать тест:

private val attributesAdded = mutableListOf<Pair<String, String>>()

private val subject = FulfilOrderService()

@Test
fun `adds order id to the current request within newrelic`() {

    val order = Order(id = "some-id", amount = 142)
    
    subject.fulfil(order)
    
    val expectedAttributes = listOf(
            Pair("orderId", "some-id"),
            Pair("orderAmount", "142"))
    assertEquals(expectedAttributes, attributesAdded)
    
}

Говоря о тестах и ​​рефакторинге…

Хотите научиться писать приемный тест в Kotlin? Может быть, как использовать мощность IntelliJ IDEA себе на пользу?

Возможно, вы хотите научиться хорошо создавать приложения в Kotlin? — будь то приложения командной строки, веб-приложения или приложения для Android?

Я СЛУЧАЙНО написал лучшую электронную книгу о том, как начать работу с Kotlin. 350 страниц практического учебника, которым вы можете следить.

Вы почувствуете себя так, как будто я сижу с вами вместе и мы наслаждаемся временем, создавая полноценную программу командной строки.

заинтересованы?

Загрузите полный учебник здесь. Кстати, это бесплатно и так будет всегда!

Возвращаясь к нашему тесту.

Все выглядит правильно, но не работает, потому что никто не добавляет никаких элементов в список attributesAdded. Поскольку у нас есть этот маленький защищенный метод, мы можем «взломать» его:

private val subject: FulfilOrderService = object :
                                          FulfilOrderService() {
                                          
    override fun addAttributesToRequest(key: String,
                                        value: String) {
                                        
        attributesAdded.add(Pair(key, value))
        
    }
    
}

Если вы запустите тест, он пройден. Вы можете изменить значение в тестовом или рабочем коде, чтобы увидеть ошибку и убедиться, что он действительно тестирует то, что, по вашему мнению, делает.

Давайте посмотрим весь тестовый код:

import org.junit.Assert.*
import org.junit.Test

@Suppress("FunctionName")
class FulfilOrderServiceTest {

    private val attributesAdded = 
            mutableListOf<Pair<String, String>>()
            
    private val subject: FulfilOrderService = object :
                                      FulfilOrderService() {
                                      
        override fun addAttributesToRequest(key: String,
                                            value: String) {
                                            
            attributesAdded.add(Pair(key, value))
            
        }
        
    }
    
    @Test
    fun `adds order id to the current request within newrelic`() {
    
        val order = Order(id = "some-id", amount = 142)
        
        subject.fulfil(order)
        
        val expectedAttributes = listOf(
                Pair("orderId", "some-id"),
                Pair("orderAmount", "142"))
        assertEquals(expectedAttributes, attributesAdded)
        
    }
    
}

Итак, что тут только что произошло?

Видите, я сделал несколько иную версию FulfilOrderService класс – тестовый. Единственным недостатком этого метода тестирования является то, что если кто облажался addAttributesToRequest функция, ни один тест не нарушится.

С другой стороны, эта функция никогда не должна содержать более одной строки простого кода и, вероятно, не будет меняться так часто. Это произойдет только в том случае, если авторы сторонней библиотеки, которую мы используем, собираются внести критическое изменение в этот единственный метод.

Это маловероятно. Пожалуй, будет происходить каждые несколько лет.

И знаете что?

Даже если вы все-таки проверите его как-то более «черно-ящичное», чем то, что я здесь предлагаю, когда такое критическое изменение придет вокруг блока, вам все равно придется повторно посетить все использование и исправить их. Вероятно, вам тоже придется выбросить или переписать все сопутствующие тесты.

О, и в случае таких разрушительных изменений я бы все равно рекомендовал протестировать вручную по крайней мере один раз, чтобы проверить, правильно ли вы поняли новый API и взаимодействует ли он с посторонней системой так, как вы считаете, что это должно быть.

Учитывая всю эту информацию, я считаю, что можно оставить эту одну строчку без проверки.

Но если такое изменение наступит вокруг квартала, вам придется искать все места, куда мы звоним. NewRelicClient?

Короткий ответ – да.

Долгий ответ: в текущем дизайне – да. Но вы думали, что мы кончили?

нет.

Дизайн ужасен, как сейчас. Давайте исправим это с помощью удаления Humble Object. Когда мы это сделаем, во всей кодовой базе останется только одно место, которое потребует перемен – этот скромный объект.

К сожалению, IntelliJ не поддерживает Move method или Extract method object рефакторинга для Kotlin еще нет, поэтому нам придется выполнить это вручную.

Но знаешь, что? — Это нормально, потому что мы уже имеем соответствующие тесты, подтверждающие нас!

Чтобы сделать Extract method object рефакторинга, нам нужно будет заменить реализацию внутри метода созданием объекта и немедленным вызовом метода этого объекта с теми же аргументами, которые имеет рефакторингованный метод:

protected open fun addAttributesToRequest(key: String,
                                          value: String) {
                                          
//   NewRelicClient.addAttributesToCurrentRequest(key, value)
    NewRelicHumbleObject().addAttributesToRequest(key, value)
    
}

Тогда нам нужно будет создать этот класс и создать метод. Наконец, мы поместим содержимое рефакторизированного метода, который мы закомментировали, к только что созданному методу; не забудьте удалить комментарий, поскольку он нам больше не нужен:

class NewRelicHumbleObject {
    
    fun addAttributesToRequest(key: String, value: String) {
        
        NewRelicClient.addAttributesToCurrentRequest(key, value)
        
    }
    
}

Мы завершили этот этап рефакторинга и сейчас мы должны запустить наши тесты. Они все должны пройти, если бы мы не совершили никакой ошибки — и они делают!

Следующим шагом в этом рефакторинге является перенос создания скромного объекта в поле. Здесь мы можем выполнить автоматизированный рефакторинг, чтобы получить поле из выражения NewRelicHumbleObject(). Вот что вы должны получить после рефакторинга:

private val newRelicHumbleObject = NewRelicHumbleObject()

protected open fun addAttributesToRequest(key: String,
                                          value: String) {
                                          
    newRelicHumbleObject.addAttributesToRequest(key, value)
    
}

Теперь, поскольку у нас есть значение в поле, мы можем переместить его в конструктор. Для этого также существует автоматизированный рефакторинг! Это называется Move to constructor. Вы должны получить следующий результат:

open class FulfilOrderService(
        private val newRelicHumbleObject: NewRelicHumbleObject =
                                          NewRelicHumbleObject()) {
                                          
    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        addAttributesToRequest("orderId", order.id)
        addAttributesToRequest("orderAmount",
                               order.amount.toString())
                               
    }
    
    protected open fun addAttributesToRequest(key: String,
                                              value: String) {
                                              
        newRelicHumbleObject.addAttributesToRequest(key, value)
        
    }
    
}

Это сделает чрезвычайно простым использование зависимости от теста. И заметьте, это простой объект с одним нестатическим способом.

Вы знаете, что это значит?

Да! Вы можете использовать свой любимый инструмент издевательства, чтобы высмеять это. Давайте сделаем это сейчас. Я буду использовать mockito для этого примера.

Сначала нам нужно будет создать макет в нашем тесте:

private val newRelicHumbleObject =
        Mockito.mock(NewRelicHumbleObject::class.java)

Чтобы иметь возможность поиздеваться над нашим скромным предметом, нам придется сделать его классом open и метод addAttributesToRequest открыть также:

open class NewRelicHumbleObject {

    open fun addAttributesToRequest(key: String, value: String) {
        // ...
        
    }
    
}

Тогда нам нужно будет предоставить этот макет как аргумент FulfilOrderServiceконструктор:

private val subject = FulfilOrderService(newRelicHumbleObject)

Наконец, мы хотим заменить наше утверждение на mockitoпроверка:

Mockito.verify(newRelicHumbleObject)
        .addAttributesToRequest("orderId", "some-id")
Mockito.verify(newRelicHumbleObject)
        .addAttributesToRequest("orderAmount", "142")
Mockito.verifyNoMoreInteractions(newRelicHumbleObject)

Здесь мы проверяем метод нашего скромного объекта addAttributesToRequest было вызвано с соответствующими аргументами дважды и больше ничего. А нам не нужно attributesAdded поле больше, поэтому давайте избавимся от этого.

Вот что вы должны сейчас получить:

class FulfilOrderServiceTest {

    private val newRelicHumbleObject =
            Mockito.mock(NewRelicHumbleObject::class.java)
            
    private val subject = FulfilOrderService(newRelicHumbleObject)
    
    @Test
    fun `adds order id to the current request within newrelic`() {
    
        val order = Order(id = "some-id", amount = 142)
        
        subject.fulfil(order)
        
        Mockito.verify(newRelicHumbleObject)
                .addAttributesToRequest("orderId", "some-id")
        Mockito.verify(newRelicHumbleObject)
                .addAttributesToRequest("orderAmount", "142")
        Mockito.verifyNoMoreInteractions(newRelicHumbleObject)
        
    }
    
}

Теперь, когда мы не переопределяем этот защищенный метод, мы можем его встроить. Кстати, классу быть не обязательно open больше. наш FulfilOrderService теперь класс готов принять изменения, которые мы хотели внести, поскольку сейчас его можно проверить (по крайней мере newrelic атрибуты запроса):

class FulfilOrderService(
        private val newRelicHumbleObject: NewRelicHumbleObject = 
                                          NewRelicHumbleObject()) {
                                          
    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        newRelicHumbleObject.addAttributesToRequest(
                "orderId", order.id)
        newRelicHumbleObject.addAttributesToRequest(
                "orderAmount", order.amount.toString())
                
    }
    
}

Давайте запустим все тесты снова, просто так! – они все проходят.

Прекрасно, я думаю, мы закончили.

Поделитесь своим мнением о Humble Object!

Спасибо за чтение!

Мне было бы приятно, если бы вы поделились своим мнением о таком рефакторинге в комментариях. Знаете ли вы более простой способ рефакторинга? — поделитесь!

Кроме того, если вам нравится то, что вы видите, вы можете похлопать мне на Medium и поделиться статьей в социальных сетях.

Если вы заинтересованы в изучении Kotlin и вам нравится мой стиль письма, возьмите мое лучшее руководство по началу работы с Kotlin.

Как «@Deprecated» Котлина облегчает колоссальный рефакторинг?
Я собираюсь рассказать вам реальную историю, как мы сэкономили себе кучу времени. Мощность рефакторинга Kotlin @Deprecated…
hackernoon.com

Как Kotlin Calamity уничтожает ваши приложения Java, как молния?
Я слышу, что вы говорите. Вокруг того, что Android активно использует Kotlin как основное программирование, есть шум…
hackernoon.com

Рефакторинг параллельных изменений
Parallel Change – это техника рефакторинга, которая позволяет внедрять обратно несовместимые изменения в API в безопасном…
medium.com

Добавить комментарий

Ваш адрес email не будет опубликован.