Дипломная работа: Разработка технологии непрерывного тестирования программного кода при контейнерной виртуализации на примере многопользовательского мультиплатформенного приложения

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

protocol IRequest {

var urlRequest: URLRequest? { get }

}

protocol IParser {

associatedtype Model

func parse(data: Data) -> Model?

}

struct RequestConfig<Parser> where Parser: IParser {

let request: IRequest

let parser: Parser

}

protocol IRequestSender {

func send<Parser>(config: RequestConfig<Parser>, completionHandler: @escaping (Result<Parser.Model>) -> Void)

},

где IRequest - интерфейс структуры, хранящей в себе информацию о запросе;

IParser - дженерик интерфейс для структур, отвечающих за распознавание приходящих данных, где Model - тип или структура данных, получаемая после преобразования;

RequestConfig - необходимый уровень абстракции, позволяющий совместить в одном месте структуры запроса и ответа;

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

Реализация классов будет приведена в приложении.

Слой сервисов стоит следующим после основного слоя в иерархии и содержит в себе бизнес-логику конкретного приложения, под которое этот слой был написан. Именно в этом слое находится логика работы с геолокацией и веб-сокетами - основной слой для этих технологий писать не имеет смысла, так как он предоставляется в библиотеках, которые импортируются перед написанием кода класса сервиса. Так, для работы с геолокацией была выбрана библиотека, написанная компанией Apple - CoreLocation, а для работы с сокетами - библиотека с открытым исходным кодом - Starscream. Сервис, при необходимости, может содержать в себе экземпляр нужного ему класса из основного слоя, инжектируемый, как правило, посредствам композиции.

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

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

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

Так же в программе повсеместно используются принципы SOLID[]:

Single responsibility principle (Принцип единой ответственности) - каждый класс, будь то сервис или компонент основного слоя, или же элемент отображения, имеет всего одну ответственность, что позволяет повысить его переиспользуемость;

Open/Closed principle (Принцип открытости/закрытости) - классы, модули, функции должны быть открыты для расширения, но закрыты для изменения;

Liskov substitution principle (Принцип подстановки Лисков) - функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом;

Interface segregation principle (Принцип разделения интерфейсов) - лучше создавать много отдельных интерфейсов, чем один универсальный.

Dependency inversion principle (Принцип инверсии зависимостей) - модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те и другие должны зависеть от абстракций.

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

В программе особое внимание уделено отсутствию антипаттернов проектирования. Таким образом в коде не используется паттерн Singleton (невозможно оттестировать класс, созданный в согласии с этим паттерном), нет жесткой связности (при соблюдении принципа инверсии зависимостей данной проблемы не возникает). Код написан с расчетом на последующее тестирование. В программе отсутствует преждевременная оптимизация - все архитектурные решения были приняты из явной необходимости. Все переменные названы в соответствии с их назначением, что дает возможность не всегда погружаться в код, а понимать происходящие из именования. Повторение кода сведено к минимуму, но с оглядкой на отсутствие преждевременной оптимизации. Существует, так называемое, правило трех - согласно которому если код повторился один раз, то проблемы нет, но если его приходится переписывать в третьем месте, то лучше инкапсулировать логику. Это имеет смысл, так как инкапсулировать логику для двух вызовов не всегда оправданно и является нарушением принципа YAGNI (You ain't gonna need it - тебе это не понадобится).

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

7.2 Разработка алгоритма непрерывного тестирования кода

В ходе работы над алгоритмом непрерывного тестирования он претерпел большое количество изменений, доработок и улучшений. Изначально, в качестве системы контроля версий был выбран Gitlab, а для запуска тестов и доставки сборок - Gitlab CI. Было решено запускать тесты на локальной машине, которая в рамках терминологии Gitlab называется Runner'ом и конфигурируется перед добавлением в репозиторий. Был написан файл конфигурации .yml и все необходимые скрипты, о которых речь пойдет ниже. Выбор в сторону такого стока технологий был сделан из-за удобства и замкнутости системы - репозиторий готов к работе с системой непрерывного тестирования сразу и это позволяет сосредоточиться на других аспектах разработки.

В вышеописанном подходе существуют недостатки, главные из которых - привязанность к определенной системе контроля версий (не всегда Gitlab удобнее аналогов), локальная машина может сломаться или затормозить, иногда нужно больше одной машины для параллельной прогонки тестов или выгрузки сборок (у одной машины может не хватить мощностей). Для решения вышеописанных проблем было решено перейти на другую систему, а именно - Travis CI. Так же было решено поменять систему контроля версий на Github для возможности сделать исходный код проекта открытым (Github лучше подходит для подобных целей, чем любая другая платформа). Travis CI был интегрирован с репозиторием проекта на Github и сконфигурирован для выкладки сборок.

Для исполнения скриптов и написания логики был выбран Fastlane. Fastlane - это обертка над скриптами IDE XCode, которая включает в себя базовые проверки и решает определенное количество низкоуровневых проблем. Эта система была выбрана, чтобы избежать уже решенных ранее сообществом проблем и перейти к дальнейшей разработке, а также из-за возможности посмотреть открытый исходный код, выложенный на Github. Так же Fastlane предоставляет скрипты для взаимодействия с API iTunes Connect, что значительно упрощает процедуру электронной подписи программного кода посредствам сертификата и профиля приложения.

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

Тем не менее у Fabric тоже есть свои ограничения - эту платформу можно использовать только для внутреннего тестирования в рамках компании, так как подписывать сборку требуется профилем типа AdHoc, то есть при выкладке сборки нужно указать на какие устройства эта сборка может быть поставлена (нужно включить в профиль список UUID возможных устройств). Если сторонний тестировщик попробует поставить сборку на незарегистрированное в сертификате устройство - сделать этого не получится и придется добавлять его устройство в профиль и переподписывать сборку, что так же занимает время.

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

Было решено настроить процесс выкладки таким образом, что при заливании кода в ветку develop происходила выкладка на платформу Fabric, а при заливании в master - в Testflight. Такое решение было принято исходя из идеологического назначения соответственно проименованных веток. Именование develop используется для ветки, в которой ведется разработка, иными словами - в нее довольно часто заливается код с доработками из других веток. Такие сборки нет смысла выкладывать для широкого круга тестировщиков, так как в них будет серьезное количество багов, которые могут быть пойманы без их участия. В master же выкладываются сборки, которые предполагается выложить в магазин приложений, то есть выкладка происходит уже значительно реже, чем в develop, и требует более тщательной проверки, что предполагает распространение сборки на внешнее тестирование.

Файл конфигурации на этот момент разработки выглядел следующим образом:

language: swift

osx_image: xcode10

xcode_project: Point.xcworkspace

xcode_scheme: Point

xcode_destination: platform=iOS Simulator, OS=11.4, name=iPhone X

before_install:

- sudo gem install fastlane -NV

- sudo gem install cocoapods --pre

script:

- sh build.sh

branches:

only:

- develop

- release

- master

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

Затем идет поле before_install, в котором указываются скрипты, которые необходимо исполнить перед запуском основного функционала, в данном случае требуется установить Fastlane и Cocoapods - менеджер зависимостей, который позволяет использовать в проекте сторонние библиотеки.

После идет самое значимое поле - script. В нем размещается необходимый для исполнения код. В ходе разработки было принято решение перенести код этого раздела в отдельный файл - build.sh и разместить его в директории проекта. Полный код данного файла доступен в Приложении.

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

После настройки системы доставки, было решено настроить запуск тестов. В среде разработке XCode существует возможность создавать родные тесты для проектов, и это касается как unit-, так и end-to-end-тестов. Unit-тесты с самого начала предполагалось писать с помощью встроенных возможностей IDE, так как на их прогонку затрачивается намного меньше времени, чем на остальные виды тестов.

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

final class URLSessionMock: URLSession {

typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void

var data: Data?

var error: Error?

override func dataTask(with request: URLRequest,

completionHandler: @escaping CompletionHandler) -> URLSessionDataTask {

let data = self.data

let error = self.error

return URLSessionDataTaskMock {

completionHandler(data, nil, error)

}

}

}

final class URLSessionDataTaskMock: URLSessionDataTask {

private let closure: () -> Void

init(closure: @escaping () -> Void) {

self.closure = closure

}

override func resume() {

closure()

}

}

Для удобства был обозначен новый тип данных CompletionHandler, затем объявлены два поля - поле данных (data) и поле ошибки (error), которые нужно заполнять при использовании класса, в зависимости от желаемого поведения (именно из-за этого они не объявлены как приватные). Затем указанные объекты при создании объекта задачи передаются в объект-заглушку сессии, которая переопределяет метод, отвечающий за обращение к сети (resume) и передает в ассинхронном ответе заранее известные данные. Подобная практика часто используется в unit-тестировании для тестирования внутренней логики приложения, без зависимости от работы сторонних сервисов.