Dev Easy Notes
3.17K subscribers
123 photos
1 video
147 links
Работаю в IT уже 8 лет. Рассказываю про разработку простым языком. Полезность скрыта под тупыми шутками и слоем мата. Лучший underground канал про разработку, который вы сможете найти.

По сотрудничеству писать @haroncode
Download Telegram
Инвариантность, ковариантность и контравариантность

👇
👍45🔥2
Идем дальше, есть три понятия инвариантность, ковариантность и контравариантность. Не пугайтесь названий, сейчас все раскидаем, что поймет каждый. Буду объяснять на примере List и двух классов Developer и MobileDeveloper . Как вы понимаете MobileDeveloper наследует Developer, т.е Developer стоит выше по иерархии наследования. Это значит что мы любую ссылку на MobileDeveloper можем привести к Developer. Типо такого:

MobileDeveloper mobileDeveloper = MobileDeveloper()
Developer developer = mobileDeveloper 


Прежде чем пойдем дальше стоит понять такую штуку, как производный тип. Производный тип получается когда один класс, является одним из компонентов другого типа. Проще на примере, для наших классов это может быть – List<Developer>. List<Developer> – отдельный тип, однако в него входит наш Developer. Думаю тут суть ясна.

Значит, инвариантность это когда List<Developer> и List<MobileDeveloper> это два абсолютно разных типа. Другими словами мы не можем один тип привести к другому. Уже нельзя взять и привести ссылку на список List<MobileDeveloper>, к List<Developer> вас не пропустит компилятор. Все дженерики в Java и в Kotlin по дефолту инвариантные.

Ковариантность это сохранение иерархии в производных типах. Или проще, ковариантность это когда List<MobileDeveloper> является наследником List<Developer>. Раз он наследник, значит можно приводить одну ссылку в другой. Примерно так: 

List<MobileDeveloper> mobileDevelopers = new ArrayList<>();
List<? extends Developer> developers = mobileDevelopers;


Как вы заметили для этого нужно было использовать ? extends Developer. Это и есть синтаксическая реализация ковариантности в Java. Работает это примерно так, когда мы используем строку ? extends Developer мы говорим комплятору, в текущем списке, лежат объекты, которые 100% можно привести к Developer, ведь они или и есть Developer или ниже по иерархии наследованния. 

Для чего это нужно? Очень удобно теперь делать функцию, которая итерируется по списку List<? extends Developer>. Ведь теперь не имеет значение какой именно наследник внутри этого списка. Помимо этого, мы теперь не можем в списке использовать модифицирующие операторы. А вот почему не можем, расскажу в следующем посте.

И последний компонент это Контравариантность. Он противоположен ковариантности. Другими словами контравариантность это когда List<Developer> является наследником List<MobileDeveloper>. Иерархия в производных типах поворачивается на 180 градусов. А пример вот такой:

List<Developer> developers = new ArrayList<>();
List<? super MobileDeveloper> mobileDevelopers = developers;


Синтаксически контравариантность реализуется при помощи ? super MobileDeveloper. Этим мы говорим компилятору, что в списке mobileDevelopers либо MobileDeveloper, либо выше по иерархии. 

Нужно это для того, чтобы делать функции заполнения. Аля такой, вот у меня метод fill(), он принимает вот такой список List<? super MobileDeveloper>. Теперь я могу передавать в этот метод как List<Developer> так и List<MobileDeveloper>, и он отработает одинаково хорошо. Ровно как и с Ковариантностью, у нас минус одна операция. Когда используем ? super MobileDeveloper нельзя использовать операции чтения. Если попробовать использовать get, компилятор упадет. 

Откуда такие ограничения на чтение и запись при контравариантности и ковариантности расскажу в следующем посте. Это одна из самых сложных вещей в дженериках, поймете это, и больше у вас никогда не возникнет проблем.
👍74🔥7👏21
Значит, смотрите ковариантность или ? extends Developer у нас накладывает ограничение на запись, и вообще всех методов, где дженерик это один из аргументов. Контравариантность накладывает ограничения на чтение, или всех методов, где джереник это тип возвращаемого элемента. Почему так?

Ну краткий ответ это потому что компилятор  иначе не может гарантировать нам безопасность типов. Более подробный ответ звучит так.

Начнем с ковариантности. Причина этому такая, ведь когда ставим ? extends Developer мы говорим компилятору, что нам нужна гарантия, что объекты внутри будут либо Developer, либо ниже по иерархии. Однако, во время вставки компилятор не может понять какой именно тип дженерика нужно использовать в методе set. Мы не можем указать тип ссылки вставляемого объекта как ? extends Developer, это синтаксически невозможно. И раз так, мы не можем использовать любые методы, где аргумент это дженерик. Вот поясняющий пример:

List<? extends Number> list = new ArrayList<Double>();
list.add(new Integer(1));

Ссылка на список у нас ? extends Number и как бы кажется, что мы можем безопасно вставлять Number и все что ниже. Однако объект, на который ссылается list это список именно Double. И если бы компилятор тут позволил сделать вставку, то после при чтении из такого листа мы бы просто упали с ClassCastException

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

List<? super Double> list = new ArrayList<Number>();
list.get(0);

У нас ссылка ? super Double, что означает что в объекте списка, на который ссылается эта ссылка, либо объекты Double, либо выше по иерархии, вообще хз. И как в таком случае компилятору кастить объект на выходе? Правильно, никак, поэтому если мы читаем из такого листа, то можем получить только Object, т.к все объекты ниже по иерархии.

Я понимаю, что это может звучать супер запутанно, чтобы понять, еще раз перечитайте прошлый пост и повторите примеры. Потрогать эту штуку руками это самый рабочий метод понять эту теорию. Помимо этого, очень сильно рекомендую прочитать про правило PECS. После прочтения этого правила у вас все встанет на свои места.
👍448👎1
В предыдущем посте был коммент что у начинающих может возникнуть вопрос, вроде: ну вот ковариантность и контравариантность, а кроме собесов то, где это использовать? Вопрос хороший, постараюсь ответить. 

Первое, что тут посоветую пойти, найти книгу “Эффективная Java”, затем прочитать главу про правило PECS (Producer extend, Consumer super). В этой главе полностью и на достаточно глубоком уровне дается ответ на этот вопрос. Вообще на мой взгляд это лучшая книга по Java которую я читал, многие вещи правда уже не актуальны, однако большая часть советов просто топ. Поэтому крайне рекомендую прочитать полностью.

Если же хочется более краткого и простого ответа, то… его не будет. Серьезно, я тут даю вам зонтик от душнил, а не пытаюсь объяснить досконально каждую вещь в Java, на это и жизни не хватит) Для этого есть книги, вроде той, что описал выше и видосы наших друзей из Индии.
👍24😁8
Отличия в дженериках Kotlin и Java

👇
👍19
На самом деле по большей части отличий довольно мало. Первое отличие синтаксическое ? super заменили на in, ? extends заменили на out:

List<? extends Developer> developers = new ArrayList<MobileDeveloper>();
val developers: List<out Developer> = ArrayList<MobileDeveloper>();

List<? super Double> numbers = new ArrayList<Number>();
val numbers: List<in Double> = ArrayList<Number>();


Помимо этого теперь если у класса есть дженерик, мы не сможем создать объект этого класса без указания какого либо дженерика, как это было в Java. Это все довольно простые вещи, однако у дженериков в Kotlin есть два отличия, которые сильно все меняют. 

Первое, теперь можно в объявлении класса указать ковариантность и контравариантность. Смотрите, в Java мы эти вещи могли указывать только на уровне ссылок. В Kotlin эту вещь упростили, и например если мы хотим ковариантность сразу на всех объектах нашего класса, достаточно в объявлении указать out возле дженерика. На практике это работает так:

val numbers: List<Int> = listOf<Number>();
 
Видите в чем прикол. В Kotlin у List дженерик помечен как out на уровне интерфейса. Это автоматически дает свойство ковариантности всем объектам этого List. Аналогично работает и свойство контравариантности, для этого нужно поставить in

В предыдущем посте я писал про то, что свойство ковариантность и контравариантность накладывает ограничения на запись или чтение. В Kotlin эти ограничения сохраняются. И если вы в дженерике класса поставите out, вы не сможете создать функцию в этом классе, такую, где дженерик был бы входным аргументом. То же самое и про in, там ограничения на функции, где дженерик это возвращаемый тип.

Второе отличие, это возможность получить тип дженерика в runtime без регистрации, смс и рефлексии. В Kotlin ввели inline функции, т.е. функции, код которых встраивается в место использования. И вот, раз этот код будет заинлайнен, то по идее мы можем сразу узнать тип конкретного дженерика. Правда для этого нужно использовать ключевое слово reified

inline fun <reified T : Any> fn() {
    val kClass = T::class
    println("Тип дженерика $kClass")
}


Подводя итог, основных отличия три:
Нельзя создавать класс без указания дженерика
Свойство ковариантность и контравариантность можно сразу задать всем объектам класса 
Через reified можем получить тип дженерика в runtime
🔥405
Два друга @JvmWildcard и @JvmSuppressWildcards.

В комментах накинули вопрос про @JvmSuppressWildcards. Тоже довольно неочевидная штука, но порой приходится разбираться. Суть в чем, Java и Kotlin полностью совместимы, что означает что из Java можно вызывать Kotlin, и наоборот. На стыке этих двух языков начинаются приколы, например в Kotlin все типы делятся на Nullable и NotNullable, а в Java такого разделения нет. 

Работа дженериков тоже немного отличается. Буквально в предыдущем посте я описывал, что ковариантность и контравариантность в Kotlin может работать на уровне классов, а не ссылок. И вот пример:

class Box<out T>(val value: T)

fun box(value: Integer): Box< Integer> = Box(value)
fun unbox(box: Box<Number>): Number = box.value


Как видите в Box дженерик помечен как out, что означает ковариантность на уровне всех объектов. Только это работает на уровне Kotlin. Если попытаемся вызвать метод box из Java, то он вернет Box без дженерика. Это как бы немного нарушает логику ковариантности которую обещает Kotlin. При этом в методе unbox автоматически появится wildcard. В итоге в Java получим вот это:

Box box(Integer value)
Number unbox(Box<? extend Number>)


В целом все не так страшно, однако иногда охото подкрутить это поведение. Например, мы не хотим, чтобы в методе unbox автоматически проставлялся wildcard. Или же напротив проставлялся в результатах функции, в методе box. И вот для этого, нужны наши волшебные аннотации. Как всегда, лучше на примере.

Делаем в Kotlin:  

fun box(value: Integer): Box<@JvmWildcard Integer> = Box(value)

Получаем в Java:

Box<? extend Integer> box(Integer value)

Делаем в Kotlin:  

fun unbox(box: Box<@JvmSuppressWildcards Number>): Number = box.value

Получаем в Java:

Number unbox(Box<Number> box)

Сравните эти две полученные функции с дефолтным поведением и суть станет ясна.

Где это нужно на практике? У меня такое было только при работе с Dagger. Фишка в чем, в том что Dagger работает на базе Kapt. Если вдруг не знали, то kapt работает в два приседания. Сперва он перегоняет весь код в Java Stub и только потом генерит нужный код. Поэтому он такой долгий и по этой причине иногда нужно делать подсказки компилятору, когда пишем модули на Kotlin. В противном случае кодогенератор начинает чудить и падать.
🔥23👍4🤔1
Соглашение по конфигурации

👇
😁91👍1
Все же хотя бы краем уха слышали про Ruby On Rails. Пожалуй самый знаменитый из всех веб фреймворков, которые есть в индустрии. Он получился настолько удачным, что в принципе язык Ruby редко рассматривается без Ruby On Rails. Фреймворк крут тем, что принес много интересных концепций. 

Одна из таких концепций – соглашения по конфигурации (Convention over configuration). Суть в том, что когда мы затаскиваем какую-то либу, или настраиваем фреймворк, или билд систему нам нужно сделать конфигурацию, хотя бы минимальную. И когда вы делаете это в первый раз, то особых проблем это не вызывает. Но на 5-10 раз начинаешь бомбить из-за копипасты. И вот концепция соглашения вместо конфигурации позволяет решить эту проблему и уменьшить количество дублируемого кода.

Работает это так: мы просто делаем систему такой, чтобы все работало сразу из коробки без минимальной конфигурации. И только в том случае, если тебе нужно подкрутить дефолтное поведение, ты уже делаешь конфигурацию. Ruby On Rails для примера запускает целый сервак без какой-либо настройки. Из-за этого кстати его все боялись, когда фреймворк только вышел. Потому как всем казалось, что не может быть так просто и ты явно за это где-то заплатишь. 

На практике в Android разработке этот подход можно использовать в Gradle. Когда у вас в проекте 2-3 модуля, можно просто копировать конфигурацию. Однако когда их переваливает за 500, копипастой уже не отделаешься. Простое изменение параметра может привести к тому, что придется переделывать везде, да и новые модули создавать запарно. Эту проблему и помогает решить наша концепция. 

Соглашения в Gradle можно реализовать в виде своих плагинов, у Gradle даже есть дока для этого. Просто выносим конфигурацию в такой плагин, и все что остается сделать это в новом подключаемом модуле просто заиспользовать этот самый плагин. Все работает как нужно, прям из коробки, а если нужно в отдельном модуле подкрутить поведение, делаешь это только в этом конкретном модуле.
👍312🤔1
Подходы работы с Git

👇
👍9😁4
{1/2} Немного отойдем от разговора о языке и поговорим о инженерных практиках, а конкретно про работу с гитом. Так уж получилось что если говорить о гите, то у нас два стула, основных подхода как можно с ним работать (нет шутка про стулья меня еще не заебала):

Старый дедовские Git Flow
Адекватный Trunk Base

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

В Git Flow, есть ветка dev, ветка master и ветки(feature/bug/tech). Разработка ведется в ветках feature, которые начинаются всегда от dev. В ветке feature мы разрабатываем фичу, затем на этой же ветке ее тестируем и после сливаем в dev. В момент релиза мы из ветки dev делаем ветку release, проводим всякие регрессы, делаем небольшие фиксы, если нашли баги. Затем мержим ветку release в master и в dev. При этом в момент мержа в мастер ставим tag с версией релиза. Чтобы в случае чего мы могли быстро к нему откатится и все такое. 

Проблемы у подхода в том, что он излишне формальный. Куча движений нужно как на релиз, так и на простую задачу. Git Flow не выдерживает большого количества людей в команде. Долгоживущие ветки, вероятно будут приводить к конфликтам, которые придется часто разруливать. Код ревью таких веток может и будет длиться вечность.

Этот подход хорош тогда, когда у нас продукт в котором очень важно выпустить очень сильно протестированное ПО. Или у нас вообще сложная доставка нашего ПО, вроде того как раньше распространяли все через диски. В веб разработке и в частности в мобильной ПО уже давно доставляется через инет.
👍20🔥1
{2/2} Trunk Base изначально может показаться странной системой ветвления, особенно для тех кто не работал в больших продуктах, или работал только по гитфлоу. В чем тут суть, у нас всего 2 ветки, это master и feature. Происходит работа следующим образом. Когда начинаем делать новую фичу, то делаем feature ветку из master и делаем очень маленькое изменение. Например, просто добавляем новый фрагмент или Activity. Этот кусок мы сразу мержим, т.к он мелкий и его крайне легко ревьювить. Затем уже отдельной веткой делаем UI, накидываем верстку, некоторые базовые анимации, затем мержим и эту ветку. Затем делаем логику отдельной веткой, ну думаю суть вы уловили. Тут может возникнуть вопрос, а как можно релизить половину фичи? Для этого мы всегда делаем систему фича тоглов.

Фиче тогл – система, которая позволяет удаленно, выключить и включить фичу в уже зарелиженой версии приложения. Бонусом мы получаем систему A/B тестов, когда например включаем фичу только на половине пользователей. Еще можно выкатывать приложение постепенно, не сразу на всех, а сначала проверить на 5% пользователей например. Каждую новую фичу мы просто покрываем вот таким фича тоглом, и просто не включаем фичи, которые еще не готовы к релизу. Код недоработанной фичи уже будет у клиента на устройстве, посто не будет работать)

Разумеется Trunk Base, накладывает некоторые ограничения на подходы к коду. Вроде того, что, в проекте должна быть хоть какая-то система тоглов, чтобы можно было закрывать фичи. Система не обязательно должна позволять включать и выключать фичи в проде, но должна быть возможно включать и выключать их на сборке для QA. Помимо этого, не обязательно, но очень желательно чтобы была хорошо настроенная система тестирования, ведь в ветке master должен быть всегда рабочий код готовый к релизу.

Gitflow появился на свет в 2010 году. За последние 13 лет индустрия сильно изменилась, уже никто не поставляет ПО на носителях, у нас давно любая программа скачивает обновления сама и сама себя обновляет. Сейчас на рынке выигрывает тот, кто тупо чаще может релизиться. И транк бэйз очень сильно помогает в том, чтобы релизиться чаще. 

Мне нравится идея из книги Liquid Software, в которой есть метафора описывающая этот процесс. Когда ты пишешь код, важно чтобы он оказывался на проде как можно быстрее. Можно представить себе поставку нового кода, в виде потока воды, от тебя как от разработчика который пишет код, до работающей сборки на машине клиента. 

И меня просто нереально начинает бомбить, когда в 2023 году, какие-то команды заявляют, что они делают приложения под мобилки и при этом у них Git flow. Перестаньте его использовать, в веб разработке Git flow дичайше устарел, это каргокультрая дичь, которая только тормозит разработку.
👍30👎52
Что за goAsync в Broadcast receiver?
😁25🔥5
Спорим я смогу рассказать одну штуку про Broadcast Receiver (BR), о которой с большой долей вероятности вы могли не знать? Как вы знаете основное применение BR это слушать события в системе и как-то на них реагировать. Происходит какое-то событие, система создает процесс для вашего приложения, выполняется Application.onCreate, после вызывается метод onReceive у нужного BR и собственно все.

В фоне вы ничего не сможете сделать, начиная с 10 Android запретили показывать Activity если приложение не видно пользователю. Теперь, даже если произошло что-то прикольное, максимум, что вы сможете сделать это показать уведомление. 

Однако, представьте если во время какого-либо события в системе, вам нужно сгонять на сервер и получить какие-то данные. И нужно получить их именно сейчас, например скачать картинку дабы показать красивое и привлекательное уведомление. В идеале конечно скачать все заранее, но представим что по-другому никак. 

Как вы знаете, методы BR вызываются на UI потоке, благодаря чему просто синхронным вызовом это решить нельзя. И даже если это обойти, то если метод не ответит за 10 секунд, система нас прибьет. При этом если просто запустить другой Thread, после завершения метода onReceive система тупо убьет ваш процесс. Вот был бы способ, задержать процесс после выхода из метода. И такой способ есть, причем очень даже легальный. 

Представляю вашему вниманию метод – goAsync. Вот как это работает. Метод возвращает объект PendingResult, у которого есть метод finish. После использования метода goAsync, система убьет ваш процесс, только после того, как вы вызовете у PendingResult метод finish. Получаете PendingResult, создаете новый трэд, и передаете туда PendingResult. Далее делаете долгие походы в сеть и затем просто вызываете finish  у PendingResult.  Примерно вот так:

override fun onReceive(context: Context, intent: Intent) {
    val pendingResult = goAsync()
    thread {
        // getApiResults()
        pendingResult.finish()
    }
}


Разумеется было бы странно если бы у этого метода не было ограничений по времени. На все про все у вас будет 30 секунд, если не уложитесь, система не будет разбираться.
👍1021
Сейчас я напишу довольно странную на первый взгляд вещь, но мы в работе все меньше и меньше пишем код в парадигме ООП. Для начала пару слов про парадигмы. Великий Брагилевский пару лет назад написал довольно интересную вещь: “Нет никаких парадигм уже давно, не стоит кичиться знанием слов.” Более подробную эту тему он раскрыл в одном из выпусков подлодки (к сожалению не помню в каком точно).

Суть в чем, раньше понятие парадигма имело смысл. Языки делались с упором на ту или иную парадигму. Аля Java – ООП, Хаскель – функциональное, С – процедурное. Однако сейчас в 2023 году, такая вещь как парадигма уже устарела, т.к сейчас большинство популярных языков мультипарадигменные. Можно довольно просто писать в ООП стиле на Python, и в процедурном на kotlin. 

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

Посмотрите на все лучшие практики которые мы стараемся использовать в коде. Мы делаем всю бизнес логику в интеракторах или репозиториях у которых нет состояния. Все для чего нужен объект это именно конструктор, в который мы просто передаем набор интерфейсов. И если какое-то состояние где-то и появляется (в 90% случаем это какой-то InMemory Cache), то оно скрыто за интерфейсом и выглядит просто как набор функций. Все сущности для бизнес логики просто берут данные откуда-то, что-то с ними делают и передают дальше. Ничего не напоминает? 

Причем это даже не ограничивается бизнес логикой. Посмотрите на UI, ради которого ООП в принципе и создавался. Все новые UI фреймворки: SwiftUI, Compose, React говорят о подходе описания UI через функции. Единственное место где у нас есть состояние это State в MVI, от которого мы пытаемся убежать, спрятав функционал работы со State в отдельную библиотеку. Да и даже если на чистый MVVM посмотреть, там тоже нет такого понятия как состояние, там же все реактивное, мы подписываемся на данные в модели, затем как-то их обрабатываем и сразу отправляем на UI через LiveData или Flow. 

Объекты мы используем только как данные, по сути как структуры в C. Причем у нас буквально в Kotlin есть пометка, что класс только для данных – data class. Он конечно не запрещает делать внутри меняемое состояние, но так делать моветон.

Стоит признать, что индустрия пришла к тому, что разработчики довольно плохо мыслят в парадигме объектов. Время показало, что с объектами больше проблем чем пользы. Такой подход сложнее в многопоточности, поддержке и легче накосячить. 

При этом, я разумеется не говорю, что мы пишем чисто в функциональном стиле. Я уже говорил, что чисто функциональный стиль не годится для промышленной разработки. Мы просто отобрали лучшее из ООП  – полиморфизм и некоторые паттерны GOF, из функционального – идея чистых функций и неизменяемости объектов, смешали все это в коктейль и называем Best Practice.

Поэтому как мне кажется в пору на собесе у джунов спрашивать не про ООП, а больше про функциональные подходы.
👍77🔥9😁2🤔1
Одна из самых каргокультных вещей в разработке это тесты. Мало кто говорит про действительно хорошие практики в тестировании и реально крутые идеи не получают особого распространения. Говорить про написание полезных тестов гораздо скучнее, чем холиварить об архитектуре, ведь тут приходится думать. В инфополе все какбудто сводится к двум идеям: покрывайте все юнитами (как дед завещал) и пишите тесты перед кодом. Реально, я постарался загуглить доклады про тестирование и они делятся на два типа: либо очередной рассказ про то как правильно все мокать в юнитах, либо как избавится от флаков на UI тестах. Создается впечатление, что новых идей тут нет.

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

👉 Сложности в понимании типов тестирования
👉 Почему пирамида тестирования устарела?
👉 Действительно TDD работает, или это как коммунизм?
👉 Как еще можно обеспечить качество без тестов?
👉 Несколько идей о том, как писать тесты в Android?
🔥64🥰16👍143😁1
Первый пост чисто для разогрева, тут ничего супер нового, однако я обозначу пару проблем с пониманием типов тестирования.

Большинство разработчиков черпают идеи о тестировании из книг или статей, которые читают в начале карьеры. Все после прочтения Фаулера с идеей пирамиды тестирования и Кена Бека с TDD обретают непоколебимую уверенность, что все можно решить юнит тестами и пирамида тестирования это универсальная модель которая подходит любому проекту. Реальность разумеется не так проста.

Начну я вот с какой проблемы, у нас нет четкого понимания, в чем отличие разных типов тестов? Не спешите токсить в коментах, сначала дочитайте что бы понять, что я имею в виду)

По пирамиде тестирования есть 4 типа тестов:
👉 end-to-end
👉 системные
👉 интеграционные
👉 юниты

С end-to-end тестами вроде как все понятно. Поднимаем все приложение, библиотеку или что мы там разрабатываем, которая работает в условиях очень близко к продовым. А вот остальные 3 это котел холивара. Юнит тесты мы пишем вроде как на один модуль или класс. Интеграционные тесты затрагивают несколько компонентов, классов, модулей и т.д. Системные это вроде как что-то между интеграционным и end-to-end. Даже из описания системного теста появляется вот какая проблема.

Смотрите, я делаю класс A. Затем пишу тесты только на этот класс. Казалось бы это юнит тест. Затем в этом классе A из-за сложности, я выношу часть функционала в другой класс B. Класс A использует код капотом класс B. И вот юнит тесты которые я написал на первый класс А это все еще юнит тесты или уже интеграционные, ведь вроде как уже несколько компонентов?

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

Эта баллада к тому, что не существует абсолютной шкалы или разделения тестов. То что для одного проекта будет интеграционным тестом, для другого будет просто юнитом и наоборот. Например если мы делаем какую-то библиотеку, то в ней end-to-end тестом может быть просто проверка вызова метода какого-то системного API.

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

Другими словами когда мы не думаем о том, юнит это тест или интеграционный, а когда мы сосредотачиваемся на разделении, тестов на условно тесты для ViewModel и на тесты для Interactor. Помимо этого если мы говорим конкретно о тестах на Android, стоит больше уделять внимания разделению на тесты которые гоняются на эмуляторе и тесты которые можем прогнать локально.

Короче, как бы это не звучало банально, все сводится к старому доброму “все относительно“ и не слушайте фанатиков пирамиды.
18👍13🔥6👏5🤔3👎1
Не пирамидой мы едины
👇
👏15😁5
Теперь подробнее про пирамиду. Это попытка создать единую модель тестирования, которая подходила бы большей части проектов. Впервые о пирамиде заговорил Mike Conh в своей книге “Succeeding with Agile”. Он и предложил делать базу тестирования на основе того, что все будет тестироваться юнит тестами. Что дает очень точечное понимание проблемы, чтобы даже QA мог по отчету тестирования понять в чем проблема, да странные представления раньше были.

Пирамида была лишь идеей, но все подхватили ее как истину и понеслась. Такая же фигня была и с методологией waterfall, чувак, который описал ее в своей статье, также написал что у подхода огромные риски связанные с тем, что тестирование находится после разработки. Однако на такие детали всем было пофигу. Видимо у нас индустрия такая, что до нас не всегда сразу доходит. И вот с пирамидой та же история.

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

Смотрите на примере. Допустим у меня есть несколько интеракторов, в которых есть какая-то бизнес логика. После изменения некоторых требований я понимаю, что их можно объединить. И в этот момент половина тестов, перестают быть валидными, так как теперь нужно снова их переделывать. Другими словами у нас нет регресса во время рефакторинга кода, а тогда смысл этих тестов?

Довольно знаменитые ребята из фронтенда предложили следующую модель – кубок тестирования. Как я уже упоминал в предыдущем посте, никакая модель не может подходить любому проекту, однако в этой модели есть интересные мысли. Например, тут мы делаем акцент в сторону именно интеграционных тестов, или тех тестов которые пронизывают сразу много уровней нашего приложения. Мы сосредотачиваемся на таких тестах, которые не переписываются при рефакторнге, а меняется только если меняются требования.

Если говорить конкретно про Android разработку, вот один из вариантов как можно сделать такие тесты. Берем что-то вроде robolectric, чтобы кликать по кнопкам и при этом без эмулятора. Далее делаем какие-то действия с нашим UI, а после сравниваем State слоя презентора. Разумеется в идеале, чтобы был MVI, потому как у него мы можем получать единый State и сразу его проверять, однако и на MVVM можно такое сделать.

Очень желательно чтобы при этом не было завязки на MVVM или MVI, аля сделать специальную прослойку для получения State, таким образом, чтобы если мы начали рефакторинг на другую архитектуру тесты остались нетронутыми. Разумеется если у вас MVP, то какого хрена в 2023 у вас MVP, переходите на что-то другое!

Разумеется вовсе не обязательно брать именно robolectric, тут я описываю идею концептуально. Все базируется на идеях бережливого тестирования. Нам не нужно покрывать все что можно тестами. Нужно писать минимальное количество тестов, чтобы они покрывали максимальное количество кода.
👍22😁5
Действительно TDD работает, или это как коммунизм?

Начну я с примера создания библиотеки. Действительно хорошие решения чаще всего появляются следующим образом. У вас в проекте появляется какая-то проблема, вы ее решаете и понимаете что такая же проблема возникает и в других проектах. Поэтому вы решение выносите в отдельный модуль который потом выносите в open source.

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

Прикол в том, что в реальности вы используете библиотеку в каком-то контексте уникальном для своего приложения. Например, вы не ходите в либу напрямую, а через какой-то интерфейс. Но если все это нести в доку, вашей либой никто пользоваться не будет, ибо ебать какая сложная интеграция. Поэтому примеры и sample в библиотеках максимально упрощённые и удаленные от реального использования либы. В этом кстати частично есть и маркетинговый интерес, чем сильнее вы завяжитесь на библиотеку, тем сложнее вам будет с нее слезть.

При этом разработчики, не всегда это понимают и затаскают либы к себе прям как указано в sample без оберток и т.д. Не всегда разумеется, но признайтесь частенько такое можно увидеть. И как мне кажется с TDD аналогичная проблема.

Все начинают изучать TDD по Кенту Бэку с его книги. В ней максимально упрощенный пример для системы работающей с валютами. Вся эта книга это и есть тот самый sample для библиотеки. Аля вот супер простой пример, чтобы вы поняли суть, однако на практике все немного по-другому. И все начитают использовать TDD вот прям как в книге.

Разумеется TDD в чистом или теоретическом виде, когда мы перед написанием любого кода пишем тесты совершенно неработающая вещь. У нас есть куча кода на который тесты не нужны: конфиги, инициализация DI, аналитика и этот список можно увеличивать до бесконечности. Попробуйте перед тем, как начать делать верстку на Compose, сначала написать UI тест который не проходит, так как нужного UI еще нет. Правда удобно?) Поэтому когда вы видите человека, который утверждает, что весь код пишет по TDD, вероятно всего он не до конца честен, или у него в голове другое понятие про "весь код".

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

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

Вывод, как и с пирамидой тестирования, не нужно фанатично следовать за одной практикой, всегда стоит подбирать лучшее для вашей конкретной ситуации. Где-то лучше начать с кода и потом накидать тесты, где-то наоборот быстрее будет начать с теста и только потом накидать реализацию. Единого рецепта тут нет, а как где лучше может подсказать ваш скилл и опыт, поэтому чем раньше начнете писать тесты, тем лучше.
👍43🔥3
Как еще можно обеспечить качество без тестов?

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

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

⚙️ Инвариант или защитное программирование
Есть такая штука как инвариант (писал про него тут) мне нравится называть подход защитным программированием. Представьте функцию, которая делает какую-то непростую логику. Сначала мы ставим проверки, что к нам пришли данные в нужном диапазоне, далее в конце функции ставим еще проверки на корректность данных, например чтобы размер массива был нужный или типо того. И если хотя бы одна из проверок не прошла мы сразу падаем.

На первый взгляд это кажется странной практикой, однако теперь нет особой необходимости в куче тестов на эту функцию. Если кто-то что-то поменяет в алгоритме, то этот код либо упадает на более верхнеуровневом тесте, либо при проверке QA на регрессе. И даже если код упадет в проде, я об этом быстро узнаю по аналитике. Этот подход позволяет защититься от изменений в других частях программы. Конечно подход не везде можно использовать, но полезно знать что такое вообще есть.

Помимо инварианта есть много проверок которые за нас может сделать компилятор. В kotlin ярким примером является when с sealed class. Когда добавляем новый подкласс, не нужно писать тест, что мы обрабатываем все варианты, за нас это сделает компилятор.

🔬Мониторинг
В современном мире гораздо больше решает мониторинг, нежели исчерпывающее тестирование. Не для всех проектов разумеется, ПО для реакторов все же лучше протестировать настолько насколько это возможно. Однако если мы говорим про типичное вэб приложение, то в нем гораздо важнее быстрее выпустить новую фичу, нежели протестировать на все 100%.

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

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

🎛️ Фичатоглы
В одном из постов я уже указывал что фичатоглы довольно существенно ускоряют разработку так как позволяют выключить функционал если в нем обнаружились ошибки.

Сами по себе фичатоглы не про качество, а скорее про A/B тестирование. Однако они транзитивно связаны с качеством. Фичатоглы позволяют не так сильно парится о том, есть ли у нас баг или нет, мы можем в любой момент выключить фичу. Разумеется это, работает только с грамотным мониторингом, иначе как понять нужно ли выключать фичу?
👍34