...

четверг, 10 августа 2017 г.

UI-тесты для iOS: почему нужно поверить в дружбу QA и разработки, но не обольщаться


С недавних пор мы взялись за внедрение UI-тестирования в iOS для iFunny. Путь этот тернист, долог и холиварен. Но все равно хочется поделиться с умными людьми своими первыми шагами в этом направлении. На истину не претендуем – всё примеряли к собственному продукту. Поэтому под катом немного информации о том, что такое iFunny на iOS и зачем нам понадобился UI + много фидбека по инструментам и примеров кода.

Что такое iFunny на iOS


iFunny — это популярное в США приложение про юмор и мемы с ежемесячной аудиторией в 10М. Подробнее о том, как все затевалось, можно прочитать здесь. Разработка приложения на iOS стартовала 6 лет назад, и мы до сих пор обходимся без каких-либо революционных вкраплений:
  • 99% кода держим на Objective-C;
  • придерживаемся классического MVC с аккуратными делениями на модули;
  • активно работаем с Cocoapods для зависимостей;
  • используем собственный проигрыватель webm-контента: сторонние решения тормозили, не давали контенту зацикливаться и прочее. В случае с iFunny, который является полностью UGC, эта тема критична;
  • форк от SDWebImage используем не только для картинок, но и для остального загружаемого контента;
  • для API выбираем RestKit – достаточно зрелый фреймворк, за несколько лет работы с которым почти не было проблем.

Unit-тесты


У нас все наоборот: работать — значит мемы смотреть :)

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 основных фреймворка, которые сейчас чаще всего используются для тестирования мобильных приложений. Мы примерили каждый из них.

Appium – популярный кроссплатформенный фреймворк. Бытует мнение, что именно он станет стандартом в тестировании мобильных приложений в ближайшем будущем. Несколько месяцев назад мы решили потестить его как с полгода вышедшей iOS 10, но немного огорчились: версия Appium с ее поддержкой была в бете, а использовать в проде нестабильную версию не очень хотелось. Appium Inspector, который работает на Android, тоже использовать не смогли: не было поддержки Xcode 8 и iOS 10. Вскоре они выпустили stable-версию, но ждать полгода после обновления оси для нас крайне нежелательно. Решили не мучить ни себя, ни Appium.

Calabash – кроссплатформенное open source решение, которое использует подход BDD в написании тестов и до последнего времени поддерживалось компанией Xamarin. Недавно разработчики сообщили, что поддержка – всё. Мы тоже решили дальше не идти.

И, наконец, 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?!

Разработчик спешит на помощь


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

Черный ящик



Плюс черного ящика оборачивается в минус: мы не можем знать о текущем состоянии приложения ни на девайсе, ни на симуляторе. Нам необходимо его обнулить и создать по аналогии с 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.
Поехал велосипед, и получилось такое решение:

  1. В accessibilityValue записываем json-строку.
  2. В тесте читаем и декодируем.
  3. Для 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, свайпы по всем осям в разные стороны. Чтобы это решить, мы использовали те же стандартные методы, конфигурируя параметры. Большой занозой это не оказалось.

Пример простого теста с использованием подхода:

  • запускаем 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.

Комментарии (0)

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий