Русский
Русский
English
Статистика
Реклама

Блог компании redmadrobot

Очень технический выпуск про DDD и проектирование сложных систем

12.02.2021 16:06:23 | Автор: admin

В свежем выпуске подкаста Сушите вёсла обсудили методологии проектирования сложных систем. Много говорили о Domain Driven Design, Event Sourcing и CQRS. Тема непростая, но, как говорится, очень интересная.

Артём Кулаков и Рома Чорыев разработчики Redmadrobot, они записывают подкаст, где обсуждают различные стороны создания ИТ-продуктов. Ниже ссылка на новый выпуск, тайминг и ответы на душещипательные вопросы. Но вначале небольшой дисклеймер:

Почему все больше и больше ведется разговоров о различных аспектах и методологиях проектирования систем? Потому что наши системы стали действительно большими. Чтобы разобраться, как проектировать такие системы, мы позвали Алексея Мерсона системного архитектора из Karuna. В выпуске попробовали разобраться, что такое Domain Driven Design, как он связан с Event Sourcing и при чем тут CQRS и микросервисы. Снять удалось только первый слой, да и то неравномерно. Но всем, кто хочет начать погружаться в тему, этот выпуск будет несомненно полезен. И обязательно ознакомьтесь с материалами к выпуску.

Тайминг

02:29 Гость студии Алексей Мерсон и как он начинал;

05:02 .Net и DDD;

12:26 почему сейчас все чаще говорят о DDD;

15:30 полезная литература о DDD;

23:01 как начать проектировать систему по DDD;

25:05 Event storming и Miro;

45:15 что такое Event sourcing;

55:00 CQRS и его связь с DDD и Event sourcing;

01:06:10 с чего начать.

DDD что это и почему сейчас?

Domain-Driven Design предметно-ориентированное проектирование. Понятие известно давно, но в последнее время в русскоязычном сообществе о нем говорят всё чаще.

Алексей считает, что возможно, это связано с тем, в Agile-мире запускаются проекты, которые без применения DDD скатятся в печальное состояние, потому что появится расхождение между предметной областью и реализацией.

В первую очередь Domain-Driven Design это история о проектировании, и предметная область в нем ставится во главу угла. И основной акцент в этом подходе делается на взаимодействии с бизнесом с заказчиками ПО и приложений.

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

Как спроектировать сложную систему с нуля?

В студии прозвучал резонный вопрос: представим, что мы поняли, что у нас сложная система, которую будем делать по DDD и у нас на это даже есть много времени. С чего стоит начать? Алексей дал несколько советов.

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

И, кстати, многие для этих целей используют системой Event storming это такой способ собрать разработчиков и бизнес-экспертов в одном месте, чтобы вместе исследовать предстоящую разработку.

В Event storming собираются все заинтересованные лица и на доске с помощью стикеров выстраивают последовательность событий и тех объектов, которые генерируют эти события. Получается некая визуальная карта, в которой становится понятно, как взаимодействуют различные сущности и как они объединяются по контекстам.

Event Storming фото Daniel GomesEvent Storming фото Daniel Gomes

Изначально активность проводили офлайн, но сейчас подобное можно спокойно провести онлайн, например, в Miro.

Event Sourcing (не путать с Event storming)

Event Sourcing еще одна популярная сегодня тема. Это архитектурный шаблон, упоминание которого часто всплывает в связи с DDD.

На самом деле, связаны они косвенно, и прямой зависимости между ними нет дело в том, что Event Storming помогает генерировать события, которые можно потом переложить в Event Sourcing архитектуру. В общем, в студии попытались разобраться, почему об Event Sourcing говорят.

Недавно в сообществе SpbDotNet был митап, где рассказывали о том, как одна команда применяла Event Sourcing. Они применяли его в приложении, которое реализовывало аукционы. Там есть ставки, и они, по сути, события, которые конкурируют: важно, в какой момент каждое из них произошло, важно получить из них правильную последовательность, чтобы понять конечный статус агрегата, например, лота или аукциона. И в этой ситуации Event Sourcing вполне оправдан.

Но иногда случается, что Event Sourcing это просто ненужное усложнение процесса. Ведь его легко сделать неправильно и очень сложно сделать правильно, потому что в нем есть много мест, где можно свернуть не туда.

Event Sourcing лучше всего применять в конкурентных и неизменяемых историях, говорит Алексей. Классическое место для этого шаблона финансовые транзакции. Есть финансовые события: зачисления или списания, и разработчику важно, чтобы их последовательность была, во-первых, четкой. Во-вторых, ее невозможно было отменить необходим однозначный конечный баланс. И Event Sourcing дает возможность сделать это так, чтобы цепочка событий всегда шла только вперед.

Подробнее обо всём начинается с 45:15

CQRS yay or nay?

Ребята обсудили, что CQRS это, скорее, паттерн, применяемый в технической области. Связан ли CQRS и DDD?

DDD больше заточено на side effects и изменения, а CQRS больше относится к отображению. И это его качество, как раз, применяется в Event Sourcing, потому что фактически есть только набор ивентов, а показывать, как правило, нужно данные, состояния объектов. А для того чтобы данные эти получить, нужно из ивентов делать проекции. В общем, если смотреть на CQRS под этим углом, получается история о синхронном взаимодействии с точки зрения UI/UX.

Подробное обсуждение этого непростого вопроса с 55:00.

Где и как научиться всему этому? (желательно до выхода на пенсию)

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

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

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

Под конец разговора Рома ещё задал интересный вопрос: Kакая проблема должна стоять перед разработчиком, чтобы он понял, что пришло время углубиться в DDD? Если коротко, то сложно ответить коротко :) А подробно рассказали с 1:10:00.

Полезные материалы

Предыдущие выпуски подкаста Сушите вёсла

Подробнее..

Осторожно, печеньки! советы начинающим тестировщикам в сфере безопасности

25.02.2021 18:11:56 | Автор: admin

Привет, меня зовут Вика Бегенчева, я QA-инженер в Redmadrobot. Я расскажу, как злоумышленники крадут наши данные, и что можно сделать, чтобы от этого защититься.

Термин тестирование безопасности звучит довольно серьёзно. Многие боятся погружаться в эту тему, думая, что их ждут непроходимые дебри из непонятных слов, сложной литературы и загадочных аббревиатур. Я разберу основы тестирования безопасности и вы убедитесь, что на самом деле это просто и интересно.

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

Что хранят cookie

Допустим, мы зашли на сайт интернет-магазина ошейников для собак и выбрали французский язык (почему бы и нет). Добавили в корзину пару шлеек и поводок. Что будет, если мы закроем вкладку и зайдём на сайт снова? Всё останется прежним: интерфейс на французском и три товара в корзине. Магия? Нет, cookie.

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

На вашем устройстве есть текстовый файл, в котором содержится различная информация о пользователе для каждого сайта cookie. Она хранится для разных целей, в том числе и для удобства пользования сайтом. Cookie бывают временные (cookie-сессии)и постоянные. Давайте взглянем на них поближе.

Открываем панель разработчика в Chrome и переходим на рандомную статью на Хабре. Во вкладке Network находим первый запрос, и в хедерах реквеста видим как проставляются cookie:

Хабр в ChromeХабр в Chrome

И в респонсе (запросе) видим cookie:

Set-Cookie: fl=ru; expires=Fri, 25-Feb-2022 08:33:31 GMT; Max-Age=31536000; path=/

Тут уже работает логика и Google (если очень нужно): cookie типа fl=ru (параметр, вероятно, отвечающий за язык) или те, которые хранят товары в корзине постоянные cookie. Они не меняются, если пользователь их не трогает. Нас же интересуют временные или сессионные cookie. Они хранят информацию, которая помогает сайту понять, что это всё тот же пользователь в текущей сессии.

Например, при авторизации на сайте, в файл cookie проставляется условный session_id уникальный идентификатор сессии на данный момент времени, к которому привязаны текущий браузер и пользователь.

Когда мы совершаем действия, доступные только этому пользователю, мы отправляем в хедер запроса (заголовок запроса, в него передаётся способ общения с сервером) и данные, которые локально сохранили в cookie, чтоб подтвердить, что это мы, а не условный программист из Финляндии. Временные cookie имеют срок годности и стираются в конце сессии, или становятся неактуальными с течением времени.

Как понять, что ваши данные в опасности

Если хакеру удастся угнать cookie пользователя, то он сможет действовать от его лица или же просто получит конфиденциальную информацию юзера. Как узнать, всё ли впорядке?

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

Во-вторых, нужно проверить, чтобы все конфиденциальные cookie были с флагом httpOnly и secure.

Cookie с флагом secure передаются на сервер только по протоколу HTTPS. Как правило, в этом случае есть сертификат SSL или TLS. Cookie с флагом httpOnly защищены от манипуляция JavaScript через документ, где хранятся cookie.

Открываем в Chrome инструменты разработчика и переходим во вкладку Application.

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

Теперь рассмотрим сценарии кражи пользовательских данных и как от них защититься.

HTTP и HTTPS

Гуляя по сайтам мы до сих пор встречаем предупреждения браузеров о незащищённом соединении. Иногда строгий браузер вообще не пускает нас на сайт, потому что не хочет нести за него ответственность. А чего он боится?

А боится он протокола HTTP он древний и небезопасный. Все современные сайты общаются по протоколу HTTPS и вот их браузер любит. Разница между HTTP и HTTPS всего в одной букве, но эта буква означает много Secure. Безопасность передачи данных главный приоритет современных браузеров.

Сайты, которые общаются с сервером по протоколу HTTPS, используют сертификат TLS (его предшественник SSL). Такой сертификат может защищать как один домен, так и группу поддоменов.

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

Если сайт общается по протоколу HTTPS, то между магазином и сервером построен безопасный мост. SSL-сертификат позволяет шифровать данные нашей карты и преобразует их в нечитаемый набор символов.

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

Чтобы защититься от кражи данных, убедитесь:

  1. Что сайт общается через HTTPS, а не через HTTP.

  2. Есть редирект с http:// на https:// злоумышленник может разместить ссылку с http://, тогда данные можно будет угнать. Редирект насильно переводит пользователя на https:// ради его же блага. И все счастливы.

  3. Включен HSTS.

Brute force атаки

Допустим, в нашем любимом магазине ошейников для питомцев есть админка. Владелец сайта решил оставить ссылку админки по адресу https:// [].com/admin. Злоумышленник переходит по этому адресу и его ждёт страница входа в панель администратора. Допустим, наш хакер вводит логин admin, а пароль подбирает с помощью скрипта, который сам будет перебирать пароли.

Если логин верный, то узнать пароль дело нескольких часов. Запустил скрипт на ночь, лёг спать, а утром уже есть доступ к панели администратора. Такой перебор и есть brute force атака.

Защититься от взлома можно несколькими способами (про авторизацию/аутентификацию и oauth2 мы поговорим ниже). Но, если речь идёт о brute force атаке, то простейшая защита тут установка лимита на попытки ввода пароля.

Лимиты устанавливают в зависимости от специфики продукта и решения проектной команды:

  • ограниченное число попыток в единицу времени;

  • ограниченное число попыток с последующей блокировкой функционала (например, с восстановлением доступа через почту когда попытки закончились);

  • установление тайм-аута после n попыток ввода.

Естественно, это не избавит от всех проблем, но сильно усложнит жизнь злоумышленнику.

Токены и сессии

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

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

А что будет, если наш пропуск-ключ украдут? По нему просто войдут в здание и узнают все секреты закрытого клуба. Токены сессии это ключи к ресурсам нашего сайта. Когда мы логинимся на сайте, мы отправляем запрос на авторизацию/аутентификацию пользователя. На сервере проверяются его права и генерируется токен.

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

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

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

На разных ресурсах, требования ко времени жизни токена разные. На одном сайте токен живёт год, а на другом всего лишь час. После того как срок действия токена истечёт, система попросит пользователя снова авторизоваться. Чтобы не вводить всё время логин и пароль после истечения access-токена, придумали refresh-токен.

У refresh-токена только одна цель обновлять access-токен. Это работает так:

  • отправляем запрос на закрытый ресурс с истекшим аксесс-токеном;

  • сервер возвращает ошибку о протухшем токене;

  • клиент видит эту ошибку и сразу формирует запрос на обновление аксесс-токена;

  • в хедеры этого запроса проставляется значение рефреш-токена;

  • сервер сверяет рефреш-токен с базой данных, формирует новый аксесс-токен и отправляет его обратно на клиент;

  • клиент автоматически повторяет запрос на закрытый ресурс уже с новым аксесс-токеном.

Готово! А пользователи сайта даже ничего не увидели и не поняли. Как этим пользуются хакеры?

Access- и refresh-токены это токены сессии. Если злоумышленнику удастся перехватить их, то он докажет, что он это мы, просто подставив в запрос. Он получит доступ к нашим ресурсам и сможет совершать действия от нашего имени.

Чтобы защититься, нужно:

  • проверить, что токены сессии не храняться в файлах Cookie или LocalStorage;

  • убедиться, что все запросы с токенами передаются по зашифрованному HTTPS;

  • проверить, что нет доступа к ресурсу без предъявления токена отправить запрос без токена;

  • проверить, что нет доступа с чужим токеном, а также проверить запросы с несуществующим токеном;

  • проверить запросы с истекшим токеном;

  • поменять в базе данных вручную время жизни токена (продлить/просрочить);

  • удалить access-token токен из базы данных и отправить запрос.

Авторизация и аутентификация

Чтобы обеспечить безопасность системе и разрешить взаимодействовать с ней разным ролям пользователей (админы, модераторы и т.д.), важно понимать различия между авторизацией и аутентификацией.

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

Аутентификация это проверка на соответствие заявленного имени пользователя (паспорта) с идентификатором в системе (Лаврентий Куашников). Авторизация это предоставление нам прав в соответствии с нашей ролью в системе (отдельная комната для спикера).

Вернёмся к примеру с админкой нашего магазина ошейников. Снова переходим по адресу админки и нас ждёт страница с вводом логина и пароля. Логин это идентификатор пользователя в системе. Пароль это доказательство пользователя, что он это он (ведь только он может знать пароль).

Когда в запросе мы отправляем эти данные, сервер проверяет их на соответствие это аутентификация. Если аутентификация прошла успешно, то сервер смотрит в базе данных на нашу роль в системе и какие возможности у нас есть это авторизация.

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

А ещё наш сервер умный, и он знает, как реагировать на ошибки авторизации/аутентификации. У него есть два кода ошибок:

  • 401 если логин/пароль не совпадают или если для доступа к ресурсу нужна аутентификация;

  • 403 когда сервер понял, что за юзер к нему ломится, но отказывает ему в доступе.

Если хакер доказал, что он это мы, то система примет его с распростертыми объятиями. Злоумышленник получит доступ к информации и действиям, доступным только нам.

Самый простой способ помочь хакеру пройти авторизацию в системе передать логин и пароль в запросе в незашифрованном виде. Ну и хранить информацию в cookie, конечно. Но мы не хотим помогать хакерам. Так что делать?

Есть ряд проверок, которые снижают вероятность взлома:

  • проверьте, что пароли авторизации не хранятся в cookie. Токены хранятся только в httpOnly куках;

  • все запросы, где используется авторизация, передаются по зашифрованному соединению https.

Также проверьте пользовательский доступ к ресурсам с кэшем страницы:

  1. Авторизуйтесь в системе.

  2. Откройте ту же страницу в соседней вкладке, при этом пользователь должен быть авторизован.

  3. Разлогиньтесь из второй вкладки.

  4. Вернитесь на первую вкладку (кэш сайта покажет, что вы ещё в системе).

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

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

Проверьте также время жизни токенов в системе. Можно попросить разработчика поменять время жизни на 3 минуты, вместо 7 дней. Также можно попросить поменять время сервера. Но время жизни токена в любом случае должно быть ограничено.

Проверьте API:

  1. Отправьте запрос без хедеров авторизации.

  2. Отправьте запрос с чужими значениями параметров авторизации.

  3. Проверьте запрос с истекшим токеном или с тем же токеном после логаута (после логаута токен должен стать недействительным).

  4. Проверьте, чтобы логин/пароль не передавались в параметрах запроса. Пример, как не нужно передавать логин/пароль в параметрах http запроса: http://site.ru/page.php?login=testqa&password=12345678.

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

  1. Авторизуйтесь в системе в одном браузере.

  2. Авторизуйтесь в системе в другом браузере (или во вкладке инкогнито).

  3. Смените пароль в системе через первый браузер.

  4. Проверьте доступ к ресурсам на втором браузере.

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

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

Третье:проверьте наличие постоянного тайм-аута сессии(Session-Timeout), они бывают нескольких видов. Наличиепостоянного тайм-аута запрещает доступ к ресурсу через n минут после авторизации. Наличиединамическоготайм-аута запрещает доступ к ресурсу через n минут после последнего запроса. Другими словами, динамический тай-маут закрывает доступ из-за вашего бездействия в системе.

И напоследок, не забудьте проверить, доступна ли двухфакторная аутентификация на сайте.

Двухфакторная аутентификация

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

Так работает и двухфакторная аутентификация. У нас есть основной замок логин и пароль. И у нас есть второй тип защиты чаще всего это код, приходящий по SMS, электронной почте или отображаемый в программе двухфакторной аутентификации (например, в Google Authenticator).

Ещё неплохой практикой является использование на сайте протокола oAuth2. Простыми словами, oAuth2 это когда мы авторизуемся в одном сервисе с помощью другого. Например, когда регистрируемся/логинимся на сайте с помощью Gmail.

Схема примера работы oAuth2Схема примера работы oAuth2

SQL-инъекции кода

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

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

ДОБАВИТЬ перец И УБРАТЬ сахар.

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

Схема зараженияСхема заражения

Если у базы данных нет фильтрации запросов, то злоумышленник может манипулировать данными: получать данные пользователей (в том числе логин и пароль), размещать файлы, заменять значения. Как же злоумышленник может отправлять такие запросы? Это можно сделать через параметры HTTP, добавив в адресной строке параметры:

http://some.site.ru/test/index.php?name=1 UNION SELECT 1,2,3,4,5 + &password=1234

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

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

Во-вторых, проверяем, чтобы при намерено кривом запросе не выводились ошибки, через которые можно сделать выводы о составе базы данных. Например, ошибка Unknown column 4 in order clause говорит о том, что данные из таблицы выбираются по трём колонкам или меньше.

Сегодня существует множество фреймворков и библиотек, которые предоставляют защиту от SQL-инъекций. Например, библиотека node-mysql для node.js.

XSS

XSS расшифровывается как Cross-Site Scripting. Эта уязвимость входит в список OWASP TOP-10. Её смысл в том, что злоумышленник принудительно внедряет JavaScript-код через инпут на сайте: в поле ввода пушит код, который сохраняется на странице. Впредь он будет исполняться каждый раз при вызове страницы. Это происходит потому, что на сайте отсутствует экранирование спец символов.

У хакера много вариантов воспользоваться этой уязвимостью: от отображения алерта (всплывающего окна) с рекламой до кражи cookie и редиректа на сайт-зеркало.

Не забывайте, что проверять наличие уязвимостей на чужом сайте по законодательству РФ (и многих других стран) запрещено. Это воспринимается, как попытка намеренного причинение вреда сайту. И что тогда делать?

Первым деломвводим во инпуты на сайте скрипты например. Если уязвимость есть, то при перезагрузке страницы появится алерт со значением test.

После этоговводим во инпуты строку:

\\ test :grin: /?.&&*@#$%^*;~`  <iNpUt type=text />фыва.&%\ <b>t_e_st</b>:sunglasses:<img src=x onerror=alert(xss)/>

Это универсальная проверка сразу на несколько кейсов.Если часть символов исчезла (например, в поле поиска) или не записалась в БД (если это поле имени пользователя при регистрации, например), то уязвимость есть. Все символы не должны восприниматься, как часть исполняемого поля. Если часть символов исчезла (например, в поле поиска) или не записалась в БД (если это поле имени пользователя при регистрации, например), то уязвимость есть. Все символы не должны восприниматься, как часть исполняемого поля.

В конце вводим скрипт или спецсимволы в окно фронтенда вставляем прям в код сайта через dev tools в браузере.

Напутствие

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

Думай, как хакер! Пытайся получить выгоду или навредить. Но только на своём проекте. Следи за новостями безопасности. Смотри реестр уязвимостей, читай статьи, которые пишут фирмы по обеспечению безопасности и мониторь новости про утечки данных. Обязательно пытайся разобраться в сути. И будь на стороне добра! :)

Подробнее..

Как работать тестировщику выстраиваем планирование, общаемся с командой разработки и проверяем сайты на безопасность

03.03.2021 14:04:54 | Автор: admin

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

Современные паттерны тестирования

Рассказывает директор по развитию бизнеса в IT-компании @BSL_Dev и ex-руководитель отдела обеспечения качества Redmadrobot Марина Куликова @Marishunya_QA.

Коротко в чем суть.

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

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

Основная цель тестирования своевременно оповещать команду о реальном состоянии системы или продукта

Основные точки опоры для построения тестирования

  1. Анализ требований или технического задания.

  2. Инфраструктура важно настроить окружение, выбрать целевые устройства, какие тестовые данные потребуются.

  3. Процесс коммуникации нужно договориться, в каком формате мы выстраиваем обратную связь с командой (например, делаем отчеты о тестировании в Jira).

  4. Разбираемся, как именно организовываем тестирование: какие виды тестирования, на каких этапах применяем, как распределяем ресурсы, планирование и так далее.

  5. Уделяем время написанию отчетности и договариваемся, какая отчетность и в каком виде нужна.

  6. Постоянно внедряем улучшения и анализируем изменения.

О чем тестировщик должен сообщать команде

  • Что нужно автоматизировать. В процессе коммуникации с разработчиками и менеджерами тестировщику нужно определить и рассказать, какие тесты должны быть автоматизированы и на каком уровне.

  • Как оптимизировать работу. Нужно делать постоянную оптимизацию тестов, чтобы они затрачивали меньше времени. Задача тестировщика минимизировать время выполнения тестов, оптимизировать фича тесты и регресс. Это нужно, для того чтобы быстро получать информацию о состоянии продукта.

  • Обеспечить полезные метрики для бизнеса. Например, количество багов на продакшене, количество активностей, как двигается процесс написания документации, время, затраченное на тестирование и т.д. Такие метрики будут сигнализировать, всё ли у проекта хорошо. Кроме того, с помощью них тестировщик может показать результаты работы всей команды. Но стоит помнить, что не стоит закапываться в метриках, а нужно использовать скорее их как опорные точки. Ведь, помимо метрик, есть и другие системы отчетности.

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

Тестирование это как круговая оборона. Весь продукт охранять очень сложно, поэтому тестировщики создают датчики (некие красные флажки), которые в нужный момент сообщают: alarm! И подобные датчики это метрики, а также автотесты, различные приемы и т.д. Задача команды тестирования выстроить многослойную систему защиты, состоящую из нужных датчиков. Также стоит помнить, что помимо того, что QA тестирует, команда ещё сообщает реальное состояние системы, где все окей, где начинает рваться, а где все плохо и нужно срочно исправлять.

Как тестировщику обеспечивать обратную связь для команды

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

  2. С помощью Code Reviews. Это не совсем работа тестировщика, но в проекте через Code Reviews можно получить обратную связь. Например, если у нас фича автоматизации типовая, и она долго ревьюится на разных проектах, то нужно выяснить причину.

  3. Через модульные тесты (Unit tests).

  4. С помощью автоматизированных интеграционных тестов (Automated Integration Test).

  5. С помощью автоматизированных Acceptance Tests. Эту активность можно разделить с продакт-менеджерами.

  6. По возможности следует автоматизировать Regression Test.

  7. Постоянный Exploratory Testing.

  8. Обратная связь от пользователей или бизнес-юзеров.

  9. Постоянное UAT-тестирование + DEMO-сессии.

Через что тестировщик может организовать обратную связь с командой

  • С помощью работы с дефектами (багами) нужно определить формат их заведения и оповещения о них, например, делать это в Jira или другом удобном инструменте. Разработчикам не всегда хватает информации в дефекте, чтобы приступить к фиксу, поэтому иногда требуется дополнительная коммуникация с багрепортером.

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

  • Кросс-обучение внутри тестирования. Все специалисты разные, они могут учиться друг у друга, поэтому важно иногда собираться и обсуждать результаты работы и делиться лайфхаками и знаниями продукта. Это некий knowledge transfer.

  • Тест-документация собирать отчетность по фичам, сборкам и по приемке. Для составления отчетности поможет изучение ГОСТов: в них описаны градации дефектов, как с ними быть и так далее. Для проектов, связанных с государственным подрядом, поможет изучение ГОСТ 34.603-92.

Как тестировщикам работать с Google-таблицами (и зачем)

Руководитель отдела тестирования Redmadrobot Саша Строкин собрал собственные Google-таблицы, с помощью которых он выстраивает работу, начиная от планирования и заканчивая аналитикой для тестировщиков.

На нескольких примерах Саша рассказал об инструментах и формулах, которые он использует в работе.

Google-таблицы при планировании подготовка к процессу тестирования

В тестировании есть четыре главных процесса:

  1. Планирование подготовка к самим работам по тестированию,

  2. Test development крафтинг артефактов, разработка сценариев тестирования,

  3. Test execution само выполнение тестирования,

  4. Test analysis оценка результатов тестирования, выделение процессов, которые нужно улучшать или, наоборот, не стоит менять.

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

Чтобы облегчить процесс планирования, в Sheets можно создать вкладку Dictionary, где описываются все существующие проекты для работы, список участников, роль инженера на проекте и так далее.

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

Интеграция Google-таблиц с Jira

Интеграция Excel c рабочим инструментом большинства команд разработки и тестирования Jira возможна через специальный плагин Jira Cloud of Sheets.

C помощью этого плагина можно вытаскивать любые данные из бэклога Jira по тому же фильтру, по которому обычно тестировщики фильтруют дефекты, только с переводом не на графическое изображение, а на JQL.

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

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

Для формирования такой статистики нужно воспользоваться вкладкой MounthStat (она вытягивает данные о дате создания из общей выгрузки, где мы можем выбрать дату создания дефекта). С помощью функции Trim даты можно рассортировать по месяцам.

Все готовые таблицы для работы из презентации Саши.

Тестирование и безопасность web-сайтов для начинающих тестировщиков

Рассказала и показала QA-инженер Redmadrobot Вика Бегенчева @vikusti.

Основные уловки мошенников, как они могут навредить пользователям или системам:

С помощью cookies

Вика рассказала, что такое cookies на примере интернет-магазина. Допустим, мы открываем браузер, заходим на сайт и кладем в корзину печеньки, которые нам понравились. Если мы закроем окно браузера, а затем откроем заново, то вся информация про начавшиеся покупки сохранится. Это происходит с помощью cookies различной информации о пользователе, которая хранится в браузере локально.

Эта информация нужна для удобства пользователя, чтобы сайт запоминал определенные данные и нам не приходилось постоянно их указывать. Cookies бывают двух видов: временные (cookies-сессии) и постоянные. Cookies-сессии хранятся определенное ограниченное время и могут меняться, когда пользователь перелогинился. Постоянные сookies хранятся всегда, пока вы их не сотрете.

Как хакеры могут использовать cookies

Хакер может угнать ваши cookies и с помощью этого доказать системе, что он это вы. Тогда он сможет переиспользовать их и продолжить сессию. Это происходит так:

Через протоколы: HTTP и HTTPS

Копаясь на просторах интернета, мы до сих пор можем попадать на сайты, о небезопасности которых нас предупреждает браузер.

Почему так? Потому что браузер умный, и он считает ненадежным сайты, в которых происходит соединение по HTTP, а не HTTPS. В протоколе HTTPS есть последняя буква S, это значит, что добавляются повышенные требования к безопасности. В этом протоколе при общении браузера с сервером по протоколу https добавляется сертификат безопасности: если хакер попробует перехватить такие запросы, он получит лишь набор символов и не сможет их расшифровать.

Подбор пароля Brute force

Это атака перебора мошенник может знать логин и с помощью специального скрипта подбирать пароль. Обычно подбор пароля с помощью скрипта занимает около 10 часов.

Как проверять сайты на безопасность

  1. Посмотреть, как устроена безопасность хранения cookies: открываем Инспектор в браузере, заходим в вкладку application и видим свои cookies для сайта. Для проверки безопасности нужно обратить внимание на столбцы под названием httpOnly и secure. Если галочки стоят, то на сайте предусмотрена защита от угона cookies.

  2. Нужно проверять, чтобы все запросы шли через протокол HTTPS. Например, работаем с сайтом диванынадом.ру. Нужно убрать букву S из протокола и проверить, можно ли зайти только по ссылке с HTTP. Если да, то это плохо, мошенник может создать такую ссылку и перехватывать запросы пользователей. Чтобы этого избежать, нужно создать редирект автоматически перебрасывать пользователей на безопасную страницу.

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

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

Полезные ссылки

  • Telegram-канал Google Таблицы: много информации о фишках Google Sheets;

  • YouTube STM Solutions: видеоуроки Google Docs, Google Sheets, Google Apps Script;

  • Jira Cloud for Sheets: плагин для интеграции Google Sheets с основным рабочим инструментом большинства команд Jira;

  • owasp.org: некоммерческий фонд, который работает над повышением уровня безопасности ПО;

  • hackthebox.eu: тренировочная онлайн-платформа, на которой можно проверить навыки тестирования в сфере безопасности сайтов;

  • xss-game.appspot.com: тренировочная игра для обнаружения и устранения ошибок XSS.

Подробнее..

Дизайнеру приложений как создать и передать в разработку тёмную тему

16.07.2020 12:18:27 | Автор: admin


В конце 2019 года зарелизили iOS 13 и Android 10 с поддержкой автопереключения на тёмную тему. Мы решили добавить её в приложение Ростелеком Ключ под iOS и Android, над которым работали в тот момент. В процессе не обошлось без сложностей. Рассказываем о нашем опыте, чтобы вы в аналогичной ситуации сэкономили время и нервы.

Зачем делать тёмную тему


Может показаться, что это всё на волне хайпа. Но не только :)

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

Если мы выходим из тёмного помещения в светлое, то ненадолго слепнем, но привыкаем к новым условиям уже через 46 минут. Когда же мы переходим из светлого места в тёмное, адаптация занимает больше времени: в среднем 3045 минут. Если человеку постоянно приходится переводить взгляд между ярким приложением и тёмным окружением, адаптация к яркому дисплею будет быстрой, а вот обратная адаптация к темноте медленной.

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

В нашем случае приложение РТ Ключ кросс-платформенный сервис для управления устройствами на территории дома: домофонами, шлагбаумами, камерами видеонаблюдения во дворе и в подъезде. Жители домов используют его как днём, так и ночью.

Справедливо заметить, что тёмная тема также помогает экономить заряд (для некоторых типов экранов: OLED/amoled да, LCD нет). А в долгосрочной перспективе она может замедлить развитие близорукости.

Как перейти на тёмную сторону: пошаговая инструкция


Если вы совсем ничего не знаете о тёмной теме, то можно начать знакомство со статей в Human Interface Guidelines для iOS и в Material Guide для Android. Там подробно разобрано, как цвета и слои взаимодействуют друг с другом в ночном режиме. Перейдём к нашим советам:

1. Приведите в порядок макеты и соберите UI kit

UI kit серьёзно ускоряет работу дизайнера и разработчиков. Обычно мы собираем его сразу после утверждения визуальной концепции на примере нескольких основных экранов приложения. В UI kit входят:

  • Цветовая палитра стилей для обычного состояния элементов, состояния при нажатии, цвета для неактивных элементов, инпутов в фокусе и так далее.
  • Текстовые стили.
  • Все элементы интерфейса (кнопки, поля ввода, элементы списков, блоки с заголовками и так далее) в различных состояниях и ситуациях в виде master components.
  • Сет иконок в черном цвете.
  • Иллюстрации.

UI kit нашего приложения можно рассмотреть в Figma.

Все последующие экраны собираем уже из готовых компонентов. Но при экспорте макетов из Sketch в Figma наш UI kit серьёзно пострадал: пришлось его обновить, заново назначить цветовой стиль для каждого элемента и пересобрать экраны из компонентов.

Все последующие экраны собираем уже из готовых компонентов. Но при экспорте макетов из Sketch в Figma наш UI kit серьёзно пострадал: пришлось его обновить, заново назначить цветовой стиль для каждого элемента и пересобрать экраны из компонентов.

2. Договоритесь о названиях цветов

Чтобы дизайнерам, iOS и Android-разработчикам было проще общаться между собой, цвета мы решили назвать универсально для обеих платформ. В прошлой версии UI kit цветовые стили мы обозначили незатейливо по номерам: C1, C2, C3 Это было не слишком удобно: при обсуждении все называли цвета не по цифрам, а по оттенкам: фиолетовый, оранжевый, чёрный и т. д.

Однако с появлением тёмной темы использовать оттенки в наименовании цветов стало невозможно: например, белый цвет фона в светлой теме сменился на тёмный. Значит, нужно было придумать новый принцип наименований.

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

  • Назначение цвета или элемент, на котором он используется.
  • Приоритетность использования (опционально).
  • Состояние элемента, если это применимо (опционально).


Слева наименования для цветов кнопки в обычном состоянии, при нажатии на iOS, цвет Ripple в Android и неактивная кнопка на обеих платформах. Справа имена для текстов на различных поверхностях

В общем, если в вашем проекте ещё нет UI kit, а вместо цветовых стилей назначены обычные цвета, пора причесать макеты.

3. Подберите цвета для тёмной темы

Катя Рокитян, дизайнер Redmadrobot:
Чтобы не тратить драгоценное время, мы не прорабатывали все макеты в тёмной теме, а выбрали только несколько экранов с принципиально различающимися компонентами. С их помощью мы подбирали удачные цвета и добавляли их в палитру. Затем разработчики обновляли цвета в коде приложения, готовили для нас тестовую сборку, а мы изучали, как тёмная тема смотрится живьём. Если выявляли ошибки, правили макеты и палитру.

3.1. Фоновый цвет

Начать перерисовку макета стоит с фонового цвета: он занимает большую часть экрана. В гайдах Material Design советуют брать за основу нейтральный тёмно-серый (#121212). Он, в отличие от черного, оставляет простор для построения глубины экрана.

Фоновый цвет должен хорошо сочетаться с цветом интерактивных элементов: кнопок, иконок и т. д. У нас в светлой палитре для интерактивных элементов использовался фирменный фиолетовый цвет Ростелекома #7700ff. В дальнейшей работе отталкивались от него.

Нейтральный тёмно-серый плохо смотрелся с брендовым фиолетовым, поэтому мы последовали советам гайдлайнов Material Design. Ребята рекомендуют наложить поверх нейтрального фонового серого #121212 фирменный цвет с 8% непрозрачности.


Нейтральные и брендированные цвета фона в тёмной теме

Помимо основного фона у нас встречаются карточки с небольшим подъемом над поверхностью. В Material Design рекомендуют в тёмной теме создавать элевацию элементов за счёт осветления фона: чем ближе карточка к пользователю, тем светлее.

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



3.2. Создание базовой палитры

В Material Design рекомендуют при создании брендированной тёмной темы заменять цвета на менее насыщенные аналогичного оттенка. В качестве фирменного цвета для РТ Ключ мы использовали фиолетовый.

При попытке осветлить его по рецепту Material Design его оттенок стал розоватым. Оригинальный фирменный цвет выглядел синее, поэтому мы вручную изменили его оттенок.


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

В светлой теме мы использовали один фиолетовый цвет для больших кнопок, линейных иконок и навигационной панели (navbar). При подборе цветов для тёмной темы мы не смогли найти фиолетовый оттенок, который бы одинаково удачно смотрелся на всех этих формах.

Navbar и крупные кнопки в осветленной фиолетовой версии особенно сильно отвлекали внимание от контента, а если мы снижали яркость, линейные иконки терялись на тёмном фоне.


В исходной светлой теме насыщенный фиолетовый цвет одинаково хорошо смотрится на крупных блоках с белым текстом и на тонких линейных иконках на светлом фоне. А на тёмном фоне всё не так

Пришлось как в тёмной, так и в светлой теме заменить один исходный цвет на три: один для контурных иконок, второй для больших кнопок с фоном, третий для уголка на главном экране и navbar на остальных.


Заменили один фиолетовый на три так намного лучше

Помимо фиолетового мы использовали оранжевый, желтый, бирюзовый и красный. Оранжевый для call to action элементов, желтый для уведомлений, бирюзовый для подтверждения операции, красный для ошибок.

Когда мы осветлили эти четыре цвета по совету Material Design, серьезно изменилась контрастность приложения по сравнению со светлой темой. Пришлось отступить от гайдов: оранжевый, бирюзовый и красный мы не осветлили, а затемнили. Жёлтый оставили неизменным, поскольку при затемнении он грязнил.



3.3. Особенности палитры iOS

В iOS можно задать оттеночный цвет (он же tint), которым будут окрашены все интерактивные элементы: кнопки, иконки, переключатели и т. д. Например, в настройках iPhone используется синий цвет, а в приложении часы оранжевый.

В светлой теме у нас tint совпадает с цветом больших кнопок, а в тёмной теме мы сделали его чуть светлее, чтобы линейные иконки были хорошо различимы на тёмном фоне.


Оттеночный цвет (tint) в темной теме

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

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

3.4. Особенности палитры Android

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

Your browser does not support HTML5 video.

Ripple в Android приложении

Также в Android есть особенности отрисовки status bar и navigation bar. Status bar строка состояния, где отображаются уведомления, уровень сигнала, заряд батареи и время. Navigation bar панель, где располагаются кнопки назад, домой и недавние приложения.

С появлением смартфонов с крупными дисплеями, доходящими до самого края устройства, челками и дырками под камеру, в Material Design представили концепцию edge to edge. Её суть в том, что служебные элементы не должны съедать полезную площадь, и как можно большее место на экране должен занимать полезный контент.

Для этого в Android доработали поддержку прозрачных status bar и navigation bar. Дело в том, что эти панельки не на всех телефонах имеют одинаковую высоту. И если до начала поддержки edge to edge мы назначали им прозрачный фон, на некоторых устройствах они некрасиво накладывались на контент экрана. Теперь в материальных компонентах появились системные отступы: разработчики могут определять размер status bar и navigation bar и задавать соответствующий отступ для контента. Поэтому раньше в Material Design рекомендовали выбирать непрозрачный фон для status bar и navigation bar, а теперь наоборот.

Однако важно учитывать, что не во всех поддерживаемых версиях Android можно назначить цвет иконок в системных компонентах:

  • до 6.0 иконки в status bar и navigation bar всегда белые;
  • с версии 6.0 можно задать, белыми или черными будут иконки в status bar, но navigation bar будет вести себя так, как в предыдущих версиях.
  • с версии 8.1 можно выбрать цвет иконок как в status, так и в navigation bar.

Чтобы избежать странных наложений на контент в Ключе, мы поступили так:

  • в старых версиях Android до 6.0 для обеих панелек задали черный фон с 50% прозрачности на нем хорошо смотрятся белые иконки;
  • с 6.0 и до 8.1 navigation bar остается с полупрозрачным черным фоном, а status bar полностью прозрачный;
  • с версии 8.1 фон обеих панелек полностью прозрачный.

Если вы по каким-то причинам не готовы к такой поддержке edge to edge, лучше сделать status bar и navigation bar универсальными. Поддержка edge to edge:

3.5. Проверьте контрастность элементов

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

Лучше всего проверять контрастность с помощью тестирования: открыть интерфейс на смартфоне, используя Figma Mirror или аналогичный софт, выйти на яркое солнце (тёмная тема предназначена для использования в темноте, но людям ничто не помешает использовать её на свету).

Также будет полезно попросить посмотреть на приложение людей с нарушениями зрения (близорукостью, дальнозоркостью, дальтонизмом). Но если такой возможности нет, контрастность можно проверить на сайте contrast-ratio или с помощью плагина в Figma.

3.6. Иллюстрации и анимации
Если в вашем приложении используются иллюстрации, для тёмной темы их придётся перекрашивать. Насыщенные цвета, которые отлично смотрятся на светлых поверхностях, на тёмных режут глаз и затрудняют восприятие. Используйте менее насыщенные тона.


Для тёмной темы приходится делать отдельные версии иллюстраций и анимаций в тёмных цветах

Как передавать в разработку

Мы работали в связке Figma + Zeplin. Это может показаться странным, но мы всей компанией перешли на Figma из Sketch в конце лета 2019, прямо перед началом работы нам тёмной темой. И чтобы сэкономить время на адаптацию разработчиков к новому инструменту, продолжили работать с Zeplin. И тут он преподнес нам несколько сюрпризов.

В палитре Zeplin нельзя создать цветовые стили с одинаковыми HEX. Поэтому нам пришлось незначительно, практически незаметно для глаза, менять HEX у фиолетового цвета в светлой теме.
Даниил Субботин, iOS-разработчик Redmadrobot subdan:
Обнаружилось, что ни один инструмент дизайнера, в том числе Zeplin, не поддерживает темную тему и поэтому не позволяет иметь несколько цветовых палитр в одном проекте. Пришлось искать пути обхода. Например, мы создали два проекта: один со светлой палитрой, а другой с тёмной.


1. Особенности iOS dev


Следующий сюрприз, который нам преподнес Zeplin: из него можно экспортировать только одну палитру в Xcode-проект. Либо светлую, либо тёмную. При попытке экспортировать цвета тёмной палитры Zeplin просто заменял ими все светлые. Мы стали переносить цвета вручную, копируя HEX-значения. То же самое делали и с изображениями.

Даниил Субботин, iOS-разработчик Redmadrobot subdan:
После долгих мучений я написал утилиту, которая выгружает обе цветовые палитры прямо в Xcode-проект, используя Zeplin API. Это сильно упростило жизнь. Дизайнер сообщает, что добавил новый цвет или изменил старый, я запускаю скрипт, и все изменения автоматически подтягиваются в проект

2. Особенности Android dev


На Android тёмная тема доступна на всех версиях ОС (мы тестировали начиная с пятой). Но переключать её в системе можно только на 10-м Android, поэтому стоит добавить в приложение возможность переключать её вручную.

Владислав Шипугин, android-разработчик Redmadrobot shipa_o:
Мы добавили следующие варианты выбора темы: всегда светлая, всегда тёмная, выбирается в зависимости от режима энергосбережения (9-я версия андроида и ниже), переключается в зависимости от настроек системы (10-я версия андроида и выше). Но важно учесть, что выбранная пользователем тема приложения в системе не сохраняется. Нужно запоминать её внутри и активировать при каждом запуске приложения


В Android есть своя система цветов для материальных компонентов (кнопок, app bar, текстовых полей и т. д.). Гайдлайны Material Design о цвете.

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

Как поддерживать и развивать


С тех пор как мы проработали тёмную тему, у нас добавилось несколько новых функций, появились планшетные пользователи, а продуктовый офис в Ростелекоме задумался о том, чтобы выпустить приложение РТ Ключ в виде white label для застройщиков в их фирменных цветах.

Оля Сартакова, арт-директор Redmadrobot:
Мы поняли, что текущую версию дизайна, разработанную для запуска MVP, пора переосмыслить как структурно, так и визуально. Мы полностью переработали структуру приложения с учетом тех функций, которые должны появиться в ближайшие два года, добавили дашборд для быстрого доступа к самым популярным функциям, избавились от ярких фирменных элементов в оформлении (вроде уголка на главном экране и цветного nav bar). А вот грамотная работа над цветовой системой позволила практически полностью сохранить её при тотальном редизайне приложения.

Вторая версия дизайна приложения в той же цветовой палитре:


Даниил Субботин, iOS-разработчик Redmadrobot subdan:
К моменту запуска второй версии мы безболезненно переехали на Figma, я адаптировал утилиту, которую изначально написал для Zeplin, под Figma. Теперь мы обновляем цвета, иконки и картинки в Xcode и Android Studio в один клик


Скачать нашу утилиту можно здесь.

Выводы: как у нас, только лучше


Если в вашем приложении нужна тёмная тема, её лучше всего заложить на старте, сразу после утверждения визуальной концепции. Это поможет избежать почти всех проблем, с которыми столкнулись мы. Как это делать:

  • Соберите UI kit.
  • Продумайте систему наименования цветов или воспользуйтесь нашей.
  • Подберите брендированный фоновый цвет.
  • Подберите оттенок для основного и второстепенных фирменных цветов.
  • Если ваше приложение кроссплатформенное, составьте список отличий цветовых схем iOS и Android. А также проверьте, не нужно ли добавить в приложение на Android ручную настройку тёмной темы.
  • Проверьте контрастность выбранных цветов.
  • Подготовьте иллюстрации и анимации для тёмной темы.
  • Обновите UI kit.
  • Воспользуйтесь нашей утилитой для передачи элементов в разработку.

Полезные материалы


Чтобы ещё глубже погрузится в тёмную тему:


И немного полезностей для разработчиков:

Подробнее..

Быстрый старт гайд по автоматизированному тестированию для Android-разработчика. JVM

14.12.2020 14:08:42 | Автор: admin

Привет! Меня зовут Сергей Иванов, я ведущий разработчик Android в Redmadrobot. С 2016 использую автотесты различных категорий и успел в этом набить немало шишек. Именно поэтому решил поделиться опытом. Возможно, что кому-то статья поможет систематизировать знания или начать применять эту практику в работе.

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

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

Подробнее расскажу про тесты на JVM, а не про UI-тесты, о которых в последнее время пишут часто. Материал будет хорошей отправной точкой для изучения темы, а также поможет дополнить уже имеющиеся знания.

Дисклеймер: статья получилась большой, поэтому указал основные темы, которые рассмотрю.

  • базовые понятие автоматизированного тестирования;

  • категории тестов их специфика на Android;

  • как писать тестируемый код;

  • как и какие инструменты использовать для тестирования;

  • как писать полезные и поддерживаемые тесты;

  • что тестировать;

  • как и когда применять методологию Test Driven Development.


При производстве приложений автотесты помогают:

  1. Находить баги на раннем этапе разработки. Это позволяет раньше устранять проблемы, при этом расходуя меньше ресурсов.

  2. Локализовать проблему. Чем более низкоуровневым является тест, тем более точно он способен указать на причину ошибки.

  3. Ускорить разработку. Это вытекает из предыдущих пунктов и из того, что благодаря автотестам разработка разных частей фичи может быть оперативно разделена на несколько разработчиков. Установив контракты между компонентами приложения, разработчик может разработать свой компонент и проверить его корректность при отсутствии остальных (например, при полном отсутствии UI).

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

Но есть и проблемы:

  1. Нужно время на внедрение, написание и поддержку.

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

Важные базовые понятия автоматизированного тестирования

System Under Test (SUT) тестируемая система. В зависимости от типа теста системой могут быть разные сущности (о них подробнее написал в разделе категории тестов).

Для различия уровня тестирования по использованию знаний о SUT существуют понятия:

Black box testing тестирование SUT без знания о деталях его внутреннего устройства.

White box testing тестирование SUT с учётом деталей его внутреннего устройства.

Выделяют также Gray box testing, комбинацию подходов, но ради упрощения он будет опущен.

Для обеспечения базового качества автотестов важно соблюдать некоторые правила написания. Роберт Мартин сформулировал в книге "Clean Code" глобальные принципы F.I.R.S.T.

Fast тесты должны выполняться быстро.

Independent тесты не должны зависеть друг от друга и должны иметь возможность выполняться в любом порядке.

Repeatable тесты должны выполняться с одинаковым результатом независимо от среды выполнения.

Self-validating тесты должны однозначно сообщать о том, успешно их прохождение или нет.

Timely тесты должны создаваться своевременно. Unit-тесты пишутся непосредственно перед кодом продукта.

Структура теста состоит как минимум из двух логических блоков:

  • cовершение действия над SUT,

  • проверка результата действия.

Проверка результата заключается в оценке:

  • состояния SUT или выданного ею результата,

  • cостояний взаимодействующих с SUT объектов,

  • поведения (набор и порядок вызовов функций других объектов, которые должен совершить SUT, переданные в них аргументы).

При необходимости также добавляются блоки подготовки и сброса тестового окружения, отчасти связанные с первыми тремя принципам F.I.R.S.T.

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

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

Зачастую для настройки окружения применяются тестовые дублеры.

Test doubles (Тестовые дублёры) фиктивные объекты, заменяющие реальные объекты, от которых зависит SUT, для достижения целей теста.

Тестовые дублеры позволяют:

  • зафиксировать тестовое окружение, имитируя неважные, нереализованные, нестабильные или медленные внешние объекты (например, БД или сервер),

  • совершать проверки своих вызовов (обращений к функциям, свойствам).

Самая популярная классификация включает 5 видов тестовых дублеров, различных по своим свойствам: Dummy, Fake, Stub, Spy, Mock.

Stub объект, который при вызовах его функций или свойств возвращает предустановленные (hardcoded) результаты, а не выполняет код реального объекта. Если же функция не имеет возвращаемого значения, то вызов просто игнорируется.

Mock объект, позволяющий проверять поведение SUT путём отслеживания обращений к функциям и свойствам объекта: были ли в ходе теста вызваны функции мока, в правильном ли порядке, ожидаемые ли аргументы были в них переданы и т.д. Может также включать функциональность Stub.

Почитать об этих и остальных видах дублеров можно в первоисточнике.

Эта классификация не является стандартом, и в фреймворках для создания тестовых дублёров часто ради удобства API несколько типов обобщают термином Mock. А вот чем они на самом деле будут являться, зависит от их последующей конфигурации и применения в тесте. Например, при использовании фреймворка Mockito, экземпляр тестового дублера может быть создан как Dummy, а потом превращен в Stub и в Mock.

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

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

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

Категории тестов

Есть разные версии категоризации тестов, по разным характеристикам, поэтомусуществует некоторая путаница.

Покажу основные категории уровней тестов, на которых тестируется система, на примере одного из самых распространенных вариантов пирамиды тестирования:

Unit-тесты проверяют корректность работы отдельного unit-а (модуля). Unit-ом (то есть SUT данного типа тестирования) может быть класс, функция или совокупность классов.

Integration-тесты (в приложении) проверяют корректность взаимодействия модулей или наборов этих модулей (компонентов). Определение SUT данной категории является еще более расплывчатым, т.к. в свою очередь зависит от того, что считается модулем.

Грань между Unit- и Integration-тестированием довольно тонкая. Интеграционными тестами, в зависимости от масштаба и контекста, в принципе могут называть тесты, проверяющие взаимодействие чего-либо с чем-либо с определенной долей абстракции: приложение(клиент)-сервер, приложение-приложение, приложение-ОС и др. Но в дальнейшем я буду говорить об интеграционном тестировании в рамках приложения.

End-to-end-тесты (E2E) интеграционные тесты, которые воздействуют на приложение и проверяют результат его работы через самый высокоуровневый интерфейс (UI), то есть на уровне пользователя. Использование тестовых дублеров на этом уровне исключено, а значит обязательно используются именно реальные сервер, БД и т.д.

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

Вернёмся к категориям. В Android сложность категоризации автотестов усугубляется еще и тем, что они могут работать на JVM или в Instrumentation-среде (эмулятор или реальное устройство). Последние называют инструментальными.

Чтобы было удобнее ориентироваться в видах тестов, не путаясь в терминологии, предлагаю такую категоризацию для мобильного приложения на Android:

JVM Integration tests интеграционные тесты, проверяющие взаимодействие модулей или совокупностей модулей без использования Instrumentation. Характеризуются они высокой скоростью исполнения, сравнимой с Unit-тестами, также выполняющимися на JVM.

Instrumentation Integration non-UI tests интеграционные тесты, исполняемые уже в реальной Android-среде, но без UI.

Component UI tests интеграционные инструментальные тесты с использованием UI и фиктивных сервера и БД, если таковые требуются. Тест может состоять как из одного экрана, запущенного в изоляции, так и из нескольких экранов с соблюдением их реального флоу.

E2E UI tests интеграционные инструментальные UI-тесты без тестовых дублеров только с реальным флоу экранов. Максимально приближены к ручным тестам.

Если Unit-тесты являются сильно завязанными на детали реализации, очень быстро выполняются, относительно легко пишутся и наиболее точно при поломке указывают на причину ошибки, то в случае E2E UI ситуация противоположная. Изменение этих характеристик происходит постепенно от низа к верху пирамиды.

При переходе от тестов на JVM к тестам на Instrumentation из-за использования настоящей Android-среды происходит резкое падение скорости выполнения этих тестов. Это становится серьезным ограничением. Особенно когда тесты необходимо запускать часто и много раз подряд. Поэтому к написанию инструментальных тестов следует прибегать лишь в случаях, когда использование настоящих Android-зависимостей действительно необходимо.

UI-тесты

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

Часто они оказываются нестабильны в своём поведении и могут то выполняться, то падать, даже если не вносилось никаких изменений в реализацию (нестабильные тесты называют Flaky). Мало того, UI-тесты могут совершенно по-разному себя вести на разных устройствах, эмуляторах и версиях Android. Когда же UI-тесты являются еще и E2E, добавляется хрупкость и снижается скорость выполнения из-за реальных внешних зависимостей. Причем в случае ошибки найти её причину бывает затруднительно, поскольку проверки в таких тестах осуществляются на уровне состояния UI. В таких ситуациях выгоднее обойтись силами QA-инженеров.

Конечно, UI-тесты способны приносить и весьма существенную пользу. Мобильные приложения имеют свойство разрастаться, и в какой-то момент их ручное регрессионное тестирование выходит за адекватные временные рамки. Тогда часть проверок может быть делегирована E2E UI-тестам, что при удачном исполнении может здорово сократить время тестирования.

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

Unit-тесты

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

Степень хрупкости же можно снизить за счет использования black box-стиля написания даже на этом уровне, когда возможно. Но не следует злоупотреблять применением тестовых дублеров: если уже реализованная сущность имеет тривиальную логику или наличие логики не подразумевается, стоит использовать ее настоящую реализацию.

А заменять дублером следует только то, что действительно необходимо для приемлемой изоляции SUT в конкретном случае. Иногда (но далеко не всегда!) бывает оптимальнее сделать переиспользуемый рукописный дублер, чем конфигурировать его фреймворком для создания дублеров в множестве мест.

Хочу отметить, что какими бы хорошими не были автотесты, полностью отказываться от ручного тестирования нельзя. Человеческий глаз и смекалка пока что незаменимы.

Подытожим

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

  • Ручные тесты самые достоверные и важные тесты. Unit-тесты, имеющие меньше всего общего с ручными, могут позволить проверить такие ситуации, краевые кейсы, которые проверять вручную будет чрезвычайно дорого. Unit-тесты являются наиболее важными среди автоматизированных.

  • Лучше делать акцент на быстро выполняющиеся тесты. Так, после Unit-тестов рекомендую проверять JVM Integration-тестами интеграцию в том масштабе, который можно комфортно обеспечить без использования Instrumentation от ViewModel до слоя данных.

Дальше я буду говорить преимущественно о тестах на JVM. Но некоторые моменты актуальны и для остальных категорий.

Инструментарий

Раньше для написания JVM-тестов наши разработчики использовали фреймворки Junit 4 и Junit 5, но потом переключились на молодой перспективный Spek 2. Junit 4 нужен для инструментальных тестов с другими фреймворками они не работают.

Для проверок (assert) используем AssertJ отличную библиотеку с богатым набором читабельных ассертов и удобных дополнительных функций.

Для создания тестовых дублеров применяем Mockito-Kotlin 2 Mockito 2, адаптированный для Kotlin.

Для стаббинга и мокирования сервера MockWebServer библиотеку от Square, рассчитанную на работу с OkHttp.

Фреймворки PowerMock и Robolectric не используем из соображений скорости выполнения тестов и их надёжности. Кроме того, эти фреймворки поощряют плохо пахнущий код это дополнительные зависимости, без которых вполне можно обойтись. Для этого код должен быть тестируемым.

Дизайн кода

Признаки нетестируемого кода:

  • Наличие неявных зависимостей, сильная связанность. Это затрудняет изолированное unit-тестирование, тестирование на раннем этапе развития фичи, распараллеливание разработки. Использование статических функций, создание сложных объектов внутри класса, ServiceLocator исключают возможность использования тестовых дублеров.

  • Обилие Android-зависимостей. Они требуют Instrumentation или объемную подготовку среды на JVM с тестовыми дублерами, если их использование вообще возможно (см. прошлый пункт).

  • Наличие явного управления асинхронным и многопоточным поведением. Если результат работы SUT зависит от выполнения асинхронной работы, особенно порученной другому потоку (или нескольким), то не получится просто так гарантировать правильность и стабильность выполнения тестов. Тест может совершить проверки и завершиться раньше, чем асинхронная работа будет выполнена, и результат не будет соответствовать желаемому. При этом принудительное ожидание в тестах (в первую очередь на JVM) плохая практика, поскольку нарушается принцип Fast.

Пример
class ExampleViewModel constructor(val context: Context) : BaseViewModel() {    private lateinit var timer: CountDownTimer    fun onTimeAccepted(seconds: Long) {        val milliseconds = MILLISECONDS.convert(seconds, SECONDS)        // Неявная зависимость, Android-зависимость, запуск асинхронной работы        timer = object : CountDownTimer(milliseconds, 1000L) {            override fun onTick(millisUntilFinished: Long) {                showTimeLeft(millisUntilFinished)            }            override fun onFinish() {                // Неявная зависимость. Вызов статической функции с Android-зависимостью                WorkManager.getInstance(context)                    .cancelUniqueWork(SeriousWorker.NAME)            }        }        timer.start()    }

Как сделать код тестируемым

Следовать принципам SOLID, использовать слоистую архитектуру. Грамотное разделение и реализация сущностей позволит писать изолированные тесты именно на интересующую часть функционала, не допускать чрезмерного разрастания тестового файла и, при необходимости, осуществлять распараллеливание разработки. DI позволит подменять настоящие реализации тестовыми дублёрами.

Стремиться к чистоте функций. Это функции, которые:

  1. При одинаковом наборе входных данных возвращают одинаковый результат.

  2. Не имеют побочных эффектов, т.е. не модифицируют внешние переменные (класса, глобальные) и переданные в качестве входных данных параметры.

Пример теста такой функции:

val result = formatter.toUppercase("адвокат")assertThat(result).isEqualTo("АДВОКАТ")

Минимизировать количество Android-зависимостей. Часто прямое использование Android-зависимостей в SUT не является необходимым. Тогда их следует выносить вовне, оперируя в SUT типами, поддерживающимися на JVM.

Самая распространенная Android-зависимость в потенциально тестируемых классах ресурсы, и их выносить из, скажем, ViewModel, ну, совсем не хочется. В таком случае можно внедрить Resources во ViewModel, чтобы стаббить конкретные ресурсы (их id актуальны на JVM) и проверять конкретные значения:

mock<Resources> { on { getString(R.string.error_no_internet) } doReturn "Нет интернета" }

Но лучше поместить Resources во Wrapper, предоставляющий только необходимый функционал работы с ресурсами, и сделать его тестовую реализацию. Это избавит SUT от прямой зависимости от фреймворка и упростит подготовку окружения в тестах:

interface ResourceProvider {    fun getString(@StringRes res: Int, vararg args: Any): String}class ApplicationResourceProvider(private val resources: Resources) : ResourceProvider {    override fun getString(res: Int, vararg args: Any): String {        return resources.getString(res, *args)    }}class TestResourceProvider : ResourceProvider {    override fun getString(res: Int, vararg args: Any): String = "$res"}

При таком поведении TestResourceProvider по умолчанию правильность строки в ожидаемом результате можно сверять по id ресурса:

val string = TestResourceProvider().getString(R.string.error_no_internet)assertThat(string).isEqualTo(R.string.error_no_internet.toString())

В общем случае лучше вообще не заменять дублерами типы, принадлежащие сторонним библиотекам и фреймворкам. Это может привести к проблемам при обновлении их API. Обезопасить себя можно также с помощью Wrapper. Подробнее ситуация разобрана в статье Dont Mock Types You Dont Own.

Использовать Wrapper-ы для статический функций, управления асинхронным и многопоточным поведением. Существует немало стандартных статических функций или Android-зависимостей в виде таких функций. Если нужно иметь с ними дело, то следует помещать их во Wrapper-ы и внедрять в SUT для последующей подмены.

Это поможет и при работе с асинхронностью и многопоточностью: инкапсулирующий управление ими Wrapper можно заменить тестовым дублером, который позволит проверяемому коду выполняться в одном потоке и синхронно вызвать асинхронный код. Для RxJava и Kotlin Coroutines есть стандартные решения от их авторов.

Дизайн тестов

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

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

Spoiler
public void testSubClassSerializerInvokedForBaseClassFieldsHoldingArrayOfSubClassInstances() {    Gson gson = new GsonBuilder()            .registerTypeAdapter(Base.class, new BaseSerializer())            .registerTypeAdapter(Sub.class, new SubSerializer())            .create();    ClassWithBaseArrayField target = new ClassWithBaseArrayField(new Base[] {new Sub(), new Sub()});    JsonObject json = (JsonObject) gson.toJsonTree(target);    JsonArray array = json.get("base").getAsJsonArray();    for (JsonElement element : array) {        JsonElement serializerKey = element.getAsJsonObject().get(Base.SERIALIZER_KEY);        assertEquals(SubSerializer.NAME, serializerKey.getAsString());    }}

Чтобы достичь желаемого эффекта от тестов, необходимо уделить внимание качеству их дизайна.

Наименование теста и разделение на блоки

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

  • Given настройка SUT и среды;

  • When действие, инициирующее работу SUT, результат работы которой нужно проверить;

  • Then проверка результатов на соответствие ожиданиям.

Пример разделения тела теста:

@Testfun `when create - while has 1 interval from beginning of day and ending not in end of day - should return enabled and disabled items`() {    // given    val intervalStart = createDateTime(BEGINNING_OF_DAY)    val intervalEnd = createDateTime("2019-01-01T18:00:00Z")    val intervals = listOf(        ArchiveInterval(startDate = intervalStart, endDate = intervalEnd)    )    // when    val result = progressItemsfactory.createItemsForIntervalsWithinDay(intervals)    // then    val expected = listOf(        SeekBarProgressItem.createEnabled(intervalStart, intervalEnd),        SeekBarProgressItem.createDisabled(intervalEnd, createDateTime(END_OF_DAY))    )    assertThat(result).isEqualTo(expected)}

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

В тестах на JVM Kotlin позволяет использовать пробел и дефис при обрамлении названия функции обратными кавычками. Это здорово повышает читабельность. В инструментальных тестах это не работает, поэтому текст пишется в CamelCase, а вместо дефисов используются нижние подчеркивания.

Для тестов на Junit применим следующий паттерн именования в простых случаях:

  • when - should

    when аналогично блоку When;

    should аналогично блоку Then.

В более сложных случаях, когда есть дополнительные условия:

  • when - while/and - should , где

    while предусловие до вызова целевой функции SUT;

    and условие после вызова функции SUT.

Пример:

@Testfun `when doesValueSatisfyRegex - while value is incorrect - should return false`() {

Так имя теста написано в виде требования, и в случае падения будет сразу видно, какой сценарий отработал некорректно:

Фреймворк Spek 2 выводит всё это на новый уровень. Он предоставляет из коробки DSL в стиле Gherkin (BDD).

object GetCameraGroupsInteractorTest : Spek({    Feature("Transform cached cameras to groups of cameras") {        ...        Scenario("subscribe while has non-grouped camera and unsorted by groups order cameras") {            ...            Given("non-grouped camera and unsorted by groups order cameras") {                ...            }            When("subscribe") {                ...            }            Then("should return four groups") {                ...            }            ...        }    }})

Блоки Given, When, Then подтесты глобального теста, описанного с помощью блока Scenario. Теперь нет необходимости ставить всё описание в названии, можно просто расположить все части в соответствующих блоках.

Результат выполнения имеет иерархический вид:

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

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

Чтобы добиться схожего разделения и отображения результатов с помощью Junit 5, понадобилось бы написать в тестах много бойлерплейта с аннотациями.

Устранение лишнего кода

Чтобы сделать содержимое тестов читабельнее, нужно следовать нескольким правилам:

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

В Spek 2 вместо создания полностью отдельных тестов, если они концептуально относятся к одному сценарию, разделение проверок можно сделать с помощью блоков Then/And внутри Scenario:

...Then("should return four groups") {...}And("they should be alphabetically sorted") {...}And("other group should contain one camera") {...}And("other group should be the last") {...}...

В Junit 4 такой возможности нет. На помощь приходит механизм SoftAssertions из AssertJ, который гарантирует выполнение всех assert в тесте. Например:

// thenassertSoftly {    it.assertThat(capabilityState)        .describedAs("Capability state")        .isInstanceOf(Available::class.java)    it.assertThat((capabilityState as Available).disclaimer)        .describedAs("Disclaimer")        .isNull()}

2. Если проверки объемные, нежелательные к разделению и повторяющиеся, следует выносить их в отдельную функцию с говорящим названием.

3. Использовать обобщающие конструкции тестового фреймворка для одинаковой настройки окружения, если настройка повторяется для большого количества тестов, находящихся на одном уровне иерархии (например, beforeEachScenario и afterEachScenario в случае Spek 2). Если настройка одинакова для нескольких тестовых файлов, можно использовать Extension для Junit 5, Rule для Junit 4, а для Spek 2 подобного механизма из коробки нет, поэтому нужно обходиться конструкциями before/after.

4. Объемные схожие настройки тестового окружения следует также выносить в отдельную функцию.

5. Использовать статические импорты для повсеместно применяемых функций вроде функций проверок AssertJ и Mockito.

6. Если создание вспомогательных объектов объемное, используется в разных тестовых файлах и с разными параметрами, следует завести генератор с дефолтными значениями:

Пример генератора
object DeviceGenerator {    fun createDevice(        description: String? = null,        deviceGroups: List<String> = emptyList(),        deviceType: DeviceType = DeviceType.CAMERA,        offset: Int = 0,        id: String = "",        photoUrl: String? = null,        isActive: Boolean = false,        isFavorite: Boolean = false,        isPublic: Boolean = false,        model: String? = null,        vendor: String? = null,        title: String = "",        serialNumber: String = "",        streamData: StreamData? = null    ): Device {        return Device(            description = description,            deviceGroups = deviceGroups,            deviceType = deviceType,            offset = offset,            id = id,            photoUrl = photoUrl,            isActive = isActive,            isFavorite = isFavorite,            isPublic = isPublic,            model = model,            vendor = vendor,            title = title,            serialNumber = serialNumber,            streamData = streamData        )    }}Given("initial favorite camera") {    val devices = listOf(        createDevice(id = deviceId, isFavorite = true)    )    ...}

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

Тесты как документация

Когда предыдущие пункты соблюдены, тесты уже можно применять как документацию, свернув тестовые функции в IDE.

Для сворачивания и разворачивания всех блоков кода в файле в случае Mac используются комбинации клавиш Shift + + - и Shift + + +, для управления конкретным блоком + - и + + соответственно.

В тестах на Junit 4 можно сделать еще лучше, сгруппировав тесты по регионам, ведь их тоже можно сворачивать.

Пример

В тестах на Spek 2 нет нужды делать разделение тестов по регионам, поскольку их можно хорошо сгруппировать с помощью блоков Scenario и Feature.

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

Наконец пример тестов на Spek 2 в режиме документации

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

Она лучше обычной текстовой, поскольку в отличие от тестов, обычную документацию можно забыть актуализировать. Чем тесты более высокоуровневые, тем более близкими к составленным аналитиком функциональным требованиям будут их названия. Это будет заметно в разделе "JVM Integration Testing".

Параметрические тесты

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

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

В документации Spek 2 не написано о возможности написания параметрических тестов, хотя она есть, и писать их проще, чем в Junit 4 и Junit 5. Для этих целей удобно использовать стиль тестов Specification.

Пример параметрического теста в Speck 2
class OrientationTypeTest : Spek({    describe("Orientation type") {        mapOf(            -1 to Unknown,            -239 to Unknown,            361 to Unknown,            2048 to Unknown,            340 to Portrait,            350 to Portrait,            360 to Portrait,            0 to Portrait,            ...        ).forEach { (tiltAngle, expectedOrientation) ->            describe("get orientation by tilt angle $tiltAngle") {                val result = OrientationType.getOrientation(tiltAngle)                it("return $expectedOrientation type") {                    assertThat(result).isEqualTo(expectedOrientation)                }            }        }    }})

Результат выполнения:

Снижение хрупкости non-UI тестов

Я писал, что степень хрупкости unit-тестов при изменениях исходного кода, обусловленную их привязкой к деталям реализации модуля, можно снизить. Это применимо для всех non-UI тестов.

Написание тестов в стиле White box искушает расширять видимость функций/свойств SUT для проверок или установки состояний. Это простой путь, который влечет за собой не только увеличение хрупкости тестов, но и нарушение инкапсуляции SUT.

Избежать этого помогут правила. Можно сказать, что взаимодействие с SUT будет в стиле Black box.

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

  2. Нужно стараться делать функции чистыми. Об этом я говорил выше.

  3. Проверки в тесте следует осуществлять по возвращаемому значению вызываемой публичной функции, публичным свойствам или, в крайнем случае, по взаимодействию с mock-объектами (с помощью функции verify() и механизма ArgumentCaptor в Mockito)

  4. Делать только необходимые проверки в рамках теста. Например, если в тесте проверяется, что при вызове функции A у SUT происходит вызов функции X у другого класса, то не следует до кучи проверять значения её публичных полей, особо не имеющих отношения к делу, и что у SUT не будет более никаких взаимодействий с другими функциями связанного класса (функция verifyNoMoreInteractions() в Mockito).

  5. Если для проведения определенного теста невозможно привести SUT в требуемое предварительное состояние с помощью аргументов целевой функции, моков/стабов или изменения полей, то следует вызвать другие публичные функции, вызов которых приводит SUT в интересующее состояние в условиях реальной работы приложения. Например, вызвать функции onLoginInputChanged и onPasswordInputChanged для подготовки теста onEnterButtonClick во ViewModel

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

Тестирование асинхронного кода с RxJava

Лучше избегать прямого управления асинхронным и многопоточным поведением в SUT. Для тестирования же кода, использующего RxJava или Coroutines, применяются специфичные решения. Сейчас в большинстве наших проектов используется RxJava, поэтому расскажу именно про нее.

Для тестирования SUT, осуществляющей планирование Rx-операций, нужно произвести замену реализаций Scheduler-ов так, чтобы весь код выполнялся в одном потоке. Также важно иметь в виду, что на JVM нельзя использовать AndroidSchedulers.mainThread().

В большинстве случаев все Scheduler-ы достаточно заменить на Schedulers.trampoline(). В случаях, когда нужен больший контроль над временем события, лучше использовать io.reactivex.schedulers.TestScheduler с его функциями triggerActions(), advanceTimeBy(), advanceTimeTo().

Замену реализаций можно совершить двумя способами:

  • RxPlugins (RxJavaPlugins & RxAndroidPlugins);

  • Подход Schedulers Injection.

Первый способ официальный и может быть применен независимо от того, как спроектирована SUT. Он имеет не самое удачное API и неприятные нюансы работы, усложняющие применение в некоторых ситуациях (например, когда внутри тестового файла в одних тестах нужно использовать Schedulers.trampoline(), а в других TestScheduler).

Суть подхода Schedulers Injection заключается в следующем: экземпляры Scheduler-ов попадают в SUT через конструктор, благодаря чему в тесте они могут быть заменены на иные реализации. Этот подход является очень прозрачным и гибким. Также он останется неизменным независимо от выбранного тестового фреймворка (Junit 4, Junit 5, Spek 2) чего нельзя сказать об RxPlugins, которыми придется в каждом управлять по-своему.

Из минусов Shedulers Injection можно выделить необходимость внедрения дополнительного аргумента в SUT и необходимость использования вместо rx-операторов с Sheduler по умолчанию (таких как delay()) их перегруженные варианты с явным указанием Scheduler.

Есть две неплохие статьи на тему обоих подходов: раз, два. Но там упомянуты не все нюансы RxPlugins.

Я предпочитаю второй подход. Чтобы упростить внедрение и подмену реализаций в тесте, я написал SchedulersProvider:

Реализация и применение SchedulersProvider
interface SchedulersProvider {    fun ui(): Scheduler    fun io(): Scheduler    fun computation(): Scheduler}class SchedulersProviderImpl @Inject constructor() : SchedulersProvider {    override fun ui(): Scheduler = AndroidSchedulers.mainThread()    override fun io(): Scheduler = Schedulers.io()    override fun computation(): Scheduler = Schedulers.computation()}fun <T> Single<T>.scheduleIoToUi(schedulers: SchedulersProvider): Single<T> {    return subscribeOn(schedulers.io()).observeOn(schedulers.ui())}// другие необходимые функции-расширения...

Его применение в коде:

class AuthViewModel(    ...    private val schedulers: SchedulersProvider) : BaseViewModel() {    ...    loginInteractor        .invoke(login, password)        .scheduleIoToUi(schedulers)    ...

А вот и его тестовая реализация с Scheduler-ами по умолчанию, вместо которых при надобности можно передать TestScheduler:

class TestSchedulersProvider(    private val backgroundScheduler: Scheduler = Schedulers.trampoline(),    private val uiScheduler: Scheduler = Schedulers.trampoline()) : SchedulersProvider {    override fun ui(): Scheduler = uiScheduler    override fun io(): Scheduler = backgroundScheduler    override fun computation(): Scheduler = backgroundScheduler}

Применение в тесте:

authViewModel = AuthViewModel(    ...    router = mock(),    schedulers = TestSchedulersProvider(),    loginInteractor = loginInteractor,    ...)

Вообще, RxJava из коробки имеет и другие полезные инструменты для тестирования (TestObserver, TestSubscriber), но они не входят в рамки статьи.

JVM Integration Testing

JVM Integration-тесты проверяют взаимодействие модулей или совокупностей модулей на JVM. Какие именно связки стоит тестировать, зависит от конкретных случаев.

В самых масштабных тестах этого типа проверяется взаимодействие всей цепочки модулей от ViewModel до Http-клиента, поскольку в этом промежутке обычно располагается основная логика, требующая проверки. Обеспечивать работу View на JVM обычно накладно и не имеет большого смысла.

Тест взаимодействует с SUT через ViewModel, инициируя действия и проверяя результат.

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

  • android.content.res.Resources или собственный Wrapper. Обычно достаточно стаба, обеспечивающего исправный возврат строк из ресурсов.

  • androidx.arch.core.executor.TaskExecutor. Требуется в любых тестах на JVM, у которых SUT использует LiveData, поскольку стандартная реализация имеет Android-зависимость. Подробнее можно почитать в этой статье. Google предлагает готовое решение этой проблемы в форме Rule лишь для Junit 4, поэтому для Spek 2 и Junit 5 использую рукописный класс, содержащий код из того самого решения:

object TestLiveDataExecutionController {    fun enableTestMode() {        ArchTaskExecutor.getInstance()            .setDelegate(object : TaskExecutor() {                override fun executeOnDiskIO(runnable: Runnable) = runnable.run()                override fun postToMainThread(runnable: Runnable) = runnable.run()                override fun isMainThread(): Boolean = true            })    }    fun disableTestMode() {        ArchTaskExecutor.getInstance().setDelegate(null)    }}

Соответствующие функции достаточно вызывать перед первым и после последнего теста в тестовом файле. Пример применения в Spek 2:

object DeviceDetailViewModelIntegrationTest : Spek({    beforeGroup { TestLiveDataExecutionController.enableTestMode() }    afterGroup { TestLiveDataExecutionController.disableTestMode() }...
  • Сервер. Для имитации сервера используется MockWebServer от создателей OkHttp. Он позволяет предустанавливать ответы на конкретные запросы, проверять состав запросов, факты их вызова и др.

  • Interceptors с Android-зависимостями. Не следует пренебрегать добавлением интерцепторов в тестовую конфигурацию клиента OkHttp, соблюдая тот же порядок, что и в настоящем клиенте, чтобы серверные запросы и ответы правильно обрабатывались. Однако некоторые интерцепторы могут иметь Android-зависимости их следует подменить. Например, это могут быть интерцепторы логирования. Интерцепторы последовательно передают данные друг другу и эту цепочку нельзя прерывать, поэтому фиктивный интерцептор должен выполнять это минимальное требование:

// StubInterceptorInterceptor { chain ->    return@Interceptor chain.proceed(chain.request().newBuilder().build())}
  • Персистентные хранилища данных (SharedPreferences, Room и т.д.)

Базовая логика управления тестовым сетевым окружением сконцентрирована в классе BaseTestNetworkEnvironment. Он используется на JVM и в Instrumentation. За специфическую конфигурацию под каждую из сред отвечают его классы-наследники: JvmTestNetworkEnvironment и InstrumentationTestNetworkEnvironment.

Сервер запускается при создании экземпляра *NetworkEnvironment до запуска теста и отключается функцией shutdownServer() после завершения теста (в случае Gherkin-стиля Spek 2 до и после Scenario соответственно).

Для удобной настройки ответов на конкретные запросы используется функция dispatchResponses. При необходимости к mockServer можно обратиться напрямую.

Реализация BaseTestNetworkEnvironment
abstract class BaseTestNetworkEnvironment {    companion object {        private const val BASE_URL = "/"        private const val ENDPOINT_TITLE = "Mock server"    }    val mockServer: MockWebServer = MockWebServer().also {         it.startSilently()     }    // класс, специфичный для инфраструктуры проекта    protected val mockNetworkConfig: NetworkConfig    init {        val mockWebServerUrl = mockServer.url(BASE_URL).toString()        mockNetworkConfig = TestNetworkConfigFactory.create(mockWebServerUrl, BASE_URL)    }    /**     * Используется для предустановки фиктивных ответов на конкретные запросы к [MockWebServer].     *     * [pathAndResponsePairs] пара путь запроса - ответ на запрос.     *     * Если [MockWebServer] получит запрос по пути, которого нет среди ключей [pathAndResponsePairs],     * то будет возвращена ошибка [HttpURLConnection.HTTP_NOT_FOUND].     */    fun dispatchResponses(vararg pathAndResponsePairs: Pair<String, MockResponse>) {        val pathAndResponseMap = pathAndResponsePairs.toMap()        val dispatcher = object : Dispatcher() {            override fun dispatch(request: RecordedRequest): MockResponse {                val mockResponse = request.path?.let {                   pathAndResponseMap[it]                 }                return mockResponse ?: mockResponse(HttpURLConnection.HTTP_NOT_FOUND)            }        }        mockServer.dispatcher = dispatcher    }    fun shutdownServer() {        mockServer.shutdown()    }    /**     * Запуск сервера с отключенными логами     */    private fun MockWebServer.startSilently() {        Logger.getLogger(this::class.java.name).level = Level.WARNING        start()    }}

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

Пример реализации JvmTestNetworkEnvironment
// Если не передавать в конструктор класса специфические экземпляры тестовых дублеров, то будут использоваться// стабы с минимальным предустановленным поведением, необходимым для функционирования сетевого флоу.class JvmTestNetworkEnvironment(    val mockPersistentStorage: PersistentStorage = mockPersistentStorageWithMockedAccessToken(),    val mockResources: ResourceProvider = TestResourceProvider()) : BaseTestNetworkEnvironment() {    private val nonAuthZoneApiHolderProvider: NonAuthZoneApiHolderProvider    private val authZoneApiHolderProvider: AuthZoneApiHolderProvider    init {        val moshiFactory = MoshiFactory()        val serverErrorConverter = ServerErrorConverter(moshiFactory, mockResources)        val stubInterceptorProvider = StubInterceptorProvider()        val interceptorFactory = InterceptorFactory(            ErrorInterceptorProvider(serverErrorConverter).get(),            AuthInterceptorProvider(mockPersistentStorage).get(),            stubInterceptorProvider.get(),            stubInterceptorProvider.get()        )        nonAuthZoneApiHolderProvider = NonAuthZoneApiHolderProvider(            interceptorFactory,            moshiFactory,            mockNetworkConfig        )        authZoneApiHolderProvider = AuthZoneApiHolderProvider(            interceptorFactory,            moshiFactory,            UserAuthenticator(),            mockNetworkConfig        )    }    fun provideNonAuthZoneApiHolder() = nonAuthZoneApiHolderProvider.get()    fun provideAuthZoneApiHolder() = authZoneApiHolderProvider.get()}

Функции для упрощения создания серверных ответов:

fun mockResponse(code: Int, body: String): MockResponse = MockResponse().setResponseCode(code).setBody(body)fun mockResponse(code: Int): MockResponse = MockResponse().setResponseCode(code)fun mockSuccessResponse(body: String): MockResponse = MockResponse().setBody(body)

Тела фиктивных серверных ответов сгруппированы по object-ам, соответствующим разным запросам. Это делает тестовые файлы чище и позволяет переиспользовать ответы и значения их полей в разных тестах. Одни и те же ответы используются тестами на JVM и Instrumentation (в том числе UI).

После добавления комментария "language=JSON" IDE подсвечивает синтаксис JSON. Подробнее о Language injections можно почитать тут.

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

Пример object с фиктивными серверными ответами
object LoginResponses {    const val INVALID_CREDENTIALS_ERROR_DESCRIPTION = "Неверный логин или пароль"        fun invalidCredentialsErrorJson(        errorDescription: String = INVALID_CREDENTIALS_ERROR_DESCRIPTION    ): String {        // language=JSON        return """            {              "error": {                "code": "invalid_credentials",                "description": "$errorDescription",                "title": "Введены неверные данные"              }            }            """.trimIndent()    }...}

Схожим образом вынесены и пути запросов:

const val LOGIN_REQUEST_PATH = "/auth/login"object GetCameraRequest {    const val DEVICE_ID = "1337"    const val GET_CAMERA_REQUEST_PATH = "/devices/camera/$DEVICE_ID"}...

Общие для JVM и Instrumentation файлы должны находиться в директории, доступной обоим окружениям. Доступ настраивается в build.gradle:

android {    sourceSets {        // Instrumentation        androidTest {            java.srcDirs += 'src/androidTest/kotlin'            java.srcDirs += 'src/commonTest/kotlin'        }        // JVM        test {            java.srcDirs += 'src/test/kotlin'            java.srcDirs += 'src/commonTest/kotlin'        }    }}

Взаимодействие View и ViewModel построено особым способом, благодаря которому очень удобно писать unit-тесты ViewModel и integration-тесты. Публичные функции ViewModel представляют события со стороны View (обычно они соответствуют действиям со стороны пользователя) и именуются в событийном стиле:

ViewModel воздействует на View посредством двух LiveData:

  • state описание состояния View

  • events однократные события, не сохраняющиеся в state

Этот подход в более удобном виде реализован в нашей библиотеке.

Пример организации ViewModel, ViewState и ViewEvents
class AuthViewModel(...) {    val state = MutableLiveData<AuthViewState>()    val events = EventsQueue<ViewEvent>()    ...}sealed class AuthViewState {    object Loading : AuthViewState()    data class Content(        val login: String = "",        val password: String = "",        val loginFieldState: InputFieldState = Default,        val passwordFieldState: InputFieldState = Default,        val enterButtonState: EnterButtonState = Disabled    ) : AuthViewState() {        sealed class InputFieldState {            object Default : InputFieldState()            object Error : InputFieldState()            object Blocked : InputFieldState()        }...    }}class EventsQueue<T> : MutableLiveData<Queue<T>>() {    fun onNext(value: T) {        val events = getValue() ?: LinkedList()        events.add(value)        setValue(events)    }}// ViewEvents:interface ViewEventdata class ShowSnackbarError(val message: String) : ViewEventclass OpenPlayStoreApp : ViewEvent...
Наконец, пример JVM Integration-теста
object AuthViewModelIntegrationTest : Spek({    Feature("Login") {        // region Fields and functions        lateinit var authViewModel: AuthViewModel        lateinit var networkEnvironment: JvmTestNetworkEnvironment        val login = "log"        val password = "pass"        fun setUpServerScenario() {            networkEnvironment = JvmTestNetworkEnvironment()            val authRepository = networkEnvironment.let {                AuthRepositoryImpl(                    nonAuthApi = it.provideNonAuthZoneApiHolder(),                    authApi = it.provideAuthZoneApiHolder(),                    persistentStorage = it.mockPersistentStorage,                    inMemoryStorage = InMemoryStorage()                )            }            val clientInfo = ClientInfo(...)            val loginInteractor = LoginInteractor(authRepository, clientInfo)            authViewModel = AuthViewModel(                resources = networkEnvironment.mockResources,                schedulers = TestSchedulersProvider(),                loginInteractor = loginInteractor                analytics = mock()            )        }        beforeFeature { TestLiveDataExecutionController.enableTestMode() }        afterFeature { TestLiveDataExecutionController.disableTestMode() }        beforeEachScenario { setUpServerScenario() }        afterEachScenario { networkEnvironment.shutdownServer() }        // endregion        Scenario("input credentials") {...}        Scenario("click enter button and receive invalid_credentials error from server") {            Given("invalid_credentials error on server") {                networkEnvironment.dispatchResponses(                    LOGIN_REQUEST_PATH to mockResponse(HTTP_UNAUTHORIZED, invalidCredentialsErrorJson())                )            }            When("enter not blank credentials") {                authViewModel.onCredentialsChanged(login, password)            }            And("click enter button") {                authViewModel.onEnterButtonClick(login, password)            }            Then("reset password, mark login and password input fields as invalid and disable enter button") {                val state = authViewModel.state.value                val expectedState = Content(                    login = login,                    password = "",                    loginFieldState = Content.InputFieldState.Error,                    passwordFieldState = Content.InputFieldState.Error,                    enterButtonState = Content.EnterButtonState.Disabled                )                assertThat(state).isEqualTo(expectedState)            }            And("create snackbar error event with message from server") {                val expectedEvent = authViewModel.events.value!!.peek()                assertThat(expectedEvent).isEqualTo(ShowSnackbarError(INVALID_CREDENTIALS_ERROR_DESCRIPTION))            }        }        ...    }    ...})

Так тестируется основная логика пользовательских сценариев. Эти сценарии с теми же данными затем могут быть проверены в UI-тестах.

Что в итоге нужно тестировать?

Не нужно тестировать чужие библиотеки это ответственность разработчиков библиотек (исследовательское тестирование исключение). Тестировать нужно свой код.

Unit-тесты следует писать на логику, в которой есть реальная вероятность совершения ошибки. Это могут быть ViewModel, Interactor, Repository, функции форматирования (денег, дат и т.д.) и другие стандартные и нестандартные сущности. Тривиальную логику тестировать не стоит. Но нужно следить за изменением непокрытой тестами логики, если она при очередном изменении перестанет быть тривиальной, то тогда её нужно протестировать.

100%-е покрытие кода тестами несёт с собой вред: трата лишнего времени на написание бесполезных тестов, боль при изменении реализации, при поддержке бесполезных тестов, иллюзия хорошо протестированной системы. Процент покрытия не отражает реальной картины того, насколько хорошо система протестирована.

Я предпочитаю не устанавливать минимальный порог тестового покрытия для нового кода. Однако для разработчика всё же может быть полезной точечная проверка покрытия SUT, над которой он работает.

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

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

JVM Integration-тесты от ViewModel до слоя данных следует писать для каждого экрана. Менее масштабные JVM Integration при надобности. Возможны случаи, когда большинство модулей, включая ViewModel, сами по себе являются слишком простыми, чтобы их стоило покрывать unit-тестами. Однако создание масштабного JVM integration-теста на всю цепочку будет очень кстати, тем более что пишутся такие тесты достаточно просто и однотипно.

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

Тесты Instrumentation Integration non-UI только когда нужно проверить что-то, что нельзя адекватно проверить на JVM.

E2E UI- и Component UI-тесты нужны для замены части ручных тестов при регрессионном тестировании. Разумно доверить их написание QA-инженерам. В настоящее время мы с коллегами ищем оптимальный подход к тому, как организовывать UI-тесты, в каком количестве их писать и как сочетать с более низкоуровневыми тестами.

Test Driven Development

Можно подумать, что о написании тестов уже известно достаточно и пора идти в бой, но есть еще один момент Вы, вероятно, собрались написать очередную фичу и затем покрыть её тестами? Замечательная идея. Именно так и стоит делать, пока навык написания тестов не будет более менее отработан. Такой подход называют Test Last. Конечно же, среди пишущих тесты разработчиков он наиболее распространен. Но он имеет серьезные недостатки:

  • несмотря на знания о том, каким должен быть тестируемый код, всё равно может получиться реализация, протестировать которую сходу не получится. Может понадобиться рефакторинг, чтобы появилась возможность написать тесты и сделать их не корявыми :)

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

  • тесты остаются на последнюю очередь и на них зачастую не остается времени.

Решить эти проблемы можно, используя принцип Test First, придуманным Кентом Беком. Он основан на идее: "Never write a single line of code unless you have a failing automated test" (не стоит писать код реализации, пока для него не написан падающий тест).

На базе этого принципа Кент Бек создал методологию Test Driven Development (TDD, разработка через тестирование). Согласно ей, разработка должна вестись итеративно, путем цикличного повторения шагов Red-Green-Refactor (микро-цикл):

  • написать тест на логику, которую предстоит реализовать, и убедиться, что он падает;

  • написать простейшую реализацию, чтобы тест выполнился успешно;

  • провести рефакторинг реализации, не сломав тесты.

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

Позже Роберт Мартин развил TDD, сформулировав Three Laws of TDD (нано-цикл):

  • перед написанием какого-либо кода реализации необходимо написать падающий тест;

  • тест не должен содержать больше, чем нужно для его падения или провала компиляции;

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

Из-за второго правила работа сводится к поочерёдному написанию строчки реализации SUT за строчкой теста. Нано-циклы заставляют фокусироваться на очень маленькой подзадаче. Это также помогает, когда не знаешь, с чего начать реализацию.

Со временем Робертом были сформулированы еще два более масштабных цикла. Про всех них можно почитать в его статье.

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

Я несколько отступился от канонов и нашел эффективным такой алгоритм работы при реализации новой фичи:

1. Вникнуть в задачу, спроектировать связи между модулями, определить их ответственность.

2. Создать SUT, описать его интерфейс.

  • Если функции должны возвращать какой-то результат, можно писать в их теле TODO(), чтобы код мог скомпилироваться, тогда при вызове функции тест будет прерван эксепшеном. Другой вариант хардкодить возврат простого объекта или null. Так тесты смогут совершить проверки после вызова функции, но тут лучше быть поаккуратнее.

fun doSomething(): Boolean { TODO() }

3. Создать тестовый файл для SUT, объявить тесты-требования.

  • Описать столько кейсов, сколько получится. Нормально, если в ходе написания реализации на ум придут еще какие-то кейсы.

    В пустые тесты/блоки можно добавлять вызов функции fail() (из Junit или AssertJ), чтобы не забыть реализовать какой-то из тестов, поскольку пустой тест при запуске выдает положительный результат.

@Testfun `when invoke - should do something`() {    fail { "not implemented" }}

4. Реализовать тест(ы)

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

5. Реализовать SUT, чтобы реализованные тесты успешно выполнились.

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

6. Отрефакторить SUT, сохранив успешность выполнения реализованных тестов.

7. Если остались нереализованные тесты, перейти к пункту #4.

Алгоритм доработки SUT, которая уже покрыта тестами:

  1. Объявить новые тесты согласно новым требованиям,

  2. Реализовать новые тесты,

  3. Реализовать доработку в SUT, чтобы новые тесты выполнились успешно

  4. Если старые тесты упали:

    • Они актуальны при новых требованиях исправить реализацию SUT и/или эти тесты,

    • Они неактуальны удалить.

  5. Отрефакторить SUT, сохранив успешность выполнения реализованных тестов,

  6. Если остались нереализованные тесты, перейти к пункту 2.

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

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

По итогу получаем от подхода следующие преимущества:

  • Предварительное написание тестов вынуждает реализовывать SUT заведомо тестируемой. Тесты оказываются слабо связанными с деталями реализации.

  • Тесты являются спецификацией SUT (если при этом соблюдать правила их именования). Часто они вытекают напрямую из функциональных требований к задаче. Сфокусированное перечисление тестов-требований до начала реализации помогает лучше понять и запомнить требования, лучше продумать детали интерфейса SUT. Увеличивается вероятность заблаговременного выявления всех необходимых краевых кейсов. Это само по себе уже помогает допускать меньше ошибок в будущей реализации, а возможность запуска готовых тестов в любой момент дает уверенность в том, что реализация осуществляется правильно.

  • Наличие тестов делает рефакторинг реализации безопасным. После каждого изменения реализации можно быстро прогнать все тесты SUT и в случае обнаружения поломки сразу же её устранить. Время, затрачиваемое на отладку, очень сильно сокращается.

  • На тесты хватает времени, ведь они неотъемлемая часть процесса разработки

  • Все эти факторы в совокупности сокращают время, затрачиваемое на разработку и на развитие приложения в будущем.

  • Приятно видеть, как красные тесты один за другим превращаются в зелёные

TDD это в первую очередь подход к разработке. Методология замечательно показывает себя при реализации SUT с unit- и JVM integration-тестами, поскольку их можно быстро и часто запускать. С Instrumentation non-UI-тестами применять её можно, но из-за длительности запуска придется запускать тесты реже. Применять же TDD с UI-тестами крайне не рекомендуется.

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

Заключение

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

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

Можно извлекать существенную пользу за счет применения TDD с тестами на JVM. Применять его следует не всегда. Разработчику нужно самостоятельно подобрать комфортный размер шага в цикле разработки по TDD.

Полезные материалы

Подробнее..

Ликбез для дизайнеров с конференции Apple WWDC 2020

10.07.2020 18:18:20 | Автор: admin


Роботы просмотрели обучающие сессии и отобрали полезное для тематического дайджеста Redmadrobot Design Lab.

Учиться и подсматривать за лучшими важная часть нашей работы. Поэтому, как и в предыдущие годы, в этом мы и десятки миллионов разработчиков, дизайнеров и инженеров следили за WWDC. И Keynote от Apple точно задал новую планку для формата онлайн-конференций: все эти классные смены пространств, переключения между спикерами и динамика в нужных моментах.

После вводной презентации началась сама конференция с обучающими лекциями (сессиями) и рабочими консультациями (о том, как всё устроено мы уже писали). И несмотря на название (Worldwide Developers Conference) Apple большое внимание традиционно уделяется дизайну: многие лекции читают дизайнеры и евангелисты, код на их слайдах отсутствует совсем или присутствует в малом количестве.

Евгений Бондарев, креативный директор дизайн-лаборатории Redmadrobot:
Если обратить внимание на то, как Apple презентуют каждую фичу в iPadOS, iOS и др., то мы заметим, что сначала идет рассказ про дизайн и потом про функциональность. Во-первых, это просто красиво, а во-вторых, дизайн создаёт эмоции. И Apple выстраивает взаимодействие с пользователями на уровне эмоций, которые работают в связке с технологиями.

Например, на WWDC был анонс про увеличение межстрочного расстояния в выпадающих списках меню! Кажется, что только Apple в презентациях международного масштаба может рассказывать про увеличение расстояния межстрочного интервала. Это говорит о том, что внутри компании по-прежнему главенствует мощнейшая дизайн-культура, которая стоит, возможно, даже впереди технологий.

iPad


В iPadOS 14 обновили дизайн домашнего экрана, добавили боковую панель навигации, распознавание рукописного текста и более реалистичные AR-объекты.

Отныне на большом экране iPad будет меньше пустого пространства и больше контента. У многих приложений появилась боковая панель для навигации по папкам или вспомогательной информации. Иконки файлов стали меньше, за счет чего на экране iPad может быть на 300% больше контента, чем раньше.



Функция От руки для Apple Pencil преобразует рукописный текст в машинописный, отличает надписи от рисунков, а геометрические фигуры делает ровными и правильными, что неплохо для скетчинга в заметках. Правда пока программа распознает только английский и китайский языки.



Евгений Бондарев, креативный директор дизайн-лаборатории Redmadrobot
Серьезно изменилось взаимодействие с Pencil для iPad Apple добилась почти бесшовного опыта взаимодействия пользователя с планшетом и пером. Например, раньше, чтобы найти что-то нужно было переключиться на клавиатуру, вбить фразу и найти. Сейчас всё стало проще можно написать текст Pencil и функция От руки преобразует его в машинописный. В итоге, практически 99% возможностей доступны с помощью пера, без необходимости использовать клавиатуру.


  • Как именно работать с функцией От руки, Apple рассказывает в сессии Meet Scribble for iPad.
  • Если хочется самому сделать что-то с рисованием карандашом, то можно поглядеть на новые штуки в сессии Inspect, modify, and construct PencilKit drawings.
  • Про проектирование UI для приложений на iPad с поддержкой тач бара и мыши смотрите в сессии Design for the iPadOS pointer.

Также Apple объявили, что приложения для iOS / iPadOS начнут запускаться на новых Маках с процессорами Apple Silicon. Поэтому советуем вам посмотреть на то, как там всё устроено, например, в сессиях What's new in Mac Catalyst и Adopt the new look of macOS.

Евгений Бондарев, креативный директор дизайн-лаборатории Redmadrobot
В этом релизе операционные системы Apple стали более консистентными: iOS стала ближе к iPadOS, а iPadOS ближе к macOS.

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

Функции приложений, которые изначально были для Mac, теперь перетекают в iPad. Из дизайнерских вышла производительная версия Photoshop, есть приложение для работы с Figma. В общем, можно проектировать интерфейсы прямо на планшете. Все эти вещи говорят, что Apple двигается в сторону iPad, который заменит ноутбук для большого числа профессий.


Посмотрите на кнопки и ползунки центра управления в macOS Big Sur. Вам не кажется, что Apple на что-то пытается намекнуть нам?

Подробнее про Sidebar в iPadOS


Прежде мы привыкли видеть на iPad знакомый нам с айфона Tab Bar он позволял переключаться между разделами приложения по нажатию на иконки внизу экрана.



В новой iPadOS дизайнеры Apple предлагают нам иную навигацию с помощью Sidebar. Sidebar размещается слева от контента приложения и не только содержит ссылки на основные разделы экрана, но и позволяет пользователям добавлять их собственные ссылки.

Sidebar на iPad выглядит логичнее Tab Bar-а. Прежний Tab Bar растягивался на всю ширину экрана, из-за чего занимал необоснованно много пространства экрана. Sidebar призван сделать навигацию насыщеннее, персональнее и более похожей на навигацию в macOS.

Несколько советов по проектированию Sidebar:

  • Используйте компонент для верхнеуровневой навигации.
  • Добавьте возможность сворачивать навигацию.
  • Позволяйте пользователям кастомизировать разделы в Sidebar.
  • Поддерживайте Dragndrop.
  • Используйте строуковые (outlined) иконки.

Важно помнить, что нельзя использовать одновременно Tab Bar и Sidebar это собьёт пользователей с толку. Также не забывайте, что на iPhone (Compact Width) мы все также используем Tab Bar как основной инструмент навигации. Подробнее об этом компоненте можно узнать из презентации Design for iPad.

AR и обновленные иконки


Apple представила ARKit 4 с новым Depth API. Сканер LiDAR на iPad Pro 2020 оценивает расстояние до предметов и собирает данные об окружающей среде.



С помощью функции привязки к местности можно добавлять AR элементы на географические карты и рассматривать их со всех сторон в приложениях для iPhone и iPad. Больше о новинках для iPad в соответствующей сессии.


Скульптура художника KAWS в приложении Acute Art

Евгений Бондарев, креативный директор дизайн-лаборатории Redmadrobot
Новые иконки приложений стали более объемными, многослойными, у них появились тени. Может показаться, что это шаг в сторону скевоморфизма, но есть подозрение, что эти иконки будут круто смотреться в дополненной реальности. Когда мы начнем взаимодействовать с интерфейсами в пространстве, иконки будут немного двигаться, показывать разные грани. Эта многослойность будет круто работать в пространственных интерфейсах. Это лишь предположение, но уже сегодня мы видим мощную прокачку ARKit. Есть слухи, что уже в следующем году Apple выпустит очки с дополненной реальностью и возможно, обновленные иконки мы увидим уже там.

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

Автомобили



Часть Keynote WWDC про Car Key и пример его работы с BMW (внутри английский)

Евгений Бондарев, креативный директор дизайн-лаборатории Redmadrobot
Apple продолжает активно выходить на рынок ПО для автомобилей. Раньше мы видели Car Play, сейчас представлена интеграция авто с картами Apple Map и цифровой ключ Car Key. С функцией Car Key открывать и закрывать автомобиль можно через iPhone или Apple Watch. При этом разряженное устройство сможет работать еще 5 часов для открытия и закрытия автомобиля. Помимо этого, интеграция с Apple Map позволит при построении маршрута учитывать заряд аккумулятора электромобиля. Всё это сделает iPhone еще более полезным девайсом. За этим могут последовать различные изменения в автомобильной индустрии. Например, вскоре через автомобиль, как гаджет, мы сможем оплачивать покупки, автозаправку и так далее.

Шрифты и символы


Шрифты San Francisco и New York теперь доступны в формате вариативного шрифта. Сочетание разных стилей в одном файле с поддержкой интерполяции позволяет создавать промежуточные стили. При этом созданные шрифты адаптируются для разных размеров. Использование обеих гарнитур поможет создать визуальные иерархии или выделить смысловые различия в контенте. Apple предупреждает, что использование вариативного шрифта на более ранних версиях iOS может привести к неожиданным результатам.

В библиотеке Apple также пополнение 750 новых черно-белых значков и еще 150 цветных. Цветные значки это новинка для SF Symbols 2, они автоматически адаптируются к темной или светлой теме.

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

App Clips


App Clips это мини-приложения, которые можно использовать без установки полной версии. Сейчас для аренды самоката нужно сначала установить приложение, зарегистрироваться, ввести данные для платежа. С App Clip всё проще и быстрее: берем самокат, сканируем QR-код или используем NFC метку, регистрируемся в один клик через Sign in with Apple, оплачиваем аренду через Apple Pay и катаемся себе на здоровье. Если приложение понравилось переходим по ссылке и устанавливаем полную версию.



Другой сценарий оплата парковки в разных городах (в отпуске или командировке), чтобы не скачивать каждый раз новое приложение. Разработчики также предлагают сценарии для интернет-магазинов, кофеен, доставки еды и путешествий. Подробнее в секции от Apple.

Евгений Бондарев, креативный директор дизайн-лаборатории Redmadrobot
App Сlips сильно снижает порог входа для использования мобильных приложений, открывает большие возможности для платформы и сильно влияет на UX, так как можно пользоваться большим числом приложений, просто сканируя код.

Мини-приложения не должны превышать 10 Мбайт, используют SwiftUI и UIkit, имеют доступ к тем же функциям, что и полная версия использование камеры, геолокация, Bluetooth и другие. App Clips могут рассылать push-уведомления в течение суток или, с разрешения пользователя, в течение недели.

Your browser does not support HTML5 video.

Widgets


В iPhone, iPad и Mac добавлены виджеты для главного экрана (Home). По мнению Apple, так главная страница приобретет кардинально новый и более информативный вид, а все самое необходимое будет под рукой. Пользователь может группировать виджеты или использовать автоматическую расстановку, например рекомендованные, недавно открытые.

Your browser does not support HTML5 video.

На iPhone и iPad доступна функция Smart Stack это по сути виджет для виджетов. Позволит объединить виджеты в группу по какому-либо признаку и перелистывать эти группы между собой. Например, утром на главном экране будет группа с погодой и новостями, днем календарь, а вечером карта и информация о пробках.

Виджеты доступны в трех размерах: S, M, L для разной информационной нагрузки. В виджетах используются яркие цвета, фотографии и четкий шрифт, который легко прочесть с первого взгляда.

Пара советов из Widget UI kit: на виджете не нужно указывать название приложения или логотип, только полезный контент. Виджеты должны выглядеть одинаково хорошо в темной и светлой теме. Текст должен быть всегда, но не переводите его в растр, чтобы функция Voice Over работала корректно. Больше информации и советов по использованию виджетов в сессии Apple.

И ещё немного полезного про виджеты можно найти в сессии про возможности операционной системы для решения задач пользователей в различных сценариях (Siri, уведомления, виджеты) Design for intelligence: Discover new opportunities.


Интересные размышления про виджеты и наглядную демонстрацию работы можно подглядеть в обзоре от The Verge.

Новые дизайн-компоненты и не только


В iOS обновлены пикеры, контекстное меню и добавлен выбор цвета.

В Apple переосмыслили визуальный стиль пикеров и взаимодействие пользователя с ними. Выбор даты доступен в 4 режимах: обратный отсчет, время, дата, время и дата. Так будет легче переключаться между годами и месяцами в календаре. Время теперь можно ввести с клавиатуры, а не скроллить переключатель. К пикерам также добавили светло-серый бэкграунд, чтобы выбранный элемент был заметнее. Контекстное меню можно добавить к любой кнопке, при этом меню не будет перекрывать экран смартфона. Сессия о дизайн-компонентах.

Обновление для API SwiftUI упрощает выбор цвета для приложений. У разработчиков теперь есть Color Picker:

  • выбрать цвет из сетки, спектра или с помощью ползунков;
  • изменить прозрачность и переключаться между цветовыми пространствами (Display P3 и sRGB);
  • выбрать цвет из любой точки экрана.
  • О пользовательской палитре Color Picker есть статья на Medium.

Про обновления в watchOS 7, в том числе чем отличается UI на часах и почему именно такие компоненты надо использовать смотрите в сессии Whats new in watchOS design.

И еще несколько сессий по дизайну, менее крутых, по нашему мнению, но можно посмотреть их для вдохновения:

  • Evaluate and optimize voice interaction for your app.
  • Design for location privacy.
  • Design for intelligence: Apps, evolved.
  • Design for intelligence: Make friends with The System.
  • Design for intelligence: Meet people where they are.

Пишите в комментариях, чем вам запомнился этот WWDC. А мы на этом завершаем наш яблочный дизайн-дайджест. Подписывайтесь на Design Jam в Telegram, и да пребудет с вами сила роботов!

P.S. Особые благодарности за участие в подготовке материала выражаем vani2 (Head of iOS Redmadrobot), bealex (CTO Redmadrobot SPb), ex-железному дизайнеру Артуру Абрарову и креативному директору Redmadrobot Жене Бондареву.

P.P.S. И напоследок ещё пример UX-магии от Apple, замеченной после выхода беты iOS 14

With Back Tap, you can perform various action by double or tripple tap on the back of your iPhone. Heres one I set double tap to take screenshot. pic.twitter.com/uZKv4Cjorf

Aditya Daniel (@adityadaniel) June 23, 2020
Подробнее..

Как приручить консоль, или 5 шагов к жизни с командной строкой

25.01.2021 16:17:27 | Автор: admin

Всем привет! Меня зовут Осип, я Android-разработчик в Redmadrobot и я люблю автоматизировать всё, что автоматизируется. В этом мне помогает консоль, поэтому решил поделиться опытом, как настроить командную оболочку так, чтобы в ней было приятно работать и она ежедневно помогала вам решать задачи.

Статья для тех, кто использует Linux или macOS. Если у вас Windows, вы можете использовать WSL (приравнивается к Ubuntu).

Есть задачи, которые проще выполнить в командном интерфейсе, а не в графическом, к примеру:

  • посчитать количество строк кода в проекте,

  • скопировать все файлы с расширением .png из одной папки в другую,

  • постучаться API и посмотреть какой ответ он выдаёт.

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

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

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


Статья только началась, а по тексту уже встречались и командная строка, и командная оболочка. Чем отличаются консоль, терминал, командная оболочка и командная строка?

Если объяснять из центра наружу: командная строка строка, где пользователь пишет команды; командная оболочка программа, которая интерпретирует команды, введённые в командную строку и выводит результат.

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

Подробнее о различиях можно почитать на Ask Ubuntu: What is the difference between Terminal, Console, Shell, and Command Line?

В статье будут встречаться примеры команд. Если по ходу прочтения вы не понимаете, что делает консольная команда, скопируйте её и вставьте в ExplainShell. Благо Роскомнадзор перестал его блокировать после разблокировки Telegram.

Зачем вообще использовать командную строку

Дисклеймер: Я не призываю использовать только командную строку и отказаться от графического интерфейса. Нужно просто учитывать командную строку при выборе инструмента для решения задачи.

Когда хотят рассказать чем же хорош CLI, выделяют разные преимущества перед GUI:

  • Доступность. Командная строка доступна везде. Внутри Android Studio есть вкладка с командной строкой. Можно и вовсе настроить drop-down терминал (ещё его называют quake style), который будет появляться поверх всех приложений по нажатию сочетания клавиш.

  • Многофункциональность. Одна точка доступа к любым утилитам.

  • Простота. Простой командный интерфейс может заменить сложный графический интерфейс. Сделать графический интерфейс который позволит ввести, например, пять параметров может быть довольно нетривиальной задачей, а сделать то же самое с командным интерфейсом просто.

  • Легковесность. Как правило, CLI утилиты используют меньше ресурсов.

Меня как разработчика больше всего впечатляет, как можно комбинировать CLI утилиты. Текст интерфейс общения, который понятен для всех утилит с командным интерфейсом. Утилиты принимают на вход текст и возвращают тоже текст. Это один из принципов Unix, которые сформулировал Дуглас Макилрой в 1978 году:

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


Пишите программы, которые бы работали вместе.


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

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

Примеры из жизни

Я задал вопрос коллегам-роботам: Для чего вы чаще всего открываете терминал? Получился такой ТОП-5:

  1. Работа с Git там, где не хватает графического интерфейса.

  2. Установка пакетов и управление зависимостями (подробнее про менеджер пакетов поговорим в разделе Устанавливаем менеджер пакетов).

  3. Работа с SSH.

  4. Проверка API с помощью curl.

  5. Когда нужно грохнуть процесс.

Есть и менее очевидные применения:

  1. Скачать видео из YouTube поможет youtube-dl. Качаете подкаст и нужна только аудио-дорожка? Добавьте к команде флаг --audio. Хотите скачать весь плейлист или даже весь канал? Подставляйте ссылку на канал и готовьте побольше свободного места.

  2. Хотите посмотреть отличия между файлами? Выполните команду diff и укажите пути до файлов, которые надо сравнить.

Шаг 1: Открываем терминал

Не терминал, а эмулятор терминала. (c) Департамент зануд

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

cool-retro-terminalcool-retro-terminal

Выбор терминала это тема для отдельной статьи. Кратко: если у вас Linux, начните с этого списка. На macOS популярен iTerm2, но я его не использовал, поэтому не могу ни поругать, ни похвалить.

Для меня важно чтобы и на компьютере с Linux, и на рабочем ноутбуке с macOS был один и тот же терминал с одинаковыми настройками. Я выбирал среди кроссплатформенных и остановился на kitty.

Шаг 2: Устанавливаем менеджер пакетов

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

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

Это даже проще, чем искать надёжный источник, из которого можно скачать программу, и удобнее, чем магазины приложений в macOS или Windows, где зачастую нет нужных программ.

Менеджеры пакетов в Linux

В дистрибутивах Linux менеджер пакетов есть по умолчанию. В Ubuntu, Debian и Mint это apt-get, а в Manjaro и ArchLinux pacman.

Чтобы установить пакет достаточно в терминале написать apt-get install [package]. Если у вас pacman, то pacman -S [package]. Может понадобиться sudo в начале, чтобы выполнить команду с правами root.

Чтобы обновить все пакеты с помощью apt-get введите команду apt-get update && apt-get upgrade. В pacman pacman -Syu.

В pacman много флагов и сложно сразу запомнить нужные. Ещё одно неудобство он не поддерживает установку пакетов из репозитория AUR. Это репозиторий, в который могут загружать пакеты любые пользователи. Исправить минусы помогут утилиты, которые упрощают работу с pacman. Рекомендую попробовать yay.

Менеджеры пакетов в macOS

В macOS придется установить пакетный менеджер. Самые популярные Homebrew и MacPorts. Homebrew активнее поддерживается сообществом, а пакеты в нём обновляются чаще, поэтому лучше использовать его. Для установки скопируйте актуальную команду установки c официального сайта. Эта команда скачает скрипт установки и запустит его.

Может понадобиться установка XCode Command Line Tools. Это базовый набор консольных инструментов clang, git, make и других. Он не зависит от XCode, а называется так, потому что необходим XCode для компиляции.

Теперь, чтобы установить пакет, достаточно в терминале написать brew install [package].

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

Шаг 3: Устанавливаем командную оболочку

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

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

Чтобы узнать, какая оболочка используется по умолчанию у вас, выполните команду echo $SHELL. Скорее всего, команда выведет /env/bash или /env/zsh это самые популярные оболочки. Если хотите сравнить возможности bash, zsh и fish, посмотрите эту таблицу.

Установим fish c помощью менеджера пакетов:

# Если pacmansudo pacman -S fish# Если apt-getsudo apt-get install fish# Если brewbrew install fish

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

Fish установлен. Запускаем его командой fish:

osip@homepc ~ % fishWelcome to fish, the friendly interactive shellType `help` for instructions on how to use fishosip@homepc ~>

Теперь можно настроить внешний вид. Команда fish_config откроет в браузере страницу настроек. Здесь можно настроить цветовую схему, приглашение командной строки (prompt), посмотреть список функций, переменные окружения, историю команд и список горячих клавиш. Здесь же можно добавлять, редактировать и удалять аббревиатуры команд. Про аббревиатуры поговорим чуть позже.

Fish по умолчанию

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

  1. Назначить fish командной оболочкой по умолчанию.

    Нужно учитывать, что скрипты инициализации текущей командной оболочки не будут выполняться. Команды и переменные окружения из .bashrc, .bash_profile, .zshrc и т.д, нужно переместить в .config/fish/fish.config , а затем адаптировать под синтаксис fish.

  2. Использовать fish только как интерактивную оболочку.

    Это более безболезненный способ, потому что не нужно мигрировать скрипты и переменные окружения. В конце скрипта инициализации текущей командной оболочки нужно запустить fish. Добавьте строку exec fish в файл .bash_profile, если у вас bash или в .zshrc, если zsh. Эти файлы находятся в корневой директории пользователя.

    На ArchWIki есть более подробное описание этого и еще нескольких способов.

Поиск по истории

Давайте-ка посмотрим, что умеет fish. Если еще не установили, можно попробовать в браузере. Я изменил только цвета и prompt, больше ничего не настраивал.

Когда вы начинаете набирать команду, fish подсказывает команды и аргументы, которые вы использовали раньше. Чтобы применить подсказку нажмите . Подставить одно слово из подсказки Ctrl+.

sample_command это демонстрационная функция. Она принимает любые параметры и ничего не возвращает.

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

Автодополнение

Начните писать любую команду и нажмите Tab, не дописывая её до конца. Попробуйте с командой git config:

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

Если утилита не поддерживает автодополнение, fish умеет создавать дополнения из документации man. Для этого нужно выполнить команду fish_update_completions.

А что с путями? Например, хотим перейти в папку dev/tools/jarjar/:

Дополнение путей тоже работает на Tab. Для перехода по пути не обязательно писать команду cd в начале. А еще работает дополнение, если написать первую букву имени каждой папки в пути. Если указан несуществующий путь, он подсвечивается красным.

Сложно запомнить все нужные флаги у команд. Хочу вывести дерево файлов, но не помню, как ограничить его глубину и сделать так, чтобы вывод был цветным. Для такого случая есть Shift+Tab дополнение с поиском:

Автодополнение может сработать в самых неожиданных местах, например, так работает автодополнение для команды kill:

Убийство Android Studio на глазах у studentdУбийство Android Studio на глазах у studentd

Wildcards

В fish, как и в bash, есть поддержка wildcards. Wildcards позволяют выполнить команду для нескольких файлов.

Выводим все файлы с расширением .md в текущей папкеВыводим все файлы с расширением .md в текущей папке

* соответствует любой строке
** соответствует любой иерархии папок, то есть рекурсивно заходит во вложенные папки

Применим wildcard, чтобы скопировать все файлы apk после сборки в папку output:

  • cp build/*.apk output/ скопирует все apk из папки build.

  • cp build/**.apk output/ скопирует все apk из папки build и из всех вложенных папок. То, что надо.

Функции, алиасы и аббревиатуры

Большиство команд fish это функции. Можно писать и свои функции. Синтаксис такой:

funcion [название]    [тело функции]end

Если функцию нужно сохранить для будущих сессий, не забудьте выполнить команду funcsave [название] после того как напишете функцию и проверите, что она корректно работает.

Для часто используемых команд можно создать более короткие синонимы алиасы. В fish команда alias создаёт однострочную функцию.

Как выглядит alias?

Флаг --save указывает, что нужно автоматически вызвать команду funcsave после создания алиаса. Таким образом алиасы сохранятся для будущих сессий.

Другой вариант сокращения команд аббревиатуры. Они настраиваются командой abbr или в fish_config во вкладке Abbreviations.

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

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

И па и gf превращается в git fetchИ па и gf превращается в git fetch

Шаг 4: Изучаем арсенал

Командная оболочка есть, теперь нужны команды.

Консольные утилиты могут быть с CLI и TUI. Command Line Interface (CLI) программа принимает команды через командную строку. Так работает большинство утилит. Text User Interface (TUI) интерфейс рисуется псевдографикой и по нему можно кликать мышкой как по GUI.

TUI для SpotifyTUI для Spotify

CLI не нужно бояться. На замену старым утилитам появляются новые с улучшенным UX, ориентированные на человека, а не на скрипты.

Например, многие старые утилиты, которые выводят размер файла, по умолчанию выводят его в байтах. А утилита df вообще выводит свободное место на диске в количестве блоков по 512 байт.

Чтобы выводились понятные человеку размерности, нужно добавить флаг -h (human readable). Цветной вывод удобнее читать, но он тоже по умолчанию обычно отключен и включается добавлением флага, чаще всего -C. В современных же утилитах по умолчанию включен цветной человекопонятный вывод.

Стандартные команды

Чтобы пользоваться командной строкой, нужно знать несколько стандартных команд:

  • cd [сhange directory] команда для навигации по файловой системе. Если запустить её без аргументов, вы окажетесь в домашней папке;

  • cp [copy], mv [move], rm [remove] команды для копирования, перемещения и удаления файлов, соответственно;

  • mkdir [make directory] команда для создания папки;

  • echo выводит строку, которую ей передали.

Если команда долго работает и вы не хотите дожидаться её завершения, прервите её выполнение сочетанием клавиш Ctrl + C.

Помощь: man, help, tldr

Есть несколько способов получить справку по команде.

man выводит полную справку:

  • описание команды,

  • список аргументов и описание каждого из них,

  • какие переменные окружения использует утилита и для чего,

  • известные баги,

  • советы и примеры использования,

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

Если ввести man man, вы получите справку по команде man, где всё это подробно описано.

man это утилита с TUI, в ней есть горячие клавиши. Для поиска нажмите /, а для выхода q. / и q стандартные клавиши для поиска и выхода, они работают во многих TUI утилитах. Ещё один стандартная клавиша ?, она открывает справку.

Можно выполнить команду из man для этого нажмите ! и введите команду. Хотите открыть man для другой команды внутри man или сразу попробовать выполнить команду, следуя документации? Легко.

Страницы в man пишут разработчики утилит. Если разработчик не написал справку, man выдаст No manual entry for [command]. Но даже если нет страницы в man можно вывести краткую справку с помощью флага --help. Попробуйте написать man --help.

Для команд fish можно открыть справку в браузере командой help <command>.

Если читать мануалы некогда, то поможет утилита tldr. Она отображает типичные случаи использования команд:

tldr tldrtldr tldr

Объединяем команды

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

Чтобы направить вывод одной команды на вход другой, используется оператор |. Он называется pipe, а на русский его переводят как конвейер. Если мы хотим подать вывод команды find_bone на вход команде eat, нужно между этими командами поставить трубу (pipe):

$ find_bone | eat

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

$ echo -e "spot\\nhandle\\npick\\natlas" > robots.txt$ cat robots.txt | sortatlashandlepickspot

Оператор | нам уже знаком, но что делает >? Этот оператор направляет вывод команды в файл. После этого командой cat мы достаём содержимое файла и с помощью оператора | отдаём на сортировку.

Современные утилиты

Просмотр списка файлов: ls, tree exa

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

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

Скриншоты. Сравнение ls, tree и exa.
Сравнение вывода ls и exaСравнение вывода ls и exaСравнение вывода tree и exaСравнение вывода tree и exa

Бонус: В exa можно совместить два режима вывода.

Просмотр запущенных процессов: top htop

top и htop. Обе утилиты выводят список запущенных процессов, но htop делает это гораздо приятнее.

В htop можно сортировать процессы по разным столбцам, нажимая на них. Кнопки в нижнем меню тоже нажимаются. Столбцы и информационные блоки в шапке настраиваются можно убрать лишнее и добавить нужное.

А как выглядит top?

Работа с JSON: jq

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

Валидируем json:

$ echo '{"model": spot}' | jq typeparse error: Invalid numeric literal at line 1, column 15$ echo '{"model": "spot"}' | jq type"object"

Форматируем json:

$ echo '{"model":"spot"," type":"robodog"}' | jq{  "model": "spot",  "type": "robodog"}

Выкусываем из json'а только то, что нужно:

$ set json '[{"model": "spot", "type": "robodog"}, {"model": "atlas", "type": "humanoid"}]'$ echo $json | jq 'map(.model)' --compact-output["spot","atlas"]$ echo $json | jq .[0].model"spot"# А теперь пример посложнее$ echo $json | jq 'map({(.model): .type}) | add'{  "spot": "robodog",  "atlas": "humanoid"}

Это только малая часть возможностей. Все возможности смотрите в доке.

Другие утилиты

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

Консольный HTTP клиент: curl, wget httpie

httpie делает то же что curl отправляет запросы в сеть. Но посмотрите как отличается синтаксис и оформление вывода в curl и httpie.

На фотографии слева направо: curl и httpieНа фотографии слева направо: curl и httpie
Отображение содержимого файла: cat bat

cat и bat выводят содержимое файла, но bat подсвечивает синтаксис и отображает изменения из гита.

Поиск по содержимому файлов: grep ripgrep

ripgrep более быстрая версия grep. Сравнение скорости работы показывает, что ripgrep быстрее всех :)

Поиск файлов: find fd, fzf

Для поиска файлов можно использовать стандартную утилиту find. Для неё есть альтернатива fd. Она работает быстрее, поддерживает цветной вывод, по умолчанию игнорирует скрытые файлы и файлы из .gitignore. Посмотрите на гифку, которая демонстрирует работу fd.

Ещё одна утилита для поиска fzf [fuzzy finder]. Это утилита с TUI для интерактивного поиска файлов с использованием нечёткого поиска по названиям.

Ещё из приятного есть предпросмотр содержимого.

Подсчёт количества строк кода: wc tokei

Стандартная утилита wc [word count] считает количество слов, символов и строк в файлах, но чтобы с помощью неё посчитать количество строк кода в проекте, придётся написать что-то такое:

$ fd -g '*.kt' | xargs wc -l

У такой команды есть сразу несколько недостатков:

  • считаются все строки, включая комментарии и пустые строки,

  • ищутся только файлы с расширением .kt, для поиска других придётся менять команду,

  • сгенерированные файлы и остальные файлы, которые заигнорены в гите, тоже попадут в статистику,

  • такую команду долго писать.

Утилита tokei лишена перечисленных недостатков. Вот пример вывода tokei на одном из наших проектов:

Упс, файлы proguard засчитались в пользу PrologУпс, файлы proguard засчитались в пользу Prolog
Свободное место на диске: du ncdu

Ещё один пример разницы CLI и TUI. В отличие от du, ncdu это TUI. Тут же можно перейти внутрь папки или удалить ненужное нажав d.

Хм, накопилось много врапперов и кэшей Gradle. Можно почистить.Хм, накопилось много врапперов и кэшей Gradle. Можно почистить.
Сравнение файлов: diff delta

Отличная замена старому-доброму diff - delta. Можно использовать режим отображения side-by-side, если больше нравится, включить отображение номеров строк. Даже без дополнительных настроек диффы выглядят приятно:

Измерение времени работы программы: time hyperfine

Не верьте на слово, если я говорю, что одна утилита работает быстрее другой. Лучше проверьте.

Можно измерить время выполнения команды с помощью time (в macOS gtime). Эта утилита не предназначена для бенчмарков нет возможности прогрева, команда выполняется один раз. hyperfine подойдёт лучше, потому что изначально разработан для бенчмарков.

Попробуем замерить время выполнения команды tree:

Вывод команды tree перенаправлен в пустоту (/dev/null), потому что здесь не важен вывод команды, важно только время её выполнения. С hyperfine этого делать не нужно, он сам отбрасывает вывод команды.

Hyperfine умеет отслеживать признаки неправильного бенчмарка. Вот некоторые подсказки:

  • если во время замера есть выбросы, hyperfine посоветует закрыть другие программы, потому что они могут влиять на бенчмарк,

  • если первый запуск команды дольше остальных, hyperfine посоветует сделать прогрев, задав параметр --warmup N. Перед бенчмарком программа выполнится N раз.

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

$ hyperfine 'command_one' 'command_two' 'command_three'

Шаг 5: Сохраняем настройки

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

Конфиги это файлы. Обычно они хранятся в корневой директории пользователя вместе со скриптами инициализации командной оболочки, например, в папке .config/. Если вы установили fish, то найдёте папку .config/fish/ и в ней файлы с настройками. Самый простой способ сохранить конфиги сохранить их в Git-репозиторий.

Имена файлов и папок с настройками обычно начинаются с точки, поэтому одним словом их называют dotfiles. На момент написания статьи на GitHub опубликовано 138 425 репозиториев с именем dotfiles есть куда подсмотреть.

На странице awesome-dotfiles вы найдёте много информации про dotfiles. Там же есть ссылки на инструменты, которые помогают управлять dotfiles.

Я использую yadm. Мне важна кроссплатформенность, поэтому пригождается его возможность создавать альтернативные версии файлов для разных ОС.

Заключение

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

Чтобы не настраивать рабочее окружение на каждом новом компьютере заново, сохраняйте конфиги в Git-репозиторий. Но не забывайте о безопасности следите, чтобы в репозиторий не попал ключи и пароли.

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

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

Если будут вопросы или вам понадобится помощь с освоением консоли, пишите мне в Telegram@osipxd. Ещё я иногда пишу в канал @rareilly заметки про Android и вообще про всё интересное, что нахожу. Спасибо за внимание!

Что ещё почитать

Подробнее..

Автоматизация тестирования продуктовой аналитики в мобильных приложениях

12.10.2020 12:10:07 | Автор: admin

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

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

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

Тестирование аналитики вручную

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

  1. Можно отправить локальное уведомление (типа Push) с названием и параметрами события. Это неудобно, так как перекрывает интерфейс приложения, а также сложно тестировать цепочку событий из-за того, что каждое новое уведомление перекрывает старые.

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

  3. Либо события аналитики можно логировать и сразу отслеживать в консоли.

События аналитики в Console.appСобытия аналитики в Console.app

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

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

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

Тестирование аналитики UI-тестами

У любого события есть имя, у некоторых бывают еще и параметры. Например, у успешность авторизации имя authorization и булевый параметр success.

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

На практике есть два способа передачи данных из приложения в UI-тесты:

  • Можно сохранить текстовые данные в невидимое текстовое поле или в свойство accessibilityLabel невидимой вьюшки. Но в этом случае меняется иерархия вьюшек, и это может привести к багам. Кроме того, не получится очистить список отправленных событий из UI-тестов.

  • Или можно сохранить текстовые данные в буфер обмена, к которому у UI-тестов есть доступ. Этот вариант лучше, так как иерархия вьюшек не изменяется. Буфер обмена можно очистить из UI-тестов, а еще это проще в реализации.

Когда приложение запущено в режиме UI-тестирования, то можно подменить сервис отправки событий аналитики. Например, вместо AppMetrica подставить свой сервис, который будет отправлять события в буфер обмена. Далее в UI-тестах происходит чтение текстовых данных из буфера, преобразование их в массив событий и проверка.

Так в итоге будет выглядеть UI-тест, проверяющий события аналитики на экране авторизации:

func testLoginSuccess() {    // Запустить приложение    launchApp()        // Проверить что отправилось событие показа экрана авторизации    analytics.assertContains(name: "open_login_screen")        // Успешно залогиниться    loginScreen.login(success: true)        // Проверить что отправилось событие успешной авторизации    analytics.assertContains("authorization", ["success": true])}

Доработки со стороны приложения

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

Базовые сущности

Представим событие аналитики в виде следующей структуры:

public struct MetricEvent: Equatable {     public let name: String        public let values: [String: AnyHashable]?     public init(name: String, values: [String: AnyHashable]? = nil) {        self.name = name        self.values = values    }}

Структура MetricEvent будет использоваться и в коде приложения, и в коде UI-тестов. Поэтому вынесем её в отдельный модуль MetricExampleCore. Для этого нужно создать новый Target типа Framework.

Событие что-то должно отправлять, поэтому объявим соответствующий протокол:

import MetricExampleCore /// Сервис отправки событий в аналитикуpublic protocol MetricService {        func send(event: MetricEvent)    }

В первой строчке импортируем модуль, в котором объявили структуру MetricEvent.

Сервисы отправки событий

Этому протоколу будут соответствовать классы, отправляющие события куда-либо. К примеру, класс для отправки событий в AppMetrica:

import Foundationimport MetricExampleCoreimport YandexMobileMetrica open class AppMetricaService: MetricService {     public init(configuration: YMMYandexMetricaConfiguration) {        YMMYandexMetrica.activate(with: configuration)    }     open func send(event: MetricEvent) {        YMMYandexMetrica.reportEvent(event.name, parameters: event.values, onFailure: nil)    }}

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

import Foundationimport MetricExampleCoreimport UIKit final class MetricServiceForUITests: MetricService {     // Массив всех отправленных событий аналитики    private var metricEvents: [MetricEvent] = []     func send(event: MetricEvent) {        guard ProcessInfo.processInfo.isUITesting,              ProcessInfo.processInfo.sendMetricsToPasteboard else {            return        }                if UIPasteboard.general.string == nil ||           UIPasteboard.general.string?.isEmpty == true {            metricEvents = []        }         metricEvents.append(event)         if let metricsString = try? encodeMetricEvents(metricEvents) {            UIPasteboard.general.string = metricsString        }    }     private func encodeMetricEvents(_ events: [MetricEvent]) throws -> String {        let arrayOfEvents: [NSDictionary] = events.map { $0.asJSONObject }        let data = try JSONSerialization.data(withJSONObject: arrayOfEvents)        return String(decoding: data, as: UTF8.self)    }}

В методе send можно проверить, что приложение запущено в режиме UI-тестирования и разрешена отправка событий в буфер обмена. Затем в массив всех отправленных событий добавляется новое.

После этого массив представляется в виде текста с использованием метода encodeMetricEvents. Там каждое событие преобразуется в словарь и полученный массив сериализуется. После этого строка сохраняется в буфер обмена.

// MetricEvent.swift...    /// Представляет событие в виде словаря для передачи в JSONSerialization.data(withJSONObject:)    public var asJSONObject: NSDictionary {        return [            "name": name,            "values": values ?? [:]        ]    }...

Каждый UIViewController, который будет отправлять события, получит в инициализатор зависимость MetricService.

final class LoginViewController: UIViewController {        private let metricService: MetricService        init(metricService: MetricService = ServiceLayer.shared.metricService) {        self.metricService = metricService        super.init(nibName: nil, bundle: nil)    }    ...

Чтобы не передавать каждый раз вручную эту зависимость, можно использовать паттерн Service Locator и создать класс ServiceLayer. В нем будет создаваться и храниться MetricService, который будет передаваться во все контроллеры.

import Foundationimport YandexMobileMetrica final class ServiceLayer {        static let shared = ServiceLayer()        private(set) lazy var metricService: MetricService = {        if ProcessInfo.processInfo.isUITesting {            return MetricServiceForUITests()        } else {            let config = YMMYandexMetricaConfiguration(apiKey: "APP_METRICA_API_KEY")            return AppMetricaService(configuration: config)        }    }()}

Если приложение запущено в режиме UI-тестирования, то для отправки событий используется MetricServiceForUITests. В ином случае AppMetricaService.

Отправка событий

Осталось объявить все события, которые будут отправляться. Для этого нужно написать расширение MetricEvent:

import Foundationimport MetricExampleCore extension MetricEvent {        /// Пользователь перешел на экран авторизации    static var openLogin: MetricEvent {        MetricEvent(name: "open_login_screen")    }     /// Пользователь ввел логин и пароль и инициировал авторизацию.    ///    /// - Parameter success: Успешность запроса.    /// - Returns: Событие метрики.    static func authorization(success: Bool) -> MetricEvent {        MetricEvent(            name: "authorization",            values: ["success": success]        )    }}

Теперь события можно отправлять:

metricService.send(event: .openLogin)metricService.send(event: .authorization(success: true))metricService.send(event: .authorization(success: false))

Аргументы запуска

Я уже упоминал такие вещи, как:

ProcessInfo.processInfo.isUITestingProcessInfo.processInfo.sendMetricsToPasteboard

При запуске UI-тестов на аналитику будут передаваться два аргумента: --UI-TESTING и --SEND-METRICS-TO-PASTEBOARD. Первый показывает, что приложение запущено в режиме UI-тестирования. Второй что приложению разрешено отправлять события аналитики в буфер обмена. Чтобы получить доступ к этим аргументам, нужно написать расширение для ProcessInfo:

import Foundation extension ProcessInfo {    var isUITesting: Bool { arguments.contains("--UI-TESTING") }    var sendMetricsToPasteboard: Bool { arguments.contains("--SEND-METRICS-TO-PASTEBOARD") }}

Доработки со стороны UI-тестов

Теперь расскажу, как на стороне UI-тестов получить список отправленных событий из буфера обмена и проверить их.

Получение списка отправленных событий

Чтобы получить текстовые данные из буфера, используем UIPasteboard.general.string. Затем строку нужно преобразовать в массив событий (MetricEvent). В методе decodeMetricEvents строка преобразуется в объект Data и десериализуется в массив с помощью JSONSerialization:

/// Возвращает список всех событий аналитики произошедших с момента запуска приложенияfunc extractAnalytics() -> [MetricEvent] {    let string = UIPasteboard.general.string!    if let events = try? decodeMetricEvents(from: string) {        return events    } else {        return []    }} /// Преобразует строку с массивом событий в массив объектов [MetricEvent]private func decodeMetricEvents(from string: String) throws -> [MetricEvent] {    guard !string.isEmpty else { return [] }    let data = Data(string.utf8)     guard let arrayOfEvents: [NSDictionary] = try JSONSerialization.jsonObject(with: data) as? [NSDictionary] else {        return []    }     return arrayOfEvents.compactMap { MetricEvent(from: $0) }}

Далее массив словарей преобразуется в массив MetricEvent. Для этого у MetricEvent нужно добавить инициализатор из словаря:

/// Пытается создать объект MetricEvent из словаряpublic init?(from dict: NSDictionary) {    guard let eventName = dict["name"] as? String else { return nil }    self = MetricEvent(        name: eventName,        values: dict["values"] as? [String: AnyHashable])}

Теперь можно получить массив событий [MetricEvent] и проанализировать его.

Если в процессе тестирования понадобится очистить список событий, то тут поможет:

UIPasteboard.general.string = ""

Проверки списка событий

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

/// Проверяет наличие события с указанным именем/// - Parameters:///   - name: Название события///   - count: Количество событий с указанным именем. По умолчанию равно 1.func assertContains(    name: String,    count: Int = 1) {     let records = extractAnalytics()     XCTAssertEqual(        records.filter { $0.name == name }.count,        count,        "Событие с именем \(name) не найдено.")}

В итоге получился класс AnalyticsTestBase. Посмотреть его можно на GitHub AnalyticsTestBase.swift

Создадим класс, наследника XCTestCase, от которого будут наследоваться классы, тестирующие аналитику. Он создает класс AnalyticsTestBase для тестирования аналитики и метод launchApp, запускающий приложение.

import XCTestclass TestCaseBase: XCTestCase {        var app: XCUIApplication!    var analytics: AnalyticsTestBase!        override func setUp() {        super.setUp()                app = XCUIApplication()        analytics = AnalyticsTestBase(app: app)    }        /// Запускает приложение для UI-тестирования с указанными параметрами.    func launchApp(with parameters: AppLaunchParameters = AppLaunchParameters()) {        app.launchArguments = parameters.launchArguments        app.launch()    }}

Метод будет принимать AppLaunchParameters (параметры запуска приложения, о которых я говорил выше).

struct AppLaunchParameters {        /// Отправлять аналитику в UIPasteboard    private let sendMetricsToPasteboard: Bool        init(sendMetricsToPasteboard: Bool = false) {        self.sendMetricsToPasteboard = sendMetricsToPasteboard    }        var launchArguments: [String] {        var arguments = ["--UI-TESTING"]        if sendMetricsToPasteboard {            arguments.append("--SEND-METRICS-TO-PASTEBOARD")        }        return arguments    }}

В обычных UI-тестах приложение будет запускаться с параметрами:

AppLaunchParameters(sendMetricsToPasteboard: false)

А в UI-тестах на аналитику:

AppLaunchParameters(sendMetricsToPasteboard: true)

Теперь можно писать тесты на аналитику. Например, это тест на экран входа:

final class LoginAnalyticsTests: TestCaseBase {        private let loginScreen = LoginScreen()        func testLoginSuccess() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                // Проверить что отправилось событие показа экрана входа        analytics.assertContains(name: "open_login_screen")                // Успешно залогинится        loginScreen.login(success: true)                // Проверить что отправилось событие успешной авторизации        analytics.assertContains("authorization", ["success": true])    }}

LoginScreen это Page Object, описывающий экран авторизации. Посмотреть его можно на GitHub LoginScreen.swift

Примеры

Example проект

iOS-проект, где используется автоматизированное тестирование аналитики UI-тестами.

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

Тест, покрывающий все эти события:

import XCTest final class AnalyticsTests: TestCaseBase {        private let loginScreen = LoginScreen()    private let menuScreen = MenuScreen()        // MARK: - Login        func testLoginSuccess() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                analytics.assertContains(name: "open_login_screen")        loginScreen.login(success: true)         analytics.assertContains("authorization", ["success": true])    }        func testLoginFailed() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                analytics.assertContains(name: "open_login_screen")        loginScreen.login(success: false)        analytics.assertContains("authorization", ["success": false])    }        // MARK: - Menu        func testOpenMenu() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))         loginScreen.login(success: true)        waitForElement(menuScreen.title)        analytics.assertContains(name: "open_menu_screen")    }        func testMenuSelection() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                loginScreen.login(success: true)        waitForElement(menuScreen.title)         menuScreen.profileCell.tap()                analytics.assertContains("menu_item_selected", ["name": "Профиль"])                menuScreen.messagesCell.tap()        analytics.assertContains("menu_item_selected", ["name": "Сообщения"])    }}

Реальный проект

Пример UI-тестов на аналитику экрана авторизации из реального проекта LoginAnalyticsTests.swift

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

Итоги

Плюсы подхода:

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

  2. Если у вас настроен CI, то UI-тесты на аналитику можно запускать по расписанию, например, раз в неделю или по команде из Slack.

Есть и минусы:

  1. UI-тесты выполняются относительно долго. Имеет смысл запускать их только в процессе регрессионного тестирования перед каждым релизом.

  2. UI-тесты на аналитику смогут написать только те тестировщики, которые имеют опыт написания нативных UI-тестов.

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

Поэтому в случае большого проекта есть смысл автоматизировать проверку событий аналитики.

Подробнее..

Android-разработчикам как сократить время реализации тёмной темы с пары месяцев до недели

20.10.2020 14:07:37 | Автор: admin

Привет, меня зовут Влад Шипугин, я Android-разработчик в Redmadrobot. В этой статье я хочу поделится опытом реализации тёмной темы, создания удобного UI Kit, как для разработки, так и для дизайнеров. Я расскажу про использование Material Components и работу с Vector Drawable. Также вы узнаете, как быстро поддержать режим edge-to-edge с использованием Window Insets и познакомитесь с моей библиотекой edge-to-edge-decorator.

Где-то полгода назад мы начали разработку тёмной темы для приложения Ростелеком Ключ. Наши дизайнеры уже писали про ценность тёмной темы, как её спроектировать и передать в разработку. В этой статье я продолжу рассказ от лица разработчика, расскажу с какими проблемами мы столкнулись в процессе реализации тёмной темы, почему это заняло у нас 3 месяца и как реализовать тёмную тему всего за одну неделю.

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

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

Как сделать удобный UI Kit

Изначально у нас был простой план: дизайнеры делают тёмную тему, а мы просто добавляем файл value-night/color.xml и всё. Но позже мы столкнулись с проблемами. И теперь, когда приложение давно опубликовано в сторе, я могу рассказать о решении этих проблем, чтобы вы никогда не наступали на наши грабли.

Проблема 1: сложность при выборе названий для палитры цветов

Первый вариант именования цветов использование названия цвета как есть: realblue, darkgrey и так далее.

Пример палитры цветов из ZeplinПример палитры цветов из Zeplin

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

Второй вариант это абстрактные названия, мы решили использовать C1, C2 (С от слова Color) и так далее. Эта абстракция позволяет отвязать значение цвета от его названия. Сегодня это может быть красный, а потом желтый это не важно, и вам не нужно рефакторить всё приложение.

Пример палитры цветов из ZeplinПример палитры цветов из Zeplin

Но тут появляется другая проблема ты не можешь держать таблицу соответствий цветов в голове. Приходится всё время её открывать: при обсуждении с дизайнером или при разработке всегда нужно проверять конкретный цвет приложения.

Редко, когда удается запомнить конкретную цифру цвета. Какой тут должен быть цвет: С4 или С7? станет самым частым вопросом на обсуждениях дизайна. Мы долгое время пытались найти баланс между понятным названием цвета, таким как realblue и максимально абстрактным цветом для простоты рефакторинга и редизайна: С1, C2, C3 и так далее.

Есть и альтернативный, третий вариант когда названия цветов зависят от компонента, например, гайдлайны Material Design или Apple HIG.

material.iomaterial.io

Этот вариант казался самым логичным, но в нём было больше всего проблем:

  1. Цвета даёт дизайнер. И начинающим дизайнерам сложно придумывать названия цветов, а подключать всегда арт-директора для такой мелочи невыгодно.

  2. Некоторые цвета могут содержать одинаковый hex. В целом, в этом нет проблемы, но как оказалось, в палитре цветов Zeplin не может содержаться два цвета с одинаковым hex, но разными названиями.

  3. Сложно объединить рекомендации Material Design и Apple HIG в одну палитру, да и стандартных цветов может быть недостаточно для приложения.

Много встреч прошло за обсуждением названий цветов. Они должны были быть удобными для всех: Android- и iOS-разработчиков, и дизайнеров. Через некоторое время мы остановились на третьем варианте, но сформировали чёткие правила по наименованию цветов, чтобы не тратить много времени на придумывание названия. Также мы договорились добавлять цвета с одинаковым hex отличающимся на единицу.

Название цвета

iOS

Android

text/primary

textPrimary

text_primary

button/pressed

buttonPressed

button_pressed

Вот пример готовой палитры из Zeplin:

Пример финальной палитры из ZeplinПример финальной палитры из Zeplin

Позже мы отказались от Zeplin и перешли на Figma. Вот такая палитра в Figma у нас получилась. А подробнее про переход с Zeplin на Figma уже писал наш iOS разработчик Даниил Субботин @subdan в статье про утилиту экспорта UI Kit из Figma figma-export.

Проблема 2: непонятные стили шрифтов

Особых проблем в работе со штифтовыми стилями у нас не было. Дизайнеры обычно заносят все шрифтовые стили приложения в UI Kit, но их наличие не гарантирует, что в макетах будут использоваться только они. Периодически то там, то тут, Zeplin не определял указанный шрифт и приходилось отвлекать дизайнера, чтобы узнать какой именно шрифт необходимо использовать.

Сейчас в макетах всегда отображаются правильные стили шрифтов. Дизайнеры добились этого за счёт перехода со Sketch + Zeplin на Figma она лучше распознает шрифтовые стили.

Да, иногда ошибки встречаются, но это происходит крайне редко, потому что все шрифты прописаны в мастер-компонентах и UI Kit. Это уже ответственность дизайнеров. Если я находил ошибку, то оставлял комментарий и дизайнер всё исправлял.

Пример наших шрифтовых стилей можно посмотреть тут.

Проблема 3: дублирование иконок и трудности с их именованием

Пока дизайнеры переделывали палитру цветов, я столкнулся с новой проблемой иконки должны быть разных цветов в зависимости от выбранной пользователем темы. Самый простой вариант это добавить альтернативный набор иконок в директорию drawable-night. Но за простоту нужно платить:

  1. Количество иконок увеличивается в два раза, а значит, приложение весит больше. И App Bundle не поможет, потому что тема меняется динамически и все иконки должны находиться в итоговом APK.

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

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

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

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

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

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

Чтобы такого не было, следует сразу договориться о правильном именовании иконок. Мы остановились на таком варианте:

Название иконки

iOS

Android

ic/24/flash_on

ic24flashOn

ic_24_flash_on

ic/24/flash_off

ic24flashOff

ic_24_flash_off

Пример с нашим набором иконок можно посмотреть здесь.

Проблема 4: организация иллюстраций

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

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

Вот пример того, что у нас получилось.

Проблема 5: отсутствие базовых компонентов или неправильное их использование

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

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

Но как только дизайн-макеты начинают передавать в разработку, то единственным источником правды должен стать UI Kit и мастер-компоненты в нем. Иначе UI Kit будет вам мешать и замедлять работу, а не ускорять её.

Ценность UI Kit в том, что вы реализуете все базовые компоненты, а потом из этих компонентов собираете экраны. Для этого можно использовать мастер-компоненты в Figma, и styles или CustomViews в Android. В таком случае UI Kit экономит вам много времени.

Вот пример с описанием кнопок приложения, а полный UI Kit можно посмотреть тут.

Пример описания кнопок приложенияПример описания кнопок приложения

Как правильно реализовать UI Kit

После создания дизайнерами UI Kit можно подключаться и разработчикам. Мой план по реализации UI Kit с учетом темной темы был такой:

  1. Привести цвета, тему приложения и стили компонентов в порядок.

    • сделать палитру цветов (color.xml);

    • описать тему приложения;

    • описать стили компонентов, которые отличаются от базовой темы.

  2. Заменить все иконки на черный и окрашивать их в нужный цвет в момент отрисовки.

  3. Добавить альтернативные цвета values-night/color.xml.

  4. Добавить выбор темы в настройках приложения.

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

Реализуем палитру цветов

С помощью Figma-export создание палитры цветов происходит одной командой:

./figma-export colors -i figma-export.yaml

После этого в вашем приложении добавится или изменится файл color.xml.

Реализуем тему приложения

Для реализации темы в Android-приложении следует использовать библиотеку material-components. Именно так я и поступил: создал палитру цветов в color.xml и начал делать тему приложения. Но после этого я столкнулся с проблемой toolbar, cardview и ещё пару компонентов имели не тот цвет.

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

На тот момент, в библиотеке material-components, ещё не было документации по темам и стилям. Как и многие Android-разработчики, я не знал, как правильно описывать тему приложения. Разработчики из Google даже шутили про это на Android Dev Summit 2019.

Моя тема приложения была описана неправильно и материальные компоненты, такие как кнопки, иконки и текстовые поля, выглядели не так, как я планировал. Например, цвет toolbar использовал цвет primary для светлой темы и, почему-то, переключался на surface в темной.

Позже оказалось, что в теме приложения, по умолчанию, цвет toolbar принимает значение, равное атрибуту ?attr/colorPrimarySurface. Тогда, чтобы понять почему так происходит, мне пришлось ковыряться в исходниках материальных компонентов. Мне удалось понять, какой смысл вкладывали авторы в описание темы приложения, и потом статья про темы и стили в Android-приложениях подтвердила мои догадки.

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

Примеры работы PrimarySurface атрибутаПримеры работы PrimarySurface атрибута

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

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

Доп. информацию по материальным компонентам можно найти тут и в конце статьи:

Также, при реализации темы приложения, следует подключить дизайнера, чтобы он помог создать описание темы приложения и сконвертировать удобную палитру цветов для дизайна, в тему материальных компонентов. Здесь на помощь приходит раздел материальных компонентов, MaterialThemeBuilder и примеры от Google:

В итоге, я остановился на таком варианте описания темы и стилей.

Структура организации ресурсов:

  1. colors.xml цвета приложения;

  2. type.xml шрифты приложения;

  3. shape.xml формы приложения;

  4. themes.xml темы приложения;

  5. styles_button.xml кнопки приложения;

  6. styles_text_input.xml текстовые поля;

  7. styles_list_item.xml элементы списков;

  8. styles.xml прочие стили виджетов.

Итоговая тема приложения
<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">    <!-- Base color attributes -->    <item name="colorPrimary">@color/tint</item>    <item name="colorSecondary">@color/tint</item>    <item name="colorControlHighlight">@color/tint_ripple</item>    <item name="android:colorBackground">@color/background_primary</item>    <item name="colorSurface">@color/background_secondary</item>    <item name="colorError">@color/error</item>    <item name="colorOnPrimary">@color/text_primary</item>    <item name="colorOnSecondary">@color/text_primary</item>    <item name="colorOnBackground">@color/text_secondary</item>    <item name="colorOnError">@color/text_primary</item>    <item name="colorOnSurface">@color/text_secondary</item>    <item name="android:windowBackground">@color/background_primary</item>    <item name="android:statusBarColor">@android:color/black</item>    <item name="android:navigationBarColor">@android:color/black</item>    <item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>    <item name="android:listDivider">@drawable/divider_horizontal_primary</item>    <!--Material shape attributes-->    <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item>    <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>    <item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item>    <!--Component styles-->    <item name="appBarLayoutStyle">@style/Widget.App.AppBarLayout</item>    <item name="toolbarStyle">@style/Widget.App.Toolbar</item>    <item name="drawerArrowStyle">@style/Widget.App.DrawerArrowToggle</item>    <item name="toolbarNavigationButtonStyle">@style/Widget.App.Toolbar.Button.Navigation.Tinted</item>    <item name="bottomNavigationStyle">@style/Widget.App.BottomNavigationView</item>    <item name="cardViewStyle">@style/Widget.App.CardView</item>    <item name="textInputStyle">@style/Widget.App.TextInputLayout</item>    <item name="editTextStyle">@style/Widget.App.TextInputEditText</item>    <item name="switchStyle">@style/Widget.App.Switch</item>    <item name="materialCalendarTheme">@style/ThemeOverlay.App.Calendar</item>    <item name="dialogTheme">@style/ThemeOverlay.App.Dialog</item></style>

Реализуем стили шрифтов

В теме приложения с material-components стандартным решением для реализации шрифтов является textAppearance. В нашем приложении используются в два раза меньше шрифтов и всего три цвета для текста. А ещё textAppearance можно описывать не все атрибуты например, там нет свойства android:lineSpacingMultiplier. Поэтому, я решил не использовать textAppearance, а использовал просто стили, которые прописывались каждому текстовому полю.

Например, мы нигде не использовали стиль Header2 и вместо него применяли унаследованный от него стиль с указанием цвета: Header2.Primary или Header2.Secondary. Такой вариант позволял сразу определить и цвет текстового поля и его шрифт.

Иногда приходилось делать исключения и использовать просто стиль, как Header2, с указанием цвета прямо в верстке экрана, например, для отображения ошибок, но таких мест всего 23 в приложении.

Стиль шрифта содержит следующие атрибуты:

  • textSize размер;

  • lineHeight межстрочный интервал, когда в текстовом поле две строки;

  • android:minHeight и android:gravity нужны, чтобы указать межстрочный интервал, когда в текстовом поле всего одна строка (да, lineHeight в таком случае игнорируется и приходится выкручиваться костылями :))

  • android:fontFamily начертание шрифта.

    Вот один из примерв описания шрифта:

<style name="Header2">    <item name="android:textSize">18sp</item>    <item name="lineHeight">24sp</item>    <item name="android:minHeight">24sp</item>    <item name="android:gravity">center_vertical</item>    <item name="android:fontFamily">@font/basis_grotesque_pro_bold</item></style><style name="Header2.Primary">    <item name="android:textColor">@color/text_primary</item></style><style name="Header2.Secondary">    <item name="android:textColor">@color/text_secondary</item></style>

Переиспользуем иконки при помощи окрашивания

Как я писал ранее, все иконки в приложении следует делать одного цвета, и потом их раскрашивать. Чтобы добавить все иконки из Figmа, вновь воспользуемся утилитой Figma-export:

./figma-export icons -i figma-export.yaml

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

В Android давно добавили поддержку перекрашивания иконок, это различные tint, но до сих пор это работает плохо и не на всех версиях Android, поэтому я написал extension для работы с drawable через drawable compat, и придерживался следующего алгоритма:

  1. Если можешь сделать tint в верстке делай tint.

  2. Если это кнопка или компонент с несколькими состояниями, то тут поможет selector.

  3. Если требуется программная смена цвета, то необходимо использовать DrawableCompat для корректной установки цвета, и обязательно нужно сделать mutate, иначе иконка закешируется, и поменяет цвет во всем приложении. Для этого я написал следующие extension-функции:

fun Drawable.withTint(context: Context, @ColorRes color: Int): Drawable {    return DrawableCompat.wrap(this).mutate().apply {        DrawableCompat.setTint(this, ContextCompat.getColor(context, color))    }}fun Int.toDrawableWithTint(context: Context, @ColorRes color: Int): Drawable {    return requireNotNull(AppCompatResources.getDrawable(context, this)).withTint(context, color)}

Добавляем иллюстрации

С добавлением иллюстраций тоже всё просто. В Figma-export есть нужная команда для их добавления в проект:

./figma-export images -i figma-export.yaml

Вызываем команду, и она добавляет иллюстрации в values и, если, вы поддерживаете тёмную тему, то и в values-night.

Реализуем стили компонентов

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

Пример того, как я организовал стили компонентов кнопок в styles-button.xml
<resources>   <style name="Widget.AppTheme.Button" parent="Widget.MaterialComponents.Button.UnelevatedButton">       <item name="backgroundTint">@drawable/selector_button</item>       <item name="rippleColor">@color/button_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:textColor">@color/button_text_color</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingTop">12dp</item>       <item name="android:paddingEnd">16dp</item>       <item name="android:paddingBottom">12dp</item>       <item name="android:insetTop">0dp</item>       <item name="android:insetBottom">0dp</item>   </style>   <style name="Widget.AppTheme.Button.Secondary">       <item name="backgroundTint">@color/background_secondary</item>       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textColor">@color/button_secondary_text_color</item>   </style>   <style name="Widget.AppTheme.Button.Onboarding">       <item name="backgroundTint">@color/onboarding_button</item>       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textColor">@color/button_secondary_text_color</item>   </style>   <style name="Widget.AppTheme.Button.Accent">       <item name="backgroundTint">@color/accent</item>       <item name="rippleColor">@color/tint_ripple</item>   </style>   <style name="Widget.AppTheme.TextButton" parent="Widget.MaterialComponents.Button.TextButton">       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:insetTop">0dp</item>       <item name="android:insetBottom">0dp</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>   </style>   <style name="Widget.AppTheme.Button.TextButton.Dialog" parent="Widget.MaterialComponents.Button.TextButton.Dialog">       <item name="android:textAllCaps">true</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>       <item name="rippleColor">@color/tint_ripple</item>   </style>   <style name="Widget.AppTheme.TextButton.Icon" parent="Widget.MaterialComponents.Button.TextButton.Icon">       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:textColor">@color/tint</item>       <item name="android:insetTop">0dp</item>       <item name="android:insetBottom">0dp</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>   </style>   <style name="Widget.AppTheme.ToolbarButton" parent="Widget.MaterialComponents.Button.TextButton">       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Header1</item>       <item name="android:textColor">@color/toolbar_button_text_color</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>   </style></resources>

Также стоит учитывать, что в верстке вы можете использовать просто Button или AppCompatButton, потому что есть такой компонент, как MaterialComponentsViewInflater, который автоматически будет переводить их в MaterialButton, если ваша тема наследуется от "material-components".

Вот кусочек кода из него:

@NonNull@Overrideprotected AppCompatButton createButton(    @NonNull Context context,     @NonNull AttributeSet attrs  ) {    if (shouldInflateAppCompatButton(context, attrs)) {    return new AppCompatButton(context, attrs);  }  return new MaterialButton(context, attrs);}

Как поддержать режимedge-to-edge

На этапе проектирования палитры цветов, нам очень сильно мешали цвета statusBar, да и в целом, окрашивание statusBar всегда вызывало проблемы на разных версиях Android. Раньше его цвет был равен colorPrimaryDark, а теперь Google отказались от этого варианта и рекомендуют использовать режим edge-to-edge. Кроме этого, мой OnePlus получил обновление до Android 10, поэтому я решил попробовать добавить поддержку режима edge-to-edge.

Режим edge-to-edge это новая концепция из материального дизайна, которая заключается в том, что вы отрисовываете контент под системными компонентами: statusBar и navigationBar и телефон становится визуально более безрамочным.

Отрисовка контента под statusBar позволяет добавить в ваше приложение поддержку челок и вырезов под камеру, а под navigationBar визуально улучшает работу с жестами из новой жестовой навигации в системе Android.

Для того, чтобы добавить поддержку режива edge-to-edge в ваше приложение нужно:

  1. Добавить поддержку системных отступов (insets).

  2. Активировать режим edge-to-edge для statusBar и navigationBar. По факту вам нужно сделать их прозрачными.

Добавляем поддержку Window Insets

Если простыми словами, то при работе с Window Insets, вы получаете размер системных компонентов и вставляете их как padding в верстку для ваших компонентов экрана AppBar или RootView. Insets поддерживается всеми версиями Android, что позволяет реализовать концепцию edge-to-edge для всех пользователей. Подробности можно почитать или посмотреть в докладе Константина Цховребова с AppsConf.

Сначала я использовал его решение, а потом, когда вышла новая версия библиотеки Insetter от Криса Бэйнса, перешел на неё. Вам предлагаю сразу использовать Insetter.

Также я собрал пару полезных статей, на случай, если вы захотите разобраться в этом детальнее:

Материалы по режиму edge-to-edge лежат тут и в конце статьи:

Окрашиваем statusBar и navigationBar

Эффект безрамочности в режиме edge-to-edge достигается за счет того, что вы отрисовываете контент под statusBar и navigationBar и делаете их прозрачными. При этом, нужно сохранять контрастность иконок в этих компонентах.

Тут существует одна проблема, которая находится глубоко в системе и исправить её после релиза OS уже нельзя. Это изменение цвета иконок в системных компонентах (statusBar и navigationBar) со светлого на темный. Поэтому, нужно учитывать следующие правила, в зависимости от версии Android:

  • до 6.0 версии Android иконки statusBar и navigationBar всегда светлые и перекрасить их в темный цвет нельзя. Флаг View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR доступен с 23 API. Если у вас контент всегда темного цвета, то проблем не будет. Но чтобы сохранить контрастность иконок на фоне контента, следует добавлять на системные компоненты наложение фона, например, черного фона с 50% прозрачности;

  • с версии Android 6.0 можно задать, какими будут иконки в statusBar: белыми или черными. Однако navigationBar будет вести себя как в предыдущих версиях, поэтому наложение можно убрать только для statusBar. Флаг View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR доступен с 26 API;

  • с версии Android 8.0 можно выбрать белый или черный цвет иконок для обоих компонентов. Поэтому наложения можно убрать полностью.

Я нашел интересный пример WindowPreferencesManager, который реализовывал эту логику в приложении-каталоге материальных компонентов. Но там было много лишнего и разбираться в этом, думаю, захочет не каждый, поэтому я сделал мини утилиту edge-to-edge-decorator. Она хорошо кастомизируется под ваши нужды и реализует логику окрашивания statusBar и navigationBar за вас. Подробнее про реализацию можно почитать в документации.

Пример работы библиотеки:

Android 5.0

(API level 21)

Android 7.1

(API level 25)

Android 9

(API level 28)

Android 11

(API level 30)

Добавляем тёмную тему приложения

Теперь, после того, как у вас готов UI Kit приложения и вы изучили и поддержали новый подход с использованием Material Components, можно вернуться к реализации темной темы в приложении.

Я рекомендую следовать согласно следующему алгоритму:

  1. Изучаем гайды и статьи по проектированию темной темы (ссылки лежат в конце статьи).

  2. Дизайнер готовит первый прототип и цветовую схему темной темы приложения.

  3. Создание полноценной темной палитры цветов.

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

Если основная тема вашего приложения описана правильно, то добавление темной темы не создаст проблем: нужно просто добавить color.xml в values-night, как мы и планировали в самом начале (как же мы тогда ошибались :))

Сама активация темной темы хорошо описана в документации. Коротко, что нужно сделать:

1) Поменять базовую тему приложения на DayNight.

<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">

2) Установить нужный режим отображения через метод AppCompatDelegate.setDefaultNightMode.

В системе доступно 4 варианта темы:

  • всегда светлая: AppCompatDelegate.MODE_NIGHT_NO;

  • всегда тёмная: AppCompatDelegate.MODE_NIGHT_YES;

  • выбирается в зависимости от режима энергосбережения (Android 9 и ниже): AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY;

  • переключается в зависимости от настроек системы (Android 10 и выше): AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;

Добавляем выбор темы приложения

Светлая тема

Темная тема

После того, как вы реализовали тёмную тему, вишенкой на торте станет выбор темы в настройках приложения. Почему это важно? Потому что ресурсы values-night были добавлены ещё в API level 8, но включение темной темы на уровне системы реализовали только в Android 10. Чтобы темная тема работала у всех пользователей, необходимо добавить возможность её выбора в приложении.

Для удобного API я написал вот такой класс:
enum class NightModeType(   val customOrdinal: Int,   @NightMode val value: Int,   @StringRes val title: Int) {   MODE_NIGHT_NO(       0,       AppCompatDelegate.MODE_NIGHT_NO,       R.string.mode_night_no   ),   MODE_NIGHT_YES(       1,       AppCompatDelegate.MODE_NIGHT_YES,       R.string.mode_night_yes   ),   MODE_NIGHT_FOLLOW_SYSTEM(       2,       AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,       R.string.mode_night_follow_system   ),   MODE_NIGHT_AUTO_BATTERY(       2,       AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY,       R.string.mode_night_auto_battery   );   companion object {       fun fromValue(@NightMode value: Int) = values().firstOrNull { it.value == value } ?: getDefaultMode()       fun fromCustomOrdinal(ordinal: Int): NightModeType {           return if (ordinal == 2) {               getDefaultMode()           } else {               values().firstOrNull { it.customOrdinal == ordinal } ?: getDefaultMode()           }       }       fun getDefaultMode(): NightModeType {           return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {               MODE_NIGHT_FOLLOW_SYSTEM           } else {               MODE_NIGHT_AUTO_BATTERY           }       }   }}
А выбор темы можно реализовать таким образом:
private fun createNightModeChooserDialog(command: ShowNightModeChooserDialog): AlertDialog {   return AlertDialog       .Builder(ContextThemeWrapper(requireContext(), R.style.ThemeOverlay_AppTheme_AlertDialog))       .apply {           setTitle(getString(R.string.item_dark_theme_text_view_title_text))           val nightModes = arrayOf(               getString(NightModeType.MODE_NIGHT_NO.title),               getString(NightModeType.MODE_NIGHT_YES.title),               getString(NightModeType.getDefaultMode().title)           )           val selectedMode = command.selectedMode.customOrdinal           setSingleChoiceItems(nightModes, selectedMode) { dialog, which ->               val nightMode = NightModeType.fromCustomOrdinal(which)               persistentStorage.saveNightMode(nightMode.value)               AppCompatDelegate.setDefaultNightMode(nightMode.value)               dialog.dismiss()           }           setNegativeButton(               getString(R.string.fragment_dialog_night_mode_chooser_button_cancel_text),               null           )       }       .create()}

И тут тоже есть проблема: выбранная пользователем тема нигде не запоминается, поэтому её необходимо сохранять. Сделать это можно вот такой проверкой в методе onCreate у вашего activity:

override fun onCreate(savedInstanceState: Bundle?) {   checkNightMode()   setTheme(R.style.AppTheme)   super.onCreate(savedInstanceState)}private fun checkNightMode() {   val savedNightModeValue = persistentStorage.getSavedNightMode(AppCompatDelegate.MODE_NIGHT_UNSPECIFIED)   val selectedNightMode = NightModeType.fromValue(savedNightModeValue)   AppCompatDelegate.setDefaultNightMode(selectedNightMode.value)}

Заключение

Вместо пары недель на реализацию тёмной темы у нас ушло три месяца. Но мы не просто сделали тёмную тему на проекте Ростелеком Ключ, но и подняли дизайн приложения на новый уровень:

  • сформировали четкий и полный UI kit в Figma;

  • автоматизировали экспорт UI kit в Figma и опубликовали утилиту figma-export;

  • правильно реализовали все базовые компоненты в Android приложении;

  • поддержали новый режим edge-to-edge, и опубликовали библиотеку edge-to-edge-decorator, которая поможет быстро добавить режим edge-to-edge на других проектах.

Так сказать, мы вышли из зоны комфорта, ради темной темы, и поправили кучу моментов в основном процессе работы :)

Материалы для глубокого изучения

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

Подробнее..

Дизайн-концепт мерча для сотрудников

03.07.2020 14:20:11 | Автор: admin


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

Как появилась идея


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


Магазин решение сразу нескольких задач. Во-первых, хотелось делать качественный мерч в единой концепции, который продвигал бы бренд. Во-вторых, хотели набор предметов, который ребята могли бы получать за активности: выступления на мероприятиях, менторство новых роботов, обучение на внешних площадках и др. И наконец, хотели придумать, как поздравлять сотрудников с днем рождения.
Анастасия Зальцман, HR-директор

Масштаб и детали


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

Нам нужен был такой мерч, который стал бы продолжением фирменного стиля. Мы хотели сделать его настолько классным, чтобы его было приятно носить самим и люди из других компаний захотели бы себе такой же.
Евгений Бондарев, креативный директор дизайн-лаборатории

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

Примеры технической маркировки из мудборда по мерчу

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


Принт на первых футболках, спереди и на спине. Футболка корпус робота

Решение, которое мы нашли, понравилось: оно было понятным, небанальным и дружило с образом робота. И теперь нам нужно было придумать, как масштабировать идею и для других вещей.

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

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

Айдентика и фирменные элементы


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


Шрифты и цветовая гамма


Обязательное условие, чтобы мерч не шёл вразрез с корпоративным стилем, а был его частью. Объединяет всё наш шрифт, по стилю отсылающий к интерфейсам 80-х 90-х.

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

Типичная иллюстрация к статье в блог или в соцсети

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

Этикетка со штрихкодом


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


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

Иконографика


За основу мы взяли иконки-предупреждения на коробках, в которых упакована техника: стекло, опасно и др. Например, на зонте и дождевике есть знак капли, а на зарядке энергии.



Порядковый номер


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

Этикетки для разных предметов. Фирменные элементы: штрихкод, надпись, описание, номер устройства. Для значка исключение: это идентификатор, а не девайс, поэтому вместо номера ID

Изначально мы выбрали цветовую гамму, сочетающую в себе все фирменные цвета: красный, синий, черный белый. Но с развитием концепции перешли на более строгую гамму, которая логично вытекала из эстетики этикеток: прямоугольные белые наклейки с черным текстом. Это перекликалось с нашим представлением о красивой и минималистичной технике (не без влияния Apple, конечно): светлые цвета, отсутствие декора, полупрозрачные или серебристые материалы. Но мы же красные, страстные, поэтому все же захотелось добавить небольшие яркие элементы. Так придумали добавлять к каждой этикетке красный ярлычок с номером девайса.
Полина Резванова, дизайнер


Этикетка с описанием содержимого кружки

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

Что в итоге получилось


В октябре 2019 мы запустили магазин мерча Заправочная. Сейчас в нём 10 лотов, которые можно купить за специальные жетоны. Скоро появятся носки, пледы, поп-сокеты, чехлы для телефонов, панамы, картхолдеры, робот-конструктор, тряпочки для гаджетов и экологичный многоразовый стакан. Для того чтобы презентовать мерч сотрудникам, мы отсняли фотосессию:

Дождевик ждали почти 8 месяцев, нужную ткань купили только в Китае и искали производство для пошива изделия.

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



Инструкция к роботу:

Зонт.



Свитшот скоро появится в магазине.

Рюкзак всем понравился по дизайну, но он оказался не очень прочным в носке, поэтому в следующей партии мы планируем заменить фурнитуру.


Бутылка для воды.



Зарядка востребованный товар магазина. Нанесено на пауэрбанки Xiaomi.



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



Поясная сумка.



Значки один из самых легких лотов в производстве. Мы просто их заказали. Чтобы заработать на значок, нужно, например, чтобы твой комментарий опубликовали в СМИ.



Фирменный скотч. Боль нашего менеджера по работе с производством. Если коротко, то скотч почти опоздал к фотосессии мерча.


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


Советы тем, кто думает сделать мерч


  • Подумайте про ваш культурный код: чем вы отличаетесь от других компаний и придумайте историю, объединяющую ваших сотрудников, ценности и мерч. Крепкая коммуникационная идея превращает мерч в нечто большее, чем просто вещи.
  • Выделите критерии качества для предметов. Если вещь вам не нравится, не нужно запускать ее производство, сотрудникам она тоже не понравится. Лучше сделать меньше предметов, но они все должны быть такими классными, чтобы вам самим очень хотелось их приобрести.
  • Договоритесь в команде. Как правило, в компании есть те, кто топит за выделяющееся лого на вещах, но люди не хотят быть рекламными щитами, поэтому надо искать баланс. Мы выбрали путь без очевидного брендинга: кое-где есть аккуратный робот, а где-то только лого.
  • Мерч отдельный проект со сроками, задачами, командой. Без организационной системы всё это несерьезно.
Подробнее..

Как построить самовоспроизводящуюся практику

13.10.2020 12:18:27 | Автор: admin


Сегодня расскажем, как организовать устойчивое развитие команды, которая не подвержена кадровым кризисам. Где руководитель не тонет в оперативке, сотрудники не скучают годами на одинаковых задачах, старшие специалисты не бронзовеют, а привлечение новых сотрудников не стоит как крыло самолёта. Если не найдёте в тексте ничего нового для себя, поздравляем! А мы наступили на все приведённые грабли. Может быть, кого-то наш опыт убережёт от ошибок. Или подскажет решение.

В основе этой статьи воркшоп, который мы ведём для руководителей производственных команд. Начинаем его с опроса: Что вам мешает выкладываться и получать удовольствие от работы? Ответы получаем такие: руководители по горло в администрировании, опытные ребята со временем упираются в потолок и гаснут. Талантливые начинают звездить и манипулировать. На молодых специалистов особо не ставят, ими некому заниматься работа горит. Некогда точить, надо пилить и вот это всё.

Так выходит не потому, что какие-то неправильные люди плохо работают, а потому что в практике не выстроены процессы.

Практикой (или центром компетенций) мы называем группу специалистов одного направления, то, что в других компаниях называют отделом, подразделением, департаментом: практика дизайнеров, проджект-менеджеров, бизнес-аналитиков, QA-инженеров, разработчиков. В Redmadrobot, например, три практики разработчиков по направлениям: iOS-, Android- и Front-end + Back-end. Кроме решения функциональных задач на проектах практика производит и управляет знаниями. И работает относительно автономно от других практик, например, сама себе ставит квартальные цели в рамках общей стратегии компании.

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

Практика здорового человека

Теперь по порядку, почему всё нормально работает.

Команда


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

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

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

А вот картина, которая встречается гораздо чаще:

Практика курильщика

Здесь лид за всё отвечает сам, остальные специалисты плотно заняты на проектах. Есть ли в команде стажёры или молодые специалисты? Обычно нет. Это затраты и головная боль а команда и так еле успевает делать основную работу.

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

Если долго сидеть без движения, даже классные и лояльные специалисты начинают закисать. Начинаются манипуляции, предложения повысить зарплату за выслугу лет а этого компания не может себе позволить, пока не увеличит прибыль (или количество и размер проектов, если это сервисная компания). Недовольство легко распространяется и сказывается на всех в команде.

Это можно предотвратить, если настроить процессы на двух контурах: на внешнем работу с профсообществом, и на внутреннем работу со стажёрами. Сначала разберём внешний.


Работа с профсообществом

Повестка, технологии, кейсы


Если компания амбициозная, она старается быть в технологическом авангарде, делать передовые проекты и зарабатывать на них. Такие компании есть в каждой отрасли, на них все равняются. Работа с R&D прописана у них на уровне стратегии. Важная функций лида практики быть в курсе изменений в своей области и регулярно предлагать команде свежую повестку для освоения и реализации на проектах.

Повестка берётся из профессионального сообщества локального или международного масштаба. Это так называемые best practices: новые технологии и методики работы в ответ на новые запросы бизнеса и потребности пользователей. Примеры такой повестки: переход на server-side Kotlin в разработке, внедрение кастдева вместо рыночной аналитики в маркетинге и т.д, и т.п.

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

Чтобы специалисты были в курсе повестки, лид планирует загрузку команды таким образом, чтобы оставалось время на освоение, разработку и внедрение технологий на проекты. И организует распространение знаний между сотрудниками. Мы в Redmadrobot передаём знания через групповые мероприятия: технологизации, школы, воркшопы.

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

Так результаты работы компании и отдельных практик становятся видимыми. У заинтересованных специалистов появляется способ сделать профессиональную репутацию себе и команде. Потенциальные клиенты могут рассмотреть, что и как делает компания. Будущие сотрудники с какими задачами и технологиями работает команда, кто её заказчики.

Например, повестка аналитика данных в HR. Когда мы раскусили эту тему: собрали модель данных, описали процесс и критерии для сборки, получили первую статистику сразу же выпустили отчётный текст, чтобы показать, как работает технология в команде из нескольких сотен сотрудников. Этот кейс принёс нам первое место от hh.ru.

Ещё примеры:

Когда внешний контур не выстроен, последствий больше, чем кажется на первый взгляд:

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

Бывает и так, что руководство создаёт экспериментальное R&D направление, например, внутри практики разработки. Направление генерирует активность, но новые технологии не тестируются на проектах и не приносят прибыль. А компания ещё и тратит бюджет на работу таких экспериментаторов.

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

А ещё бывает, что руководитель единственный известный эксперт в практике. Он плотно вовлечён в профсообщество, развивает личный бренд: пишет статьи и выступает, на его громкое имя в компанию приходят новые сотрудники. У него большой простор для манипуляций: если решит уйти, за ним могут уйти и все, кто пришёл на него. Если вы не успели вырастить преемников, практика посыпется. Классический бас-фактор.

Работа со стажёрами

Фреймворки, знания, стажёры


Это внутренний контур. Если процессы в нём налажены хорошо, освоенные и реализованные на проектах технологии пополняют базу знаний практики. Знания библиотеки и фреймворки, репозитории в GitHub, борды в Miro, инструкции по работе с инструментами, а также учебные программы на основе отработанных паттернов решения задач. В производстве знаний участвуют сами специалисты, знания свободно распространяются внутри компании, загружаются в новых сотрудников и тех, кто идёт на повышение или меняет роль.

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

Мы учили разработчиков с момента основания Redmadrobot в 2008. Брали на работу лучших из лучших, но всех приходилось переучивать программировать под мобильные платформы с языков С, С# и др. Это было связано с особенностями разработки под мобилку: для iOS и Android ещё не было требований, кроме набора платформенных фреймворков. Специалисты не знали общепринятых правил разработки и не имели опыта за исключением разового, т.к. на их прошлой работе не было налаженной практики.

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

После первой же стажировки стало ясно, что нанимать разработчиков с необходимыми компетенциями стало проще. И что есть выхлоп на рынок. В комментариях к анонсу здесь, на Хабре, спрашивали: Когда следующий набор?. Выпускники рассказывали в соцсетях, как всё устроено изнутри, чему они научились за 2 месяца. На собеседования стали приходить ребята, которые через анонс узнали про компанию. Так к нам в команду попало много классных людей.

Узнаваемость главный эффект, а дополнительный наши ребята, организаторы и ведущие занятий, тоже получают от стажировок хороший опыт. Готовят контент для лекций, из которого потом получаются технические статьи и выступления. Прокачиваются как лекторы и наставники, после каждой лекции получают обратную связь о качестве материала и преподавания. С каждым годом к нам приходят всё более прокаченные ребята, и готовиться нужно ещё ответственнее. С 2015 года время мы прокачали 120 разработчиков и 22 пригласили в команду.

За ошибки во внутреннем контуре считаем не налаженную систему передачи знаний, отсутствие программ развития специалистов и работы с потенциальными сотрудниками. Самые частые последствия: специалисты много времени тратят на одинаковые ошибки. Мидлы и синиоры не видят перспектив роста. Компания много сил и денег расходует на привлечение новых сотрудников.

Если в практике всего пара человек


Допустим, всего два специалиста, старший и младший, а больше пока не требуется как им организоваться? Они могут объединиться с другой командой в супер-практику. До недавнего времени у нас так работала команда фронтенд и бэкенд разработки. Технологизации у них были общие, развитием команды занимался лид с опытом работы в обеих областях, работала система кросс-менторинга.

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

Как построить воспроизводящуюся практику. Главное:


  • Транслировать стратегию равномерно на всю компанию, чтобы каждый понимал что он делает и зачем.
  • Организовать систему передачи знаний и фреймворков внутри практики, чтобы исключить бас-фактор.
  • Растить помощников для руководителя, чтобы помогали развивать практику и высвободили у лида рабочие часы на стратегическое управление.
  • Осваивать и реализовывать новые технологии прямо на проектах, чтобы не терять деньги.
  • Наладить производство кейсов, чтобы визуализировать результаты.
  • Запустить систему работы со стажёрами, чтобы обеспечить приток молодых специалистов и держать в тонусе всю команду.


Текст: Настя Зальцман, HR-директор, и Артур Сахаров, директор производства.
Редактура: Таня Павлова, картинки: Полина Резванова.
Подробнее..

Идеи и советы по производству мерча для сотрудников

29.07.2020 14:20:10 | Автор: admin


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

Зачем вообще нужен мерч


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

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

Дождевик


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


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

С ней пришли на производство, где нам изготовили лекала под найденный дождевик, отшили пару образцов, которые мы кастомизировали под себя: к примеру, добавили карманы для удобства, а производство меняло лекала и вновь готовило нам образцы.

На что обратить внимание, если вы решили сделать брендированную верхнюю одежду:


  • Насколько ткань устойчива к влаге,
  • Насколько прочная ткань. Желательно устроить краш-тест: мять, тянуть, поливать водой и примерять на сотрудников,
  • Как смотрятся принты на ткани и как они ведут себя после стирки.




Для тестирования размеров мерча мы подобрали две типовые фигуры среди сотрудников. Так у лекал появились названия Женя и Полина. А Кристина была экстремальным пользователем, тестирующим пригодность размеров для миниатюрных фигур.

Для вещи с нуля запасайтесь примерно двумя месяцами:


  • 57 дней делают лекало и образец,
  • 34 дня доставка и тестирование образца,
  • 30 дней запуск в производство,
  • 14 дней пошив изделия.

Носки


Носки сделали в коллаборации с backend-разработчиками. Коллекция Redmadrobot X Back-end, включает изделия для выходных и праздников плюс входные носки для рабочих дней. Казалось, что носки достаточно популярный товар, поэтому с ним проблем быть не должно.


Но мы прошли Москву, МО, Питер, Нижний Новгород, сделали несколько тестовых образцов (стоимость одного от 1500 до 2500 рублей), и везде качество было не на высоте. Спасли Новосибирск и производство Stereo Socks.

На что обратить внимание при производстве трикотажа


  • Качество материала: ткань должна быть приятной к телу,
  • Качество резинки, особенно для спортивных моделей (чтобы не жало),
  • Как смотрятся вышивка и лого при растяжении: чтобы не было излишнего искажения изображения,
  • Насколько аккуратно выполнен внутренний шов,
  • Допустимые размеры шрифта для нанесения: нам пришлось сократить текст, чтобы он был читабелен.



Скотч


Скотч быстрый способ брендировать любую вещь. Кроме того, срок производства и цена радуют его можно сделать за одну-две недели. У нас получилась себестоимость в партии 216 рублей широкий и 360 рублей узкий. Вся партия составила 27 159 рублей.

Что ещё интересного


Не все вещи доставались нам через такие приключения, как скотч и дождевик. Например, значки сделать достаточно легко, они просто отливаются в фиксированной форме. Хорошие металлические значки сделали в питерском Pinhead.



Или самый популярный товар в магазине зарядка. Основа готовая модель. Мы выбрали Xiaomi качественные пауэрбанки, на которые можно делать нанесение (это возможно не на всех моделях), и они подходят для большинства телефонов. Примерка макета, не финальное нанесение:

А вот рюкзаки были задуманы как новогодний подарок. Мы увидели классную модель на производстве и попросили ее доработать с учетом пожеланий. Но несмотря на то, что рюкзак тестировали критично, в следующую партию внесем изменения: заменим фурнитуру на более износостойкую.



Все макеты обязательно распечатывали и примеряли. Если размеры мерча варьировались (как, например, у толстовок), то дизайнер выбирал тот размер этикеток, который одинаково хорошо смотрелся бы и на маленькой, и на большой толстовке. Всё потому, что производить этикетки разного размера стоило бы дополнительных денег.

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

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


Материал для нашивок важен, так как от него зависит качество печати. Заказывали в ГалаПолиграфе

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

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

Наконец, мы нашли единственный образец ленты, ширина которой была близка к нашим макетам и удовлетворяла всем дизайнерским требованиям. Так что нам оставалось в очередной раз (примерно 10-й по счету), но уже последний, подогнать макеты под нужный размер.

Факап с бутылкой для воды и что мы ещё не сделали


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

Маски для сна, которые мы придумали, нетиповые, а новые лекала никто не запускает в производство небольшим тиражом (от 30 до 100 штук). Удивительно, но мы также не нашли белую обложку на паспорт, которая была бы хорошего качества приятная на ощупь и не жесткая.

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

Опечатка, отправившаяся в производство


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


Если очень присмотреться, то можно заметить

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

Восемь советов, как выпустить хороший мерч


Мерч и магазин мерча полноценный проект, с командой, менеджментом, бюджетом, KPI и целью.

  • В основе всего идея. Придумайте крепкую коммуникационную идею, которую можно масштабировать под разные товары.Тогда у вас будут не просто брендированные вещи, а целая история. Продумайте, как мерч работает с культурным кодом компании и какие ценности несет.
  • Мерч для сотрудников. Привлекайте коллег из разных отделов к работе над проектом. Например, ребята из разработки придумывали с нами носки. И на съемку мы пригласили наших сотрудников, потому что они и есть носители продукции.
  • Принцип 80/20. Основная работа подготовка. Сядьте, представьте вашу цель, напишите план, потом делайте. Не бойтесь перед пошивом десять раз примерить к одежде распечатанные лекала, чтобы убедиться что всё на своем месте.
  • Точно распределите роли, кто и что делает. Работа над магазином проект, и четкий менеджмент важен. Группа собирается не просто повеселиться: у нее есть дедлайны и задачи.
  • Хороший поставщик половина успеха. Если не нашли поставщика в своем городе, смотрите другие регионы. В большинстве случаев стоимость регионального производства ниже, и доставка себя окупает.
  • Правило семь раз отмерь, один раз отрежь применяется не только к пошиву, но и к формулировкам ТЗ. Производственные лекала достаточно дорогие, например, лекало дождевика стоило семь тысяч, поэтому комментарии к образцам нужно давать подробные, и когда вся команда их обсудила и уверена в своих пожеланиях. Пишите техническое задание для производства (вплоть до мелочей), но будьте готовы к диалогу с исполнителями: иногда они лучше тебя знают, как правильно сделать.
  • Делайте тестовые образцы. Мы переделывали вещи по три-четыре раза и это нормально. Тестируйте ткань и нанесение. Не стесняйтесь устраивать краш-тесты образцам: стирайте, гладьте, рвите и т. д. Если не нравятся образцы, ищите качественные вещи в продаже и потом показывайте их в качестве образца производству. А также обязательно примеряйте мерч на людях с разным ростом, разными типами фигур и даже с разными привычками (например, кто-то носит сумку через плечо, а кто-то на поясе).


И последний совет запаситесь пространством для хранения

Контакты производств



А у вас есть мерч? Как он выглядит? Если нет, то планируете?
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru