Тестирование – важная часть iOS разработки. В том, что это действительно так, лишний раз убедились эксперты Microsoft в ходе недавнего исследования. У них получилось, что поддержка в команде автотестов – самозапускающихся тестов – уменьшает число ошибок, возникающих при очередном обновлении IT-продукта на 20,9%. Согласно ряду других исследований результативности функционального, автоматического, модульного, сквозного тестирования, с помощью практик тестов удается предотвратить от 60 до 90% всех ошибок, из-за которых компании терпят убытки, теряют время и отстают в конкурентной борьбе.

В данной статье Алексей Кондаков, инженер-разработчик с более чем 20-летним опытом работы в таких организациях, как Motorola, Dell и других крупных международных цифровых компаниях, расскажет об особенностях работы с тестами и правилах их эффективности. Дело не только в том, что благодаря им снижается количество багов, а еще и в стоимости устранения, которая зависит от того, на какой стадии их удалось отловить. По данным исследования IBM: цена устранения багов после релиза в 4-5 раз выше, нежели борьба с ними на этапе проектирования. Если эта практика столь полезна, какие рекомендации можно дать, чтобы тесты действительно работали при разработке приложений под iOS?

Юнит-тестирование в Swift

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

Кон разработал пирамиду автоматизированного тестирования, в которой наибольший акцент сделан именно на модульных тестах. Их доля в иерархии проверок должна составлять ни много ни мало 80%. Остальные 20% приходится на интеграционные, приемочные и интерфейсные тесты. Кон считал, что именно модульные тесты действуют на том уровне и в тот момент, когда можно отловить максимум ошибок при минимуме усилий. Таким образом, вопрос об обеспечении качества iOS разработки, это на 80% вопрос Unit-тестирования. Для Unit-тестирования можно использовать разные библиотеки, которые устанавливаются в корпоративное окружение.

Одними из наиболее популярных являются XCTest, UITest. Алексей привел пример распространенного теста-asserta, суть которого заключается в том, что вы прогоняете через класс или функцию «боевого» кода определенные аргументы, а потом сравниваете результат, получившийся de facto, с ожидаемым, который положили в переменную retrievedItem. Сравнение того и другого происходит вот здесь:

XCTAssertEqual(retrievedItem, item)

class checkAuthClass: XCTestCase {

    func testPasswordInput() {

        var collection = ContentCollection()

        let item = Item(id: "an-item", title: "A title")

        collection.add(item)

        let retrievedItem = collection.item(withID: "an-item")

        XCTAssertEqual(retrievedItem, item)

    }

}

1.   Чистота кода и архитектуры

Понятие чистых архитектур, чистого кода применяется для характеристики качества кодовой базы вообще. Однако тесты – одна из миллиона причин, причем, возможно, важнейшая из них, почему этого принципа надо придерживаться. Наличие хаотичных инъекций зависимостей (dependency injections), когда различные элементы программы не инкапсулированы должным образом, а переплетены между собой и взаимозависимы на разных уровнях – опасно. Это не только создает ситуации, когда после релиза может отказать совершенно неожиданный сегмент «боевого» кода. Как утверждает Кондаков, такие архитектуры и такой код чрезвычайно трудно тестировать. Если ваш класс на вход должен принять некую информацию, то формироваться она должна снаружи класса/функции. Ваш самостоятельный инкапсулированный класс должен не формировать эту информацию внутри себя, а получать на вход. Это необходимо в том числе для того, чтобы при тестировании вы могли взять интересующий класс и передать ему любые сведения, будучи уверенными, что в контексте «боевого» проекта он сработает точно так же, как в тесте. Поэтому Алексей советует выдерживать выбранные архитектурные решения, например, если с самого начала в качестве паттерна разработки был выбран MVP (Model, View, Presenter), как часто и бывает, то нужно сохранять правильные связи этих частей единого проекта.

К сожалению, популярная в прошлом архитектура MVC не удовлетворяет этим требованием и сейчас не может быть рекомендована к использованию. Примеров архитектур сейчас представлено очень много, от общепринятых MMVM, VIPER до довольно экзотических, и дело архитекторов - оценить их плюсы и минусы и выбрать подходящую. Главное - они должны позволять разделять ответственность классов и методов, поддерживая инъекции кода и данных.

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

2.   Покрывайте Swift-код протоколами  

В языке Swift имеется очень полезная конструкция «протокол», которая примерно соответствует интерфейсам других языков программирования. Как пояснил Кондаков, протокол - это шаблон, которому должен удовлетворять класс, реализующий его и устанавливающий направления и ограничения для последующей разработки. Это работает как poka-yoke (защита от дурака). Вы просто не можете создать класс, частично удовлетворяющий требованиям протокола, компилятор заставить Вас написать реализацию, пусть даже самую базовую.  И при этом ошибки предотвращаются на этапе, когда в тестировании нет необходимости. Стоит использовать по максимуму функционал протоколов, чтобы обеспечить возможность тестировать компоненты вашего приложения независимо. В частности, протоколами важно покрыть уровень, на котором UI приложения на iOS взаимодействует с «бэком»: базы данных, сетевые запросы. Если все это унифицировано и сделано правильно, то несложно будет создавать mocking-объекты для модульного тестирования. Причем тесты, направленные на них будут отражать то, как реально отработают их «боевые» оригиналы, от которых зависит работа приложения.

3.   «Моки» и лучшие практики 

«Моки» (mocks) представляют собой классы, функции и другие компоненты, имитирующий работу своих реальных прототипов из «прода» и способные производить инъекции нужных Вам тестовых данных в нужное время. Копии могут быть полным аналогом оригинала, а могут быть его упрощенным вариантом. Существует пять видов «моков», но сейчас мы на этом останавливаться не будем. Это совсем другая история. Скажем лишь, что «моки» создают, чтобы безопасно имитировать на них внешние и внутренние зависимости, то есть передать в объект или функцию некие данные и посмотреть, как «мок» их обработает. Нередко «фронтовая» часть приложения для Apple, написанная на Swift, взаимодействует через сетевые запросы (HTTP) с API на C или Java, можно имитировать получение каким-либо объектом Swift json, как будто он приходит из API. Передать его в объект и выполнить юнит-тест по какому-либо признаку

{

    "glossary": {

        "title": "example glossary",

        "GlossDiv": {

            "title": "S",

            "GlossList": {

                "GlossEntry": {

                    "ID": "SGML",

                    "SortAs": "SGML",

                    "GlossTerm": "Standard Generalized Markup Language",

                    "Acronym": "SGML",

                    "Abbrev": "ISO 8879:1986",

                    "GlossDef": {

                        "para": "A specification, used to create markup languages such as DocBook.",

                        "GlossSeeAlso": ["GML", "XML"]

                    },

                    "GlossSee": "markup"

                }

            }

        }

    }

}

Из опыта Алексея, при использовании «моков» объектов «в бою» следует три момента:

●     организуйте сведение мок-классов, которые создают отдельные разработчики для тестирования своих компонентов проекта в библиотеки мок-классов. Это позволит не тратить время, в сотый раз изобретая велосипед и использовать уже проверенные решения. Пример: представьте, что пишется сетевой запрос для проверки одного из классов. С большой вероятность можно сказать, что такой запрос понадобится в десятках тестов, которые еще будут разработаны в ближайшее время. Разработчикам следует сначала по библиотеке проверить, было ли уже написано то, что им нужно, а уж затем тратить время на написание своего собственного «мока»;

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

●     используйте в проекте библиотеки для инверсии зависимостей. я уже говорил про необходимость хорошей архитектуры проекта для эффективных тестов. Чтобы создание «моков» на основе ваших реальных объектов было эффективным, используйте библиотеки подключения зависимостей. Например, SWinject. Как и протоколы, он позволят обеспечить инверсию зависимостей, то есть перенести во вне класса и метода то, что иначе формировалось бы внутри него, через конструктор или вызов объекта внутри метода. Необходимо добиться, чтобы любой класс и метод в вашей «боевой» версии проекта только обрабатывал информацию, а не получал/создавал ее внутри. Если это не реализовать, mocking-тестирование сильно осложнится. Не получится так же эффективно брать какой-нибудь метод отдельно и имитировать его работу внутри проекта, создавая «фейки» и «стабы» – имитацию настоящих взаимосвязей в проекте.

4.   Если руководство требует покрыть тестами user interface (UI)

Упомянутые выше выводы Майкла Кона, согласно которым интерфейсному тестированию не стоит уделять значительного внимания, дают понять, что лучше сосредоточиться на модульном тестировании. Вдобавок, unit-тесты для проверки интерфейсов считаются слишком долгими в написании. Тем не менее, возможны обращения со стороны клиентов, которые заставят руководство потребовать полноценного покрытия классов интерфейсов юнит тестами. По мнению Алексея, лучше всего, если будет принято решение доработать модульные тесты, чтобы в UI-интерфейсах проблемы не давали о себе знать. Но если уже пришлось заниматься интерфейсным тестированием, то надо знать, что не любой фреймворк, рассчитанный на Unit тесты годится для такого. Не буду останавливаться на тестировании SwiftUI, а больше сосредоточусь на возможности тестирования UIKit-based кода.

5.   Тестирование UIViewController  

В типичной для iOS приложения на Swift архитектуре MVP, так называемые контроллеры – классы-посредники управляют взаимодействием классов, обращающихся к базе данных и классами View, показывающими пользователю экраны и элементы интерфейса. Главный среди контроллеров – UIViewController. Этот класс является унаследованным от стандартного UIViewController и его использование рекомендовано в официальной документации для iOS-разработчиков от Apple. Типичная стратегия проверки интерфейсов заключается в сквозном тестировании и тестировании снимков экрана. Снимки хороши для тестирования визуальной составляющей, но ничего не говорят о содержании страницы. Сквозные тесты (XCUI-driven-tests) слишком далеки от кода, который они проверяют, поэтому могут многое пропустить. В случае ошибки отследить проблему будет крайне сложно. Оба типа тестов очень дороги с точки зрения экономики разработки, ведь UI приложения постоянно изменяется. Можно тестировать интерфейсы на уровне работы контроллеров и других элементов UI кита с помощью юнит-тестов. Однако модульное тестирование классов-контроллеров, которые выводят View приложения могут быть хуже, чем бесполезными, если проверяете вы не то. Что же тогда тестировать?

Так как с модульными тестами проверить внешний вид того, что видит пользователь невозможно, то тестировать нам надо стили и роуты UIViewController и UIView. В качестве примера того, как можно делать это неправильно, приведу несколько конкретных примеров:

●     неправильно тестировать цвет фона элемента в UIView;

●     неправильно проверять цвет текста, шрифт и размер в UILabel;

●     неправильно проверять Auto layout и ограничения.

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

●     правильное количество ячеек сетки формируется в интерфейсе?

●     текст UILabel правильный?

●     кнопка enabled?

●     frame UIView корректен?

Контроллеры, управляющие выводом можно протестировать, если они изолированы от своих зависимостей. О DI – dependency injections – была информация выше. Во время тестирования происходит замена реальных зависимостей имитацией. Чтобы проводить такие тесты было легко, контроллеры должны быть пассивными. Это значит, что они только маршрутизируют пользователя в ответ на события и показывают экраны. Никакого подтягивания данных из модели прямо в контроллере! Никакого обновления самих себя из модели! Это сделает написание полноценных «моков» невозможным.

6.   Mockingbird и модульное тестирование Swift

Кондаков также отмечает, что данная статья будет неполной, если не упомянуть полезные фреймворки. С одной стороны, это облегчает изоляцию исследуемого элемента от зависимостей чистоты кода и архитектуры, с другой – ускорить работу по тестированию могут библиотеки, многие решения в которых заложены «под капот» и не требуют «изобретать велосипед». Такая библиотека, как Mockingbird в Swift незаменима, потому что позволяет парой строк кода создавать «моки», stubs, заглушки, которые имитируют реальные зависимости, которых работает исследуемый объект.

7.   Mockingbird vs builder class

Иногда случается так, что нужный аспект работы объекта не удается проверить с помощью библиотек. В этом случае можно прибегнуть к возможностям самого языка.. Прежде всего к «билдеру классов». Прописать необходимый эксперимент, а затем проверить работу с помощью функции build(). К достоинству этого способа Алексей относит то, что он не ограничен узким функционалом фреймворка и вы можете с его помощью все, что вообще можете в языке Swift. Вот пример «билдера»:

let view = ArticleViewBuilder()

    .withTitle(article.title)

    .withSubtitle(article.subtitle)

    .withImage(article.image)

    .build()

«Билдером» для модульного тестирования можно пользоваться и для проверки языка «бэка» многих iOS-приложений. Речь об Objective C.  

Unit-тестирование в Objective C 

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

1.   OCMock и swizzling: встроенные возможности языка

Применение объекта OCMock и метода swizzling для создания «моков» из встроенной в Objective C библиотеки требует большего количества кода для написания теста, но дает заметно больше вариантов в решении проблем конкретного текста, чем другие библиотеки. Например, если класс отдается внешней библиотекой, то сымитировать его зависимости без OCMock становится крайне сложно. С методом swizzling невозможное становится возможным.

2.   OCMock как рабочий вариант

Те, кто привык в Swift, могут не посмотреть в сторону объекта OCMock при тестировании объектов в Objective C, между тем, именно в этом языке он становится реальной альтернативой функционалу других библиотек, поскольку именно в Objective C метод swizzling ничем не ограничен и его можно использовать по максимуму. Алексей Кондаков дал несколько рекомендаций при тестировании в Objective C с использованием объекта OCMock:

●     если OCMock ограничен в вашем окружении (блокируется чем-либо) или Вам нужно “переопределить”   init-метод, корректная работа с которым OCMock не гарантируется - используйте swizzing. Это более громоздкий метод, но иногда только он позволяет достигнуть нужного результата;

●     если вы тестируете какие-то компоненты проекта, связанные с библиотеками на C, не забудьте сделать макросы вида: MOCK_DEF, MOCK_REF, MOCK_SET, MOCK_UNSET. Благодаря этому у вас будет готовая переменная с указателем на функцию, которой можно удобно и быстро пользоваться внутри самих unit-тестов:  вы сможете присваивать этой переменной указатель на свою мок-функцию, и из нее уже реализовывать необходимое мок-поведение.

3.   Mockito и OCMock: что выбрать?

Так же, как в Swift, в Objective C у вас есть популярная альтернатива OCMock в виде внешней библиотеки Mockito. Если сравнивать их, то быстро оказывается, что OCMock обладает гораздо большим набором методов и функций. Со своей стороны, Mockito, очень легкая и быстрая. Разработчикам решать, особенности какого фреймворка для них наиболее актуальны.

Вместо заключения, или: а что если на «бэке» использовали Objective C++

Этот вариант встречается реже, но столкнуться с ним можно. Речь идет о коде, написанном с включениями сегментов C++. В этом случае можно порекомендовать фреймворк GMock (Google Mock), как лучший вариант для этого конкретного случая. Он, по мнению Алексея, выглядит более выигрышно по сравнению с другими вариантами по простоте, подбору методов, другим параметрам.

Источники :

Сейчас на главной

21 окт. 2024 г., 20:48:39
Актуальная линейка трехфазных ИБП Systeme Electric

Совместное мероприятие компании OCS и Systeme Electric, российской производственной компании с экспертизой в области управления электроэнергией. В рамках презентации Сергей Смолин, менеджер по продукту «Трехфазные ИБП», представил следующие темы: • Обзор продуктовой линейки трехфазных ИБП Systeme Electric; • Технические решения и преимущества каждой из линеек ИБП; • Сервисная поддержка оборудования.