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

По сотрудничеству писать @haroncode
Download Telegram
Два друга @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
Один из вариантов как можно обеспечить тестирование в android
👍8
После всего выше сказанного в воздухе витает вопрос, а как правильно тестировать то епт? Плохая новость – никто не знает, каждый проект по своему уникальный. Хорошая – у меня есть концепция, используя которую, вы с меньшей вероятностью наплодите хрень.

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

В итоге я предлагаю вариант, который находится посередине этих двух. Вариант требует дохера ресурсов и ничего не тестит, прям как джун QA. Ладно, ладно шутки в сторону.

На проекте должен быть или MVVM или MVI, в идеале чтобы было единое состояние. Дальше вы делаете следующим образом, либо берете Robolectric и нажимаете на кнопки через него, либо делаете специальную прослойку, которая позволяет прокинуть события до MVVM или MVI. Эта прослойка нужна, чтобы если вы перешли на другую либу MVI или перешли с MVVM на MVI, такой переход не вызывал переписывание всех тестов.

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

Затем у вас Store начинает выдавать поток State, через Flow или LiveData тут значения не имеет. И вы в тесте, просто проверяете этот самый State. На этом все, никаких страданий с мокито и проверкой, что нужный аргумент вызвался с нужными данными, никаких дурацких непонятных проверок. Максимально просто, вот мы нажали на кнопку, вот проверили состояние.

Чего имеем с таким подходом? Да в начале придется поебаться с настройкой либ, общего кода, и моковым локальным сервером, но лишь один раз и в начале. А какие плюсы всего этого?

👉 Первое. Любой тест будет очень близко повторять сценарий пользователя. Вы что-то делаете на экране, а затем проверяете объект State или Single Live event для показа диалогов и т.д. Вы тестируете то, как реально ведет себя ваша программа, а не абстрактные вещи в вакууме.

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

👉 Третье. Такие тесты будут (на самом деле не обязательно, уж как настроите) медленнее чем Unit, но при этом в десятки раз быстрее UI и при этом не будут требовать инфраструктуры с эмуляторами.

Разумеется от unit и ui тестов не нужно отказываться, но их должно быть меньше тех, про которые описал в посте.

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

Тут я описал лишь концепцию, а вот уже конкретную реализацию я возможно как-нибудь оформлю на github и скину сюда. Ставь ❤️ если хочется увидеть код.
55👍6
Хочу оформить главные, можно назвать их заповедями тестирования или постулатами, называйте как хотите. Я уже в разработке почти 6 лет и вот весь мой опыт, который есть на данный момент можно оформить так:

Зачем тесты?

👉 Тесты не избавляют от багов, не делают программу надежнее и не заставляют тебя писать чистый код. Тесты нужны лишь убедится, что ты новым кодом не сломал предыдущий, позволяют не оглядываться назад на каждом шагу
👉 Тесты имеют смысл только в том случае, если они постоянно запускаются на CI и блокируют МРы, иначе это бессмысленная трата времени, плавали знаем
👉 Тест плох, если он падает постоянно, на него перестают обращать внимания и забивают
👉 Тест плох если всегда проходит, значит он нихрена не тестирует
👉 Тест пишем только тогда, когда уверены, что нет другого механизма обеспечить качество (см. прошлые посты)
👉 Флакающие/мигающие тесты, от таких тестов нужно избавляться также быстро как гугл отказывается от проектов. Мигающие тесты приносят целый ворох проблем: не дают никакой информации, нагружают систему т.к приходится их перезапускать, и увеличивают вероятность оказаться вне поля зрения.

Как не нужно тестировать?

👉 Не нужно писать тесты для банальных вещей когда у вас репозиторий тупо в сеть ходит и выдает список или проверять правильно ли мы вызываем какой-то метод
👉 Не нужно делать тесты, которые протестируют все возможные кейсы на свете. Сосредоточтесь на базовых сценариях, для всего остального есть мониторинг, поддержка и грамотный подход к ошибкам
👉 Конкретно в мобильной разработке, нахер не нужны тесты которые тестируют цвета, иконки, правильность отображения или не дай бог анимации. Такие тесты ебанутся какие дорогие, а выхлоп от них никакущий.
👉 Не нужно делать тестов на "всякий случай", нужно чтобы у каждого теста было четкое обоснование зачем он нужен
👉 Попытки добится какого либо процента покрытия кода бессмысленная дроч, которая ничего не дает, а только вынуждает идти на хаки и писать тесты на сэтеры (кринж даже от упоминания)

Как лучше тестировать?

👉 Тесты не должны ломаться при рефакторинге, и быть обузой. Иначе разрабы просто будут боятся рефакторить код, а это приведет к протуханию кода
👉 Тесты должны писаться также легко как и основной код
👉 В тестах может быть дублирование, так как избавление от дублирование это уже абстракция, абстракция это сложно, а сложность ведет к багам. Вам нужны баги еще и в тестах?
👉 Ассинхронность. Как показывает практика лучше ничего не подменять и тестировать в условиях реальной работы. Иначе в тестах на одном потоке все прекрасно, а в проде гонка и плавающие баги (см. подход из предыдущего поста).
👉 Тесты могут потребовать изменения в архитектуре или инфраструктуре, это ок так и должно быть. Однако внутри кода проекта не должно быть упоминаний о тестах: @visiblefortesting или idling в коде проекта это сигнал о том, что вы профакапились с архитектурой
👍2812🔥6🤔1
Итак, я уже писал свою критику разрабов, которые любят делать проксирующие интеракторы и теперь хочу обсудить, пожаловаться на проблему, которая стоит очень рядом.

Я свою карьеру начинал как backend разработчик на java. Первой моей работой была небольшая Томская конторка, которая делает медиакомплексы для общественного транспорта. Для управления этой системой был целый бэк с довольно симпатичным UI.

Вся система разрабатывалась на фреймворке spring. И вот в таких системах в коде есть одна практика, призраки которой мы можем видеть сейчас в Android разработке. Там каждому классу с логикой создавался интерфейс, а внутри уже клался класс с реализацией логики: PizdosService и внутри PizdosServiceImpl.

И вот когда я был новичком, я даже не задавался вопросом, а нафига мы так делаем. К тому времени я уже начитался статей про Solid и т.д и понимал, что мы это делаем, чтобы если вдруг нужно поменять реализацию, могли это сделать быстро. Знаете сколько раз приходилось менять реализацию? 0 раз.

Даже в текущем проекте когда я на код ревью задаю вопрос про интерфейсы в интеракторах, мне отвечают привычное: "А вдруг там нужно будет реализацию подменить" – ядрена мать!

Речь не только про интерфейсы, а про то, что все разработчики пытаются предсказать будущее и заложиться заранее. Однако будем честны, выходит у всех паршиво. И не дай бог из 100 таких интерфейсов разработчику посчастливится в одном заменить реализацию, все он уже потерян. Теперь когда он сэкономил целые 15 минут работы, убежденность на собственном опыте не даст ему рационально посмотреть на эту вещь.

В эту тему мне очень нравится доклад Кирилла Мокевкина, ментальное программирование. В докладе он рассказывал, что они (Хекслет) долго сидели на одном сервере и масштабировались исключительно покупкой более мощного железа. Они понимали, что они стартап, и если будут делать это вначале, пытаясь предсказать будущее, они просто профакапятся и закроются через месяц. И когда они уже стали успешным бизнесом, только тогда, они начали разъезжаться по серверам и сделали это сразу грамотно. Потому как уже знали что именно нужно делать.

Еще один пример Авито. Дико успешный продукт, знаете сколько у них было инстансов БД на 2018 год, когда проекту уже был почти 10 лет? Один! Один сервер всего, ну и еще одна реплика естественно. Они тоже понимали, что если начнут шардирование раньше времени, то проебутся. Через 10 лет проекта они уже точно знали как именно им нужно растаскивать данные по разным БД.

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

Если понадобится переехать на другую базу, то возьмете и переедите, появится вторая аналитика, засучите рукава и добавите. Когда у вас четко будет стоять задача, вам будет это сделать в 100 раз проще, чем предугадать туманное будущее.
👍56🔥7🤔2🤯21
Я сейчас работаю в команде инфры и большая часть задач у меня связана с тем, чтобы другим разработчикам было удобно пилить фичи. За последние пару месяцев я в основном занимался разработкой cli инструментов.

У большинства CLI программ всегда есть два варианта как передать ключи короткий "-o" и длинный "--output". Ну так вот, сделал я CLI который в 90% случаев будет вызываться только на CI. И я подумал, ну хорошая же идея прописать ключи одной буквой, будет очень минималистично, удобно и в одну строку.
Вот что получилось в итоге:

java -jar /tia-cli.jar report -t $BRANCH_NAME -o ./tia-git -b tia-coverage -a $REF_NAME

очень понятно не правда ли?

Благо до меня сразу же дошло, что человек который после меня полезет это редактировать с ума сойдет, чтобы что-то тут поменять. Ему придется как минимум или найти доку этой проги или скачать ее себе локально и вызывать с флагом -h. Пытаясь уместить команду в одну строку, я создал гемморой другому разработчику на ровном месте. Это же и не загуглишь никак и выглядит как что-то на эльфийском.

А теперь второй вариант:

java -jar /tia-cli.jar report \
--output-dir=./tia-git \
--auto-fetch \
--bucket=sme-android-tia-coverage \
--target-branch=$BRANCH_NAME \
$REF_NAME


Теперь даже если вы вообще не знаете что делает эта CLI стало в разы понятнее хотя бы примерно какой ключ за что отвечает.

Поэтому совет в общих скриптах, CI и т.д. всегда используйте длинные версии ключей. Берегите нервы других разрабов. Короткие версии используйте только если запускаете программу локально.
👍55👎1