![](https://habrastorage.org/web/a42/fe3/230/a42fe3230f2a49fe80542fef4ce0b593.jpg)
С недавних пор мы взялись за внедрение UI-тестирования в iOS для iFunny. Путь этот тернист, долог и холиварен. Но все равно хочется поделиться с умными людьми своими первыми шагами в этом направлении. На истину не претендуем – всё примеряли к собственному продукту. Поэтому под катом немного информации о том, что такое iFunny на iOS и зачем нам понадобился UI + много фидбека по инструментам и примеров кода.
Что такое iFunny на iOS
iFunny — это популярное в США приложение про юмор и мемы с ежемесячной аудиторией в 10М. Подробнее о том, как все затевалось, можно прочитать здесь. Разработка приложения на iOS стартовала 6 лет назад, и мы до сих пор обходимся без каких-либо революционных вкраплений:
- 99% кода держим на Objective-C;
- придерживаемся классического MVC с аккуратными делениями на модули;
- активно работаем с Cocoapods для зависимостей;
- используем собственный проигрыватель webm-контента: сторонние решения тормозили, не давали контенту зацикливаться и прочее. В случае с iFunny, который является полностью UGC, эта тема критична;
- форк от SDWebImage используем не только для картинок, но и для остального загружаемого контента;
- для API выбираем RestKit – достаточно зрелый фреймворк, за несколько лет работы с которым почти не было проблем.
Unit-тесты
![](https://habrastorage.org/web/629/b08/27a/629b0827afb844f69d025213f3b6d230.jpg)
Unit-тесты мы используем для критичных моментов бизнес-логики и Workarounds. Вот довольно простой тест: тестируем метод нашей модели, который проверяет поступление к нему нового контента.
- (void)testIsNewFeaturedSetForContentArrayFalse {
FNContentFeedDataSource *feedDataSource = [FNContentFeedDataSource new];
NSMutableArray *insertArray = [NSMutableArray arrayWithArray:[self baseContentArray]];
feedDataSource.currentSessionCID = @"0";
BOOL result = [feedDataSource isNewFeaturedSetForContentArray:insertArray];
XCTAssertFalse(result, @"cid check assert");
feedDataSource.currentSessionCID = @"777";
result = [feedDataSource isNewFeaturedSetForContentArray:insertArray];
XCTAssertTrue(result, @"cid check assert");
}
Второй класс тестов, который мы используем, – это тесты, которые необходимы, чтобы проверить правила переопределения классов. В один момент нам понадобилось написать много однотипных классов для системы аналитики, отличающихся набором статичных методов.
Xcode и Objective-С не давали какого-либо решения для защиты от неправильно написанного кода.
Поэтому мы написали такой тест:
- (void)testAllAnalyticParametersClasses {
NSArray *parameterClasses = [FNTestUtils classesForClassesOfType:[FNAnalyticParameter class]];
for (Class parameterClass in parameterClasses) {
FNAnalyticParameter *parameter = [parameterClass value:@"TEST_VALUE"];
XCTAssertNotNil(((FNAnalyticParameter *)parameter).key);
XCTAssertNotNil(((FNAnalyticParameter *)parameter).dictionary);
}
}
Здесь проверяется, что у класса определены 2 статичных метода, key и dictionary, необходимых для правильной работы отправки событий в системы аналитики.
UI-тесты
Мы уже достаточно хорошо изучили работу с UI-элементами и поразмышляли над тестовым окружением в процессе написания тестов для Android. Получилось примерно так:
- отдельный flavor для запуска приложения с предварительными настройками, чтобы не задавать их вручную в тестах каждый раз;
- моки для API с использованием WireMock, чтобы каждый раз не лезть за ответами на сервер и не зависеть от него;
- поигрались с процессом запуска тестов и настроили на CI Bitrise достаточно удобный флоу, в ходе которого тесты заливаются и запускаются на реальных девайсах в Amazon Device Farm, отчеты со скриншотами и видео мы можем посмотреть там же, перейдя по ссылке из Bitrise.
На поток поставить не получилось, так как занимались разработкой новой версии и ждали, когда все утрясется. Сейчас активно восстанавливаем и нарабатываем тестовую базу.
Пришла очередь iOS, и мы, команда QA и iOS-разработчики, начали с того, что еще раз собрались и аргументировали для себя, зачем нам нужны автотесты. Это был важный ритуал, и действовал он почти как мантра:
- уменьшить объем ручных проверок;
- автоматизировать регресс;
- обеспечить постоянное тестирование приложения, чтобы в любой момент времени знать, в каком состоянии оно находится.
Инструменты
Начали с выбора инструментов. На повестке было 3 основных фреймворка, которые сейчас чаще всего используются для тестирования мобильных приложений. Мы примерили каждый из них.
![](https://habrastorage.org/web/691/fca/de8/691fcade8cff4a55b3cbc92c19286ecd.png)
Appium – популярный кроссплатформенный фреймворк. Бытует мнение, что именно он станет стандартом в тестировании мобильных приложений в ближайшем будущем. Несколько месяцев назад мы решили потестить его как с полгода вышедшей iOS 10, но немного огорчились: версия Appium с ее поддержкой была в бете, а использовать в проде нестабильную версию не очень хотелось. Appium Inspector, который работает на Android, тоже использовать не смогли: не было поддержки Xcode 8 и iOS 10. Вскоре они выпустили stable-версию, но ждать полгода после обновления оси для нас крайне нежелательно. Решили не мучить ни себя, ни Appium.
![](https://habrastorage.org/web/4d9/a81/df0/4d9a81df01cb4e1694e9dcb362c99fce.png)
Calabash – кроссплатформенное open source решение, которое использует подход BDD в написании тестов и до последнего времени поддерживалось компанией Xamarin. Недавно разработчики сообщили, что поддержка – всё. Мы тоже решили дальше не идти.
![](https://habrastorage.org/web/3c1/1f3/ef8/3c11f3ef89df4c0993eb095fa010571d.png)
И, наконец, XCTest – нативный фреймворк от Apple, который мы в итоге выбрали. Поэтому почитайте про плюсы:
- нет лишних зависимостей, которых у нас в проекте и так много;
- кроме самого Apple, никто со стороны не принесет и не добавит багов. У нас уже был опыт с Appium и KIF. Получалось так, что внизу все равно используется XCTest и баги Apple накладываются на баги KIF, а это значит садись, дружок, и ковыряй большие фреймворки. Эти зависимости нам точно были не нужны;
- можно использовать стандартные языки iOS-разработки Objective-C и Swift: QA могут легко взаимодействовать с разработчиками;
- тестируемое приложение – это черный ящик, кроме того, в тесте можно работать с любым приложением в системе.
Потом рассмотрели еще и Recorder — нативный инструмент от Apple, который позиционируется как вспомогательный, без надежды на то, что он будет использоваться при написании реальных тестов. С его помощью можно изучить лейблы UI-элементов и поиграться с основными жестами. Recoder сам пишет код и генерирует указатели на объекты, если это не было сделано при разработке. Это единственное преимущество, которое мы смогли выделить. Минусов оказалось гораздо больше:
- сложно записать тест, потому что UI тормозит – делаешь какое-то действие и ждешь секунд 10-15, чтобы оно записалось. Неудобно;
- код пишется всегда разный. Сегодня я такой умный и назову этот элемент button[1], а завтра – “smilebutton”. Непонятно;
- постоянные ошибки в распознавании жестов. Вы можете сделать swipe left, а он определит, что это tap. Делаешь tap, а это уже swipe. Нестабильно;
- сломанный тест, записанный с помощью Recorder, скорее всего, придется заново полностью переписывать, потому что он не будет отражать реальной ситуации. Просто WTF?!
Разработчик спешит на помощь
А теперь про проблемы, с которыми столкнулись на практике: их мы будем решать, привлекая разработку.
Черный ящик
![](https://habrastorage.org/web/1ca/05f/58a/1ca05f58a0644ba0820bc95c52a864b5.jpg)
Плюс черного ящика оборачивается в минус: мы не можем знать о текущем состоянии приложения ни на девайсе, ни на симуляторе. Нам необходимо его обнулить и создать по аналогии с Android определенную среду, где приложению сообщается, в какой стране мы работаем и с какими пользователями хотим взаимодействовать. Все это решается с помощью настроек запуска приложения.
Также нам понадобились pre-action в Xcode. Для того, чтобы сбрасывать рабочую среду перед каждым тестом, мы решили удалять с симулятора установленное приложение, чтобы обнулить настройки пользователя и все, что сохранено в песочнице:
xcrun simctl uninstall booted ${PRODUCT_BUNDLE_IDENTIFIER}
C Environment-переменными мы работаем так:
app = [[XCUIApplication alloc] init];
app.launchEnvironment = @{
testEnviromentUserToken : @"",
testEnviromentDeviceID : @"",
testEnviromentCountry : @""
};
app.launchArguments = @[testArgumentNotClearStart];
В тесте создается объект приложения и в поля launchEnviroment и launchArguments записывается словарь (или массив) с настройками, которые нужно передать в приложение. В приложении настройки и аргументы считываются в делегате при самом старте приложения в методе:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
Так у нас выполняется обработка:
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
[FNTestAPIEnviromentHandler handleArguments:processInfo.arguments
enviroment:processInfo.environment];
Класс TestAPIEnvHandler реализует обработку словаря настроек и массива аргументов.
Свойства элементов
Когда мы начали работать с ХСТest для UI, возникла проблема: стандартный набор инструментов не дает считывать шрифты и цвета.
Мы можем работать только с жестами для элементов, но не можем читать текст, который в них записан, брать их позицию или другие интересные для UI-тестирования свойства.
После поиска альтернативных решений мы посмотрели в сторону Accessibility API, с помощью которого работают UI-тесты.
В качестве “моста” между тестом и приложением решили использовать accessibilityValue, который есть у каждого видимого элемента из iOS SDK.
Поехал велосипед, и получилось такое решение:
- В accessibilityValue записываем json-строку.
- В тесте читаем и декодируем.
- Для UI-элементов пишем категории, которые определяют набор необходимых нам в тестах полей.
Вот пример для UIButton:
@implementation UIButton (TestApi)
- (NSString *)accessibilityValue {
NSMutableDictionary *result = [NSMutableDictionary new];
UIColor *titleColor = [self titleColorForState:UIControlStateNormal];
CGColorRef cgColor = titleColor.CGColor;
CIColor *ciColor = [CIColor colorWithCGColor:cgColor];
NSString *colorString = ciColor.stringRepresentation;
if (titleColor) {
[result setObject:colorString forKey:testKeyTextColor];
}
return [FNTestAPIParametersParser encodeDictionary:result];
}
@end
Чтобы прочитать accessibilityValue в тесте нужно обратиться к ней, для этого у каждого объекта XCUElement есть поле value:
XCUIElement *button = app.buttons[@"FeedSmile"];
NSData *stringData = [button.value dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:stringData options:0 error:&error];
Пользовательские взаимодействия
Проблема жестов и экшенов решается (о чудо!) самим инструментом, благодаря большому набору стандартных методов – tap, double tap. Но в нашем приложении есть не только стандартные, но и очень нетривиальные вещи. Например triple tap, свайпы по всем осям в разные стороны. Чтобы это решить, мы использовали те же стандартные методы, конфигурируя параметры. Большой занозой это не оказалось.
Пример простого теста с использованием подхода:
![](https://habrastorage.org/web/24f/417/259/24f417259fc341e8bf413b1661d2144c.png)
- запускаем iFunny с определенными настройками;
- выбираем страну;
- выбираем нужного пользователя;
- указываем доп.настройки (первый ли это запуск приложения или нет);
- проверяем открытие ленты и загрузку контента;
- делаем смайл;
- проверяем через UI засмайлен ли контент (изменилось состояние кнопки). Продолжаем скролить;
- смотрим мемасики и радуемся жизни.
- (void)testExample {
XCUIElement *feedElement = app.otherElements[@"FeedContentItem"];
XCTAssertNotNil(feedElement);
XCUIElement *button = app.buttons[@"FeedSmile"];
[button tap];
[[[[XCUIApplication alloc] init].otherElements[@"FeedContentItem"].scrollViews childrenMatchingType:XCUIElementTypeImage].element tap];
NSDictionary *result = [FNTestAPIParametersParser decodeString:button.value];
CIColor *color = [CIColor colorWithString:result[testKeyTextColor]];
XCTAssertFalse(color.red - 1.f < FLT_EPSILON &&
color.green - 0.76f < FLT_EPSILON &&
color.blue - 0.29f < FLT_EPSILON,
@"Color not valid");
XCUIElement *feed = app.scrollViews[@"FeedContentFeed"];
[feed swipeLeft];
[feed swipeLeft];
[feed swipeLeft];
}
Мы не планировали делать полное тестовое покрытие, поэтому на этом наши эксперименты пока закончились. Стало ясно, что если мы когда-то решимся полноценно внедрить автотесты в процесс, использовать будем XCtest, но сейчас заниматься этим на постоянной основе очень трудозатратно. И вот почему:
- все равно придется изобретать велосипеды;
- QA не сможет в полном объеме тестировать приложения без разработчиков;
- UI-тесты для нашей продуктовой разработки – это лухари функционал и применять его получается только в исключительных случаях.
P.S. При съёмке превью ни один баг не пострадал. Семён продолжает вдохновлять команду QA.
![](https://habrastorage.org/web/274/9ec/6c1/2749ec6c18244937bac2987b358a6d55.jpg)